SQL バッチ処理 — LATERAL・JSONB・EXPLAINの応用

応用バッチ処理LATERAL / JSONB差分更新バッチEXPLAINROLLUPPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

LATERAL JOIN — 各カテゴリの売上TOP3商品を一括取得する

LATERAL JOINLIMITTOP N取得ランキングAPI
前提知識

LATERAL は、JOIN の右側のサブクエリが左側の行の列を参照できるようにする構文です。「左側の各行に対して、その行の値を条件にしたサブクエリを実行する」という相関サブクエリのJOIN版です。

SELECT *
FROM   categories c
JOIN LATERAL (                       -- 右側のサブクエリが左側の c.category_id を参照できる
  SELECT * FROM products p
  WHERE  p.category_id = c.category_id  -- ← 左側の c を参照(LATERAL の核心)
  ORDER BY p.revenue DESC
  LIMIT  3                            -- カテゴリごとに上位3件に絞る
) sub ON TRUE;                        -- ON TRUE: LATERAL は JOIN 条件をサブクエリ内で処理済み
「各グループのTOP N」はLATERALが最も直感的:ROW_NUMBER()でも実現できますが、LATERALは「カテゴリごとに上位3件」という意図がコードに直接現れるため可読性が高いです。
問題

categories テーブルと products テーブルを使い、各カテゴリの売上(revenue)上位3商品をカテゴリ名付きで取得してください。3商品に満たないカテゴリも全件含めること(LEFT JOIN LATERAL)。結果はカテゴリID・売上降順で並べてください。

使用テーブル
▸ categories
category_idname
1飲料
2食品
▸ products
product_idcategory_idnamerevenue
P011コーヒー50000
P021紅茶30000
P031緑茶45000
P04120000
P052パン25000
P062おにぎり18000
期待出力
category_idcategory_nameproduct_idproduct_namerevenuerank
1飲料P01コーヒー500001
1飲料P03緑茶450002
1飲料P02紅茶300003
2食品P05パン250001
2食品P06おにぎり180002
QUESTION 7

JSONB操作 — PostgreSQLのJSONBカラムを検索・集計・更新する

JSONB->> / @>JSON検索設定API
前提知識

PostgreSQL の JSONB 型は JSON データをバイナリ形式で格納し、インデックスを使った高速検索が可能です。フレキシブルなデータ構造(ユーザー設定・タグ・メタデータ等)をRDB内で扱うときに使います。

-- JSON値の取り出し(テキスト型 → キャスト可能)
col ->>  'key'            -- テキストとして取り出す(よく使う)
col ->   'key'            -- JSONB型のまま取り出す(ネスト参照に続けて使う)
col ->>  0               -- JSON配列の index 0 をテキストで取り出す

-- 包含チェック(GINインデックスと組み合わせると高速)
col @>  '{"key": "value"}'::jsonb   -- col が指定のキー/値を含むか

-- JSONB列の部分更新
jsonb_set(col, '{key}', '"new_value"'::jsonb)  -- 指定パスの値を新しい値に置き換える
text vs JSONB:JSON文字列をそのままTEXT型で保存するとSQLで中身を検索できません。JSONBを使うとキー・値を条件に絞り込めます。GINインデックスを貼ると @> 演算子が高速になります。
問題

user_settings テーブルの settings 列(JSONB型)に対して、①通知が有効(notifications = true)なユーザーを抽出するクエリと、②全ユーザーの theme 設定の集計(何人がdark/lightを使っているか)を取得してください。さらに③user_id=1 の language を 'en' → 'ja' に更新してください。

使用テーブル
▸ 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}
期待出力

期待する出力①:

user_idnotifications
1true
3true
4true

期待する出力②:

themeuser_count
dark2
light2

期待する出力③:

user_idsettings(更新後)
1{"theme":"dark","language":"ja","notifications":true}
QUESTION 8

差分更新バッチ — サブクエリで変更分類して INSERT / UPDATE / DELETE を一括実行

DML連鎖NOT EXISTS差分更新データ同期バッチ
前提知識

外部システムとのデータ同期バッチでは、「全件DELETEしてINSERT」よりも差分のみを更新する方が安全・高速です。サブクエリ(NOT EXISTS等)を使って「追加すべき行・更新すべき行・削除すべき行」を分類し、1トランザクションで実行するパターンを学びます。

-- 新データにあって現データにない → INSERT対象
WHERE NOT EXISTS (
  SELECT 1 FROM 現データ c WHERE c.id = n.id
)

-- 現データにあって新データにない → DELETE対象
WHERE NOT EXISTS (
  SELECT 1 FROM 新データ n WHERE n.id = c.id
)
NOT IN vs NOT EXISTS:NOT IN はサブクエリの結果に NULL が1つでも含まれると全体が FALSE(または UNKNOWN)になり、意図しない結果を生む罠があります。実務では NOT EXISTS を使うのが安全です。
問題

products_current(現在のDB内データ)と products_new(外部システムから取得した最新データ)を比較して、①新規追加すべき商品をINSERTし、②不要になった商品をDELETEし、③変更のあった商品をUPDATEする差分更新バッチを書いてください。

使用テーブル
▸ products_current(現在DB)
product_idnameprice
P01コーヒー300
P02紅茶250
P03緑茶200
▸ products_new(外部取得)
product_idnameprice
P01コーヒー350
P02紅茶250
P04ほうじ茶220
期待出力
product_idnameprice
P01コーヒー350
P02紅茶250
P04ほうじ茶220
QUESTION 9

EXPLAIN ANALYZE — スロークエリを読み解きインデックスで改善する

EXPLAIN ANALYZEINDEXパフォーマンスチューニング
前提知識

EXPLAIN ANALYZE はクエリの実行計画と実際の実行時間・行数を出力するコマンドです。「なぜ遅いのか」の原因(インデックス未使用・大量行スキャン等)を特定するための基本ツールです。

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

-- 代表的な出力:
-- Seq Scan on orders (cost=0.00..12.50 rows=1 width=50)
--              ↑全件スキャン(インデックス未使用)
-- Index Scan using idx_orders_user_id on orders
--              ↑インデックス使用(高速)
出力キーワード意味対処
Seq Scan全行スキャン(インデックス未使用)検索条件の列にインデックスを作成
Index Scanインデックスを使って行を特定良い状態
Hash Joinハッシュを使った JOIN(大テーブル向き)JOIN キーにインデックスを貼ると Nested Loop に変わることも
rows= 推定 vs 実際統計情報の精度大きくズレる場合は ANALYZE でテーブル統計を更新
EXPLAIN ANALYZE は実際にクエリを実行します。UPDATE/DELETEを EXPLAIN ANALYZE するときはトランザクション内で実行して ROLLBACK すること。
問題

下記の遅いクエリがあります。EXPLAIN で問題を特定し、適切なインデックスを作成してください。さらに複合インデックスが有効なケースも示してください。

遅いクエリ(大量データ想定)
▸ 問題のあるクエリ
クエリ問題
SELECT * FROM orders WHERE user_id = 'U01'user_id に INDEX なし → Seq Scan
SELECT * FROM orders WHERE user_id='U01' AND status='paid' ORDER BY ordered_at DESC複合条件 → 複合インデックス未使用
SELECT * FROM products WHERE LOWER(name) = 'coffee'関数適用 → インデックス未使用
期待出力
クエリ対処前対処後
WHERE user_id = 'U01'Seq Scan(全件)Index Scan(高速)
WHERE user_id AND status ORDER BY ordered_atSeq Scan または非効率なIndex ScanIndex Scan(複合インデックス完全活用)
WHERE LOWER(name) = 'coffee'Seq Scan(関数でインデックス無効化)Index Scan(関数インデックス)
QUESTION 10

ROLLUP / CUBE — 多次元集計レポートを1クエリで生成する

ROLLUP / CUBEGROUPING SETS多次元集計レポートバッチ
前提知識

ROLLUP は階層的な小計・合計を自動で付加するGROUP BYの拡張です。CUBE は全組み合わせの集計を生成します。GROUPING SETS はそれらを柔軟に指定できる一般化版です。バッチ集計レポートで複数の GROUP BY クエリを1本化できます。

GROUP BY ROLLUP(col1, col2)
-- col1+col2の集計、col1だけの小計、全体合計の3段階を自動生成

GROUP BY CUBE(col1, col2)
-- (col1,col2), (col1), (col2), () の全4パターンの集計を生成

GROUP BY GROUPING SETS((col1, col2), (col1), ())
-- 任意の集計パターンを列挙する(ROLLUP/CUBEの一般化版)
GROUPING() 関数:GROUPING(col1) は小計・合計行で列が集計されていると 1 を返します。これを使って小計行の NULL を '合計' 等の文字列に置き換えることができます。
問題

sales テーブルから、年・月・カテゴリ別の売上合計を ROLLUP を使って取得してください。年合計・全体合計(小計行)も含め、GROUPING() 関数で NULL を分かりやすいラベルに置き換えること。さらに同じテーブルで CUBE を使った全組み合わせ集計も示してください。

使用テーブル
▸ sales
sale_idyearmonthcategoryamount
120241food3000
220241drink2000
320242food4000
420242drink1500
5202312food5000
6202312drink3000
期待出力

期待する出力①:

year_labelmonth_labelcategory_labeltotal_amount
202312drink3000
202312food5000
202312月計8000
2023年計8000
20241drink2000
20241food3000
20241月計5000
20242drink1500
20242food4000
20242月計5500
2024年計10500
総計18500

期待する出力②:

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