02データベース5 min

最初に書いたデータスキーマは、たいてい一か月もちません

一日目のスキーマは、まだ理解していないドメインについての仮説です。そう認めると、書き方も、変えるときの痛みも変わります。

著者: Salva Castells

カバー画像: Sundaram Perumal, CC BY-SA 4.0.
この記事の内容

シフト表を生成するアプリで、二週間のあいだにスキーマを三回、互換性のない形で変えました。モデルが各作業者について行ったチェックの結果を保存するために worker_verifications というテーブルを作りました。五日後、それを丸ごと削除して、唯一役に立っていたカラム checked_rules を worker_week_rules の中に移しました。その二日後には、前回の作り直しで追加したばかりの extra_closed_days を weeks テーブルから消しました。

どれも新しいプロダクト上の決定によるものではありません。ドメインをよりよく理解したせいです。

一日目のスキーマと三十日目のスキーマ

削除したテーブルには八つのカラムがありました。ai_result、worker_hash、context_hash、verified_at、checked_rules、それにキー群です。狙いは検証のキャッシュでした。作業者のハッシュとコンテキストのハッシュが変わらなければ、モデルを呼び直さない。もっともらしく聞こえます。

実際に起きたのは、ドメインの本当の単位が「ひとつの検証」ではなく「ある週におけるある作業者のルール」だった、ということです。そしてその行はすでに存在していました。検証はその行の属性であって、エンティティではありません。それを直したマイグレーションは短く、読めばわかります。

ALTER TABLE "worker_week_rules" ADD COLUMN "checked_rules" text DEFAULT '{}' NOT NULL;

UPDATE "worker_week_rules" wwr
SET "checked_rules" = wv."checked_rules"
FROM "worker_verifications" wv
WHERE wwr."week_id" = wv."week_id"
  AND wwr."worker_id" = wv."worker_id"
  AND wv."checked_rules" != '{}';

DROP TABLE "worker_verifications";
ALTER TABLE "worker_week_rules" DROP COLUMN "created_at";

四つの文。コストは SQL ではありませんでした。

当たったしるしの変更と、外したしるしの変更

スキーマの変更には二種類あって、まるで似ていません。

  • カラムを足す。 weeks の checked_week_rules、calendars の calendar_rules。デフォルト付きの ALTER TABLE ADD COLUMN ... DEFAULT で、何も止めずにデプロイでき、古いコードもそのまま動きます。これはシステムが育っているということです。良いしるしです。
  • エンティティを別の場所に動かす。 テーブルを消す、カラム名を変える、あるデータがどこにぶら下がるかを変える。これは最初にドメインのモデリングを間違えたということであって、ドメインが変わったということではありません。

同じプロジェクトで preferred_day_off integer を preferred_days_off text に変えました。作業者が二日を希望できることが判明したからです。これも良いほうです。世界についての新しい情報であって、モデリングの誤りではありません。マイグレーションは三行に収まり、CAST でデータを保てます。

悪いほうは、差分がマイグレーションのフォルダに入っていないことで見分けがつきます。

本当のコストは SQL ではなく UI にある

AI を使ったドキュメンテーションのツールで、ユーザーがアップロードするものを最初は documents と呼んでいました。一か月後、正しい概念は contexts でした。エージェントが読み、引用し、書き直せるテキストの塊で、ファイルだけでなく URL からも来うるものです。名前が間違っていて、モデル全体を引きずっていました。

それを変えたコミットはスキーマに触っていません。触っているのはチャットのページと入力欄で、context-url-dialog、context-view、context-viewer を丸ごと削除しています。一度に 1,127 行の削除です。二秒の RENAME COLUMN と、コンポーネントをいじる丸一日。

実行はさらに安く、波及はさらに高くつく例もあります。同じプロジェクトで starCount を favoriteCount に変えました。SQL は二つのマイグレーションで五行。差分はスキーマと、データベースのアクションのファイル二つと、それを描画する UI に及びます。

そこから得た規則はこうです。カラム名を変えるコストは、その名前を文字どおり書いているコンポーネントの数に比例する。 マイグレーションの手間にではありません。

いま最初のスキーマをどう書くか

それ以来変えたことが四つあります。どれも「もっとうまく設計する」ではありません。

  • どこから来たかではなく、何であるかで名付ける。 documents は出どころ(ファイル)でした。contexts はそのデータがシステムの中で果たす役割です。後者の名前は出どころが変わっても耐えます。
  • 属性のためにテーブルを作らない。 新しいエンティティが別テーブルの各行に対して常にちょうど一行しか持たないなら、それはエンティティではありません。カラムのまとまりです。worker_verifications はまさにそれで、気づくのに五日かかりました。
  • カラムの型は最初から決める。 大きな作り直しのとき、created_at を text から timestamptz へ、week_start を text から date へ、テキストとして保存していた JSON を jsonb へ、まとめて変換しました。全部、初日に同じ手間で正しくできたはずのものです。あとから直すとなると各テーブルで ALTER COLUMN ... USING を打ち、日付を文字列として比較していたコードを全部見直すことになります。
  • UI にカラム名を繰り返さない。 スキーマから導いた型と、アクセス用の関数をいくつか。そうすれば RENAME COLUMN を grep ではなくコンパイラが捕まえてくれます。

立ち止まる価値がある場合

オフィスの間取りエディタで、DualDesk と QuadDesk という別々のコンポーネントを、席数を持つひとつの MultiDesk にまとめ、その後さらに無効化された席を扱えるようリファクタしました。二人用と四人用の机は二つのケースに見えますが、実際には同じものにパラメータが付いているだけでした。これは、2 と 4 が定数なのかデータなのかを十分だけ自問すれば初日に見えます。

使っている規則はこうです。二つのモデルの違いが数字なら、それはデータです。そこは先に考える価値があります。

それ以外については、間違っているとわかったうえでスキーマを書きます。最適化しているのは当てることではなく、来週それを直すのが短いマイグレーション一本で済み、コンポーネントをいじる一日にならないことです。