mizulba
DBスキーマ・データモデリングの設計判断
分類の下流目的を表す bool フラグは、軸の値集合が既にその目的を区別しているなら足さない
4日前
新しい分類軸(enum / lookup / タグ)を、ある下流目的(公開対象か、課金対象か、通知するか、サマリー素材か等)のために導入したとき、その目的の真偽を「軸とは別の boolean フラグ列」で持たせたくなることがある。だが軸の値集合そのものがその目的の区別と一致しているなら、フラグは情報を増やさない二重符号化になる。
判断基準
- 軸の値を「目的に該当する値の集合」と「しない値の集合」に分けたとき、その分割が軸の意味から自明(値を見れば判定できる)なら、別フラグは冗長。目的は軸の値の意味に内包させ、消費側(クエリ・プロンプト・分岐)はその値集合を直接参照する。
- フラグが非冗長になるのは「該当でも非該当でもない第3の値」を足したとき、または「同じ値でもレコードごとに目的が変わる」ときだけ。前者は値集合を増やす、後者はレコード属性として持つ、が素直。
- フラグを足す前に「この目的を実際に読む消費者は誰か。その消費者は値集合(型キー等)で判定できないのか」を確認する。消費者が値集合で判定しているならフラグは誰にも読まれない dormant 列になる。
落とし穴
- 「将来この目的でフィルタするかもしれない」で先にフラグを足すと、消費者ができるまでノイズと二重管理だけが残る。第一級化=下流が実際に filter/集計に使うこと。消費側が実在してから列を足す。
- 値集合と重複したフラグは、片方だけ更新されて不整合になる保守コストを生む。
検証
導入予定のフラグについて「この真偽は軸の値だけから決まるか」を問う。Yes なら列を作らず、消費側ロジックか軸の値の意味の文書化に一本化する。No(値だけでは決まらず実レコードに依存する)のときだけ列として持つ。
新しい分類軸を第一級にするなら、多ラベル・語彙進化・所有者を判定して表現を選ぶ
5日前
既存のタグ/カテゴリとは別の「分類軸」を新設して第一級データにしたくなったとき、いきなりスカラーな enum カラムにすると後で詰まる。先に軸の性質を判定して表現を選ぶ。
判定する性質と対応
- 多ラベルか: 1 レコードが複数値を同時に持ちうるなら、スカラー列は情報を捨て、分類器に恣意的な単一選択を強いてノイズを生む。→ 配列カラム(
text[]+ GIN)か多対多の join にする。単一値が確実なときだけスカラー列。 - 語彙が進化するか: 値の追加・改名・削除が将来起きるなら、ネイティブの DB enum 型は硬い(追加に制約、削除・改名が特に高コスト)。→
text+ CHECK 制約、または lookup テーブルにして語彙変更を安いマイグレに保つ。 - 誰が所有するか: システム固定の閉じた語彙(分析や生成が依存する)か、ユーザーが自由に増減・改名するタグか。両者を同じ仕組みに混ぜない。システム所有の軸をユーザー編集タグのテーブルに相乗りさせると、ユーザー操作で分析の前提が壊れる。
backfill と消費側
- 既存行に値が無い状態で NOT NULL + デフォルト値を入れると、本来別カテゴリのレコードが一律デフォルトに化け、その軸で取りたかった分析を汚す。→ nullable か明示的な
unclassifiedにし、go-forward(新規から付与)を基本にする。 - 第一級化=下流が filter/集計に使うということ。消費側(分析・表示)が実在してから列を足す。読む人がいない分類列は、ノイズだけ抱えた dormant フィールドになる。
- 自動分類は soft signal として扱い(hard なゲートにしない)、ユーザーが容易に訂正できるようにする。境界が曖昧な軸は定義と例を crisp にしないと信号が劣化する。
検証
新軸を入れる前に「多ラベルか/語彙は固定か/所有は誰か/消費側はあるか/既存行はどう扱うか」に答えられるか確認する。答えられないうちは、専用カラムでなくタグ的な軽い表現で検証し、固まってから第一級化する。
PostgreSQL の B-tree を TEXT カラムに張ると行サイズ上限(2704B)で長いレコードの INSERT が落ちる
6日前
PostgreSQL で TEXT/可変長カラム(タイトル+本文など)にプレーン B-tree インデックスを張ると、インデックス行が 1 行あたり約 2704 バイト(btree version 4 の上限) を超えた瞬間に INSERT/UPDATE が失敗する。
バイト基準なので、日本語など 3 バイト文字が多いと数百文字で、ASCII 中心でも約 2700 文字で頭打ちになり、「特定の長いレコードだけ書き込めない」症状になる。文字数では閾値が一定に見えない点が紛らわしい。
さらに無益なことが多い
この種の index は「キーワード検索の高速化」目的で張られがちだが、検索が col ILIKE '%word%'(先頭ワイルドカードの部分一致)なら B-tree は使われない。性能に寄与せず、行サイズ上限で書き込みを壊すだけの純粋な負債になりやすい。
判断基準・対処
- 部分一致(
ILIKE '%...%'/LIKE '%...%')には B-tree でなくpg_trgmの GIN インデックス(gin_trgm_ops)を使う。trigram GIN は行サイズ上限がなく、部分一致に実際に効く。 - 全文検索なら
to_tsvector+ GIN、意味検索なら pgvector など別系統。生 TEXT への B-tree は避ける。 - 長文を完全一致で引きたいだけなら、ハッシュ列(
md5(col)等)や先頭 N 文字の式インデックスにする。 - アプリ側バリデーションの最大長(例: 50000 文字)と index のバイト上限が乖離していると「検証は通るが INSERT で落ちる」窓ができる。index を持つなら両者を整合させる。
切り分け・検証
- 「特定の長いレコードだけ保存できない」症状では、アプリのエラー文言を鵜呑みにせず DB ログを見る(
SQLSTATE 54000/index row size ... exceeds btree ... maximum 2704が出る)。bulk 系ハンドラは内部 DB エラーを汎用 400(不正なリクエスト)にマスクしがちで、真因はログ側にしか出ないことがある。 - 再現は、対象 index がある状態で 2704 バイト超の値を INSERT し当該エラーを確認。index を trgm GIN に張り替えると通る。