この記事は一休.com アドベントカレンダーの25日目の記事です。
レストラン事業部エンジニアのid:ninjinkunです。
一休.com及び一休.comレストランはユーザー向けのシステムだけではなく、店舗や一休内の管理者向けの業務システムという性格も持っています。
業務システム経験の無かった自分が一休に転職して最初に驚いたのが、DBに履歴を保持するための履歴テーブルが大量にあることでした。
そこから履歴テーブルの存在に興味と疑問を持ち、社内外のエンジニアと履歴テーブルについて議論してきました。このエントリではそれらの議論をまとめた結果について書いていきます。
履歴テーブルのパターン
まず以下の図をご覧ください。
込み入った図かつ事例が一休特化で恐縮ですが、左上の起点から始まって、右のオレンジの部分が最終的な実装パターンです。
図にあるとおり、たいていのユースケースでは以下の3パターンの実装に落とし込めるのではないかと考えています。
- バージョンテーブル
- ログテーブル
- ElasticSearchやBigQueryなどに放り込む
いきなり「バージョンテーブル」や「ログテーブル」などの単語が出てきましたが、これは自分が「履歴テーブル」を細分化するために使っている言葉です。以下でそれぞれについて説明します。
1. バージョンテーブル
アプリケーションから特定のバージョンを参照する必要があるので、以下の様にバージョン番号を保持するテーブルを作ります。
元データテーブル
価格が変わる商品の例です。IDと最新のバージョン番号を保持しています。
id | version | price |
---|---|---|
1 | 2 | 1,000 |
2 | 1 | 2,000 |
3 | 3 | 10,000 |
バージョンテーブル
今までの価格変動がバージョン毎にすべて記録されています。
id | item_id | version | price |
---|---|---|---|
1 | 1 | 1 | 1,100 |
2 | 2 | 1 | 2,000 |
3 | 3 | 1 | 11,000 |
4 | 3 | 2 | 10,500 |
5 | 1 | 2 | 1,000 |
6 | 3 | 3 | 10,000 |
2. ログテーブル
基本的にアプリケーションから参照しない前提であり、参照する場合も一覧で確認用に表示するくらいなので、バージョン番号などは不要です。
元データテーブル
id | price |
---|---|
1 | 1,000 |
2 | 2,000 |
3 | 10,000 |
ログテーブル
元データテーブルと同じカラム + ログ用の作成日を持つ。更新があったものをどんどん放り込んでいきます。最新かどうかは作成日で判断します。
id | item_id | price | log_created_at |
---|---|---|---|
1 | 1 | 1,100 | 2018-11-10 12:00:00 |
2 | 2 | 2,000 | 2018-11-10 12:01:00 |
3 | 3 | 11,000 | 2018-11-10 12:02:00 |
4 | 3 | 10,500 | 2018-11-15 12:00:00 |
5 | 1 | 1,000 | 2018-11-18 11:00:00 |
6 | 3 | 10,000 | 2018-20-10 13:00:00 |
3. ElasticSearchやBigQueryなどに放り込む
こちらのパターンについては一休では採用していないので詳細がないのですが、社外のエンジニアに履歴について相談した際に「うちではこうしているよ」という事例として聞いたものです。
リーズナブルな解決策だと思うので、どこかで採用したいと考えています。
テーブルの詳細
テーブル設計の詳細についても触れておきます。
別テーブル vs イミュータブル
自分が最初に履歴テーブルを見た時に感じたのは「履歴として独立したテーブルは必要なのか?」という疑問でした。履歴として独立したテーブルが無くても、同じテーブルに変更を全て残すように設計すれば、別テーブルはなくても履歴は作ることができます。自分はこれをイミュータブルパターンと呼んでいます。
これの議論については以下のエントリが参考になります。
自分が見ている範囲では、上記エントリの2のパターン、つまりイミュータブルでは無く、別テーブルに元テーブルと同じカラムを作ってを記録するのが最終的に一番穏当な落としどころになるケースが多いようです。
自分が考えるそれぞれのメリット、デメリットは以下の通りです。
メリット | デメリット | |
---|---|---|
イミュータブル | テーブルが1つで済んで綺麗に見える 書き込みが一回で済む |
select する際にmax()で最新の行を絞り込む必要がある(activeフラグなどで解決は可能だが、テーブルロックを掛ける必要が発生すると思われる) |
別テーブル | 見た目で履歴であることがわかりやすい | 2つのテーブルに同時に書き込む必要がある |
イミュータブルパターンにもメリットはあるのですが、結局わかりやすさと実装のしやすさから別テーブルパターンが採用されることが多いようです。
データベーストリガー vs アプリケーション側でコピー
別テーブルとして設計する場合、データをコピーする実装にはデータベーストリガーとアプリケーションコード(要トランザクション)の2つの選択肢があります。メリットとデメリットは以下の通りです。
メリット | デメリット | |
---|---|---|
トリガー | DB側でコピーが自動的に走るために漏れが無い | アプリケーションとトリガーに実装が分かれてしまうので後から動きが追いづらい トリガのデプロイを先にしないとエラーが発生するので、デプロイ時に気を遣う必要がある |
コード | データの管理が全てコードに集中するので、動きが追いやすい テストが書きやすい |
トランザクションを張り忘れると大変 |
バージョンテーブルのパターンでは、バージョンテーブルはアプリケーションから参照されるので、アプリケーションに属する機能だと考えられます。この場合はアプリケーションコードで実装する方が自然だと思います。
一方でログテーブルのパターンでは、アプリケーションの機能として捉えるか、ただのログとして捉えるかは解釈が分かれるところです。実装方法もトリガーとコード、どちらも選択肢に入ると思います。自分はトリガはDB側の設定を見に行かないと挙動が把握できないので、極力アプリケーション側で実装したいと思っています。ただ、現場では後から証跡を残して欲しいと言われる事も多く、その場合に短時間で証跡テーブルを実装するにはトリガが選択されることも多いです。
おわりに
初めは違和感を感じていた履歴テーブルですが、これは時系列データのモデリングなのだと考えることで、今では素直に受け入れられるようになりました。
このエントリでお伝えしたいことのほとんどは最初の図に入っていますので、もし良ろしければもう一度ご覧ください。
ここで漏れているパターンも当然あると思いますので、ぜひ皆さんの考える最強の履歴テーブルについても教えていただけると嬉しいです。うちはイベントソーシングで全部解決しているよ!などの事例も知りたいです。