🧩

アンチパターンを採用したあの日

に公開1

アンチパターンを採用したあの日

――データ量の爆発と、機能に閉じた設計の話

こんにちは、しがないエンジニアの k_y16 です。

「あ、それアンチパターンですよね」

——その一言に、静かに心が折れかけた。

でも、わかってるんです。
きれいなDB設計のほうが正しい。
正規化は美しいし、SQLの教科書にはきっと「その設計はやめよう」と書かれている。
それでも私は、アンチパターンを採用しました。

なぜかって?
きれいな設計にしたら、DBが死ぬ未来が見えたからです。
(正確には、DBもアプリもエンジニアも一緒に死ぬやつ。)

この記事は、そんな「理想よりも現実を取った日」の記録です。
自分への備忘録でもあり、「同じ地雷を踏む人が少しでも減りますように」という願いでもあります。
それでは、私がアンチパターンを選んだ理由を話しましょう。


毎月約20万件の金額管理という現実

最初に与えられた条件を聞いたとき、正直「うそでしょ?」と思いました。

対象のカタログは約8万件
それぞれに2つの広告枠があり、
さらに月ごとに金額を設定できるようにしたいという要件。
単純計算で、毎月約20万件の価格データが生まれる計算です。

「まぁ、それでも月20万ならいけるっしょ」と思うかもしれません。
でも問題は、これを正規化して履歴管理しようとした瞬間に始まります。


正規化の罠

もし真面目に設計すると、
「期間テーブル」と「金額テーブル」がきれいに分かれ、
期間idでリレーションを張ることになります。

きれい。完璧。理想的。

……なんですが、これを毎月更新していくと、
1年後には200万件を超えるレコードが出来上がるわけです。
(しかもその多くは、実は同じ金額。)

これ、アプリが扱うたびにJOINしたり履歴を見たりすると、
CPUもストレージも泣きます。
正規化の美しさと、DBの悲鳴がトレードオフになる瞬間でした。


“金額なんてそんなに変わらない”という現実

ここで役立ったのが、ドメイン知識でした。
実際の運用を考えると、
金額は「毎月変わる」どころか、3ヶ月〜半年は同じことの方が多い。
むしろ、頻繁に変わるケースのほうがレアです。

このドメインの前提を知っていたからこそ、
「だったら、変わったときだけ記録すればいい」と判断できた。
もしこの前提を知らなかったら、
私は“きれいな正規化”を選んで確実に自滅していたと思います。


更新があったときだけ行を増やす方式

私が採用したのは、「期間の履歴を完全に持たない」わけではありません。
正確には、金額が変わったときだけ新しい行を作る方式です。

CREATE TABLE ad_slot_prices (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  condition_id INT UNSIGNED NOT NULL,
  slot_no TINYINT UNSIGNED NOT NULL,
  applies_from_period_id INT UNSIGNED NOT NULL,
  applies_to_period_id INT UNSIGNED DEFAULT NULL, -- NULLなら“今も有効”
  slot_price INT UNSIGNED NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

つまり、fromto を持たせ、
価格変更があった瞬間に「toを埋めて新しい行を追加する」。
そうすることで、変化がない限りデータは増えない

結果、履歴を持ちながらも、データの膨張を最小限に抑えられます。


アンチパターンを選んだ理由

設計を詰めていく中で私の中にあったのは、
DB設計は理想と現実のはざまで生きている」という感覚でした。


1. データ量の爆発を止めたかった

正規化を徹底すれば構造はきれいです。
でも、それによってDB全体が重くなるなら意味がない。
特にこのDBは他機能と共有されており、
もし自分のテーブルが負荷を生んだら、関係のない機能まで巻き込む可能性がありました。

きれいさよりも、まずは他を壊さないことを最優先にしました。


2. データを“自分の機能の中に閉じる”

このテーブル群は完全に本機能専用のデータで、
他機能から直接参照されることはありません。

だからこそ、「ここだけで完結させる」設計にできます。
正規化を崩しても外への影響はゼロ。
つまり、臭いものには蓋をするけど、その臭いは自分の部屋の中に閉じ込めるやり方です。


3. ドメイン知識がなければ判断できなかった

この判断は、単なる「パフォーマンス調整」ではありません。
ドメインを理解していたからこそ、リスクを取れた設計でした。

「金額は頻繁に変わらない」
「他機能が参照しない」
「履歴はあくまで内部的な利用」

こうした前提を正しく掴んでいなければ、
正規化を崩すことは恐ろしくてできなかったと思います。
結局、テーブル設計もドメインモデリングの一部なんですよね。


結果と学び(設計段階で見えてきたこと)

まだこの機能はリリース前ですが、
検証とシミュレーションの中でいくつか確信が持てました。


データ量の爆発を防ぐ構造になった

金額変更が発生しない限りレコードが増えないため、
想定データ量は従来案の数十分の一に。
DB全体への負荷波及を最小化できる見込みが立ちました。
設計段階でこの確信を持てたのは大きかったです。


蓋をすることで被害を最小化できた

このテーブル群は他機能から参照されません。
つまり、どれだけ癖のある設計でも、外には影響しない
アンチパターンを採用するにしても、
「閉じた範囲で完結させる」ことさえ守れば、
DB全体にとっては健全な構造です。


ドメインを理解していると、設計の“幅”が広がる

単にパフォーマンスを意識するだけではなく、
「このシステムで何が起きうるか」 を知っていることが重要です。
ドメイン知識があると、
「ここは妥協していい」「ここは正しく保つべき」という判断ができる。

正規化もアンチパターンも、文脈の上で選ぶ道具にすぎません。


まとめ:アンチパターンを採用したあの日

DB設計には、いつだって正しさと現実の間にグラデーションがあります。
今回の選択は、正しい設計をあえて裏切って、
他を壊さないための設計を選んだ例でした。

正規化を崩すことは勇気がいります。
でも、DBが共有基盤である以上、
「自分の領域で責任を持って完結させる」ことは立派な最適化です。

そしてもうひとつ大事なのは、
ドメインを理解していなければ、こういう判断はできないということ。
設計力はテーブル構造の知識だけでなく、
そのデータがどう動くかを理解しているかにかかっています。

“臭いものに蓋をした”と聞くとネガティブに聞こえますが、
その蓋は逃げではなく、防波堤でした。

アンチパターンとは、文脈を欠いたパターンのこと。
文脈があるなら、それはもう“ただの選択肢”だ。

スペースマーケット Engineer Blog

Discussion

Error401Error401

今回の場合、期間というのは独立したエンティティではなく、価格変更というイベントに従属したデータです。
「更新があったときだけ行を増やす方式」という設計そのものがアンチパターンと思っていらっしゃるのかどうか読み取れませんでしたが、このテーブルは正規化が崩れているわけではなく、良くとられる設計パターンです。
参考: スロー・チェンジ・ディメンション(Slowly Changing Dimensions)
https://zenn.dev/pei0804/articles/slowly-changing-dimensions