「ねえ、ちょっといいかな。テーブル設計の時、カラムの順番ってどうやって決めてる?」
ふと後輩のコードレビューをしていて思ったんだ。多くのエンジニアが、「なんとなくドキュメントの順番」とか「思いついた順」でカラムを並べている。機能的には全く問題ない。でもね、PostgreSQLのパフォーマンスを極限まで絞り出そうとするなら、実は「カラムの並び順」は非常に重要なんだ。
今日は、データモデリングの裏側にある「アライメント」と「パディング」の話をしよう。これを知っているだけで、数百万件、数億件のテーブルになった時のストレージ効率やメモリ効率が劇的に変わるんだ。
—
CPUは「きりのいい数字」が好き
まず、なぜカラムの順序でサイズが変わるのか。答えは「メモリのアライメント(境界調整)」にある。
CPUはメモリからデータを読み込むとき、1バイトずつちまちま読むよりも、4バイトや8バイトの塊で一気に読み込む方が圧倒的に速い。だから、PostgreSQLの内部でも、各データ型には「境界」というルールがあるんだ。
例えば、`BIGINT`(8バイト)は8バイト境界に配置される必要があるし、`INTEGER`(4バイト)は4バイト境界に配置される。もし、サイズの小さい型と大きい型を無造作に並べると、CPUが読み込みやすいように、その間に「パディング(詰め物)」という名の隙間が勝手に挿入されるんだよ。
これが「見えない無駄」の正体だ。
—
実際に実験してみよう
言葉だけじゃピンとこないよね。こんなテーブルを考えてみて。
— 最適化されていない、パディングだらけのテーブル
CREATE TABLE bad_table (
is_active BOOLEAN, — 1バイト
val_big BIGINT, — 8バイト
is_deleted BOOLEAN — 1バイト
);
このテーブル、合計で10バイトくらいに見えるよね? でも、実際にはこうなっているんだ。
1. `is_active` (1バイト)
2. パディング (7バイト) ← BIGINTを8バイト境界に合わせるための無駄!
3. `val_big` (8バイト)
4. `is_deleted` (1バイト)
5. パディング (7バイト) ← 次のレコードのために調整が必要
結果、なんと24バイトも消費している。本来なら10バイトで済むはずが、2倍以上の容量を食っているわけだ。これが1億行あったらどうなると思う? 数百メガバイト、あるいはギガバイト単位で無駄なストレージを使い、ディスクI/Oを圧迫する。キャッシュ効率も当然落ちるよね。
—
魔法のレシピ:「大きい順」に並べる
じゃあ、どうすればいいか。答えはシンプル。「大きい順に並べる」こと。これだけで劇的に変わる。
— 最適化されたテーブル
CREATE TABLE good_table (
val_big BIGINT, — 8バイト
is_active BOOLEAN, — 1バイト
is_deleted BOOLEAN — 1バイト
— ここで合計10バイト。残りの6バイトは構造の末尾に少しだけ入るけど、劇的に減る
);
こうすると、パディングが最小限に抑えられる。8バイトのBIGINTの後に1バイトのBOOLEANを置いても、まだ8バイトの枠内に余裕があるから、そこに詰め込めるんだ。
実務での鉄則はこれだ。
- 固定長でサイズの大きいもの(BIGINT, TIMESTAMP, UUIDなど)を上に持ってくる。
- 可変長型(TEXT, VARCHARなど)や、サイズの小さい型(BOOLEAN, SMALLINTなど)を下に集める。
—
でも、過剰最適化には注意してね
ここまで話しておいてなんだけど、一つだけ釘を刺させてくれ。
「じゃあ今すぐ全テーブルを書き直そう!」と意気込むのは待ってほしい。このテクニックは、「膨大なレコード数を抱えるテーブル」や「頻繁にフルスキャンが発生するテーブル」でこそ真価を発揮するんだ。
数千件程度のマスタテーブルでこれをやっても、得られるメリットは微々たるもの。それよりも、アプリケーションの可読性や、後から追加するカラムの管理しやすさを優先するべきだ。
エンジニアリングは常にトレードオフ。
「どこがボトルネックか?」を冷静に見極めて、ここぞという時にこの「アライメントの知識」を武器として抜く。それが、熟練したエンジニアの振る舞いだと思うよ。
もし今のプロジェクトで、「なんかこのテーブル、妙に肥大化してるな?」と感じたら、まずは `pg_column_size` 関数で調べてみて。そして、カラムの並び順を少し入れ替えるだけで、驚くほどスリムになる瞬間を体験してみてほしい。
それじゃ、今日はこの辺で。また何か面白い設計の悩みがあったら聞かせてくれよ!
コメント