コンテンツにスキップ

データ増加・保持設計の振り返り・解説

トピック入口と開始文へ戻る。Practice / Interviewでは、以下を先に解説せず回答を待つ。

解説図:Merge Appendの先頭候補を追う

このページの解説を理解し、疑問を深掘りしたいときは、次のプロンプトをコピーしてChatGPTに貼ってください。気になる見出しや図名があれば、末尾に追記できます。

ChatGPT開始文
https://github.com/MFQWKMR4/tech2026 の次のファイルを読み、
database-data-growth の振り返り・解説を一緒に読む復習セッション(learn)を始めてください。
共通ルール:
- CHATGPT.md
- AGENTS.md
- src/content/docs/career/index.md
このページと回答記録:
- src/content/docs/database/data-growth.mdx
- public/diagrams/database-data-growth/merge-heads.svg
- reviews/database-data-growth.yaml
- sessions/2026-09-07-database-data-growth-01.yaml
関連する図・実験資料:
- diagrams/database-data-growth/partition-query.json
- labs/database-data-growth.sql
reviewにこれより新しいsessionがあれば、そちらも確認してください。
最初に実際に参照できたファイルとcommitを短く示してください。
読めないファイルは読めないと伝え、内容を推測しないでください。
図のJSONは要素・矢印・処理順の根拠として読み、描画を見たとは扱わないでください。
まず、私が気になっている節や図があるか一つだけ聞いて、回答を待ってください。
指定がなければneeds_revisitに関係する解説から始めてください。
一節ずつ、説明 → 私の疑問 → 具体例や図による深掘り、の順で進めてください。
追加疑問が出たらそちらを優先し、一度に説明を進めすぎないでください。
区切りで自分の言葉で説明できるか一問ずつ確かめ、ヒント後の理解と自発的な説明を区別してください。
教材の例・Codexの検証結果を、私が実施した実績として扱わないでください。
終了時は .github/ISSUE_TEMPLATE/session-sync.md に従い、session_type: learn で
終了時は実際の回答を根拠に、CHATGPT.mdと既存テンプレートに従ってYAML+Session narrativeを含む [Session Sync] GitHub IssueをMFQWKMR4/tech2026に作成し、URLを返してください。作成できない場合は未作成と明示し、コピー可能なタイトルと本文を返してください。
参照した見出し・図のpathを残し、教材に戻したい説明をrepository_requestsに含めてください。

最終レビュー:2026-09-07 · 説明した(Explained)。ヒント後の理解と自力の説明を区別して記録しています。

次回、自力で確かめたいこと

  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 多数partitionを横断するPostgreSQL実行計画を読み、partition pruning、各partitionのIndex Scan、startup cost、Append系ノード、結果マージの流れを説明する。 根拠: Issue本文§11の内部処理を説明できない回答(ボトルネック条件の提示あり)。次回はヒントなしで再確認する。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 同じdata growth要件に対して、月partition・日partition・archive・検索専用projection/別テーブルを比較し、読み取り・期限切れ削除・書き込み整合性・運用コストのtrade-offを説明する。 根拠: Issue本文§12の具体設計を説明できない回答(候補提示あり)。次回はヒントなしで再確認する。

2026-09-07のセッション

partitioning導入前の確認から始め、直近30日検索・7年保持・期限切れ削除・partition粒度・user_idでの7年横断検索・cacheの整合性とhit率まで条件を一つずつ変えて検討した。partitioningについては当初の動的境界イメージから固定期間partitionへ理解を更新し、日付partitionを削除要件に適合させつつ、user_id横断検索の別対策を考える方向へ進んだ。cacheは更新頻度・stale許容度・実測hit率を条件に採否を変えた。多数partition横断時の実行計画、Index Scanの起動コスト、結果マージ、検索専用構造やarchiveの具体設計は未習得として残った。

Issue #3本文§1–12の回答要約。DB支配性→plan/index確認はヒントなし。固定partitionと境界削除は説明後。cache採否は演習上の更新・stale許容・hit率条件に応じて変更(直接ヒント不明箇所はunknown)。多数scanとarchive設計は候補提示後も説明できず。本人によるSQL実行は未確認。

問い・回答の流れをIssueで読む

回答で示せたこと(ヒントの有無を含む)
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] partitioningありきにせず、まずAPIレイテンシのうちDBクエリが支配的かをメトリクスで確認し、次に実行計画とindex利用を確認する方針を示した。ヒント: なし。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 固定期間partitionでは、partition単位で粗く対象を絞ってから条件評価するという理解に到達した。ヒント: 月単位固定partitionという追加条件あり。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 保持期限とpartition境界の関係を理解し、日単位partitionなら日単位削除をpartition単位で扱える一方、時刻単位の厳密な境界なら境界partition内の行単位処理が残ると説明した。ヒント: 月partitionを丸ごと捨てる説明あり。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 低頻度のuser_id全期間検索より、直近30日検索と期限切れ削除への適合を優先して日付partitionを維持する判断をした。ヒント: 複数partition横断時のDB側実行について補足あり。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] cacheでは、更新頻度が低い条件では長いTTL+明示的invalidationを選び、invalidation漏れリスクが増えると慎重になり、staleを1分許容できる条件ではTTL cacheを再採用した。 回答要約はIssue本文§8–9。ヒント: stale許容など演習上の追加条件あり、採否への直接ヒントはunknown。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] cache hit率が同じuser_idの再参照頻度に依存することに自分で気づき、演習上の追加条件として提示されたhit率5%では維持しない判断をした。 回答要約はIssue本文§9–10。ヒント: 再参照頻度への気づきは自発、5%は出題側の条件。
自力では説明しきれなかったこと
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 多数partitionを横断する実行計画の読み方、各Index Scanの起動コスト、結果マージの具体的な仕組みを説明できなかった。 回答:「DBが何をしているのかあまり分かっていない」(Issue本文§11)。ヒント: 多数scan起動とマージがボトルネックという演習上の追加条件あり。
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] archiveや検索専用の別構造について、具体的な設計選択肢を説明できなかった。 回答要約: 二重管理のtrade-offは述べたが、archiveの具体像は持てなかった(Issue本文§12)。ヒント: 粒度変更・検索専用構造・archive等の候補提示あり。
理由・設計を詰めたいこと
  • 2026-09-07 [2026-09-07-database-data-growth-01; Issue #3] 日単位partition数増加のtrade-offについて、partition数増加は認識したが、planner/metadata overhead、実行計画作成、partition管理などの具体的コストは根拠を持って説明できなかった。 回答要約:「構造のレイヤーが一つ増えるくらい」(Issue本文§5)。ヒント: この回答前の有無はunknown。
修正が必要な説明

記録された項目はありません。

以下の解説は、セッションで残った疑問を補う教材です。読んだことだけで学習記録の状態は変わりません。

Default — 分割の前にworkloadを確かめる

Section titled “Default — 分割の前にworkloadを確かめる”

まずAPIの待ち時間のうちDBが占める範囲を測り、遅いSQL、返す件数、頻度、実行計画を確認する。日付分割は「最近の範囲を読む」「期限切れをまとめて除く」用途の候補。user_idだけの全期間検索を同時に速くする保証はない。

以下の設計比較では演習上の追加条件として、履歴を7年保持し直近30日を頻繁に読み、期限切れを日次で処理すると置く。実在システムの観測ではない。7年の意味(暦年、日数、時刻・タイムゾーン)と削除猶予は別途確認する。

partitionの境界は、例えば2月なら [2026-02-01, 2026-03-01)。毎日「直近30日」の境界を作り直して全行を移す必要はない。将来分を追加し、期限を完全に過ぎた塊を外す。

pruningは、検索条件とpartition境界から不要なpartitionを除外すること。indexの有無とは別の判断で、残ったpartition内の検索方法をplannerが選ぶ。日付条件のない user_id = 42 だけでは、日付境界から除外できない。親のindexも全partitionを一回で引くglobal indexではなく、各子に実体がある。PostgreSQL 18: Partitioning

検証対象はPostgreSQL 18.6。2026-09-07にCodexが専用Docker DBで実行した教材検証で、学習者の実績ではない。SQLは labs/database-data-growth.sql。1〜3月の3 partitionに各10,000行を作り、各月にuser 42が1行ある。各子に (user_id, id) indexを持つ。全変更は最後にROLLBACKする。

リポジトリのターミナルで再実行できる。

ターミナルウィンドウ
npm run db:up
docker compose exec -T postgres psql -U learner -d tech2026 -v ON_ERROR_STOP=1 -f /labs/database-data-growth.sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events
WHERE user_id = 42
AND occurred_at >= DATE '2026-02-01'
AND occurred_at < DATE '2026-03-01';

実行結果の抜粋(付帯行を省略):

Index Scan using events_feb_user_id_id_idx on events_feb events
(cost=0.29..8.31 rows=1 width=16)
Index Cond: (user_id = 42)
Filter: ((occurred_at >= '2026-02-01'::date) AND (occurred_at < '2026-03-01'::date))

1月と3月は計画から外れ、2月のindexでuser 42を探す。Filterが残っていることと、partitionが除外されたことは両立する。この例は計画時pruning。パラメータによる実行時pruningでは Subplans Removed や各子の loops も確認する。never executedだけでは、pruningかLIMITによる未到達か断定しない。Partition Pruning

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events WHERE user_id = 42;
Append (cost=0.29..24.92 rows=3 width=16)
-> Index Scan using events_jan_user_id_id_idx on events_jan events_1
(cost=0.29..8.30 rows=1 width=16)
Index Cond: (user_id = 42)
-> Index Scan using events_feb_user_id_id_idx on events_feb events_2
(cost=0.29..8.30 rows=1 width=16)
Index Cond: (user_id = 42)
-> Index Scan using events_mar_user_id_id_idx on events_mar events_3
(cost=0.29..8.30 rows=1 width=16)
Index Cond: (user_id = 42)

各子のscanが1行ずつ返し、親のAppendが計3行を渡した。アプリが3本のSQLを送ったのではなく、一つのSQLの実行計画である。通常のAppendは子の結果を連結する。全行を一度ためて並べ替える処理ではなく、SQL上の順序保証もない。

同じSQLの末尾に ORDER BY id を付けた実行結果:

Merge Append (cost=0.88..24.97 rows=3 width=16)
Sort Key: events.id
-> Index Scan using events_jan_user_id_id_idx on events_jan events_1
(cost=0.29..8.30 rows=1 width=16)
-> Index Scan using events_feb_user_id_id_idx on events_feb events_2
(cost=0.29..8.30 rows=1 width=16)
-> Index Scan using events_mar_user_id_id_idx on events_mar events_3
(cost=0.29..8.30 rows=1 width=16)

user_idを等値に固定すると、今回の複合indexは子ごとのid順を供給できる。Multicolumn Indexes

Merge Appendは各子の先頭候補を比較し、次に返す最小の行を選び、その子から次の候補を補充する。候補管理にheapを使う。全結果を再sortする必要はないが、最初の行を決めるため複数の子から候補を取得する負担がある。今回、子のstartup costは各0.29、親は0.88だった。一般に別の分布・indexなら Sort → Append などにもなる。PostgreSQL 18のMerge Append実装

pruningとAppendの流れを図で開く

図はDB内の責務を分けた概念図。通信や実測時間を示さず、AとBは別々のSQLである。Cの順序付きマージは上記の説明で比較する。図の正本は diagrams/database-data-growth/partition-query.json

Merge Appendの「マージ」は何を比べるのか

Section titled “Merge Appendの「マージ」は何を比べるのか”

既存図はpruningとscan、結果をまとめる責務の関係を示す。ここではORDER BYがある場合に、次の1行を選ぶため何を保持するかを拡大する。

演習上の追加条件として、user 42の各子のid列が1月 [2, 9]、2月 [4, 8]、3月 [6, 10] と既に昇順で得られる例を置く。上のlabは各月1行なので、以下の6行は説明用に追加した値であり実測結果ではない。

最初は各月の先頭2 4 6から2を選ぶ。1月から9を補充すると先頭は9 4 6になり、次は4を選ぶ。

図を拡大する

一度2を選んでも、1月の9を続けて返すわけではない。先頭候補を補充して他の月と再比較する。これを続けると 2, 4, 6, 8, 9, 10 になる。通常のAppendによる連結には、この全体順序を作る役割はない。

子が増えるほど最初に比較する候補も増えるため、LIMITが小さくても多くの子へ到達し得る。逆に、日付条件でpruningできれば候補を供給する子自体を減らせる。図の選択はPostgreSQL 18 Merge Append実装に対応する概念説明で、メモリ配置や時間を再現したものではない(2026-09-09再確認)。

cost=0.29..8.30 の左は行を出し始めるまでの推定cost、右は最後まで取得する推定cost。単位はmsではない。親のcostに子のcostが含まれるので、全ノードを合計しない。rowsは出力行数の推定。

actual time=a..b は実行時の最初の行まで/完了までのmsで、loopsが複数なら1回あたりの平均。上位ノードの時間にも子の処理が含まれる。Planning TimeとExecution Timeを分け、推定と実測の行数差、実際に動いた子の数を読む。Using EXPLAIN

BUFFERSのshared hit/readはPostgreSQLのbuffer利用を示す。readをそのまま物理ストレージI/OとみなさずOS cacheも考慮する。EXPLAIN ANALYZEは実際にSQLを実行し、計測自体にも負担がある。EXPLAIN

Trade-off — 約2,500 partitionで何が増えるか

Section titled “Trade-off — 約2,500 partitionで何が増えるか”

7年の日partitionは約2,556個(暦の範囲で変動)、月なら約84個。3個の実験から2,500個の時間を比例計算してはいけない。

pruningできない検索では、多数の子の計画・メタデータを扱い、それぞれのindexを探索する可能性がある。空振りでも探索は必要。ただし「Index Scanの起動」は別DB接続やOSプロセスを毎回作る意味ではなく、EXPLAINのstartup costも初期化処理の実測値そのものではない。

観測したいのは、計画作成時間、実行したscan数、buffer利用、返却行数、Sortのディスク使用、APIの全体時間。親にMerge Appendがあるか、単なるAppendかで「結果マージ」の意味が変わる。2,500個あるという数だけでボトルネックを断定しない。多数partitionはplanningや各セッションのメモリ使用、作成・削除・統計管理にも影響する。Partitioning best practices

Exception — 読み取り経路を変える選択肢

Section titled “Exception — 読み取り経路を変える選択肢”

以下はこの演習への設計例であり、性能改善を検証済みの構成ではない。

候補 有効になりやすいworkload 保持・削除 代償
日partitionを維持 日単位期限と直近検索が中心、全期間検索は低頻度 期限を完全に過ぎた日をまとめて除く 全期間検索の子が多い、日次管理
月partitionへ粗くする 全期間検索もあり、境界の行DELETEを許容 境界月だけ行処理、完全に期限切れの月を除く 子の数は減るが一つが大きい。厳密な日次削除ではDELETE/VACUUMが残る
日partition+検索projection user単位の全期間一覧が高頻度で必要な列が少ない projectionにも期限削除を実装 書き込み・容量・整合性・再構築の負担
hot table+archive table 古い検索が少なく、DBからの検索は残したい 移管と最終削除を別工程にする 同じDBなら容量・backup負担は残る。全期間検索で両方を読む
hot table+別ストレージarchive 古いデータは非同期出力でよく、復元待ちを許容 manifestと期限処理、復元試験が必要 即時の全期間検索要件はそのまま満たせない

元テーブル+検索projectionの具体例

Section titled “元テーブル+検索projectionの具体例”

日付分割した events を正本にし、一覧用の user_event_search を通常テーブルとして持つ。演習上、event_idは全期間で一意とする(生成・保証方法は別途設計)。projectionに event_id, user_id, occurred_at, summary を保存し、(user_id, occurred_at DESC, event_id DESC) のindexでそのuserの最新ページを読む。正本から詳細を取る必要があれば、取得済みのoccurred_atとIDでpartitionを絞る。IDだけで全partitionを再検索しては負担が戻る。

同じDBなら正本とprojectionを同一transactionで更新する案が最小。片方が失敗したら両方をrollbackできるが、全更新経路でこの約束を守る必要がある。二重書き込み分の遅延、lockとdeadlockも増える。Transactions

非同期にする設計例では、正本変更と配送用outboxを同一transactionで保存し、workerがprojectionを更新する。再配送に備えイベントID・versionで冪等化し、古い更新が削除済み行を復活させないよう削除通知と順序を管理する。遅延許容を要件に置き、lag・失敗・欠落を監視する。これは独自projectionへの設計案で、PostgreSQLの標準機能が任意変換を自動提供するという意味ではない。

削除問題は消えない。 通常テーブルのprojectionには期限用indexとbatch DELETE、VACUUM・容量監視が必要になる。正本のpartitionをDROP/DETACHしてもprojectionの各行は自動では消えない。CDCを採用する場合も、PostgreSQL logical replicationはDDLを複製しないため、partition操作を行削除通知の代用にしない。Logical replication restrictions

初回作成・修復ではsnapshotと後続変更の境界を決め、backfill中の更新・削除を取りこぼさず追いつき、件数・キー・サンプルを照合して読取を切り替える。性能だけでなくこの再構築手順を維持できるかを判断する。

標準のmaterialized viewは結果を保存して読めるが、更新にはREFRESHが必要。継続的な行単位同期の代わりと決めつけず、許容鮮度とrefresh費用が合う集計などで比較する。Materialized Views

演習上「90日以内は即時検索、古い履歴は数分待ってよい」へ要件変更できるなら、古いpartitionをDETACHし、独立tableのまま保持するか、ファイルへ書き出す設計を考える。DETACHは所属を外すだけでデータの削除や別ストレージ転送ではない。ALTER TABLE

別ストレージに移す設計例では、期間・件数・checksum・schema version・保存先をmanifestに残し、読取検証後に元コピーを消す。古いuser検索はarchive用の検索基盤か、非同期抽出job経由にする。日付だけでファイル分割しても、user検索の全期間走査は自動では解決しない。

移行中の読取境界、遅延到着・過去更新、再試行による二重登録、復元時間、archive側の期限削除を定義する。古いデータも毎秒数十回・低遅延で返す要件が固定なら、単に低速ストレージへ置く案は適合しにくい。保存の必要性とオンライン検索の必要性を分けて確認する。

日次削除の成功だけでなく、未来partitionの用意、境界行の残存、DDL lock待ち、projection lagと削除漏れ、archiveの読取・復元を確認する。partitionのDROPや通常DETACHには親の強いlockが必要になり得る。DETACH ... CONCURRENTLYは制約付きなので、運用windowやdefault partitionの有無を含め公式仕様を確認する。ALTER TABLE

遅くなったら、同じSQL・パラメータ・返却件数で前後を比較し、まずplanningとexecutionのどちらが増えたかを分ける。scanを減らす、返す列・件数を減らす、順序をindexで供給する、検索経路を分ける、のどれが観測した原因に効くかを説明する。全件返却なら転送量も残る。

cacheも同じで、hit率だけでなく削減したDB時間・miss時負担・鮮度条件で判断する。TTL 50秒だけで「最大1分stale」を証明せず、読取元の遅延、取得中の更新競合、cache投入までの時間も含めた設計が必要。これは次回検討用の補足で、今回未出題の失敗シナリオを誤答として記録するものではない。

資料を閉じ、日付条件あり/なしの二つの計画を説明するところから一問ずつ始める。その後ORDER BYを追加し、必要な処理を説明する。難しければ図に戻り、別の期間・userで確認する。設計比較は「全期間検索が低頻度→高頻度」「古い検索は待てる→待てない」と条件を一つずつ変える。

補強元:2026-09-07-database-data-growth-01。一般化した質問:多数partitionのscanと結果連結はどう動くか/日付保持とuser検索をどう両立するか。一次資料確認日:2026-09-07、対象PostgreSQL 18、実験18.6。上記の説明・実験はCodexによる教材作成であり、学習者の再確認は未実施。

図表改善(2026-09-09):説明用SVGはSVG自体が正本。前提と判断は本文、状態・比較は図、通信順序・依存関係は既存図、実測は元の記録を参照する。教材編集を新しい学習実績にはしない。