背景
つい先日、自分の担当プロダクトがひとまずα版リリースしました。
これまでのキャリアでは既存システムの拡張などが多く、ビジネスとしては自分で0からDBのテーブル設計をする機会がなかったため、キャッキャしながら今回初めてすべての設計をしたのですが、現実は厳しい。結構な数の失敗をしてしまいました。
この記事では、そんな「失敗したな〜」と思った設計についてまとめて、供養にしようと思います。
失敗したテーブル定義
というわけで、よかれと思ってテーブル定義してみたらつらかったことたち。
ネット広告系の会社なので、広告のデータ構造のサンプル多めです。
過度なテーブル分割
広告テーブル
id | name | status | bid | url |
---|---|---|---|---|
1 | 広告1 | true | 100 | http://hoge.jp |
2 | 広告2 | true | 150 | http://fuga.jp |
3 | 広告3 | false | 100 | http://piyo.jp |
画像つき広告テーブル
id | multimedia_id | button_text |
---|---|---|
1 | 1 | もっと見る |
2 | 2 | 詳しくはこちら |
3 | 1 | もっと見る |
解説
DBの世界で悪名高き NULL
撲滅しようしようと頑張りすぎた結果、必要以上にテーブルを分割してしまいました。
この例では、広告と紐づく 画像つき広告
という概念を入れ、画像のない広告テーブルのレコードからNULLを撲滅しました。
撲滅できたところまではいいのですが、これ結局自分のシステムでは必ずJOINして使うことになりました😇
毎回JOINして性能は悪化するし、開発者にも面倒がられるし、いいことなしです。
結論
意味のある粒度以上の分割は、NULLが撲滅できても嬉しいことなかった
NOT NULL制約と処理順序
バッチ処理履歴テーブル
id | start_datetime(NOT NULL) | end_datetime | execution_id(NOT NULL) | status(NOT NULL) |
---|---|---|---|---|
1 | 2019/08/09 12:00 | 2019/08/09 12:05 | 01234567 | 完了 |
2 | 2019/08/09 13:00 | 2019/08/09 13:05 | 98765442 | 完了 |
3 | 2019/08/09 13:30 | 2019/08/09 13:35 | 03682634 | 完了 |
4 | 2019/08/09 14:00 | (NULL) | 93632837 | 実行中 |
解説
バッチ処理の実行履歴を格納するテーブルを作りました。
今回も可能な限りNULLを撲滅しようとして、NOT NULL制約をガンガンつけました。後悔はしていない。
ここで execution_id
というのは、具体的には AWS Step Functionsの 実行ARN を格納しており、実行中に中止という処理を受け付けるために確保しておきたいデータです。
さて、ここで問題が発生しました。
StepFunctionsのステートマシン内で実行するバッチの実装を進めていき、最後に、ステータスを 完了
に書き換えるバッチを実装して気づきました。ステートマシンの引数に、履歴のidが必要だと。
ということは→ステートマシン実行前に履歴レコードを生成しなくちゃいけない
ということは→履歴レコード生成時には execution_id
は決まらない
orz...
結論
制約をつけるとき、処理順序との兼ね合いには想いを馳せる必要あり。
JSONなら扱いやすい?の罠
予約設定テーブル
id | name | frequency(JSON) |
---|---|---|
1 | 予約設定1 | { "year":2019, "month":8, "day":2, "hour":11, "minute":0 } |
2 | 予約設定2 | { "days":[5,15,25], "hour":12, "minute":0 } |
解説
バッチ処理の実行はユーザによる 予約
という概念があり、
ここの frequency
にはユーザがUIから登録した頻度が登録されます。
頻度は 一回のみ
日次
週次
月次
などパターンがあり、パターンによって設定値が異なりますが、
素直にリレーショナルテーブルに適用すると、大量のNullableなカラムができて、ちょっと嫌だな、と。
というわけで、その辺り柔軟に使えるJSON型のカラムを作ってみました。
柔軟に使えたのはよかったのですが、賢明な読者のみなさまはお気づきかと思いますが、
こちら、最終的にはCron式に変換する形になります。
そうすると、
画面からの入力 -> Cron式
なら一回の変換で済むところ、RDBに入れたいというためだけに
画面からの入力 -> JSON -> Cron式
と、無駄に2回の変換をする必要が出てしまい、ロジック実装側が苦労する結果に。
果たしてJSON型のメリットを享受できたのか、という所に大いに疑問が残る設計でした。
JSON Schemaでの型チェックもDBMS側には無いので投入する箇所でやる必要があるし、
素直にNullableなカラムにすればよかったのかな…。
結論
JSON型カラムは取り扱い注意。
きちんとメリットが享受できるという確信のあるときだけ使うのがよさそう。
ここまで読んでお気づきですね?
そうです NULL
を撲滅したいという気持ちが強すぎました😇
各種の書籍で NULLヲ撲滅セヨ!
と声高に叫ばれており、その主張自体は大変同意できることなのですが、
とは言え、開発真っ只中でまだプログラムの仕様も確定していないうちは
なかなか正しくNullableかどうかの判断はできない、ということが分かりました。
こちらの書籍にまさしく書いてあった、 強過ぎる制約
ですね。
失敗から学ぶRDBの正しい歩き方 (Software Design plus)
ノウハウ的には、制約は開発が進みある程度プログラム挙動が決まってきてから、
後付けでセットするのが良さそうだな、と感じました。
失敗したテーブル定義作業
定義した内容そのもの以外に、チーム開発作業中のテーブル定義作業において失敗したことについてもせっかくなので書き残します。
テーブル変更を伴う作業の並行
テーブルの変更が必要な実装タスクが並行して走るとき、ありますよね?
これ、結構つらいコンフリクトにぶち当たります。
テーブル変更がコンフリクトするとどうなるか?
- マイグレーションスクリプトの順序がどっちが先かとか問題になったり
- 別作業のテーブル変更と内容的に衝突するとSQL開発のやり直しになったり
- ADD COLUMNの
AFTER
とか影響受ける
- ADD COLUMNの
- テストコードが大規模に見直し必要になったり
つらいこと目白押しですね。
ノウハウとしてよかったのは、作業前にまずマイグレーションスクリプトを速やかに実装、
影響箇所を最小限で修正して速やかにマージさせます。
その後、落ち着いてタスク本体の実装を開始します。
こうすれば衝突するのはマイグレーションスクリプトに限定され、コンフリ解消の量が最小限になります。
PRは小さくしましょうの原則が守られていればなんの問題もない、って話ですけど。
そもそも論
私のチームはアジャイル的な進め方をしているのですが、
とは言えテーブル設計はスピードを多少犠牲にしてでも設計を慎重にすべきだな、と感じています。
- 後から直すと影響範囲がデカい
- データが入ってからだとデータ移行もあってなおのことキツイ
- 油断するとマイグレーションスクリプトの数が爆発する