Logical deletion (soft delete) is a pattern that treats a row as deleted by setting a value in the deleted_at timestamp column instead of physically DELETEing it. Use it when an API or batch job needs to restore the pre-deletion state or retain deletion history.
-- Logical deletion: set the current time in deleted_at UPDATE users SET deleted_at = NOW() WHERE user_id = 42; -- Retrieve only active records: rows whose deleted_at is NULL SELECT * FROM users WHERE deleted_at IS NULL;
COALESCE(deleted_at, '9999-12-31') treats a NULL deleted_at as a date far in the future. Use it for date sorting instead of a logical-deletion flag.Implement each of the following requirements as a separate query.
① In the users table, logically delete users whose last_login_at is at least 90 days old (deleted_at = NOW()).
② Retrieve only active users that have not been logically deleted, sort by newest last_login_at first, and use COALESCE to put NULLs last.
| user_id | name | last_login_at | deleted_at |
|---|---|---|---|
| U01 | Alice | 2024-05-20 | NULL |
| U02 | Bob | 2024-01-10 | NULL |
| U03 | Carol | NULL | NULL |
| U04 | Dave | 2024-02-28 | NULL |
| U05 | Eve | 2024-05-01 | 2024-04-01 |
After UPDATE ① (U02 and U04 are logical-deletion targets):
| user_id | name | last_login_at | deleted_at (after update) |
|---|---|---|---|
| U01 | Alice | 2024-05-20 | NULL (active) |
| U02 | Bob | 2024-01-10 | 2024-06-01 02:00:00 |
| U03 | Carol | NULL | NULL (active) |
| U04 | Dave | 2024-02-28 | 2024-06-01 02:00:00 |
| U05 | Eve | 2024-05-01 | 2024-04-01 (already deleted) |
SELECT result ② (U01 and U03 only; last_login descending, with NULL last):
| user_id | name | last_login_at |
|---|---|---|
| U01 | Alice | 2024-05-20 |
| U03 | Carol | NULL (last) |
- 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
LAG(column, N) returns the value N rows earlier within the same partition. It is ideal for a delta batch that compares the previous batch value with the current value and extracts only changed rows.
LAG(column, 1) OVER ( PARTITION BY group_key -- Compare preceding and following values within each group ORDER BY time_col -- Return the value from the previous row after sorting chronologically ) -- The first row is NULL because there is no previous row
LAG returns the previous row's value, while LEAD returns the next row's value. Use LAG for change detection because it compares the previous and current values.Delta-batch pattern: process only changed rows instead of retrieving everything to reduce external API calls and avoid cost and rate-limit problems.
The stock_snapshots table records daily inventory snapshots. Extract only products whose quantity changed from the previous day and output the change amount (diff) as well.
| snapshot_id | product_id | snapshot_date | quantity |
|---|---|---|---|
| 1 | P01 | 2024-06-01 | 100 |
| 2 | P01 | 2024-06-02 | 85 |
| 3 | P01 | 2024-06-03 | 85 |
| 4 | P02 | 2024-06-01 | 200 |
| 5 | P02 | 2024-06-02 | 200 |
| 6 | P02 | 2024-06-03 | 215 |
Rows with a change from the previous day:
| product_id | snapshot_date | quantity | prev_quantity | diff |
|---|---|---|---|---|
| P01 | 2024-06-02 | 85 | 100 | -15 |
| P02 | 2024-06-03 | 215 | 200 | +15 |
- 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 an API returns a large dataset, LIMIT/OFFSET pagination becomes slower as OFFSET grows because more rows must be scanned. Cursor-based pagination passes the last retrieved record's ID (the cursor) in the next request and can consistently use an index.
-- OFFSET method (problematic): the page around row 1,000,000 scans all skipped rows SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 990000; -- Slow -- Cursor method (recommended): use LIMIT to take rows greater than the previous last id SELECT * FROM orders WHERE id > :last_seen_id -- Cursor: the last ID retrieved previously ORDER BY id LIMIT 10; -- Fast because it always uses the index
Write a query that retrieves the next page from orders, five rows per page using a cursor (the last order_id).
Conditions: status = 'pending', order_id ascending, and the previous last order_id is 1003 (parameter :last_id). Also output the cursor for the next page (the last order_id).
| order_id | customer_id | amount | status |
|---|---|---|---|
| 1001 | C01 | 5000 | pending |
| 1002 | C02 | 3000 | completed |
| 1003 | C03 | 7000 | pending |
| 1004 | C04 | 2500 | pending |
| 1005 | C05 | 8000 | pending |
| 1006 | C06 | 4500 | pending |
| 1007 | C01 | 6000 | cancelled |
| 1008 | C02 | 9000 | pending |
Next page, up to 5 rows (pending rows greater than 1003, ordered by order_id):
| order_id | customer_id | amount | status | next_cursor |
|---|---|---|---|---|
| 1004 | C04 | 2500 | pending | 1008 |
| 1005 | C05 | 8000 | pending | 1008 |
| 1006 | C06 | 4500 | pending | 1008 |
| 1008 | C02 | 9000 | pending | 1008 (next cursor) |
Note: There is no next page because only 4 rows remain (detectable when row count < LIMIT).
- 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
Instead of retrieving and processing every row, filter with updated_at >= the previous batch run time and process only changed records. This incremental (delta) batch pattern is common in production for API data synchronization, aggregate-table updates, and notifications.
-- Pass the previous batch run time as :last_run_at SELECT * FROM orders WHERE updated_at >= :last_run_at -- Only rows changed since the previous run AND updated_at < NOW(); -- Before the current time (exclude changes during this run)
batch_jobs. After the batch completes, overwrite it with “this run's start time” as the reference for the next run.indexing updated_at makes WHERE filtering fast. In production, it is important to include updated_at from the table-design stage.
Using the orders and batch_checkpoints tables, implement the following.
① Retrieve the previous batch run time from batch_checkpoints.
② Extract orders updated since the previous run and aggregate order count, total amount, and latest update time by customer_id.
③ Update batch_checkpoints with this batch’s start time (NOW()).
| order_id | customer_id | amount | status | updated_at |
|---|---|---|---|---|
| 1 | C01 | 5000 | completed | 2024-06-01 01:00 |
| 2 | C02 | 3000 | pending | 2024-06-01 02:00 |
| 3 | C01 | 7000 | completed | 2024-06-01 03:00 |
| 4 | C03 | 2500 | cancelled | 2024-05-31 12:00 |
| 5 | C02 | 8000 | completed | 2024-06-01 04:00 |
| job_name | last_run_at |
|---|---|
| order_sync | 2024-06-01 00:00:00 |
② Aggregated result (only orders whose updated_at changed after 6/1 00:00; order_id 4 is excluded):
| customer_id | order_count | total_amount | last_updated_at |
|---|---|---|---|
| C01 | 2 | 12000 | 2024-06-01 03:00 |
| C02 | 2 | 11000 | 2024-06-01 04:00 |
- 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
This pattern monitors execution time, stalls, and consecutive failures in a batch-job history table with one query. It is useful for API health-check endpoints and monitoring dashboards.
-- Calculate duration: convert the timestamp difference to seconds EXTRACT(EPOCH FROM (ended_at - started_at)) AS duration_sec -- EXTRACT(EPOCH FROM interval): convert an interval to seconds -- Consecutive-failure count: failures among the latest N runs COUNT(*) FILTER (WHERE status = 'failed') -- Count only matching rows
(ended_at - started_at) returns an INTERVAL, converting it to seconds calculates the execution time.Using the job_runs table, output the following summary for each job in one query.
- successes, failures, and average execution time (seconds) among the latest 5 runs
- jobs currently running for at least 30 minutes (stall detection)
- job names with at least 3 failures in the latest 5 runs (alert targets)
| run_id | job_name | status | started_at | ended_at |
|---|---|---|---|---|
| 1 | order_sync | success | 06-01 02:00 | 06-01 02:05 |
| 2 | order_sync | failed | 06-01 03:00 | 06-01 03:02 |
| 3 | email_batch | success | 06-01 02:00 | 06-01 02:30 |
| 4 | order_sync | failed | 06-01 04:00 | 06-01 04:01 |
| 5 | email_batch | failed | 06-01 03:00 | 06-01 03:05 |
| 6 | order_sync | running | 06-01 05:00 | NULL |
| 7 | email_batch | failed | 06-01 04:00 | 06-01 04:08 |
| 8 | order_sync | failed | 06-01 06:00 | 06-01 06:03 |
| 9 | email_batch | failed | 06-01 05:00 | 06-01 05:06 |
Job monitoring summary (reference time: 2024-06-01 06:30):
| job_name | success_count | fail_count | avg_sec | is_stalled | alert |
|---|---|---|---|---|---|
| email_batch | 1 | 3 | 370.0 | false | ! ALERT |
| order_sync | 1 | 3 | 165.0 | true ! | ! ALERT |
- 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