gihyo.jp
まとめ
- 日時を表すカラムがある場合に限らない
- すぐに気づきにくい
- 本質的なまずさは「特別な意味を持つタプル」が一緒くたになっていること
- テーブル分割せよ
- ただし、集計処理で利用するだけならば分割しなくても何ら問題ない
- ヒント
- ステータスやフラグを示すカラムがある
- 初期値がNULLのカラムがある
- 現在時刻との比較をしている
- オンライントランザクション中に
ORDER BY N DESC LIMIT 1
もしくはMAX()
/MIN()
/COUNT()
が用いられている
- バージョンを表すカラムがある
INSERT
/DELETE
よりもUPDATE
の比率が高い
履歴データの問題点
世界は履歴データで溢れている
履歴とリレーショナルモデルの相性問題
- そもそも履歴データをリレーションとして表現できるかどうか
- リレーションは集合なので各要素に順序はない
- 履歴には順序がある
- テーブルが巨大になりやすい
履歴データの具体例
item |
price |
start_date |
end_date |
ダンベルセット |
10000 |
2010-01-01 |
9999-12-31 |
グリッパー |
4000 |
2013-04-01 |
2014-03-31 |
グリッパー |
5000 |
2014-04-01 |
9999-12-31 |
懸垂マシン |
18000 |
2010-01-01 |
2011-12-31 |
懸垂マシン |
20000 |
2012-01-01 |
2014-12-31 |
懸垂マシン |
22000 |
2015-01-01 |
9999-12-31 |
SELECT price
FROM price_list
WHERE item = '懸垂マシン'
AND NOW() BETWEEN start_date AND end_date;
- 本章で本クエリ、テーブルの問題を認識できるようになること
履歴データの何が問題になるのか
リレーションと時間軸との直交性
- 同じクエリでも実行時刻により結果が異なる
- リレーションは「ある時点における事実の集合」
- 時間軸と直行でない(相関がある)ものはリレーションではない
NULLの可能性
9999-12-31
は「設定値なし」を表す特殊な値
- 【補】前章で「誤ったNULL対策」として挙げられていたパターン
- 代わりに
NULL
を使用する設計も考えられる
特定の行だけ意味が違う
item |
price |
start_date |
ダンベルセット |
10000 |
2010-01-01 |
グリッパー |
4000 |
2013-04-01 |
グリッパー |
5000 |
2014-04-01 |
懸垂マシン |
18000 |
2010-01-01 |
懸垂マシン |
20000 |
2012-01-01 |
SELECT price
FROM price_list
WHERE item = '懸垂マシン'
AND start_date = (SELECT MAX(start_date)
FROM price_list
WHERE item = '懸垂マシン');
- 集計でもないのになぜ集約関数
MAX()
が必要なんですか
- リレーショナルな演算でないので使わないに越したことはない
- タプルの意味が命題関数だけで決まらず、均一でない
- 暗黙の決まりごとがある
- 閉世界仮説に反する
- リレーションの個々のタプルの意味は命題関数だけで決まり、それ以上でも以下でもいけない
履歴データに対する解決策
- 絶対的な解はない
- リレーショナルモデルに収まらない以上、次善の策の域を出ない
リレーションを分割する
- 同一の命題関数で評価できないタプルを同一のテーブルに混ぜない
- DB設計の盲点
最もシンプルな分割方法
price_list
item |
price |
start_date |
ダンベルセット |
10000 |
2010-01-01 |
グリッパー |
5000 |
2014-04-01 |
懸垂マシン |
20000 |
2012-01-01 |
price_list_history
item |
price |
start_date |
グリッパー |
4000 |
2013-04-01 |
懸垂マシン |
18000 |
2010-01-01 |
SELECT price
FROM price_list
WHERE item = '懸垂マシン';
- メリット
- デメリット
- 過去から現在までの全価格に対してクエリする場合は
UNION
が必要に
WHERE
句のNOW()
や、集計でもないのにサブクエリに現れるMAX()
よりはマシ
- FK制約効かない
- 整合性の確保が必要
- 元の設計よりはずっと好ましい
重複した行を許容する
price_list
item |
price |
start_date |
ダンベルセット |
10000 |
2010-01-01 |
グリッパー |
4000 |
2013-04-01 |
グリッパー |
5000 |
2014-04-01 |
懸垂マシン |
18000 |
2010-01-01 |
懸垂マシン |
20000 |
2012-01-01 |
price_list_history
item |
price |
start_date |
グリッパー |
4000 |
2013-04-01 |
懸垂マシン |
18000 |
2010-01-01 |
price_id_master
price_list
price_id |
item |
price |
start_date |
1 |
ダンベルセット |
10000 |
2010-01-01 |
3 |
グリッパー |
5000 |
2014-04-01 |
5 |
懸垂マシン |
20000 |
2012-01-01 |
price_list_history
price_id |
item |
price |
start_date |
2 |
グリッパー |
4000 |
2013-04-01 |
4 |
懸垂マシン |
18000 |
2010-01-01 |
- メリット
- 単一の
price_id_master
テーブルに対してFK制約使える
- 価格データの重複ない
- デメリット
未来の価格はどうすべきか
price_id_master
price_list
price_id |
item |
price |
start_date |
1 |
ダンベルセット |
10000 |
2010-01-01 |
3 |
グリッパー |
5000 |
2014-04-01 |
5 |
懸垂マシン |
20000 |
2012-01-01 |
price_list_upcoming
price_id |
item |
price |
start_date |
7 |
ダンベルセット |
12000 |
2014-08-01 |
8 |
懸垂マシン |
20000 |
2015-01-01 |
price_list_history
price_id |
item |
price |
start_date |
2 |
グリッパー |
4000 |
2013-04-01 |
4 |
懸垂マシン |
18000 |
2010-01-01 |
- 「未来」そのときがやってきたら?
- 「現在の価格」を更新するバッチ処理が必要
- この設計ではリアルタイムに価格を切り替えることはできない
フラグを立てる
item |
price |
start_date |
end_date |
active |
ダンベルセット |
10000 |
2010-01-01 |
9999-12-31 |
1 |
グリッパー |
4000 |
2013-04-01 |
2014-03-31 |
0 |
グリッパー |
5000 |
2014-04-01 |
9999-12-31 |
1 |
懸垂マシン |
18000 |
2010-01-01 |
2011-12-31 |
0 |
懸垂マシン |
20000 |
2012-01-01 |
2014-12-31 |
1 |
懸垂マシン |
22000 |
2015-01-01 |
9999-12-31 |
0 |
SELECT price
FROM price_list
WHERE item = '懸垂マシン'
AND active = 1;
問題
- フラグカラムはカーディナリティ低い
- 2NFでない
- 候補キーは
{item, start_date, end_date}
- フラグカラムが候補キーの真部分集合に関数従属
{start_date, end_date) -> {active}
- activeカラムの更新バッチ処理が必要
start_date
/end_date
とNOW()
を使ったクエリとactive
を使ったクエリが混在すると不整合が生じる
active = 1
なる行は常に1つでなければならない
手続き型として実装する
コラム1 フラグのお化け
- 異なる意味合いのタプルは異なるリレーションに含めよ
- 安易にフラグカラムを追加する前に、リレーション分割を検討する
- さもないとフラグのおばけになる
コラム2 テーブルを分けたときの物理的なメリット
- テーブルのサイズが小さくなる
- 検索速くなる
- インデックス:
O(log(n))
- フルスキャン:
O(n)
- 頻繁にアクセスするテーブルの分離
- たいてい「現在の値」のほうがヒストリよりもアクセス頻度高い