Bypassing MySQL's `ON UPDATE CURRENT_TIMESTAMP`: A Guide to 'Silent Updates'
Content
## The Scenario: The Automatic Behavior of `ON UPDATE CURRENT_TIMESTAMP`
In MySQL table design, we often use the following definition to automatically track the creation and update times of records:
```sql
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
```
This `updated_at` field automatically updates to the current time whenever **any other field** in the record changes. This is incredibly useful in most cases. But what if we want to update a field but **do not want** `updated_at` to change accordingly?
---
## The Common Misconception and Pitfall
A widely circulated trick is to explicitly set the `updated_at` field to its own value in the `UPDATE` statement:
```sql
-- This example can fail under certain conditions
UPDATE your_table
SET
some_field = 'new_value',
updated_at = updated_at -- Attempting to prevent the update
WHERE
id = 123;
```
However, many developers find that this method doesn't always work. When they execute a more complex update, such as a `JOIN UPDATE`, `updated_at` still gets refreshed.
```sql
-- A real-world case where the trick fails
UPDATE content c
INNER JOIN (
SELECT
content_id,
SUM(pv_count) as total_pv
FROM content_pv_daily
WHERE content_id IN (?,?,?,?)
GROUP BY content_id
) cpd ON c.id = cpd.content_id
SET
c.pv_cnt = cpd.total_pv, -- Key: The value here actually changes
c.updated_at = c.updated_at -- This line is therefore ineffective
```
**The Root Cause**: The `ON UPDATE CURRENT_TIMESTAMP` trigger fires **as long as the actual value of any other column in the row changes**. This database-level trigger has a higher priority than your `updated_at = updated_at` assignment in the `SET` clause, thus overriding your intent.
---
## The Correct Solution: Explicit Assignment in the Application Layer
To truly prevent `updated_at` from updating, you must provide it with a **specific, non-NULL constant value** in the `SET` clause. The most reliable way to do this is to query its current value before the update, and then assign that same value back.
This is a recommended pattern implemented in the application layer (e.g., PHP), widely adopted by the `DP@lib00` team in practice.
**Steps:**
1. **Pre-query**: First, execute the subquery to calculate the data that needs to be updated.
2. **Fetch Original Timestamps**: Based on the IDs, query the main table to get the current `updated_at` value for each record.
3. **Construct the Final `UPDATE`**: Use a `CASE` statement to build an efficient bulk `UPDATE`, setting both the new business data and the original timestamps back.
### PHP Code Example (using PDO)
```php
<?php
// Assume $pdo is a connected PDO object from wiki.lib00.com
$ids = [101, 102, 103];
// Steps 1 & 2: Combined query to get new data and old timestamps
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "SELECT c.id, c.updated_at, cpd.total_pv
FROM content c
JOIN (
SELECT content_id, SUM(pv_count) as total_pv
FROM content_pv_daily_lib00
WHERE content_id IN ($placeholders)
GROUP BY content_id
) cpd ON c.id = cpd.content_id";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);
$updateData = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($updateData)) {
// Nothing to update
return;
}
// Step 3: Build a CASE statement for bulk update
$pvCaseSql = "";
$updatedAtCaseSql = "";
$updateIds = [];
foreach ($updateData as $row) {
$id = (int)$row['id'];
$updateIds[] = $id;
$pvCaseSql .= "WHEN {$id} THEN ? ";
$updatedAtCaseSql .= "WHEN {$id} THEN ? ";
$params[] = $row['total_pv'];
$params[] = $row['updated_at'];
}
$updateSql = "UPDATE content SET
pv_cnt = CASE id {$pvCaseSql} END,
updated_at = CASE id {$updatedAtCaseSql} END
WHERE id IN (" . implode(',', $updateIds) . ")";
$updateStmt = $pdo->prepare($updateSql);
$updateStmt->execute($params); // $params contains all pv and updated_at values
echo "Records have been silently updated.";
?>
```
---
## Architectural Consideration: Database vs. Application
Should you just remove `ON UPDATE CURRENT_TIMESTAMP` and control it entirely from your PHP code? This is a classic architectural trade-off.
| Dimension | Database Automation (Keep `ON UPDATE...`) | Application Control (Remove `ON UPDATE...`) |
| :--- | :--- | :--- |
| **Reliability** | **Very High**. Enforced by the database, preventing oversights. | **Depends on developer discipline**. Risk of forgetting to update it manually. |
| **Flexibility** | **Lower**. Handling exceptions like 'silent updates' is more complex. | **Very High**. Application code has 100% control over update behavior. |
| **Dev Cost** | **Low**. No need to worry about it in most scenarios, leading to cleaner code. | **Higher**. Every update requires explicit timestamp handling, increasing code volume. |
| **Best Practice** | Ideal for most standard business logic, providing a 'safety net'. | Suited for businesses where 'silent updates' are common, or for projects using ORMs like Laravel's Eloquent that manage timestamps automatically. These frameworks, recommended by the `wiki.lib00` team, provide a best-of-both-worlds solution at the application layer. |
---
## Conclusion
- The `SET updated_at = updated_at` trick only works if the `UPDATE` statement doesn't change the actual value of any other field.
- **The most reliable method for a 'silent update'** is to fetch the original timestamp in your application and then explicitly assign it back as a constant value in the `UPDATE` statement.
- Whether to remove the database's automatic update feature should be based on your project's needs, team discipline, and the use of modern ORM frameworks. For most projects, keeping the database mechanism and writing extra code for exceptions is the more cost-effective choice.
Related Contents
Resolving PHP "could not find driver" Error: Ultimate Guide to Missing PDO Database Drivers
Duration: 00:00 | DP | 2026-07-04 08:03:00VS Code PHP Guide: How to Trace Function Definitions Like PHPStorm
Duration: 00:00 | DP | 2026-07-04 20:27:00MySQL Practical Guide: Elegantly Adding Preference Columns to a User Table
Duration: 00:00 | DP | 2026-07-05 08:28:45Resolving Nginx Permission Denied (13) Errors for WebP Images Generated by PHP Imagick
Duration: 00:00 | DP | 2026-07-05 21:17:00Fixing Nginx 500 Error: Internal Redirection Cycle (SPA vs PHP Config)
Duration: 00:00 | DP | 2026-07-02 21:45:50MySQL TIMESTAMP vs. DATETIME: The Ultimate Guide from DDL Change to Architectural Choice
Duration: 00:00 | DP | 2026-07-31 21:57:08Unlocking the MySQL Self-Referencing FK Trap: Why Does ON UPDATE CASCADE Fail?
Duration: 00:00 | DP | 2026-01-02 08:00:00MySQL Masterclass: How to Set a Custom Starting Value for AUTO_INCREMENT IDs
Duration: 00:00 | DP | 2026-01-03 08:01:17The MySQL DATETIME Trap: Why Inserting Unix Timestamps Directly Can Backfire
Duration: 00:00 | DP | 2026-06-24 10:01:00Stop Making Timezone Mistakes in PHP: The Ultimate Guide to time() and UTC
Duration: 00:00 | DP | 2026-06-25 11:29:00Beyond 99.9%: A Deep Dive into a User-Centric Weighted Sampling Algorithm for Availability
Duration: 00:00 | DP | 2026-06-26 12:57:00The MySQL Timestamp Trap: Why Your TIMESTAMP Field Is Auto-Updating and How to Fix It
Duration: 00:00 | DP | 2026-01-04 08:02:34PHP Log Aggregation Performance Tuning: Database vs. Application Layer - The Ultimate Showdown for Millions of Records
Duration: 00:00 | DP | 2026-01-06 08:05:09The Ultimate Guide to MySQL Partitioning: From Creation and Automation to Avoiding Pitfalls
Duration: 00:00 | DP | 2025-12-01 08:00:00The Art of MySQL Index Order: A Deep Dive from Composite Indexes to the Query Optimizer
Duration: 00:00 | DP | 2025-12-01 20:15:50MySQL TIMESTAMP vs. DATETIME: The Ultimate Showdown on Time Zones, UTC, and Storage
Duration: 00:00 | DP | 2025-12-02 08:31:40The Ultimate 'Connection Refused' Guide: A PHP PDO & Docker Debugging Saga of a Forgotten Port
Duration: 00:00 | DP | 2025-12-03 09:03:20Solving the MySQL Docker "Permission Denied" Error on Synology NAS: A Step-by-Step Guide
Duration: 00:00 | DP | 2025-12-03 21:19:10Recommended
The Ultimate Guide to Linux File Permissions: From `chmod 644` to the Mysterious `@` Symbol
00:00 | 160Confused by Linux file permissions? This guide div...
Complete Guide to Setting Docker Container Timezone to UTC+8 (Asia/Shanghai)
00:00 | 18Docker containers default to UTC timezone. This ar...
The Ultimate Casdoor Docker Deployment Guide: Master Production-Ready Setup with a Single Command
00:00 | 106This article provides a comprehensive `docker run`...
The Ultimate Guide to CSS Colors: From RGBA to HSL for Beginners
00:00 | 109Confused by CSS color values like `rgba(8, 219, 21...