これはなに
- BiTemporal Data Modelはこういうことができるよ!という内容ではなく、導入時の注意点やちょっとしたつまづきポイントなどをまとめたもの
背景
BiTemporal Data Modelについて、すでに何社かでは導入事例もあるようで*1、たまに BiTemporal Data Modelに入門中 - だいたいよくわからないブログ を参照していただくこともあるようです。
そんな中でBiTemporal Data Modelについてこういうことができてすごいよ!という話だけでなく、導入するとこういうところが辛いとかそういうこともまとめておきたいなと思った次第です。(割とぱっと思いつくところをメモっている感じなので思い出したら追記するかも)
- DBはMySQLを想定しています。
- activated_at, deactivated_atをビジネス時間, in_z, out_zを処理時間のカラム名として利用しています。
- 説明のためactivated_at, deactivated_atが日付になったり日時になったりしています。(実際には日時で管理)
- 小ネタ集なので技術的にいくらでも回避できるだろうということも一応書いています。(ちょっとだけ考えることが色々ある。というのも注意点にはなると思うので)
- ネガキャンみたいな記事ですが、これさえ使えば完璧に幸せになれる!ってわけじゃないよ。というのが趣旨なのでBiTemporalを便利に使えるケースもいっぱいあると思います。自分の状況に合うと思ったら使い、合わないと思ったら別の手段を検討するのが良いと思います。
SQL where条件つけ忘れリスク
論理削除についての議論でもあると思うのですが、SELECT時の条件としてout_z=MAXを付け忘れている場合は当然削除されたはずのデータが処理対象になってしまいおかしなことが起こります。
さらにいうとout_z < MAXなデータって通常の本番運用ではあまりないといった使い方も結構ある(state遷移はactiavted_at,deactivated_atでやるのでout_zは障害対応でレコードを消す場合にしか使いません。など)ので、障害対応したら更に障害になった。。。ということも考えられます。
- out_z < MAXなデータをfixtureに含めたテストを行うことを検討してみると良いかもしれません
- レビュー時のチェックリスト・オペレーションでSQLを実行する前のチェックリストなどを作成するといったことをしてもいいかもしれません。
- 実装時のエンバグについてはreladomoなどの対応しているORMを利用することで回避することが可能だと思います
- リスク・コストとリターンを照らし合わせて履歴・訂正テーブルなどを作り込むことも検討しましょう。
学習コスト
初見時はやっぱり難しいです。勉強用リンク集を整備したり、練習問題つきのサンプルリポジトリがあると新規加入メンバーがキャッチアップしやすいかもしれません。
大きなシステムで大々的に使う場合は良いのですが、小さくあまりいじらないシステムで開発者が転職...といったことになると後任の運用担当者が「なにこれ・・・」となる可能性も否めません。 関わる人が幸せになるように、運用のハードルが低いシステムにするためにbitemporalを採用しないというのも選択肢としては考慮しましょう。
ユニークキーが効かない(効かせにくい)
※ PostgreSQLなどだと範囲型がある ので試したことはないんですが困らないんじゃないかなと思います。
例えば以下のような商品ごとの設定値を管理するレコードがあったとします。
item_id | 設定値 | activated_at | deactivated_at | in_z | out_z |
---|---|---|---|---|---|
1 | true | 1990/1/1 | MAX | ... | MAX |
そして、この設定値を2021/1/1からfalseにしたいとします。 正しい手順としては以下のようになります。
item_id | 設定値 | activated_at | deactivated_at | in_z | out_z |
---|---|---|---|---|---|
1 | true | 1990/1/1 | 2021/1/1 | ... | MAX |
1 | false | 2021/1/1 | MAX | ... | MAX |
ここで誤って以下のように設定値=falseのレコードを1995/1/1から有効として登録してしまったとします。
item_id | 設定値 | activated_at | deactivated_at | in_z | out_z |
---|---|---|---|---|---|
1 | true | 1990/1/1 | 2021/1/1 | ... | MAX |
1 | false | 1995/1/1 | MAX | ... | MAX |
上記のテーブルをas_of_time = 2010/1/1 で引くとマスターデータが2件取得される現象が起こります。(あたりまえ)
この手のテーブルだと有効な設定値が一件であることをunique keyなどで保証することが多いと思いますが、activated_at, deactivated_atで管理している場合はせいぜい (item_id, deactivated_at) のunique keyを付けてdeactivated_at=MAXなレコードが2件ないことをチェックする程度の制約になりそうです。in_z, out_zに関してはout_z=MAXのレコードが唯一存在していればいいという割り切りで十分ですが、activated_at, deactivated_atの場合はそうも行かないので注意が必要です。任意のas_of_timeについてactivated_at, deactivated_atの区間に重複がないことをチェックできないタイプのRDBMSでは注意が必要です。*2
性能面のデメリット
そりゃそうだろって話なんですがstatusテーブルをbitemporal data modelで管理するといったことを考えると、status更新時には以下の二行の操作が必要です。
- 旧レコードで UPDATE ~~ SET deactivated = ...;
- 新レコードを INSERT ~~~;
単純にsnapshot modelで検索するのに比べて更新コストが高くなりますし、テーブルの件数も増えるのでselectの負荷もかかるかもしれません。パフォーマンス命!の場合はログに残すなどの方法も検討できるかもしれません。
activated_at == deactivate_atの場合の取り扱い
これは細かいことではあるんですが
- 旧レコードで activated_at ~ deactiavted_at = 2000/1/1 00:00:00 ~ 2009/1/1 10:18:15
- 新レコードで activated_at ~ deactiavted_at = 2009/1/1 10:18:15 ~ 2010/1/1 00:00:00
のようなレコードを想定した場合、as_of_time=2009/1/1 10:18:15 でデータを取得したときに旧レコードが取れるのか、新レコードが取れるのか、二件取れるのか?は当然ながら実行されるSQLに依存します。
- activated_at < ${as_of_time} AND ${as_of_time} <= deactivated_atなら旧レコードが取れる
- activated_at <= ${as_of_time} AND ${as_of_time} < deactivated_atなら新レコードが取れる
- activated_at <= ${as_of_time} AND ${as_of_time} <= deactivated_at だと二件取れる
どういうSQLにするにしろ、ある程度方針を決めておかないと気づいたらアプリケーションごとにバラバラになっていたというのも新しいメンバーにとっては認知コストの上昇につながるので注意しておくとよいかもしれません。
また2件取れて嬉しいことは少ないと思うんですが $as_of_time BETWEEN activated_at AND deactivated_at
のようなクエリにすると2件とれる状態になるのでご注意ください*3
同一時刻で二回アップデート
ちょっと特殊なユースケースでしか発生しないと思うのですが、ある種のバッチなどでstatusを 1 => 2 => 3 => 4と一気に遷移させつつ、履歴は全部残したいみたいなことが限定的な状況下ではあったりするかもしれません。*4
as_of_time=2021/1/1 15:15:15時点で上記のようなstate遷移を行うバッチをシンプルに実装して実行すると以下のようになるかもしれません。(もちろん実装による)
item_id | status | activated_at | deactivated_at | in_z | out_z |
---|---|---|---|---|---|
1 | 1 | 1990/1/1 00:00:00 | 2021/1/1 15:15:15 | ... | MAX |
1 | 2 | 2021/1/1 15:15:15 | 2021/1/1 15:15:15 | ... | MAX |
1 | 3 | 2021/1/1 15:15:15 | 2021/1/1 15:15:15 | ... | MAX |
1 | 4 | 2021/1/1 15:15:15 | MAX | ... | MAX |
ここで困るのは二点
- ユニークキーとして(item_id, deactivated_at)を付けている場合にduplicate entryになってしまう。
- as_of_time=2021/1/1 15:15:15時点で有効なレコードが(SQLの条件次第では)複数件存在してしまう。(このバッチではなく、他の処理のSQLに依存してしまうため調査が必要になるカモ)
これですごい困ったことはないんであれなんですが、as_of_timeをちょっとずつずらしながら変更するみたいなハックを試みても良いかもしれません(その処理が重要ならもう少し考えても良いかもしれませんが)
またactivated_at, deactivated_atを秒精度で保存していると高速に呼ばれるAPIでも踏むことがあるかもしれません。ミリ秒精度(もしくは更に細かく)での保存・管理を検討してみてもよいでしょう。
MAXの値がUTCに変換されてしまう
MAXの値としては9999-12-31 23:59:59などを利用するとことが多いと思うのですが、アプリケーションの実装次第では9999-12-31 14:59:59 などUTCに変換された値がDBに入ることがあります。 これはまあテストで気づくでしょって感じなんですが、実装次第ではこういう事が起きるので確認したほうが良いこととしては挙げておきます。