なんらかのプロパティの過去時点での値(履歴)を参照する必要があることは多い。しかし、こういった履歴そのものも、過去について新しい情報が得られると、それに応じた修正が必要になることがある(遡及的な変更)。そこで、「バイテンポラル履歴 (bitemporal history)」は時間を2次元として扱う。1つ目の次元「実履歴 (actual history)」は、情報が一切の誤りなく伝わるとしたときに本来あるべき履歴を記録し、2つ目の次元「記録履歴 (record history)」は、履歴に対する認識の変遷を記録する。

住所や給与額など、なんらかのプロパティが時間の経過に伴って変化する過程を考えるとき、変化は直線的に連なるものと見なすのが普通である。しかし、実際はもっと複雑に絡み合った構造になることが想像以上に多く、そのせいでコンピュータ上のデータがごちゃついてしまうことも少なくない。

この話は次のような簡単な例で説明できる。

  • 自社の給与計算業務に対応しているとする。2月25日に給与支払を実行し、従業員であるサリーには、月給6000ドルに基づく給与を支給する。
  • 3月15日に人事部からお詫びの連絡が入る。それによると、サリーは2月15日付で6500ドルに昇給していたという。

では、「2月25日時点でサリーの給与額はいくらだったか」という質問には、どう回答するのが正解だろうか。ある意味では、6500ドルという答えが正しいといえる。今は2月25日の正しい給与額がわかっているからだ。しかし、2月25日時点では6000ドルのつもりだったのを無視できないことも多い。実際、その日に給与を支払ったからだ。つまり、小切手を印刷してサリーに送り、サリーが換金したのだ。この支払い処理は全体として、その時点での給与額に基づいている。もし先の質問をしてきたのが税務当局だとしたら、どう回答するかの重要度は増す。

2つの次元

こういった時間経過に伴う給与額の複雑な変化をおおむね理解するには、「時間は2次元である」と考えればよい。この「2次元」が「bitemporal(2つの時間の)」という用語の由来になっている。1つ目の次元はサリーの給与額の実際の履歴だ。ここでは、給与支給日である毎月25日時点の額を取り出したものとする。

date salary
Jan 25 6000
Feb 25 6500
Mar 25 6500

2つ目の次元は「2月25日時点でサリーの給与額の履歴についてどう認識していたか」と考えることで生まれるものだ。人事部から2月25日に連絡を受けていなかったので、給与額は常に6000ドルだと思っていた。つまり、実際の履歴と、履歴についての記録の間にズレが生じている。先ほどの表に日付の列を新たに追加すると、このズレがわかる。

record date actual date salary
Jan 25 Jan 25 6000
Feb 25 Jan 25 6000
Mar 25 Jan 25 6000
Feb 25 Feb 25 6000
Mar 25 Feb 25 6500
Mar 25 Mar 25 6500

「実履歴 (actual history)」と「記録履歴 (record history)」という用語は、この2つの次元を表すために使っている。「actual」の代わりに「valid」(または「effective」)、「record」の代わりに「transaction」という用語が使われることもある1

この表の列は「3月25日時点では、2月25日のサリーの給与額は6500ドルだと思っていた」などと唱えながら読む。このように考えると、サリーの給与の実履歴を表していた最初の表は、正確には3月25日時点で認識(記録)した実履歴だと言える。

プログラミング的な観点だと、サリーの給与額を知りたい場合、履歴が存在しないならsally.salaryのようにして給与額を取得できる。履歴(実履歴)に対応するなら、sally.salaryAt('2021-02-25')とする必要がある。バイテンポラルな世界だと、さらに別のパラメータも必要になり、sally.salaryAt('2021-02-25', '2021-03-25')とする必要がある。

表以外の方法でバイテンポラル履歴を可視化するには、x軸を実時間、y軸を記録時間とした座標平面上にグラフを描く。ここで、グラフ上の領域の色を給与額の水準に応じて変えている(未来の値は記録しようとしていないので、グラフの形は三角形になっている2)。

このグラフから、毎月25日の給与支払による実履歴の変遷を示す表を作成できる。2月25日の給与はサリーが昇給していないタイミングで支払われ、3月25日の給与は、昇給が既知の状態で支払われていることがわかる。

遡及的な変更のさらなる変更

いま、人事部から別の連絡があったとしよう。

  • 4月5日: すみません、前回のメールに誤りがありました。サリーさんは2月15日付で昇給し、6400ドルになっていました。お手数をおかけして申し訳ありません。

こういった変更は理解がひどく難しくなるものだ。しかし、バイテンポラル履歴の観点で考えれば、それほど苦労せずに理解できる。新情報を加味したグラフはこうなる。

給与支払で利用する水平な線は、記録時間のある時点における実履歴を表す。4月25日時点の認識では、サリーの給与が2月25日に6000ドルから6400ドルに昇給している。その観点からだと、サリーの給与が6500ドルであるというデータは見えない。4月25日には事実ではなくなったからだ。

さらに次の図を見てみると、垂直な線は何を意味しているだろうか。

この線は、ある日付の給与額についての認識を表現している。線の上に示す表は2月25日分として記録してきた給与額の一覧になっていて、時間経過に応じた認識の変遷を表している。

バイテンポラル履歴の使用

バイテンポラル履歴は、遡及的な変更を扱うときに履歴を作る便利な方法である。だが、使われているところはそれほど目にしない。この手法について知っている人が多くないというのもあるが、使わずに済んでしまうことも多いからだ。

バイテンポラル履歴を使わずに済ませる方法として「遡及的な変更に対応しない」というものがある。仮に、保険会社が「いかなる変更も書面受け取り時点から有効になる」としているなら、そうやって実履歴を記録履歴と強制的に一致させて、バイテンポラル履歴を避けているのだ。

遡及的な変更が問題になるのは、業務上のアクションを遡及的に変更した過去の状態に基づいて実行する場合だ。例えば、最新版の給与水準に基づいて小切手を送付するようなケースがある。一方で、単に変更の履歴を記録するだけなら、履歴の遡及的は変更については気にしなくてもよい。なぜなら、実質的には記録履歴を無視して実履歴だけを記録することになるからだ。また、あとから変更できないアクションであっても、必要な入力データと合わせてアクションを記録するなら、実履歴を記録するだけでよい。例えば、サリーへの給与支払のケースでは、小切手を発行した時点での給与額を記録しておけば、監査目的には十分だ。そういった状況なら、給与額の実履歴だけで事足りる。記録履歴は給与明細内部に記録される形になる。

また、遡及的な変更を加えた後だけ業務上のアクションが発生する場合も、実履歴だけで十分だ。たとえば、サリーの給与額が変わることを2月24日に認識していれば、給与額のデータを事前に修正できたので、誤った金額に依拠した給与支払アクションを実行せずに済んだだろう。

バイテンポラル履歴を使わずに済むのであれば、ふつうはそれが好ましい。システムが相当に複雑化するからだ。だが、たいてい遡及的な更新が原因で起きる実履歴・記録履歴の不整合に対処する必要があるなら、バイテンポラル履歴の複雑さを受け入れる必要がある。その中でとりわけ難しいのは、バイテンポラル履歴のしくみを履歴の利用者に身につけてもらうことだ。過去のデータが変化するものだと思っている人はほとんどいない。ましてや、実履歴・記録履歴という2つの次元があるなどということについては言うまでもない。

追記専用の履歴

シンプルな世界であれば、履歴は追記専用で済む。通信が一切の誤りなく瞬時に完了するなら、あらゆる新しい情報は、その情報に関心のある関係者全員が即座に知ることになる。その場合、履歴は、世界で新しくイベントが発生するたびに情報を加えていくだけのものとして扱うことができる。

一方、バイテンポラル履歴は現実を受け入れる手段といえる。現実は、通信には誤りが含まれるし、瞬時に完了することもない。バイテンポラル履歴の実履歴は追記専用ではなくなり、過去にさかのぼって履歴を変更できる。しかし、記録履歴そのものは追記専用である。つまり、2月25日時点のサリーの給与額についての認識自体は変更せず、あとからわかったことを追記するだけである。追記専用の記録履歴を実履歴の上に重ねることで、実履歴を修正しつつ、その修正についての信頼できる履歴を作ることができる。

遡及的な変更による影響

バイテンポラル履歴は値の変遷を追跡可能にするしくみであり、sally.salaryAt(actualDate, recordDate)のように履歴を取得できるので、非常に役立つ可能性がある。しかし、遡及的な変更による影響は過去時点のデータを修正するだけにとどまらない。ある専門家は次のように述べている3

時間は原因から結果への直線だと考えられている。だが実際は、非線形的客観的視野では、もっと不定的で時間的なものの大きな塊なんだ

もしサリーに6400ドル支払うべきだったのに6000ドルしか払っていないなら、支給額を正す必要がある。少なくとも後の給与支払時に差額も支給することになるが、それ以外の影響が生じる可能性もある。たとえば、差額分が増えると、サリーがなにか重要な閾値を1か月早く超えているべきだったということになったり、課税されたりする可能性があるのだ。

バイテンポラル履歴だけでは、こういった副次的な影響があるかどうかはわからない。調べるには追加のしくみ一式が必要になるが、本パターンの対象外だ。調査手法の一つとしては、正しい給与額に基づく「あるべき世界の状態」を記録するパラレルモデルを作成して、金額補正用の変更を割り出すというものがある4。バイテンポラル履歴はこういった手法を実施するのに役立つが、それ単体では「大きな塊」の一部しか解き明かせない。

記録時間の「視点」

上述の記録時間についての例では、実履歴に対する認識の変遷を記録するために日付を使った。しかし、記録履歴を記録する方法はもっと複雑になりえる。

先ほどは、議論を追いやすくするために、給与支給日の履歴だけを抜き出していた。しかし、履歴をもっと適切に表現するには、日付の範囲を使うほうがよい。2021年の給与履歴を対象とした表がこちらだ。

record dates actual dates salary
Jan 1 - Mar 14 Jan 1 - Dec 31 6000
Mar 15 - Apr 4 Jan 1 - Feb 14 6000
Mar 15 - Apr 4 Feb 15 - Dec 31 6500
Apr 5 - Dec 31 Jan 1 - Feb 14 6000
Apr 5 - Dec 31 Feb 15 - Dec 31 6400

サリーの給与は、「実キー (actual key)」と「記録キー (record key)」という2つのキー(どちらも日付の範囲)を組み合わせて記録していると考えることができる。しかし、記録キーという概念は、場合によってはもっと複雑になる。

明らかに複雑になるのは、異なる主体がそれぞれの記録履歴を持つようなケースだ。サリーの件は明らかにそのケースに該当している。人事部(HR)から給与部(Payroll)への連絡に時間がかかっていたので、部門によって実履歴の修正についての記録時間に差異が生じる。

department record dates actual dates salary
HR Jan 1 - Feb 14 Jan 1 - Dec 31 6000
HR Feb 15 - Dec 31 Jan 1 - Feb 14 6000
HR Feb 15 - Dec 31 Feb 15 - Dec 31 6400
Payroll Jan 1 - Mar 14 Jan 1 - Dec 31 6000
Payroll Mar 15 - Apr 4 Jan 1 - Feb 14 6000
Payroll Mar 15 - Apr 4 Feb 15 - Dec 31 6500
Payroll Apr 5 - Dec 31 Jan 1 - Feb 14 6000
Payroll Apr 5 - Dec 31 Feb 15 - Dec 31 6400

履歴を記録するあらゆるモノには、情報を認識した時点を表す独自のタイムスタンプがある。そのタイムスタンプに基づいて、企業はある種のデータを記録する際の決定的な主体として、とある主体を選ぶことになるだろう。しかし、主体は権限系統を横断することになるが、どれだけ企業が大きくても、取引相手の税務当局による記録日付まで変えることはない。異なる主体が同じ事実を別の時間に認識することで生じる問題の解決には、多大な労力が費やされている。

この場合に起きていることを一般化するには、「部門」と「記録日の範囲」を「視点 (perspective)」という1つの概念にまとめればよい。たとえば「2月25日時点の人事部の視点からすると、サリーの給与額は6400ドルだった」といった感じだ。表形式だと、こういった見せかたになるだろう。

perspective actual dates salary
HR, Jan 1 - Feb 14 Jan 1 - Dec 31 6000
HR, Feb 15 - Dec 31 Jan 1 - Feb 14 6000
HR, Feb 15 - Dec 31 Feb 15 - Dec 31 6400
Payroll, Jan 1 - Mar 14 Jan 1 - Dec 31 6000
Payroll, Mar 15 - Apr 4 Jan 1 - Feb 14 6000
Payroll, Mar 15 - Apr 4 Feb 15 - Dec 31 6500
Payroll, Apr 5 - Dec 31 Jan 1 - Feb 14 6000
Payroll, Apr 5 - Dec 31 Feb 15 - Dec 31 6400

視点という概念1つにまとめる利点には、他の視点がどうなるか考えられるようになることがある。一例として、既存の視点の代わりになるような視点を考えてみる。2月15日付のサリーの昇給のような個人の昇給をなくして、代わりに3月1日に全従業員が10%昇給するような視点が作れるだろう。こうすることで、サリーの給与額に対して新たな記録時間の次元が生じる。

perspective actual dates salary
real world Jan 1 - Feb 14 6000
real world Feb 15 - Dec 31 6400
with global raise Jan 1 - Feb 28 6000
with global raise Mar 1 - Dec 31 6600

このように記録時間の概念を一般化すると、複数の視点を実履歴の上に重ねることができる。つまり、遡及的な変更について考えるときと、代わりの履歴について考えるときとで、実質的に同じメカニズムが使えるようになる。

視点という次元を履歴上にたくさん重ねるのは、バイテンポラル履歴と比べても広範に役立つというわけではない。だが、この考えかたが役立つようなタイプの状況として、過去か未来において別案のシナリオを考える場合がある。その具体例は予算編成だ。独自の予想に基づいたさまざまな予算案を策定し、あとで実績値と比較するのである。

バイテンポラル履歴の保存と処理

データが履歴を持てるようにすると、複雑さは増す。バイテンポラルな世界では、サリーの給与額を参照するのに2つのパラメータが必要になる。つまりsally.salaryAt('2021-02-25', '2021-03-25')のような形になる。とはいえ、デフォルトの参照方法はシンプルにできる。記録時間のデフォルト値を今日だと見なせば、現時点を記録時間とすれば十分な処理では時間が2次元であることに起因する煩雑さを無視できるからだ。

しかし、参照方法が単純化できても、保存方法もそうできるとは限らない。バイテンポラルなデータが必要なクライアントがいるなら、なんとかして保存しておかなければならない。何段階かのテンポラル性に対応した機能を組み込みでサポートしているデータベースもあるが、割とニッチな製品ではある。また、長期間にわたって保存するデータとなれば、ニッチな技術に対して特別慎重になる傾向が賢明にも見られる。

この状況を踏まえると、独自のしくみを作り出すのが最善ということも少なくない。これには大きく分けて2つのアプローチがある。

1つ目の方法として、バイテンポラルなデータ構造がある。この方法では、必要な日付の情報を保存用のデータ構造に書き込む。保存用のデータ構造の中に日付の範囲を表すオブジェクトを入れ子にして保持するか、RDBのテーブル内に開始日・終了日のペアを保持すればよい。

record start record end actual start actual end salary
Jan 1 Mar 14 Jan 1 Dec 31 6000
Mar 15 Apr 4 Jan 1 Feb 14 6000
Mar 15 Apr 4 Feb 15 Dec 31 6500
Apr 5 Dec 31 Jan 1 Feb 14 6000
Apr 5 Dec 31 Feb 15 Dec 31 6400

この方法を使うとバイテンポラル履歴全体を参照できるようになるが、データの更新やクエリの発行がやりづらくなる。もっとも、バイテンポラルな情報へのアクセスを処理するライブラリを作成すれば、少しは扱いやすくなる。

別の方法としては、イベントソーシングがある。この場合、サリーの給与額自体(給与の「状態」)をメインのデータとして持つのではなく、給与に対する変更すべてをイベントとして保存する。そのイベントは次のような形になる。

record date actual date action value
Jan 1 Jan 1 sally.salary 6000
Mar 15 Feb 15 sally.salary 6500
Apr 5 Feb 15 sally.salary 6400

ここで注意すべきは、イベントがバイテンポラル履歴に対応する必要があるなら、イベントデータ自体もバイテンポラルにする必要があるということだ。つまり、各イベントは、現実世界でイベントが発生した時点を表す実際の日付(または時間)と、そのイベントを認識した時点を表す記録の日付(または時間)を必要とする。

イベントの保存だけなら考えかたは単純だが、クエリに応答するには処理がさらに必要になる。一方、そのような処理の結果は、アプリケーションの状態のスナップショットを取れば、ほとんどはキャッシュできる。したがって、現時点の実履歴だけが必要なデータ利用者が大半なら、実履歴だけに対応しているデータ構造を作って、イベント列に基づく履歴データの追加と新規イベント発生時の履歴データの更新を実行すればよい。バイテンポラルな履歴データが欲しい利用者であれば、実履歴だけよりは複雑なバイテンポラル履歴のデータ構造を作って、同じイベント列から履歴データを追加することもできる。そのバイテンポラルなデータが複雑であろうと、実履歴だけを表す単純なモデルを使いたい人にとってややこしい問題は起きない(仮に別の記録日における実履歴を確認したいなら、現時点の実履歴を扱うためのコードの大部分を流用すればよい)。

参考文献

80年代から90年代にかけて、さまざまなソフトウェアシステムに取り組むなかで、バイテンポラル履歴という問題に直面した。それまでに見られたパターン群について書き留めだしていたものの、本稿として仕上げないままに、他の執筆プロジェクトが忙しくなってしまった。その内容には、バイテンポラル履歴についての議論も含まれていた。本記事はその考えかたに力点を置き、もう少し明快に説明できればという意図で書いた。

同じ時期に、Richard Snodgrassが『Developing Time-Oriented Database Applications in SQL』という書籍を著した。同書は、SQLシステムにおけるこういった問題の対処方法について非常に詳しく論じており、そのアプローチは標準規格であるSQL:2011にも影響を及ぼした。

「視点」という概念は『Time Travel: A Pattern Language for Values That Change』から取り入れた。

謝辞

Alexandre Klaser、Dave Elliman、Joshua Taylor、Martha Rohte、Mauro Vilasi、Pavlo Kerestey、Pramod Sadalge、Rebecca Parsons、Saager Mhatre、Wolf Schlegelには、社内のメーリングリストで本記事についての有益な議論にご協力いただいた。

Heikki Heinonenには「視点」の表における誤りをいくつか指摘いただいた。

主な更新履歴

  • 2021年4月7日: 公開
  • 2021年3月17日: 社内レビュー依頼
  • 2021年3月2日: ドラフト執筆開始
  1. actual/record 対 valid/transaction: 「valid time」や「transaction time」という用語はSnodgrassの著作に由来するもので、標準規格であるSQL:2011でも使われている。2000年代初めごろ、私がテンポラルモデリングのワークショップを始めた当初は「valid」と「transaction」を使っていたが、参加者からすると分かりにくいとのことだった。そのため、代わりに「actual」と「record」を使うようになった。「valid」と「transaction」が用語として定着しているわけでもないので、ワークショップでの教訓を踏まえて、この記事では「actual」と「record」を使うことにする。 

  2. バイテンポラルな未来: 過去の履歴では、実時間は常に記録時間と同じかそれ以前の時間になっているが、バイテンポラルの概念は未来にも適用できる。5月5日にサリーが5月12日付でさらに昇給すると知った場合、記録時間を5月5日、実時間を5月12日として昇給を記録できる。 

  3. この引用を知らないなら、『まばたきするな』(原題:『Blink』)をウォッチリストを入れたほうがいい。映像化されたタイムトラベルものの作品の中でも屈指の傑作だ。 

  4. このテーマについては、2000年代中盤、パラレルモデルの執筆初期に広く考え始めていた。しかし、当時はその方向性での検討は中断してしまった。今後再開するのか、再開するとしたらいつなのかはわからない。