Day22 前半のゴール
「“1テーブルで全部持つ”から“賢く分ける”発想に切り替える」
Day22 のテーマは「パーティショニング」、つまりテーブル分割です。
ここまでずっと「テーブル設計」をやってきましたが、今回は「1つのテーブルをどう分割して扱うか」という話になります。
前半のゴールはこうです。
なぜ巨大テーブルをそのまま放置するとつらくなるのかをイメージで理解する。
「パーティション=テーブルをルールに従って分割した集合」という感覚を持つ。
日付やIDなどを使って「どこで分けるか」を考えられるようになる。
まずは、「そもそも何が苦しくてパーティショニングが必要になるのか」からいきます。
なぜテーブルを分割する必要が出てくるのか
「“とりあえず1テーブル”のまま成長すると、どこが痛くなるか」
イメージしやすい例として、「アクセスログ」や「イベントログ」を考えます。
例えば、こんなテーブルがあるとします。
CREATE TABLE access_logs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
path TEXT NOT NULL,
status INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
SQL最初は行数も少なくて、何の問題もありません。
しかし、サービスが成長していくと、1日で数十万行、数百万行と増えていきます。
1年経つと、何千万行、何億行という世界になります。
このとき、次のような痛みが出てきます。
最近30日分だけを集計したいのに、テーブル全体をスキャンしてしまう。
古いデータを消したいのに、DELETE が重くてロックも大きくなる。
インデックスが巨大になり、更新も検索も重くなる。
「1テーブルに全部詰め込む」設計は、最初は楽ですが、
長期運用・大規模データになると、パフォーマンスと運用の両方で苦しくなります。
そこで出てくるのが「パーティショニング」という考え方です。
パーティショニングのざっくりイメージ
「1つの論理テーブルを、複数の物理テーブルに分けて扱う」
パーティショニングを一言で言うと、
「1つのテーブルを、あるルールに従って複数の“かたまり”に分けること」
です。
例えば、access_logs を「月ごと」に分けるイメージを持ってみましょう。
2024年1月分のログは access_logs_2024_01
2024年2月分のログは access_logs_2024_02
…
でも、アプリ側から見ると、論理的には「access_logs」という1つのテーブルとして扱いたい。
SQLを書くときに、毎回テーブル名を月ごとに変えたくはないですよね。
PostgreSQL のパーティショニングは、
アプリからは1つのテーブル(親テーブル)に見える
内部的には、複数の子テーブル(パーティション)に分かれて保存される
という形で、このギャップを埋めてくれます。
例題:日付でログテーブルを分割するイメージ
「“最近30日だけ見たい”クエリを軽くする」
さっきの access_logs を例に、日付で分割するイメージをもう少し具体的にします。
やりたいことはこうです。
created_at の月ごとにテーブルを分ける。
最近30日分のクエリは、関係するパーティションだけを読む。
古い月のデータは、テーブルごとまとめて消せるようにする。
例えば、こんなクエリがよく走るとします。
SELECT
date_trunc('day', created_at) AS day,
COUNT(*) AS access_count
FROM access_logs
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY day
ORDER BY day;
SQLパーティショニングしていない場合、
このクエリは access_logs 全体を対象にします。
テーブルが1億行あれば、1億行分のインデックスや行を見に行くことになります。
パーティショニングして、「2024年1月〜12月」などに分けておけば、
WHERE 条件に合うパーティションだけを読む
(例えば、2024年5月と6月だけ)
という動きが可能になります。
これが「パーティションプルーニング」と呼ばれる動きで、
「関係ないパーティションは最初から無視する」ことで、
クエリのコストを大きく下げられます。
パーティショニングの“分け方”の軸
「日付・ID・範囲・リスト」
PostgreSQL では、いくつかの分け方(パーティションキー)が使えます。
初心者向けに、まずは2つだけ押さえておけば十分です。
日付(範囲)で分ける
IDやカテゴリ(リスト)で分ける
日付(範囲)で分ける
ログ・履歴・注文など、「時間とともに増えていく」データに向いています。
created_at の月ごと・年ごとに分ける。
注文日時 ordered_at の月ごとに分ける。
メリットは、
最近のデータだけを読むクエリが速くなる。
古いデータを「パーティションごと DROP」できるので、削除が軽い。
IDやカテゴリ(リスト)で分ける
テナントID(顧客ごとのID)や、
国コード・サービス種別などで分けるパターンです。
tenant_id ごとにパーティションを分ける。
country_code ごとに分ける。
メリットは、
特定テナントだけのクエリが速くなる。
テナントごとにデータを分離できるので、セキュリティ・運用上のメリットがある。
Day22 前半では、「日付で分ける」「IDで分ける」という2つの軸をイメージできれば十分です。
パーティショニングの“おいしいところ”
「速さだけじゃなく、運用とセキュリティにも効く」
パーティショニングは「速くするためのテクニック」と思われがちですが、
実はそれ以上に「運用」と「セキュリティ」に効きます。
運用面のメリット
古いデータをパーティションごと DROP できる。
→ 巨大な DELETE を打たなくて済む。ロックも小さく、時間も短い。
バックアップ・リストアをパーティション単位で考えられる。
→ 最近のデータだけ頻繁にバックアップする、などの戦略が取りやすい。
セキュリティ面のメリット
テナントごとにパーティションを分ければ、
「このテナントのデータだけを物理的に切り離す」といった運用がしやすくなる。
例えば、「ある企業からの要望で、自社データだけ別サーバに移したい」といったとき、
テナントIDごとのパーティションになっていれば、
そのパーティションだけを移動する、という選択肢が見えてきます。
Day22 前半では、「パーティショニング=速さ+運用+セキュリティのためのテーブル分割」というイメージを持ってほしいです。
「最初からやるか」「後からやるか」という現実的な悩み
「小さいうちは1テーブル、大きくなったら分ける、でも…」
最後に、実務で必ず出てくる悩みも触れておきます。
「最初からパーティショニングしておくべきか?
それとも、テーブルが大きくなってから分けるべきか?」
正直に言うと、答えは「ケースバイケース」です。
ただ、考え方の軸はこうです。
明らかにログ・履歴系で、将来巨大になるのが分かっているなら、
最初からパーティショニング前提で設計しておく価値が高い。
そうでないなら、まずはシンプルに1テーブルで始めて、
行数やクエリの重さを見ながら「そろそろ分けるか」を判断する。
ただし、「後からパーティショニングに移行する」のはそれなりに大仕事です。
既存データの移行、アプリ側の検証、バックアップ戦略の見直し…と、やることが増えます。
だからこそ、「このテーブルは将来どれくらいの行数になるか」「どんなクエリがどれくらい走るか」を、
設計の段階でざっくりでもいいのでイメージしておくことが大事です。
Day22 前半のまとめ
パーティショニングは、「1つの論理テーブルを、日付やIDなどの軸で複数の物理テーブル(パーティション)に分割し、関係するパーティションだけを読むことでクエリを軽くしつつ、古いデータをパーティション単位でまとめて削除・移動できるようにする」テーブル分割の仕組みであり、巨大なログ・履歴・注文テーブルなどで「最近30日だけ集計したい」「古いデータを定期的に消したい」といった要件に特に効く。
日付(範囲)やテナントID(リスト)で分けることで、パフォーマンスだけでなく運用(DROP・バックアップ)やセキュリティ(テナントごとの物理分離)にもメリットが出る――この「1テーブルで全部持つ世界」と「ルールに従って分割された世界」の違いをイメージできることが、Day22 前半の着地点になる。
