SQL Batch Processing — Applied LATERAL, JSONB, EXPLAIN

ADVBatch processingLATERAL / JSONBDifferential update batchesEXPLAINROLLUPPostgreSQL5 questions
1 / 5 · In progress 0 / 5 completed
QUESTION 6

LATERAL JOIN — Retrieve the top 3 products by revenue for each category

LATERAL JOINLIMITTop-N retrievalRanking API
Background

LATERAL allows the subquery on the right side of a JOIN to reference columns from rows on the left side. It is the JOIN version of a correlated subquery: “for each left-side row, run a subquery whose condition uses that row's values.”

SELECT *
FROM   categories c
JOIN LATERAL (                       -- The right-side subquery can reference c.category_id from the left
  SELECT * FROM products p
  WHERE  p.category_id = c.category_id  -- ← Reference c from the left (the core of LATERAL)
  ORDER BY p.revenue DESC
  LIMIT  3                            -- Keep the top 3 per category
) sub ON TRUE;                        -- ON TRUE: the LATERAL subquery already handles the join condition
“Top N per group” is especially intuitive with LATERAL: ROW_NUMBER() can also solve this, but LATERAL makes the intent “top 3 per category” explicit in the code.
Problem

Using the categories and products tables, retrieve the 3 products with the highest revenue in each category, including the category name. Include every product for categories with fewer than 3 products (LEFT JOIN LATERAL). Sort the result by category ID and descending revenue.

Tables used
▸ categories
category_idname
1Drinks
2Food
▸ products
product_idcategory_idnamerevenue
P011Coffee50000
P021Tea30000
P031Green tea45000
P041Water20000
P052Bread25000
P062Rice ball18000
Expected Output
category_idcategory_nameproduct_idproduct_namerevenuerank
1DrinksP01Coffee500001
1DrinksP03Green tea450002
1DrinksP02Tea300003
2FoodP05Bread250001
2FoodP06Rice ball180002
QUESTION 7

JSONB operations — Search, aggregate, and update a PostgreSQL JSONB column

JSONB->> / @>JSON searchSettings API
Background

PostgreSQL's JSONB type stores JSON data in binary form and enables fast searches with indexes. It is useful when flexible data structures such as user settings, tags, and metadata need to be handled inside an RDB.

-- Extract JSON values (the text result can be cast)
col ->>  'key'            -- Extract as text (commonly used)
col ->   'key'            -- Keep the JSONB type (continue into nested references)
col ->>  0               -- Extract JSON array index 0 as text

-- Containment check (fast when combined with a GIN index)
col @>  '{"key": "value"}'::jsonb   -- Check whether col contains the specified key/value

-- Partial update of a JSONB column
jsonb_set(col, '{key}', '"new_value"'::jsonb)  -- Replace the value at the specified path
text vs JSONB:When a JSON string is stored as TEXT, SQL cannot search its contents. JSONB lets you filter by keys and values. A GIN index makes the @> operator fast.
Problem

For the settings JSONB column in user_settings, write (1) a query that extracts users whose notifications are enabled (notifications = true), and (2) an aggregate showing how many users use each theme (dark/light). In addition, (3) update the language for user_id=1 from 'en' to 'ja'.

Tables used
▸ user_settings
user_idsettings (JSONB)
1{"theme":"dark","language":"en","notifications":true}
2{"theme":"light","language":"ja","notifications":false}
3{"theme":"dark","language":"ja","notifications":true}
4{"theme":"light","language":"en","notifications":true}
Expected Output

(1) Users with notifications enabled (notifications=true):

user_idnotifications
1true
3true
4true

(2) User count by theme:

themeuser_count
dark2
light2

(3) After UPDATE (user_id=1):

user_idsettings (after update)
1{"theme":"dark","language":"ja","notifications":true}
QUESTION 8

Differential update batch — Classify changes with subqueries and run INSERT / UPDATE / DELETE

DML chainNOT EXISTSDifferential updateData synchronization batch
Background

In a data-synchronization batch with an external system, updating only the differences is safer and faster than deleting every row and inserting everything again. This exercise uses subqueries such as NOT EXISTS to classify rows to add, update, or delete, then executes the changes in one transaction.

-- Present in new data but absent from current data → INSERT target
WHERE NOT EXISTS (
  SELECT 1 FROM current data c WHERE c.id = n.id
)

-- Present in current data but absent from new data → DELETE target
WHERE NOT EXISTS (
  SELECT 1 FROM new data n WHERE n.id = c.id
)
NOT IN vs NOT EXISTS:If a subquery result contains even one NULL, NOT IN can evaluate to FALSE or UNKNOWN for the whole predicate and produce unexpected results. In practice, NOT EXISTS is the safer choice.
Problem

Compare products_current (the current database data) with products_new (the latest data fetched from an external system), then write a differential update batch that (1) INSERTs new products, (2) DELETEs products that are no longer needed, and (3) UPDATEs products whose values changed.

Tables used
▸ products_current (current DB)
product_idnameprice
P01Coffee300
P02Tea250
P03Green tea200
▸ products_new (external source)
product_idnameprice
P01Coffee350
P02Tea250
P04Roasted tea220
Expected Output
product_idnameprice
P01Coffee350
P02Tea250
P04Roasted tea220
QUESTION 9

EXPLAIN ANALYZE — Understand slow queries and improve them with indexes

EXPLAIN ANALYZEINDEXPerformanceTuning
Background

EXPLAIN ANALYZE outputs a query's execution plan together with its actual execution time and row counts. It is a basic tool for identifying why a query is slow, such as an unused index or a scan of too many rows.

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 'U01';

-- Typical output:
-- Seq Scan on orders (cost=0.00..12.50 rows=1 width=50)
--              ↑ Full-table scan (index not used)
-- Index Scan using idx_orders_user_id on orders
--              ↑ Index used (fast)
Output keywordMeaningAction
Seq ScanScan every row (index not used)Create an index on the search column
Index ScanLocate rows using an indexGood state
Hash JoinJoin using a hash (suitable for large tables)An index on the join key can sometimes change it to a Nested Loop
Estimated vs actual rowsAccuracy of statisticsRun ANALYZE to update table statistics when the values differ substantially
EXPLAIN ANALYZE actually executes the query. When using EXPLAIN ANALYZE for UPDATE or DELETE, run it inside a transaction and then ROLLBACK.
Problem

Here are slow queries. Use EXPLAIN to identify the problems and create appropriate indexes. Also show a case where a composite index is effective.

Slow queries (large data set assumed)
▸ Problem queries
QueryProblem
SELECT * FROM orders WHERE user_id = 'U01'No INDEX on user_id → Seq Scan
SELECT * FROM orders WHERE user_id='U01' AND status='paid' ORDER BY ordered_at DESCMultiple conditions → composite index not used
SELECT * FROM products WHERE LOWER(name) = 'coffee'Function applied → index not used
Expected Output
QueryBeforeAfter
WHERE user_id = 'U01'Seq Scan (all rows)Index Scan (fast)
WHERE user_id AND status ORDER BY ordered_atSeq Scan or inefficient Index ScanIndex Scan (composite index fully used)
WHERE LOWER(name) = 'coffee'Seq Scan (function disables the index)Index Scan (functional index)
QUESTION 10

ROLLUP / CUBE — Generate a multidimensional aggregate report in one query

ROLLUP / CUBEGROUPING SETSMultidimensional aggregationReporting batch
Background

ROLLUP is a GROUP BY extension that automatically adds hierarchical subtotals and a grand total. CUBE generates aggregates for every combination. GROUPING SETS is the generalized form that lets you specify the combinations flexibly. These features consolidate multiple GROUP BY queries into one batch-report query.

GROUP BY ROLLUP(col1, col2)
-- Automatically generate col1+col2 detail, a col1 subtotal, and a grand total

GROUP BY CUBE(col1, col2)
-- Generate all 4 patterns: (col1,col2), (col1), (col2), ()

GROUP BY GROUPING SETS((col1, col2), (col1), ())
-- Enumerate any desired aggregate patterns (the generalization of ROLLUP/CUBE)
GROUPING() function:GROUPING(col1) returns 1 when the column is aggregated in a subtotal or total row. Use it to replace the NULL in a subtotal row with a label such as “Total.”
Problem

Using ROLLUP, retrieve total sales by year, month, and category from the sales table. Include yearly totals and a grand total (subtotal rows), and use the GROUPING() function to replace NULL with readable labels. Also show an all-combinations aggregation using CUBE on the same table.

Tables used
▸ sales
sale_idyearmonthcategoryamount
120241food3000
220241drink2000
320242food4000
420242drink1500
5202312food5000
6202312drink3000
Expected Output

ROLLUP (hierarchical aggregation: year → month → category):

year_labelmonth_labelcategory_labeltotal_amount
202312drink3000
202312food5000
202312 total8000
2023 total8000
20241drink2000
20241food3000
20241 total5000
20242drink1500
20242food4000
20242 total5500
2024 total10500
Grand total18500

CUBE (all year/category aggregation combinations):

yearcategorytotal_amountg_yearg_category
2023drink300000
2023food500000
2023NULL800001
2024drink350000
2024food700000
2024NULL1050001
NULLdrink650010
NULLfood1200010
NULLNULL1850011