
2026/07/01 22:05
監視しなくても済む DB パーティションの設計
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
本テキストは、単調増加のプライマリキー(
id)をパーティショニング戦略として使用することが、PostgreSQL および MySQL の急速に成長するテーブルにおいて、タイムスタンプベースのアプローチ(例:created_at)よりも優れていると論じています。静的な時間ベースの戦略とは異なり、それは深刻なパフォーマンス劣化を引き起こすことがあり、インデックスプローブが 1 から 36 に跳躍したり、高価なマルチカラムユニーク制約を強いるなどの例があります。一方、増加する ID は定数時間のルックアップを保証し、スキーマ設計を簡素化します。現在のツールはしばしば固定された時間境界を仮定しており、その結果、アプリケーションロジックがこれらの限界をクエリに漏洩させる傾向があります。これに対し、ID ベースのアプローチは、ライフサイクルサービスに対して時間整合的な境界を動的に導出することを可能にします(例:SELECT MAX(id))。これにより、キャッチオールパーティションが満杯になる前に前もって分割し、保持ウィンドウに基づいて古いデータを削除することができます。さらに、これは高コストな操作である ALTER TABLE ... REORGANIZE の必要性を排除し、ワークロードの不平衡を防ぎ、ダウンタイム調整を回避しながら、アプリケーションロジックに時間をハードコーディングすることなくスケーラブルなパフォーマンスを確保します。本文
データベース建築設計:パーティショニング戦略と運用管理
背景:成長によるボトルネック
サービス開始 6 ヶ月後、
p_future パーティションに 8 億行のデータが蓄積しています。これを修正する ALTER 操作はメンテナンスウィンドウが必要です。特に重要な課題は、アプリケーションコードへの 「パーティションキー」の漏洩を防ぐ適切な設計です。
TL;DR(要点)
- パーティションキー:
を使わない。プライマリキーを使用すること。created_at - 境界管理: 背景サービスで観測された成長に基づいて動的に行うこと。
- クエリ方針: 元々存在するキー(通常は ID)を使い続け、パーティションプルーニングを自動的に機能させる。
- 設計思想: パーティションカラムをアプリケーションコードに露出させない。
「Orders ダッシュボード」のパフォーマンス問題
問題の発覚と原因
パーティショニング導入後、オーダーダッシュボードの読み込みが大幅に遅くなりました。
- 疑点: 新しいインデックス戦略への問題。
- 真相:
で明らかになったパーティションプルーニングの失敗。EXPLAIN- クエリ:
SELECT * FROM orders WHERE id = 12345 - 実行計画:36 ページのパーティション (
,orders_p2025_01
) が全て読み込まれた。... - 原因:
条件にパーティションキー(日付)が含まれていないため。WHERE
- クエリ:
パターン化した問題と解決策
- 暫定対策: クエリに
を追加する。created_at >= 'YYYY-MM-DD'- 結果: 実行計画が 1 つのパーティションに絞り込まれる。
- 組織的な対応:
- マイグレーションスクリプトや監査ページでも同様のフィルタ追加が必要になる。
- 内部リンター規則として「日付フィルタなしの SELECT を検知」を導入。
- コードレビューで「パーティションフィルターを追加したか?」がチェック項目に。
結論: パーティションキーはストレージ設計の選択ではなく、全てのクエリに対する契約となります。これを忘れた場合はエラーにならず、ただ「遅いだけ」という状態になります。
パーティションキーの問題点
PostgreSQL および MySQL において、パーティションキーはテーブルの プライマリキーまたはユニーク制約の一部である必要があります。プライマリキーが単一(例:
id)の場合、データベースは一意性を保証できず全パーティションをスキャンせざるを得ません。
プライマリキーを変更することの影響
created_at でパーティション化する場合、PK を (id, created_at) に変更する必要があります。
-
影響 1: ID の一意性の喪失
- PK が
となるため、同じ(id, created_at)
でも異なる日付であれば重複として扱われます。id - アプリケーション側で単一
を保証することもスキーマ上はなくなります。id
制約を追加できず、一意性が事実上放棄されます。UNIQUE (id)
- PK が
-
影響 2: 実行計画の劣化(Const → Ref)
PK 定義 ルックアップタイプ 性能特性 PRIMARY KEY (id)const 定数時間。最適化器は正確に 1 行を把握し即座に停止。結合は「eq_ref」(最速)。 PRIMARY KEY (id, created_at)ref 左端のインデックスカラムによるプレフィックススキャン。複数の行が返る可能性があるため、「eq_ref」から「ref」へ退化。 -
影響 3: クエリ条件の必須化
のようなクエリでは、以前は 1 行の即座ヒットでしたが、現在は単なる範囲検索となります。WHERE id = 1- 元に戻すには
を追加する必要があり、これがパーティションプルーニングからの漏洩と本質的に同じ現象です。AND created_at = '...'
スキーマ変化比較
【パーティショニング前】
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 単一 ID で一意性を保証 customer_id BIGINT NOT NULL, total_cents INT NOT NULL, created_at DATETIME NOT NULL ); -- 結果: WHERE id = 1 は const ルックアップで即座に完了
【パーティション化後(日付基準)】
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT, -- 一意性を保証せず customer_id BIGINT NOT NULL, total_cents INT NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id, created_at) -- created_at が PK に強制 ) PARTITION BY RANGE (TO_DAYS(created_at)) ( PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')), ... );
パーティションプルーニングは要求しないと機能しない
パーティションプルーニングが機能するのは、クエリの
条件にパーティションキーが含まれている場合のみです。WHERE
- 機能する場合:
→ 対象の 1〜2 つのパーティションのみ読み取り。WHERE id = 12345 AND created_at >= '...' - 機能しない場合:
→ 全てのパーティション(例: 36 ページ)をスキャン。WHERE id = 12345
リスク: プルーニングが行われない場合、正しい結果が返されるだけです。例外も警告も発生せず、アプリケーションログに何も表示されません。「ただ遅いだけ」ですが、ダッシュボードのタイムアウトやスロークエリログのレビューを通じて問題が発覚します。
静的なパーティション境界は経年で機能しない
テーブル作成時にハードコードした境界は、半年後のトラフィック変化に対応できなくなります。
- 初期状態:
がキャッチオールとして想定内の成長を吸収。p_future - 6 ヶ月後: 成長加速により、
に 8 億行が蓄積され、実働の半分を占める事態に。p_future
-- 現状(問題発生) PARTITION p_future VALUES LESS THAN MAXVALUE -- 8 億行、増え続けている -- 手動再平衡の難しさ ALTER TABLE ... REORGANIZE PARTITION ... -- ・数百ギバイトのテーブルをロックする必要がある -- ・アプリケーションチームとの調整が必要 -- ・パフォーマンスインシデント発生後に修復すると莫大なコスト
より良いアプローチ:プライマリキーを活用した範囲パーティショニング
戦略:ID をパーティションキーとする
BIGINT AUTO_INCREMENT は単調増加するため、プライマリキーそのものが最適のパーティションキーとなります。
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, -- ★パーティションキー候補 customer_id BIGINT NOT NULL, total_cents INT NOT NULL, created_at DATETIME NOT NULL ) PARTITION BY RANGE (id) ( -- ★ID で範囲区切りの定義 PARTITION p0001 VALUES LESS THAN (100000000), PARTITION p0002 VALUES LESS THAN (200000000), ... PARTITION p_future VALUES LESS THAN MAXVALUE -- ★キャッチオール );
- メリット: アプリケーションコードを変更せずに、ID ベースのクエリが自動的にプルーニングされる。
- トレードオフ: 時間ベース(月単위)の境界定義は消失する。しかし、パーティショニングの主目的はインデックスサイズの管理と保守コストの低減であるため、これは致命的ではない。
範囲パーティション管理の自動化
範囲パーティショニングでは、アクティブなパーティションが満杯になる前に自動的に分割し、保留ポリシーに基づいて古いデータをドロップします。
自動化サービスの役割
- 棚卸し: カタログから現在のレイアウトを読み取る。
- サイズチェック: キャッチオールパーティション (
) が閾値を超えたか確認。p_future - 境界選択: 時間整合のパーティションへする場合、
で適切な境界を探す。SELECT MAX(id) WHERE created_at < X - 分割 (REORGANIZE): キャッチオールを新しい有界パーティションと、新しいキャッチオールに再定義する。
-- メタデータ変更のみ(高速) ALTER TABLE orders REORGANIZE PARTITION p_future INTO ( PARTITION p0037 VALUES LESS THAN (3700000000), PARTITION p_future VALUES LESS THAN MAXVALUE ); - 保留プルーニング: 保留ウィンドウを過ぎたパーティションを
で削除。DROP - 並行性ガード: DDL 実行時の競合防止と、失敗時のクリーンな回復(冪等性)を保証。
本番環境で難しい理由
中に書き込みをロックしない設計が必要。REORGANIZE- サービスクラッシュや同時マイグレーションへの耐性が求められる。
- ロジック自体は簡単だが、運用のブリケット部分(エラーハンドリング、監視)に多くの労力が要る。
キーに日付を持たせない時間に基づく境界
「時間ベースのパーティショニング」=
created_at を PK にするわけではありません。事後で境界を計算します。
- 手法:
で、日付境界に相当する ID 範囲を取得。SELECT MAX(id) FROM orders WHERE created_at < '2026-03-01' - 実装: その ID を基準として
を実行し、パーティション名は時系列(例:REORGANIZE
)を付けて管理。p2026_03
-- ID レンジで定義だが、論理的な時間境界と整合 ALTER TABLE orders REORGANIZE PARTITION p_future INTO ( PARTITION p2026_03 VALUES LESS THAN (3700842140), -- ※計算された ID 境界 PARTITION p_future VALUES LESS THAN MAXVALUE -- 新しいキャッチオール );
- 結果:
- パーティション名や保留ポリシーは時間ベース。
- アプリケーションコードには
を含めず、プルーニングも ID ベースで行うため透明性が高い。created_at
他のパーティショニング戦略への適用
ハッシュパーティショニング
- 自動化の目的: 境界調整ではなく、スーク(ホットスポット)の検出。
- 手法: パーティションごとの成長率を追跡し、低基数カラムによる不均衡を早期に表面化させる。
- 注意点: パーティション数を増やす操作はテーブル全体の再構築を伴うため、自動化で境界調整を行うのは不適切。適宜人間が判断して形状変更(
など)を行う。OPTIMIZE
リストパーティショニング
- 手法: デフォルトのキャッチオールパーティション内に新値(例: 新しいテナント)が増加した場合、独自パーティションへ「プロモート」する。
ALTER TABLE events REORGANIZE PARTITION p_default INTO ( PARTITION p_tenant_108 VALUES IN (108), PARTITION p_default VALUES IN (DEFAULT) );
結論:パーティションキーに入れるべきこと
どの戦略を採用する前に、**「全てのクエリで既に使われているカラムは何ですか?」**を問うべきです。
- OLTP システム: 答えは通常プライマリキー (ID) です。これを使えば無料のプルーニングが可能。
- マルチテナント:
を PK に含める。tenant_id - 時系列データ: 全てのクエリが日付でフィルタされる場合のみ
を PK にする(例:TimescaleDB)。created_at
警告: 既に使われているカラムではないカラム(例:
)を無理に PK にしてパーティショニングすると、アプリケーションコード全体にcreated_atというフィルタを追加を強いることになり、オーバーヘッドが発生します。 既存のクエリに追加するよりも、小さい DDL サービスによる動的な管理の方がコストは安価です。AND created_at >= ?