UPSERT (Upsert = Update + Insert) is a pattern that performs “UPDATE if the record exists, INSERT otherwise” in one query. It is one of the most common patterns for synchronizing data from an external service in an API batch.
INSERT INTO target_table (col1, col2, ...) VALUES (...) ON CONFLICT (unique_col) -- Specify the unique-key column used for conflict detection DO UPDATE SET col2 = EXCLUDED.col2; -- EXCLUDED = the value being inserted (the conflicting new data)
It is a special table reference available inside ON CONFLICT DO UPDATE. It refers to the values of the row that was going to be inserted but conflicted. Use
EXCLUDED.col for the new value and target_table.col for the existing value.DO NOTHING means “skip the row when it conflicts.” It is useful when you need idempotency: running the same data repeatedly produces the same result.
Synchronize a product master from an external EC service in a daily batch. In the products table, INSERT new products and UPDATE name/price/updated_at for existing products.
Synchronization data to write directly in VALUES: (external ID: EXT-001, name: Wireless Mouse Rev. 2, price: 3200), (EXT-002, Mechanical Keyboard, 8900), (EXT-003, 4-Port USB Hub, 1980)
| id | external_id | name | price | updated_at |
|---|---|---|---|---|
| 1 | EXT-001 | Wireless Mouse | 2800 | 2024-01-10 |
| 2 | EXT-002 | Mechanical Keyboard | 8900 | 2024-01-10 |
The products table after the query runs:
| id | external_id | name | price | updated_at |
|---|---|---|---|---|
| 1 | EXT-001 | Wireless Mouse Rev. 2 | 3200 | 2024-06-01 |
| 2 | EXT-002 | Mechanical Keyboard | 8900 | 2024-06-01 |
| 3 | EXT-003 | 4-Port USB Hub | 1980 | 2024-06-01 |
※ EXT-001 updates its name, price, and updated_at; EXT-002 keeps its name and price but gets a new updated_at; EXT-003 is inserted as a new row.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
UPDATE ... FROM is a pattern for bulk-updating a target table while reading values from another table. It is central to batch processing, for example updating a product table from a price-revision master or changing statuses by matching an approved list.
UPDATE target -- Table to update SET col = src.col -- Set a value from the source table FROM source src -- Source table (joined like a JOIN) WHERE target.key = src.key; -- Join condition (without it, every row is updated!)
PostgreSQL uses UPDATE ... FROM; MySQL uses UPDATE target JOIN source ON ....
Use the price_updates (price revision master) table to bulk-update price and updated_at in products. Only products whose product_id exists in price_updates are targets.
| product_id | name | price | updated_at |
|---|---|---|---|
| 101 | Coffee Bean A | 1200 | 2024-01-01 |
| 102 | Coffee Bean B | 1500 | 2024-01-01 |
| 103 | Black Tea Leaf C | 900 | 2024-01-01 |
| 104 | Green Tea D | 800 | 2024-01-01 |
| product_id | new_price | applied_at |
|---|---|---|
| 101 | 1380 | 2024-06-01 |
| 103 | 1050 | 2024-06-01 |
products after the update (only 101 and 103 found in price_updates change):
| product_id | name | price | updated_at |
|---|---|---|---|
| 101 | Coffee Bean A | 1380 | 2024-06-01 |
| 102 | Coffee Bean B | 1500 | 2024-01-01 |
| 103 | Black Tea Leaf C | 1050 | 2024-06-01 |
| 104 | Green Tea D | 800 | 2024-01-01 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
INSERT INTO ... SELECT inserts the result of a SELECT directly into another table. It is widely used in batch processing to move old data into an archive table or write aggregate results to a summary table.
INSERT INTO archive_table (col1, col2, ...) SELECT col1, col2, ... -- SELECT column order and types must match the target FROM source_table WHERE condition; -- Condition that narrows transfer targets
To delete rows from the source after archiving, pair DELETE + INSERT INTO...SELECT in one transaction. This prevents data from disappearing during the transfer.
Bulk-copy completed orders from before 2024-02-01 (order_date < '2024-02-01') from orders into orders_archive. Set the archived_at column to the current time after copying.
| order_id | customer_id | amount | status | order_date |
|---|---|---|---|---|
| 1001 | C01 | 12000 | completed | 2024-01-15 |
| 1002 | C02 | 8500 | cancelled | 2024-01-20 |
| 1003 | C03 | 15000 | completed | 2024-01-28 |
| 1004 | C01 | 9200 | completed | 2024-02-05 |
| 1005 | C04 | 6700 | completed | 2024-02-10 |
| order_id | customer_id | amount | status | order_date | archived_at |
|---|---|---|---|---|---|
| (empty) | |||||
After execution, orders_archive receives two rows: 1001 and 1003.
| order_id | customer_id | amount | status | order_date | archived_at |
|---|---|---|---|---|---|
| 1001 | C01 | 12000 | completed | 2024-01-15 | 2024-06-01 02:00:00 |
| 1003 | C03 | 15000 | completed | 2024-01-28 | 2024-06-01 02:00:00 |
※ 1002 (cancelled), 1004, and 1005 (February or later) are excluded.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Duplicate keys can appear in a table after a batch import or a double submission. Combining ROW_NUMBER() with a CTE lets you write a cleansing query that keeps only the latest record within each duplicate-key group.
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY dup_key -- Column used to identify duplicates ORDER BY created_at DESC -- Number rows newest first (1 is newest) ) AS rn FROM target_table ) SELECT * FROM ranked WHERE rn = 1; -- Return only rank 1 (latest) from each group
WITH name AS (SELECT ...) defines a temporary named result set. The main query can reference it by name. It is more readable than nesting subqueries and is a standard way to write batch processing queries.The user_events table contains duplicate data because the same user_id and event_type combination was imported multiple times. Write a query that keeps only the latest record for each user_id + event_type.
| event_id | user_id | event_type | payload | created_at |
|---|---|---|---|---|
| 1 | U01 | login | ip:1.2.3.4 | 2024-06-01 09:00 |
| 2 | U01 | login | ip:5.6.7.8 | 2024-06-01 12:00 |
| 3 | U01 | purchase | item:A | 2024-06-01 10:00 |
| 4 | U02 | login | ip:9.9.9.9 | 2024-06-01 11:00 |
| 5 | U01 | purchase | item:B | 2024-06-01 15:00 |
After deduplication (the latest row for each user_id + event_type):
| event_id | user_id | event_type | payload | created_at | rn |
|---|---|---|---|---|---|
| 2 | U01 | login | ip:5.6.7.8 | 2024-06-01 12:00 | 1 |
| 5 | U01 | purchase | item:B | 2024-06-01 15:00 | 1 |
| 4 | U02 | login | ip:9.9.9.9 | 2024-06-01 11:00 | 1 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
When a batch needs to UPDATE different values for several conditions, CASE WHEN in the SET clause combines the work into one query.
UPDATE orders SET status = CASE WHEN condition A THEN 'value_a' -- Rows matching condition A become value_a WHEN condition B THEN 'value_b' -- Rows matching condition B become value_b ELSE status -- Rows matching neither condition keep their value END WHERE filter condition; -- Limit the UPDATE targets (important)
ELSE column_name (preserve the current value) or ELSE 'an appropriate default'.In a nightly batch, bulk-update statuses in the jobs table using these rules.
- retry_count = 0 and status = 'failed' → 'pending' (requeue)
- retry_count ≥ 3 and status = 'failed' → 'abandoned' (give up)
- started_at is NULL and status = 'running' → 'stalled' (detect a zombie)
- Leave all other rows unchanged
| job_id | status | retry_count | started_at |
|---|---|---|---|
| J01 | failed | 0 | 2024-06-01 |
| J02 | failed | 3 | 2024-06-01 |
| J03 | running | 0 | NULL |
| J04 | completed | 0 | 2024-06-01 |
| J05 | failed | 1 | 2024-06-01 |
The jobs table after the update:
| job_id | status (after update) | Change |
|---|---|---|
| J01 | pending | failed(0) → pending |
| J02 | abandoned | failed(3) → abandoned |
| J03 | stalled | running(NULL) → stalled |
| J04 | completed | No change |
| J05 | failed | No change (retry_count=1) |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free