ねこものがたり

いちにちいっぽ

記録の不可逆性を加味してcreated_at/updated_atだけで本当に事足りるのかを考えたい

経緯

神速さんのポストきっかけで、Railsにおけるテーブルのタイムスタンプについて話が交わされています。

神速さんとはなちんさんは基本的には、必要とされるまではタイムスタンプはRailsのデフォルトを使うのが良いとおっしゃっています。

おそらく、おふたりと同じ考えの人は結構多いのではないかと思います。そして私もこの意見には一定賛成です。

「記録していない事実を後から補完することはできない」ということをDB設計では考えるべきだ、と思う

Railsのレールに乗るのが良いという意見に賛成する一方で、DB設計については私は違う考えを持っています。アプリケーションコードはYAGNIを遵守した方がいいコードになりやすいけど、DBでは後からでは遅いことがあるという理由からです。

「絶対にcreated_at / updated_at 以外のカラムを使うべきだ」というほど極端なことを言いたいわけではありません。 しかし「"事実の記録をしない"という選択は不可逆な選択肢なので、本当にcreated_at / updated_at だけで良いのかは考えるに越したことはない」と思っていて、場合によっては「あった方がいいかも」程度でもDBのカラムに持っておく判断もあり得ると考えています。

一律の正解はなくユースケース次第ではあると思うのですが、だからこそ毎回毎回「今回はどうだろうか」とうんうん唸ってでもちゃんと考えておいて損はないと思っています。(Railsのレールに乗って早く開発するぞという話の否定意見ではなくて、別の視点の話のつもり)

例えば?

神速さんの記事だと、usersテーブルにcreated_atとは別にregistered_atが必要になるケースはほとんどないからcreated_atだけで良いだろうという例と考えが記されています。

これに対して私は「私だったらregistered_atを持つ選択をするかもな」と思いました。 正確にいうと「他の日時カラムが必要かも?というスタンスで設計の検討に入るな」という感じです。

もちろん検討した結果、最終結論が「created_at だけで良い」となる場合も多々あるかと思います。それでも、一旦立ち止まる価値はあるはずです。

「(これから作ろうとしているアプリケーションの処理を考えると)userレコードが作られた段階では会員の"登録"はまだ終わっていない」ということがusersテーブルの設計時点で明らかだとすると、

  • ①usersにステータスカラムを持たせ、ステータスに「登録」という値を持つ
  • ②usersテーブルに登録日時カラムを持たせる(アプリケーションではそのカラムに値が入っていれば登録済みとして扱う)
  • ③登録イベントをエンティティ(別テーブル)として定義する

あたりの選択肢は出てきそうです。

何度も前置きすると言い訳がましいかもしれませんが、どの選択肢が絶対に正しいというものはないはずです。 状況ややりたいこと次第でどの選択肢が最適かは変わるはずではありますが、「データが記録されていないと、流れ去っていった過去の事実を遡って復元することは絶対にできない」という点は、どの選択肢を選んだとしても共通して言えることかと思います。

この点を踏まえると、私はこの中だと②を選ぶ余地はかなりあるのではないかと思います。

「updated_atの上書き」で消えていく事実

「userレコードが作られた段階では会員の"登録"はまだ終わっていない」ということが明らかなusersのテーブル設計で、①usersにステータスカラムを持たせ、ステータスに「登録」という値を持つ選択をしたとします。 登録日時は、登録した時点のupdated_atということになります。

しかし登録完了後に他のカラムを変更した場合、updated_atは別の日時に変わってしまい、「いつ会員登録したのか」はわからなくなります。

初期のうちはこれで問題なく動くでしょう。そのうち、マーケターやPMに「登録から初回購入までのタイムラグを集計したい」「月別の登録ユーザー数の推移を正確に出したい」と言われたとします。この時、既存の登録済みユーザーがいつ会員登録を済ませたのかが全データ分残っているとは限りません。*1

だからこそ、記録ロジックやUI(機能)までは初期段階で作らなくても、「いつその事象が起きたか」を保持する registered_atといった NULL許容のカラムをDBに定義する判断 には、大きな価値があると思っています。

おわりに

冒頭でも触れた通り、私は「Railsのレールに乗って素早く開発する」というアプローチを否定したいわけではありません。 不要なコードを書かないYAGNI原則は、Webアプリケーション開発において強力な指針です。それに、DBでも、いきなり何でもかんでも考慮・想像して過剰な実装になってしまうのはいいとは言えません。

しかし、「後からいつでも書き直せるアプリケーションコード」「一度失われたら二度と復元できないDBの蓄積データ」では、YAGNIを適用する際のリスクの重みが異なるように思います。

  • とりあえずデフォルトの created_at / updated_at で素早く作る
  • 「本当にこのイベントの日時は後から追わなくて大丈夫か?」と一瞬立ち止まり、必要ならカラムを追加しておく

瞬間瞬間の開発コストの差はほんのわずかなものでしょうが、数年後のプロダクトやチームに与える影響の差は決定的になり得るんじゃないかと思います。

テーブルを新規作成するその一瞬に、「記録していない事実は後から補完できない」という不可逆性を頭の片隅に置いて考えていくことで、見えてくる何かもあるのかなと思っています。

実際の開発時には、まだ世の中に正解はなくて自分たちで考えて選んでやっていくしかないってことばかりですし、開発の速さは正義みたいなところもありますし、未来は誰にもわからないから、YAGNIに反する選択をするにはよっぽど強い根拠が必要ですし、でもレールに乗ってても「あーあの時こうすればよかったなー」って思うことがあるのも事実で、色々ひっくるめて「自分たちは今どういう選択をするのが良いかを考えるようにしたいと思っている」というのが率直なところなので、この記事をまとめてみました!

*1:この例だとアプリケーションログを漁れば残っている可能性はあるかもしれませんが、保管不可能な事例もあるだろうということが伝われば幸いです

PostgreSQLのbool_or / bool_andで「1件でも当てはまるか」「全件当てはまるか」を調べる

背景

仕事でRailsアプリの開発をしてる最中、件数を出すSQLを書くことがあって「(色々複雑なテーブル結合をした上で)条件に1件でも当てはまったら+1、そうでなければ+-0」という集計をしなければなりませんでした。 条件に1件でも当てはまったらをどうやって書けばいいんだ?と思ったのですが、最近すっかり私の相棒になっているkiroに要件を伝えたところ、「わかった!」という返事と共にbool_orを使って一瞬でやってくれました。kiroのアウトプットの正しさを判断するためにもbool_orについて理解しておきたいと思います。

bool_or

bool_orというのはPostgreSQLの集約関数の1つです。集約関数というのは、複数行を1つにまとめる(=集約する)関数。今回私が知ったbool_orだと、「たくさんある行の中で、1つでも条件に当てはまるものがあったらtrueを返す。そうでなければfalse」を返すという仕事をしてくれるもので、この関数の実行結果はboolean値の1行だけです。

使い方

仮想テーブルを使って試してみました。

SELECT
    bool_or(v)
FROM
    (
        VALUES
            (TRUE),
            (TRUE),
            (FALSE)
    ) AS t (v);

/* => TRUE */

TRUEなものが2件あるので結果はTRUEとなります。 全件FALSEだと、結果はFALSEとなります。

SELECT
    bool_or(v)
FROM
    (
        VALUES
            (FALSE),
            (FALSE),
            (FALSE)
    ) AS t (v);

/* => FALSE */

なるほどわかってきた!

bool_and

ChatGPTに「bool_orのorって何?」って聞いてみたところ、集合で使うORのorだと教えてくれました。その流れでbool_andの存在も知りました。こちらは「ある条件に全件当てはまるか」の集約関数です。

1つでもFALSEがあれば結果はFALSE。全てTRUEならば結果はTRUEです。

SELECT
    bool_and(v)
FROM
    (
        VALUES
            (TRUE),
            (TRUE),
            (FALSE)
    ) AS t (v);

/* => FALSE */
SELECT
    bool_and(v)
FROM
    (
        VALUES
            (TRUE),
            (TRUE),
            (TRUE)
    ) AS t (v);

/* => TRUE */

完全に理解した。

ChatGPTによるbool_orとbool_andの違いの説明

PostgreSQL以外ではどうなのか

bool_or / bool_andという名前の関数は他のDBにはなさそうです。 代わりに使うとしたらMIN / MAXが良い模様。 ということはbool_or / bool_andは簡単便利だけど、DBを乗り換えようと思ったら、その時には実装を変更する必要が出てくるということですね。 使う時には、DB乗り換えの可能性がどれくらいあるのか、集約関数を使わなかった場合にSQLがどれくらい複雑になりメンテナンス性や可読性がどうなるのかといった観点で判断を挟む方が良さそうです。