日付/時刻の項目を文字列でもたせたがる輩の息の根を止める


はじめに

さて、今や二十一世紀も1/5が過ぎてしばらく。
コンピュータ通信が生まれてからは半世紀が経ち、我々開発者は今やほんの数回キーボードを叩けばものづくりに必要な最低限度の知識は容易に手に入る。
そんな時代であるにも関わらず、未だに少なくないシステムで許しがたいDB設計をしている馬鹿野郎どもが跡を絶たない。

正規化とか、リレーションとかインデックス、あるいはストアドやトリガなどの込み入った話ではない。これらはシステムの要件によって最適解が変わってくるものであるので、畢竟ケースバイケースだ。
ここでやり玉に上げたいのはそんな高度な話ではなく、はっきりといって低次元なアンチパターンである。
であるにも関わらず、おそらくこの業界で10年もやってれば、直接関わってはいなくとも連携先やリプレイス元などのシステムでほぼ100%のエンジニアが遭遇したことであるのではないだろうか、タイトルに入れてるのでもうおわかりだろうが、なにあろう
「日付/時刻を文字列型でもたせてるDB」
である。

私はここで「あるあるw」なんて共感をえて「だめな設計しちゃうやつはいつまでもいなくならないな」などと遠い目をしてタバコをくゆらせ悦に浸るだけで終わらせるのをもう辞めたいと思ってる。

そうだ、この愚かな決定を今後この国のエンジニアの何人(なんぴと)も行わないように、少なくとも今後生まれるシステムでこのような惨劇を繰り返させないために、徹底的に、完膚無きまでにこの方法に一切の利がないことを叩きつけたいと思う。

命名

なにかの概念を語るのに名前をつけることは大変に重要である。
おそらくこの日付/時刻の項目を文字列でもたせるアンチパターンには名前がすでについている。
どこかでみた気がするので3分ほど記憶を探ってみたが引っ張り出せなかった。だれか心当たりがあれば教えてほしい。
ここでは便宜的に「日時文字カラム」と名付けて叩きのめして行こうとおもう。

基本的な問題

細かいところは話を進めながら解説するとして、多くの読者にとって言うまでもないことであろうとは思いつつも念の為日付/時間として扱うべきデータを文字列としてもたせることによって起きる基本的な問題を共有しておきたい。

計算可能性

例えば登録されている日付に対して1日加算したいとなったとき、Postgreでいうとこれでいい

UPDATE some_table SET some_date_field = some_date_field + 1

文字で格納していた場合加工が必要となる。
こう言うと、文字で格納していても関数を噛ませばいいだけと食いつかれるかもしれない。
要するにこういうことだ。

UPDATE some_table SET some_date_string_field = to_date(some_date_string_field, 'YYYYMMDD') + 1

もちろんこれはほぼ正しく動く。値が正しく日付フォーマットに従っているならばだが。
だから本質的な問題はなにも無いと言うのであればそもそも本命題はあなたに関係がない。
日付型を二度と使うことなくことあるごとに変換を行えばいいと思う。
煽っているように読めたかもしれないが、こういった細かなコストを無視し続けられる潤沢なリソースがあるのであれば、実際のところ我々の直面する少なくない問題が問題ではなくなる。

どんなに身なりに気を使わず、清潔感がなく、臭かろうが、100億持ってるならモテる。そういうことだ。

文字列での日時の表現というのは実体としての時間を加工した結果である。
加工済みの結果から加算や別フォーマットへの変換等、別の加工を施そうとすると、一度元の実体への逆加工が必要となってしまう。

日付文字を否定する理由は実はこれだけで十分であるし、他の問題もほぼこの派生なのであるがいくつかの問題を上げないとそれっぽくないのでひねり出そう。

バリデーション

文字による登録の場合、まず入力制限など入れたりしないので、00000000や、-------等、日時として不正な入力が容易に入り込む。
さらに良くないことに、「0000000はキャンセルを意味しています!」のようにこういった日時としてありえない表現に意味を与えて特殊な状態を表現しようとする輩も出てくる始末である。
(あまつさえそれが「賢いハック」であると思っていたりする)

これが横行するとリファクタでちゃんと日付型に変換しようとしてもすぐ頓挫することになる。もはやそのテーブルは腐っているのだ。

国際化対応

あなたの作るシステムが世界にまたがりサービスを提供しようとしたとき、日付文字カラムを使っていれば本来不要な変換処理を各所で行う必要が出てくるだろう。言うまでもなくミニマムな道具立てとしてはDBへは世界標準時で登録し、地域ごとのオフセットだけあれば一貫性のある時間を示すことができる。

データサイズ

今ではほとんど気にされないものの、ストレージ容量に応じて課金されるサービスも少なくないので、
巨大なテーブルで日付文字項目が山程あれば塵積で影響は無視できなくなるかもしれない。
DateでもDateTimeでも文字化すると最低でも倍のストレージが必要となる。

日時文字カラムが採用されるケースと対処法

このようにデメリットの多い日時文字カラムであるが、採用される原因はいくつかある。
原因を紐解きながら一つ一つ潰していこう。

単に問題が理解できていない未熟な設計者

見出しの通りだ。彼らはそもそも型の意味をわかっていないので日付に限らず数値も文字にしたりすることになんらのためらいもなかったりする。
よく見られるケースではあるものの、その多くは駆け出し設計者によるもので、レビュー等のタイミングで是正されることが大半である(と思いたい)
しかし稀になんのメリットもないのに日時文字カラムがチェックの目をくぐり抜けてリリースされたりする。けだし馬鹿野郎である。

これの解決策はおそらくこの記事を全メンバーに読ませることだ。

外部システムや利用しているライブラリがYYYYMMDDのフォーマットを求めるので最初からそのまま入れちゃえば楽じゃない?

私の調べによればこれは大体全体の2割を占める。
値を利用するときに文字列として使うのだからDBにもそのフォーマットのまま格納すれば変換が不要で省力だというロジックを根拠にする。
これを言われるとレビュアーもそんな気になってしまってじゃあ文字でいいかとつい言ってしまいがちだ。
言うまでもなく出す方も通す方も馬鹿野郎である。

そもそも出力フォーマットに引きずられてデータの持ち方を決める事自体よくあるアンチパターンであり論外だ。
時間の話を時間で例えるのは上手くないが、出力に元号表記を求められたなら内部的にも元号で保持するつもりだろうか。
また外部システムが求める形式が変わる可能性や、
連携先が追加されたときそれぞれの求めるフォーマットが違うことなど想像すればそれがいかに短絡的な決定であるかわかるだろう。

これの解決策もまた、おそらくこの記事を全メンバーに読ませることだ。

日時では表現できない(ようにみえる)データを使いたい

私の調べによればこれが4割を超える一番多いパターンである。
どういうことか、何らかの開始予定時間をもたせるために、こんな具合のバリエーションの入力をさせたい、ということである。

選択入力
10:00
11:00
12:00
午前中
13:00
14:00
夜間
終日

モニターの前で(あるわぁ~)という嘆息を漏らす姿が浮かぶようだが、
この入力をなんとかしようとすればそのまま文字列で入力するしか無いように見える。もちろん馬鹿野郎である。

ではこの例でいうようなデータを表現したいのであればどうすればよいか。
このような例で、午前中という入力が示したいのは本質的にはおそらく10:00-12:00というレンジだ。
であるならそもそも一つの項目で扱うことが誤りで、
from-toで2つの時間項目をもたせればよいのである。(一つの時間とインターバルを使うテクニカルな方法もありえる)
どうしても要件が変えられず、UI上で午前中という表現がしたいのなら、それはfromが10:00、toが12:00となるデータセットの別名とすれば良い。
出力のフォーマットに引きずられるのが愚かであると言ったが、入力のフォーマットに引きずられるのもまた同様。
ユーザの認識している表現と、システムがどのようにそれを保持するかを一致させなければならないという考えは今すぐ捨て給え。

このようなデータ形式であれば、この値を元にガントチャートのようなグラフを作れと言われても一貫したルールで表現できるようになる。
いちいち

switch (rc.some_string_data_field) {
  case '午前中': /*some logic*/
...

などというようなキモサムいコードを書く必要はないし、もしユーザが夕方を加えたいと言いだしたところで、何らのバグを仕込むことなく対応が可能となるはずだ。

なお、ここでは簡単のため、時間のみのTime型項目であるように話をしている。
話が拡散するので深く触れないが、Time型をサポートしているDBは多いものの
時間は当然ある日に属する特定の点として扱うことが殆どで、日が特定されず時間だけを単独で存在させたいことは極めて限られた状況のはずである。
あるDateのカラムとその日の開始-終了を示すTimeのカラムを2つ別でもたせるような構造をときに見るがそれは2つのDateTimeであるべきで、そうすれば日をまたぐ範囲も容易に表すことができる。
基本的にはDateTime、明らかに時間が不要であると言い切れるのであればDateを使う、くらいの感覚でよいだろう。

え?24時超えることあるの?深夜2時とか?あー、、、じゃあ26:00て入力してもらえればいいんじゃない?
いいじゃんそれ!冴えてるー!

月までしかいらない

これはやや特殊でありつつも、このケースも相当多い。
ビジネスロジックにおいて利用月や支払月などの「月」を表したいことは少なくない。寡聞にしてMonth型を持っているDBMSを私は知らない。(一つくらいあっても良さそうなものだが)
年月だけあればよいというのでこの場合に6桁の文字、ないしは数値でもたせるのは一見合理的だ。
これは他と違って後述の方法も一長一短あるため馬鹿野郎と言い切ってしまえるほど愚かとは断じきれない部分があるが、
こういうのは言い切ったもの勝ちなので言ってしまおう、馬鹿野郎である。

年と月のそれぞれintでもたせる方法は多少はマシで、さらに月の更新をトリガで制御して年の繰り上げ、繰り下げを自動化してるとすればかなり偉いと言える。

しかし要するに加算減算、あるいはクエリの条件として特定の時間範囲を絞り込んだりといったことがしたいのであって私はこの場合単にDate項目として追加し、常にその月の1日ないしはほぼ中央という意味で15日に設定するという方法を勧めたい。
月としてのデータに不要な日が入ってくることが気になる向きもあるだろうしその感覚は大いに理解するが、
利用の際に単に日部分を無視すればよいだけで、こうすることのデメリットはほぼない。
あるとすれば日を何日にするかは登録ロジックに依存するため、特に重複を防ぎたい場合、ルールを明示して守らせる必要があるくらいだが、日付文字カラムはそれ以上にルーズだし、はるかにマシだろう。
個人的にはDBにロジックをもたせるのはあまり好まないものの、
どうしても制限を入れたいなら日が常に一定なるようトリガを書けば良い。入力の制限だけであれば考慮も特に不要だろう。

終わりに

斯様に、日付文字カラムは一切の情状酌量の余地ない愚行であることを改めて訴えたい。
場合によっては・・・などと甘えたことを言っている限りこの犯罪的行いはなくせないのだ。

この記事によって世の中の日付文字カラムの誕生を一つでも減らせたとすれば幸甚である。