MySQLUPDATE

UPDATE in MySQL

The UPDATE statement modifies existing rows in a table. It is one of the most frequently used DML statements — and also one of the most dangerous if used incorrectly. A forgotten WHERE clause can update every row in a table in seconds.

Basic UPDATE Syntax

SQL
UPDATE table_name
SET column1 = value1,
    column2 = value2
WHERE condition;

SQL
-- Update a single column
UPDATE employees
SET email = 'alice.smith@newdomain.com'
WHERE employee_id = 101;

-- Update multiple columns at once
UPDATE employees
SET last_name  = 'Johnson',
    email      = 'alice.johnson@example.com',
    updated_at = NOW()
WHERE employee_id = 101;
Updating Multiple Columns

All column assignments in a single UPDATE happen simultaneously — you can reference the original values of other columns in the same statement.

SQL
-- Swap two column values atomically
UPDATE products
SET price = cost,
    cost  = price
WHERE product_id = 55;

-- Increment a counter column
UPDATE page_views
SET view_count = view_count + 1
WHERE page_slug = '/home';

-- Apply a bulk percentage price increase
UPDATE products
SET price = ROUND(price * 1.05, 2)
WHERE category_id = 3;

-- Conditional update using expressions
UPDATE orders
SET discount = CASE
  WHEN total > 1000 THEN 0.10
  WHEN total > 500  THEN 0.05
  ELSE 0
END
WHERE status = 'pending';
UPDATE Without WHERE — The Danger
Warning
Omitting the WHERE clause updates EVERY row in the table. There is no confirmation prompt. Always double-check your WHERE condition before running UPDATE in production.

SQL
-- DANGEROUS: updates all 50 000 employee records
UPDATE employees SET salary = 50000;

-- Safe pattern: SELECT first, then UPDATE with the same WHERE
SELECT COUNT(*) FROM employees WHERE department_id = 5;
UPDATE employees SET salary = 55000 WHERE department_id = 5;
Safe Update Mode (sql_safe_updates)

MySQL has a safety guard called sql_safe_updates. When enabled, MySQL refuses UPDATE and DELETE statements that lack a WHERE clause using an indexed column.

SQL
-- Enable safe update mode for the current session
SET SESSION sql_safe_updates = 1;

-- This now errors because there is no key-column WHERE clause
UPDATE employees SET status = 'active';
-- ERROR 1175: You are using safe update mode

-- A WHERE on an indexed column (e.g. primary key) is allowed
UPDATE employees SET status = 'active' WHERE employee_id > 0;

-- Disable when you intentionally need a full-table update
SET SESSION sql_safe_updates = 0;
Tip
Enable sql_safe_updates = 1 by default in your MySQL Workbench or client configuration. It is a cheap safety net that prevents many accidental disasters.
UPDATE with JOIN

MySQL supports updating rows in one or more tables using JOIN syntax. This is more efficient than a correlated subquery because the join plan can use indexes from both sides.

SQL
-- Sync the location field on employees based on their department
UPDATE employees e
JOIN departments d ON e.department_id = d.id
SET e.location = d.city
WHERE d.region = 'EMEA';

-- Give a 10% raise to everyone in the Engineering department
UPDATE employees e
INNER JOIN departments d ON e.department_id = d.id
SET e.salary = ROUND(e.salary * 1.10, 2)
WHERE d.name = 'Engineering';

-- Practical example: update product prices from a reference/pricing table
UPDATE products p
JOIN pricing_overrides po ON po.product_id = p.id
SET p.price      = po.new_price,
    p.updated_at = NOW()
WHERE po.effective_date <= CURDATE()
  AND po.applied = 0;

-- Mark the overrides as applied in the same query
UPDATE products p
JOIN pricing_overrides po ON po.product_id = p.id
SET p.price       = po.new_price,
    p.updated_at  = NOW(),
    po.applied    = 1,
    po.applied_at = NOW()
WHERE po.effective_date <= CURDATE()
  AND po.applied = 0;
Note
Multi-table UPDATE does not support an ORDER BY or LIMIT clause. If you need batching, use a subquery or temporary table to identify the target row IDs first, then UPDATE with WHERE id IN (...).
UPDATE with Correlated Subquery

SQL
-- Recalculate each order's total from its line items
UPDATE orders o
SET o.total = (
  SELECT SUM(unit_price * quantity)
  FROM order_items
  WHERE order_id = o.id
);

-- Set a flag on orders that contain a specific product
UPDATE orders
SET has_premium = 1
WHERE id IN (
  SELECT DISTINCT order_id
  FROM order_items
  WHERE product_id = 99
);
Warning
MySQL does not allow you to UPDATE a table and SELECT from the same table in a direct subquery. Wrap the inner SELECT in a derived table as a workaround.

SQL
-- This FAILS in MySQL:
UPDATE employees
SET salary = salary * 1.1
WHERE employee_id IN (
  SELECT employee_id FROM employees WHERE department_id = 2
);
-- ERROR 1093

-- Fix: wrap in a derived table
UPDATE employees
SET salary = salary * 1.1
WHERE employee_id IN (
  SELECT employee_id FROM (
    SELECT employee_id FROM employees WHERE department_id = 2
  ) AS sub
);
LIMIT Batching for Large Table Updates

Adding LIMIT to an UPDATE restricts how many rows are changed per statement. Use this with a loop to process large updates in small batches, avoiding long transactions that block concurrent reads and writes.

SQL
-- Batch update: process 1000 rows at a time
UPDATE notifications
SET sent = 1
WHERE sent = 0
ORDER BY created_at
LIMIT 1000;

Bash
#!/bin/bash
# Keep updating until fewer than 1000 rows are affected
while true; do
  ROWS=$(mysql mydb -sN -e "
    UPDATE notifications SET sent=1
    WHERE sent=0 ORDER BY created_at LIMIT 1000;
    SELECT ROW_COUNT();
  ")
  echo "Updated ${ROWS} rows"
  [ "${ROWS}" -lt 1000 ] && break
  sleep 0.1
done
UPDATE with CASE WHEN for Bulk Conditional Updates

Instead of running multiple UPDATE statements, use a single UPDATE with CASE WHEN to apply different values to different rows in one pass.

SQL
-- Assign tier labels based on total spending in one query
UPDATE customers
SET tier = CASE
  WHEN lifetime_spend >= 10000 THEN 'platinum'
  WHEN lifetime_spend >= 5000  THEN 'gold'
  WHEN lifetime_spend >= 1000  THEN 'silver'
  ELSE                              'bronze'
END;

-- Bulk status update for specific order IDs
UPDATE orders
SET status = CASE id
  WHEN 1001 THEN 'shipped'
  WHEN 1002 THEN 'delivered'
  WHEN 1003 THEN 'cancelled'
END
WHERE id IN (1001, 1002, 1003);
Optimistic Locking with a Version Column

In concurrent applications, two users may read the same row and then both try to update it. The second writer would silently overwrite the first writer's changes (a "lost update").

Optimistic locking prevents this by adding a version column. Each UPDATE must include the version it read. If another writer already incremented the version, the UPDATE affects 0 rows and the application knows to retry.

SQL
-- Table with a version column
CREATE TABLE inventory (
  product_id INT UNSIGNED PRIMARY KEY,
  quantity   INT NOT NULL DEFAULT 0,
  version    INT NOT NULL DEFAULT 0
);

-- Read current state (application stores version = 3)
SELECT product_id, quantity, version
FROM inventory
WHERE product_id = 42;
-- Returns: quantity=100, version=3

-- Update: only succeeds if version hasn't changed since we read it
UPDATE inventory
SET quantity = 95,
    version  = version + 1      -- increment version
WHERE product_id = 42
  AND version    = 3;            -- must match what we read

-- Check result
SELECT ROW_COUNT() AS rows_affected;
-- 1 = success, version is now 4
-- 0 = someone else already updated it; application should re-read and retry
Note
Optimistic locking works well when conflicts are rare. If conflicts are frequent (many writers on the same row), pessimistic locking with SELECT ... FOR UPDATE inside a transaction may be more appropriate.
UPDATE Triggers (BEFORE/AFTER UPDATE)

SQL
-- BEFORE UPDATE: prevent negative inventory
CREATE TRIGGER trg_inventory_before_update
BEFORE UPDATE ON inventory
FOR EACH ROW
BEGIN
  IF NEW.quantity < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Inventory quantity cannot be negative';
  END IF;
END;

-- AFTER UPDATE: write an audit trail
CREATE TRIGGER trg_employees_after_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
  INSERT INTO employee_audit
    (employee_id, field_changed, old_value, new_value, changed_at, changed_by)
  VALUES
    (OLD.id, 'salary', OLD.salary, NEW.salary, NOW(), CURRENT_USER());
END;
Checking Rows Affected with ROW_COUNT()

SQL
-- ROW_COUNT() returns the number of rows actually CHANGED
UPDATE products SET active = 0 WHERE expires_at < NOW();
SELECT ROW_COUNT() AS rows_deactivated;

-- Important: rows that matched but were already at the new value count as 0
UPDATE users SET status = 'active' WHERE status = 'active';
SELECT ROW_COUNT();
-- Returns 0 (no rows changed value, even if many matched)
Note
MySQL reports ROW_COUNT() as the number of rows whose value actually changed, not just the number matched. To count matched rows instead, use the CLIENT_FOUND_ROWS flag in your connection options.
Transaction-Wrapped UPDATE with Rollback

SQL
-- Wrap multiple updates in a transaction for atomicity
START TRANSACTION;

UPDATE accounts
SET balance = balance - 500
WHERE account_id = 1001;

UPDATE accounts
SET balance = balance + 500
WHERE account_id = 2001;

-- Verify balances look correct before committing
SELECT account_id, balance
FROM accounts
WHERE account_id IN (1001, 2001);

-- If everything looks good:
COMMIT;

-- If something is wrong:
-- ROLLBACK;
UPDATE Pattern Comparison

Pattern

Use Case

Limitation

UPDATE ... WHERE

Standard single-table update

None

Multi-table UPDATE with JOIN

Cross-table updates from a reference table

No ORDER BY or LIMIT

UPDATE ... WHERE ... IN (subquery)

Filter by related table

Cannot reference same table directly in subquery

UPDATE ... LIMIT n

Batch processing to keep transactions short

Needs ORDER BY for consistent row selection

UPDATE with CASE WHEN

Apply different values to different rows in one pass

Can be verbose for many conditions

Optimistic locking (version col)

Concurrent update safety without DB-level locks

Requires application-level retry logic

UPDATE and InnoDB Locking

When an UPDATE runs inside a transaction, InnoDB acquires exclusive (X) row locks on every row that matches the WHERE clause — not just the rows that change value. These locks are held until the transaction commits or rolls back.

Under REPEATABLE READ (the default isolation level), InnoDB also takes gap locks on the index ranges scanned. This prevents phantom rows from appearing during the transaction but can increase lock contention in write-heavy workloads.

SQL
-- Session A: UPDATE acquires X locks on matching rows
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Row lock held on account_id=1 until COMMIT or ROLLBACK

-- Session B: trying to UPDATE the same row will WAIT
UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
-- Blocked until Session A commits or times out (innodb_lock_wait_timeout)

-- See what is locked right now
SELECT r.trx_id AS waiting_trx,
       b.trx_id AS blocking_trx,
       b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id;
Tip
Keep transactions that contain UPDATE statements as short as possible. Long transactions hold row locks for the entire duration, blocking concurrent writers and risking deadlocks.
Tracking Changes with updated_at

SQL
-- Add an auto-updating timestamp to any table
ALTER TABLE products
  ADD COLUMN updated_at DATETIME
    NOT NULL DEFAULT CURRENT_TIMESTAMP
    ON UPDATE CURRENT_TIMESTAMP;

-- Now every UPDATE automatically sets updated_at = NOW()
UPDATE products SET price = 29.99 WHERE id = 5;
SELECT id, price, updated_at FROM products WHERE id = 5;
-- updated_at is set to the current timestamp automatically

-- Find all rows updated in the last 24 hours
SELECT * FROM products WHERE updated_at >= NOW() - INTERVAL 1 DAY;
Verifying UPDATE Safety with EXPLAIN

SQL
-- Run EXPLAIN on an UPDATE to check index usage before executing on large tables
EXPLAIN UPDATE orders
SET status = 'expired'
WHERE status = 'pending'
  AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);

-- Look for type=range (index used) vs type=ALL (full table scan -- add an index!)
-- Add a composite index to cover this common update pattern:
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);

-- Re-check after adding the index
EXPLAIN UPDATE orders
SET status = 'expired'
WHERE status = 'pending'
  AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- type: range   key: idx_status_created   (much faster)
Common Mistake: Forgetting the WHERE Clause

SQL
-- Scenario: developer intends to update one user's email
-- but forgets to add the WHERE clause

-- WRONG: updates ALL 500,000 users
UPDATE users SET email = 'wrong@example.com';

-- After executing this by mistake:
-- 1. Check if you are inside an open transaction (still time to ROLLBACK)
SELECT @@autocommit;   -- if 0, you may still be in a transaction
ROLLBACK;              -- attempt rollback immediately

-- 2. If autocommit was ON, the change is committed. You need binary log recovery.
-- Check if binary logging is enabled:
SHOW VARIABLES LIKE 'log_bin';

-- 3. Prevention: enable sql_safe_updates
SET SESSION sql_safe_updates = 1;
-- Now this will error: UPDATE users SET email = 'x' WHERE email LIKE '%';
-- ERROR 1175: You are using safe update mode
Best Practices
  • Always test your WHERE clause with a SELECT before running UPDATE.

  • Enable sql_safe_updates in client tools to prevent accidental full-table updates.

  • Use transactions for multi-step updates so you can ROLLBACK if something goes wrong.

  • Batch large updates with LIMIT + ORDER BY to avoid long table locks on busy tables.

  • Add an updated_at TIMESTAMP column with DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP so you always know when a row was last changed.

  • Use SELECT ROW_COUNT() after UPDATE to verify the expected number of rows were affected.

  • Consider optimistic locking (version column) for concurrent update scenarios to prevent lost updates.

  • Index the columns used in the WHERE clause of frequent UPDATE statements.

  • Run EXPLAIN on UPDATE statements against large tables before executing in production.