Sql Update Many Rows At Once

10 min read

Updating multiple records in a single operation is a fundamental skill for database administrators and developers. Whether you are correcting a data entry error across thousands of entries, applying a seasonal price adjustment, or migrating legacy values to a new schema, understanding how to SQL update many rows at once efficiently separates performant applications from those that grind to a halt. This guide explores the syntax, strategies, and performance considerations required to execute bulk modifications safely and effectively.

Understanding the Basic Syntax for Bulk Updates

At its core, the UPDATE statement modifies existing data. Consider this: when targeting multiple rows, the logic remains identical to a single-row update; the difference lies entirely in the WHERE clause. Instead of filtering for a unique identifier like a Primary Key, you filter for a condition shared by many records.

The standard syntax follows this pattern:

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

If you omit the WHERE clause, every single row in the table will be modified. This is rarely the intention for a targeted bulk update and represents one of the most dangerous mistakes a developer can make. Always verify your WHERE clause with a SELECT statement first:

The official docs gloss over this. That's a mistake.

-- Verify the rows affected first
SELECT * FROM products WHERE category = 'Electronics' AND stock < 10;

-- Then run the update
UPDATE products
SET status = 'Low Stock', last_updated = GETDATE()
WHERE category = 'Electronics' AND stock < 10;

Leveraging CASE Expressions for Conditional Logic

One of the most powerful techniques to SQL update many rows at once with different values is the CASE expression. And that's what lets you apply distinct logic to different subsets of rows within a single statement, avoiding the need for multiple separate update queries.

Imagine you need to adjust salaries: a 10% raise for Engineering, 5% for Sales, and a flat $1000 bonus for Executives.

UPDATE employees
SET salary = CASE
    WHEN department = 'Engineering' THEN salary * 1.10
    WHEN department = 'Sales' THEN salary * 1.05
    WHEN department = 'Executive' THEN salary + 1000
    ELSE salary -- No change for other departments
END,
last_review_date = GETDATE()
WHERE department IN ('Engineering', 'Sales', 'Executive');

This approach is significantly faster than running three separate UPDATE statements because the database engine scans the table (or relevant index) only once, acquires locks once, and writes to the transaction log in a single batch Turns out it matters..

Joining Tables for Relational Updates

In normalized databases, the criteria for updating rows in Table A often depend on data in Table B. Most modern relational database management systems (RDBMS) like PostgreSQL, SQL Server, and MySQL support JOIN syntax within an UPDATE statement (though syntax varies slightly).

SQL Server / PostgreSQL Syntax:

UPDATE p
SET p.price = p.price * 1.15
FROM products p
INNER JOIN categories c ON p.category_id = c.id
WHERE c.name = 'Seasonal Items';

MySQL Syntax:

UPDATE products p
INNER JOIN categories c ON p.category_id = c.id
SET p.price = p.price * 1.15
WHERE c.name = 'Seasonal Items';

This eliminates the need for subqueries in the WHERE clause (e.g., WHERE category_id IN (SELECT id FROM categories...)), which can sometimes be less optimized by the query planner. Using joins allows the optimizer to choose the most efficient access path, often utilizing Hash Match or Merge Join operators for large datasets Worth knowing..

The Critical Role of Batching for Large Volumes

When "many rows" means hundreds of thousands or millions, a single monolithic UPDATE statement becomes a liability. It creates a massive transaction that:

  1. And Locks the table (or large portions of it) for an extended period, blocking reads and writes. But 2. Explodes the Transaction Log, risking disk space exhaustion.
  2. Makes Rollback Painful: If the query fails at 99%, rolling back takes as long as the forward execution.

The solution is Batching (or Chunking). Now, you break the large update into smaller, manageable transactions (e. Think about it: g. , 1,000 to 10,000 rows at a time).

T-SQL (SQL Server) Batching Pattern

DECLARE @BatchSize INT = 5000;
DECLARE @RowCount INT = 1;

WHILE @RowCount > 0
BEGIN
    BEGIN TRANSACTION;

    UPDATE TOP (@BatchSize) orders
    SET status = 'Archived'
    WHERE order_date < '2020-01-01' AND status <> 'Archived';

    SET @RowCount = @@ROWCOUNT;

    COMMIT TRANSACTION;
    
    -- Optional: Small delay to reduce contention
    WAITFOR DELAY '00:00:00.050'; 
END

PostgreSQL / MySQL Batching Pattern (using LIMIT)

-- Repeat this block in a loop (application side or DO block) until row_count = 0
UPDATE orders
SET status = 'Archived'
WHERE order_date < '2020-01-01' AND status <> 'Archived'
LIMIT 5000;

Key Batching Best Practices:

  • Index the Filter Column: Ensure the column in the WHERE clause (order_date, status) is indexed. Without an index, every batch performs a full Table Scan.
  • Add a "Processed" Flag: Update a tracking column (like status in the example above) so subsequent batches skip already processed rows. This prevents re-scanning the same data.
  • Order Matters: If possible, process in Primary Key order (WHERE id > @last_processed_id) to minimize page fragmentation and lock escalation.

Performance Optimization Strategies

Beyond batching, several architectural decisions dictate the speed of your bulk update.

1. Index Maintenance Overhead

Every index on the table that includes a column being updated must be modified for every row. If you update a column covered by five non-clustered indexes, you are essentially performing six write operations per row (one for the heap/clustered index, five for the non-clustered indexes) Worth keeping that in mind..

  • Strategy: If updating a massive percentage of the table (> 20-30%), consider dropping non-clustered indexes before the update and recreating them afterward. The cost of a single index rebuild is often lower than the cost of maintaining the index row-by-row during a massive update.

2. Minimize Logging (Minimal Logging)

Some engines support minimal logging for bulk operations if specific conditions are met (e.g., database in BULK_LOGGED or SIMPLE recovery model in SQL Server, using TABLOCK hint). This logs only page allocations rather than every individual row change, drastically reducing log growth and increasing speed Not complicated — just consistent..

-- SQL Server Example with Minimal Logging hint
UPDATE TOP (10000) products WITH (TABLOCK)
SET price = price * 1.1
WHERE category_id = 5;

Note: TABLOCK escalates locks to the table level, so use this only during maintenance windows.

3. Avoid Triggers and Computed Columns

AFTER UPDATE triggers fire per statement (or per row in some systems). Complex trigger logic inside a bulk update loop kills performance. If possible, disable triggers during the bulk load:

-- SQL Server
DISABLE TRIGGER ALL ON target_table;
-- ... Run Batched Updates ...
ENABLE TRIGGER ALL ON target_table;

Leveraging Set‑Based Constructs Instead of Row‑By‑Row Loops

When the data volume exceeds a few hundred thousand rows, a procedural “loop‑until‑empty” pattern inevitably becomes the bottleneck. Modern RDBMS engines are highly optimized for set‑based operations, and steering the workload toward them can shave orders of magnitude off the elapsed time.

1. MERGE for Bulk Upserts

If the goal is to update rows and insert new ones in a single pass, MERGE (also known as UPSERT) eliminates the need for separate statements and reduces lock contention It's one of those things that adds up..

-- SQL Server / Azure SQL
MERGE INTO dbo.orders AS t
USING (
    SELECT order_id, status, order_date
    FROM   staging.orders_stg
) AS s
ON t.order_id = s.order_id
WHEN MATCHED AND s.status <> t.status
    THEN UPDATE SET t.status = s.status,
                     t.order_date = s.order_date
WHEN NOT MATCHED
    THEN INSERT (order_id, status, order_date)
         VALUES (s.order_id, s.status, s.order_date);

Key take‑aways

  • The MERGE statement is executed once; the engine internally batches the work.
  • Use a unique key (e.g., order_id) on the target table to drive the join; otherwise the optimizer may revert to a cursor‑like behavior.
  • Wrap the MERGE in a single transaction when possible, but monitor log growth.

2. Partition Switching for Massive Data Loads

If the table is partitioned (e.g., by order_date), you can swap an entire partition containing “old” data with a staging partition in near‑zero time Simple as that..

-- Assume orders are partitioned by order_date, and a staging partition exists
ALTER TABLE dbo.orders
SWITCH PARTITION 4   -- old partition (e.g., < 2020‑01‑01)
TO dbo.orders_staging PARTITION 5;

Why it works

  • Switching is a metadata operation; no rows are touched, so the cost is essentially O(1).
  • After the switch, you can archive the staging partition (drop or purge) without scanning the live data.

Tip: Use this technique when the cutoff column is already a partitioning key. If not

, consider restructuring the table to use partitioning or fall back to alternative set-based methods like batched updates (discussed next). Partition switching is a powerful tool, but its applicability depends on the existing schema That's the whole idea..

3. Batched Updates with TOP or OFFSET/FETCH

When an update affects millions of rows, executing it in a single transaction can cause excessive log growth, long lock durations, and transaction log full errors. Breaking the operation into smaller batches balances resource usage while still leveraging set-based processing.

Using TOP (SQL Server, Azure SQL):

WHILE (1 = 1)
BEGIN
    UPDATE TOP (5000) dbo.orders
    SET status = 'Shipped'
    WHERE status = 'Processing' AND order_date < '2020-01-01';
    IF @@ROWCOUNT = 0 BREAK;
END

Using OFFSET/FETCH (SQL Server 2012+, PostgreSQL):

DECLARE @batch_size INT = 5000, @offset INT = 0;

WHILE (1 = 1)
BEGIN
    UPDATE dbo.orders
    SET status = 'Shipped'
    WHERE order_id IN (
        SELECT order_id
        FROM dbo.orders
        WHERE status = 'Processing' AND order_date < '20

'2020-01-01'
        ORDER BY order_id
        OFFSET @offset ROWS
        FETCH NEXT @batch_size ROWS ONLY
    );
    
    SET @offset += @batch_size;
    
    IF @@ROWCOUNT = 0 BREAK;
END

Best practices for batching

  • Choose a batch size that balances throughput and resource consumption (typically 1,000–10,000 rows).
  • Always include an ORDER BY clause when using OFFSET/FETCH to ensure deterministic batching.
  • Monitor transaction log size and adjust batch frequency accordingly.
  • Consider adding WAITFOR DELAY '00:00:01' between batches during peak hours to reduce blocking.

4. Leveraging Indexed Views for Real-Time Aggregation

For tables where the cutoff condition is frequently queried alongside other dimensions, creating an indexed view can dramatically improve performance by pre-aggregating the data.

CREATE VIEW dbo.vw_orders_summary
WITH SCHEMABINDING
AS
SELECT 
    status,
    COUNT_BIG(*) AS order_count,
    SUM(CONVERT(BIGINT, order_date)) AS total_date_sum
FROM dbo.orders
GROUP BY status, order_date;

CREATE UNIQUE CLUSTERED INDEX IX_vw_orders_summary 
ON dbo.vw_orders_summary (status, order_date);

This approach is particularly useful when the same filtering criteria are reused across multiple queries, as the aggregated results are maintained automatically by the engine.

5. Temporary Tables with Indexing

When working with large datasets that require complex joins or filtering, using a temporary table with proper indexing can significantly outperform subqueries or CTEs.

SELECT order_id, status, order_date
INTO #temp_orders
FROM dbo.orders
WHERE status = 'Processing' AND order_date < '2020-01-01';

CREATE CLUSTERED INDEX IX_temp_orders ON #temp_orders (order_id);
CREATE NONCLUSTERED INDEX IX_temp_orders_status ON #temp_orders (status);

-- Perform operations on the indexed temp table
UPDATE t
SET t.status = 'Shipped'
FROM dbo.orders t
INNER JOIN #temp_orders temp ON t.order_id = temp.order_id;

Temporary tables allow the optimizer to generate better execution plans and enable efficient lookups through indexing, especially when the intermediate result set is large.

Conclusion

Efficiently handling large-scale data updates and deletions requires a strategic combination of techniques designed for your specific database environment. While MERGE provides a unified approach for synchronizing datasets, partition switching offers unparalleled speed for schema-appropriate scenarios. Batched updates prevent resource exhaustion during massive operations, while indexed views and temporary tables optimize query performance through strategic pre-computation and indexing No workaround needed..

The key to success lies in understanding your data distribution, schema design, and workload patterns. Always test these approaches in a non-production environment, monitor system resources during execution, and choose the method that aligns with your infrastructure constraints and performance requirements. By implementing these advanced techniques, you can maintain optimal database performance even when dealing with millions of records Which is the point..

Some disagree here. Fair enough.

New In

Straight from the Editor

Others Went Here Next

Expand Your View

Thank you for reading about Sql Update Many Rows At Once. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home