SQL JSON — 深いマージ・差分抽出・述語検索の応用

応用JSON深いマージjsonb_each削除演算子JSONPath述語包含演算子PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

深いマージ — ネストしたキーだけを安全に書き換える

jsonb_set|| 演算子親キーの欠損
前提知識

ネストした1つのキーだけを差し替える方法は2つあります。jsonb_set はパスで位置を指して値を置き換え、|| は同じ階層のキーを上書きします。|| の上書きはその階層だけで、値がオブジェクトなら中身を混ぜずに丸ごと置き換わります(浅いマージ)。

SELECT jsonb_set(json_col, '{parent_key,child_key}', 'false'::jsonb, true),
       json_col || '{"top_key": 1}'::jsonb
FROM   table_name;  -- 第4引数 true = 末端のキーが無ければ追加する
親が無ければ追加もされない:第4引数が効くのはパスの末端だけです。途中の parent_key そのものが無い行では、jsonb_set は何も変えずに元の値を返します。引数のいずれかが NULL のときは、結果全体が NULL になります。
問題

settings テーブルの prefs について、notify オブジェクトの中の emailfalse にした結果を求めてください。notify の他のキーと、最上位の他のキーはそのまま残します。notify を持たない行や prefs が NULL の行でも、emailfalsenotify を持つ結果にしてください。取得列は user_id, prefs、user_id 昇順で返してください。

使用テーブル
▸ settings
user_idprefs(jsonb)
1{"theme":"dark","notify":{"email":true,"push":true}}
2{"notify":{"push":true}}
3{"theme":"light"}
4NULL
期待出力
user_idprefs
1{"theme": "dark", "notify": {"push": true, "email": false}}
2{"notify": {"push": true, "email": false}}
3{"theme": "light", "notify": {"email": false}}
4{"notify": {"email": false}}
QUESTION 7

jsonb_each — 更新前後を突き合わせて変更キーを洗い出す

jsonb_eachFULL JOINIS DISTINCT FROM
前提知識

監査ログや変更履歴では「どのキーが変わったか」を行として出したくなります。前後のドキュメントを jsonb_each で縦へ開き、キーで FULL JOIN すると、変更・追加・削除が1つの表に並びます。片側にしか無いキーは反対側が NULL になるため、比較は <> ではなく IS DISTINCT FROM を使います。

SELECT COALESCE(b.key, a.key) AS key, b.value, a.value
FROM      jsonb_each(before_col) AS b(key, value)
FULL JOIN jsonb_each(after_col)  AS a(key, value) ON a.key = b.key
WHERE  b.value IS DISTINCT FROM a.value;  -- NULL 同士は「同じ」と扱う
<> では追加・削除が落ちる:キーが片側にしか無い行では比較の片側が NULL になり、b.value <> a.value は UNKNOWN になって WHERE を通りません。値の変わったキーだけが残り、追加と削除が黙って消えます。
問題

record_versions テーブルは、1行に更新前の before_doc と更新後の after_doc を持ちます。レコードごとに変化したキーを1行ずつ出してください。値が変わったキー、追加されたキー、削除されたキーがすべて対象です。追加されたキーの before_value と、削除されたキーの after_value は NULL とします。取得列は record_id, key, before_value, after_value、record_id 昇順・key 昇順で返してください。

使用テーブル
▸ record_versions
record_idbefore_doc(jsonb)after_doc(jsonb)
101{"name":"A","price":100,"tag":"x"}{"name":"A","price":120,"tag":"x"}
102{"name":"B","price":50}{"name":"B","price":50,"color":"red"}
103{"name":"C","stock":3}{"name":"C"}
104{"name":"D"}{"name":"D"}
期待出力
record_idkeybefore_valueafter_value
101price100120
102colorNULL"red"
103stock3NULL
QUESTION 8

削除演算子 — 公開用に不要なキーと要素を落とす

- 演算子#- 演算子パス指定
前提知識

JSON から一部を取り除く演算子は2つあります。- は最上位のキー(テキスト指定)または配列の要素(整数の添字)を落とし、#- はパス配列で指した1点を落としてネストの奥にも届きます。どちらも、指した先が存在しなければ何もせず、元の値をそのまま返します。

SELECT json_col - 'top_key'                 AS a,  -- 最上位のキーを落とす
       json_col #- '{parent_key,child_key}' AS b,  -- ネストの奥を落とす
       json_col #- '{arr_key,0}'            AS c   -- 配列の先頭要素を落とす
FROM   table_name;
- にテキストを渡すと対象で意味が変わる:相手がオブジェクトなら「そのキーを落とす」ですが、配列なら「その文字列と等しい要素をすべて落とす」になります。位置で落としたいときは、整数の添字か #- のパスを使います。
問題

documents テーブルの body から、社外へ出せない項目を落とした公開用JSONを作ってください。落とすのは、最上位の internal_memo キー、owner オブジェクトの中の email キー、tags 配列の先頭要素(内部ラベル)の3つです。該当が無い文書は、その部分を変えずに返します。取得列は doc_id, public_body、doc_id 昇順で返してください。

使用テーブル
▸ documents
doc_idbody(jsonb)
1{"title":"T1","internal_memo":"社内限定","owner":{"name":"A","email":"a@example.com"},"tags":["draft","sql","index"]}
2{"title":"T2","owner":{"name":"B"}}
3{"title":"T3","internal_memo":"m3","owner":{"name":"C","email":"c@example.com"},"tags":["draft","json"]}
4{"title":"T4","owner":{"name":"D","email":"d@example.com"},"tags":[]}
期待出力
doc_idpublic_body
1{"tags": ["sql", "index"], "owner": {"name": "A"}, "title": "T1"}
2{"owner": {"name": "B"}, "title": "T2"}
3{"tags": ["json"], "owner": {"name": "C"}, "title": "T3"}
4{"tags": [], "owner": {"name": "D"}, "title": "T4"}
QUESTION 9

@@ 述語 — 数値の範囲でJSONを絞り込む

@@ 演算子JSONPath型の不一致
前提知識

包含演算子 @> が判定できるのは「この値を含むか」までで、数値の大小は書けません。JSONPath の述語を評価する @@ なら、>=&& を含む条件をそのまま渡せます。@? がパス式に該当する要素の有無を返すのに対し、@@ は述語そのものの真偽を返します。

SELECT * FROM table_name
WHERE  json_col @@ '$.num_key >= 1000 && $.text_key == "value"';

CREATE INDEX ON table_name USING gin (json_col jsonb_path_ops);
-- jsonb_path_ops は @>・@?・@@ に効く
型が違うと真でも偽でもない:$.num_key >= 1000 は JSON の数値どうしの比較です。値が "1000" と文字列で入っている行では結果が未定義になり、WHERE を通りません。キーそのものが無い行は偽になります。
問題

events テーブルから、propsamount1000 以上、かつ channelweb のイベントを取得してください。取得列は event_id, props、event_id 昇順で返してください。

使用テーブル
▸ events
event_idprops(jsonb)
1{"channel":"web","amount":1500}
2{"channel":"app","amount":2000}
3{"channel":"web","amount":800}
4{"channel":"web","amount":"1200"}
5{"channel":"web"}
6{"channel":"web","amount":1000}
期待出力
event_idprops
1{"amount": 1500, "channel": "web"}
6{"amount": 1000, "channel": "web"}
QUESTION 10

配列の包含 — 複数のタグをすべて持つ行を選ぶ

@> 演算子?& 演算子重複と順序
前提知識

JSON配列に対する @> は「右辺の要素をすべて含むか」を判定します。集合としての判定なので、要素の順序も重複も結果に影響しません。文字列だけの配列なら ?&(すべてを持つ)・?|(どれかを持つ)でも同じことが書け、どちらもGINインデックスが効きます。

SELECT * FROM table_name
WHERE  json_col -> 'arr_key' @> '["a","b"]'::jsonb;
-- 文字列の配列なら ?& array['a','b'] でも同じ判定になる
スカラーとの包含は形が違う:json_col @> '{"arr_key":"a"}' は「arr_key の値が文字列 a と等しい」という意味なので、配列を持つ行には一致しません。配列の中身を見るなら、右辺も配列で書きます。
問題

articles テーブルの metatags 配列を持ちます。sqlpostgres両方持つ記事を取得してください。取得列は article_id, tags、article_id 昇順で返してください。tagsmeta の中の配列をそのまま返します。

使用テーブル
▸ articles
article_idmeta(jsonb)
1{"title":"インデックス設計","tags":["sql","postgres","index"]}
2{"title":"SQL入門","tags":["sql"]}
3{"title":"実行計画の読み方","tags":["postgres","sql"]}
4{"title":"チューニング事例","tags":["sql","sql","postgres"]}
5{"title":"下書き","tags":[]}
6{"title":"メモ"}
期待出力
article_idtags
1["sql", "postgres", "index"]
3["postgres", "sql"]
4["sql", "sql", "postgres"]