sqlアンチパターン読書会 第10章 サーティワンフレーバー
DESCRIPTION
2013年10月17日開催のSQLアンチパターン読書会 の資料。「第10章 サーティワンフレーバー(31のフレーバー)」。TRANSCRIPT
SQLアンチパターン読書会第10章 サーティワンフレーバー(31のフレーバー)
2013/10/17ふじもと たかひさ Twitter:@tkfuji
1
アンチパターンの目的サーティワンフレーバーの目的は列を特定の値に限定すること。
値リストを列のデータ型や制約の宣言時に行うことで(値リストにない)無効な文字列の入力を防止できる。
2
敬称列の例値の種類が少ない列、連絡先情報テーブルの敬称(salutation)列が典型。
31-Flavors/intro/create-table.sql
CREATE TABLE PersonalContacts ( -- 他の列. . .
salutation VARCHAR(4) CHECK (salutation IN ('Mr.', 'Mrs.', 'Ms.', 'Dr.', 'Rev.')),);
3
敬称列の例上司からあなたに与えられたタスク:新しい敬称をサポートするために連絡先テーブルを変更せよ
敬称(salutation)列の現在の値 ‘Mr.’, ‘Mrs.’, ‘Ms.’, ‘Dr.’(博士), ‘Rev.’(聖職者)
フランスに支社を開設⇒フランス後の敬称が新たに必要。‘M.’, ‘Mme,’, ‘Mlle.’など
来月ブラジルにも支社を開設予定⇒ポルトガル語も必要?
無停止でテーブル定義が修正できるか?
4
バスキン・ロビンス社のサーティワンアイスクリーム
バスキン・ロビンス社が毎月日替わりで違う種類のアイスクリームが食べられる。キャッチフレーズ「31のフレーバー」
現在、21種類の定番フレーバー、12種類のシーズン限定フレーバー、16種類の地域限定フレーバー、今月のフレーバーと可変になっている。
(何れも『SQLアンチパターン』の記述から)
5
アンチパターンアンチパターン:限定する値を列定義で指定する。
有効なデータ値を列の定義時に指定する方法を取る。例えば、CHECK制約を定義する。CHECK制約により制約条件がFALSEになる値の挿入や更新を拒否する。
31-Flavors/anti/create-table-check.sql
CREATE TABLE Bugs ( -- 他の列. . .
status VARCHAR(20) CHECK (status IN ('NEW', 'IN PROGRESS', 'FIXED')));
6
アンチパターンの実現方法CHECK制約を用いる。
ENUMと呼ばれる非標準のデータ型を用いる(MySQL固有?)。
ドメイン(DOMAIN)を用いる。
ユーザー定義型(UDT)を用いる。
許可する値を指定したトリガーを記述する。
⇒上記には共通の短所がある。
7
アンチパターンの短所シンプルなクエリでは完全なリストを取得できない場合がある。
⇒全てのバグのステータスがNEWの場合、NEWしか返って来ない。
31-Flavors/anti/create-table-check.sql
SELECT DISTINCT status FROM Bugs;
8
アンチパターンの短所メタデータの定義を取得する。MySQLではシステムビューinformation_schemaを検索する。
⇒全てのバグのステータスを含んだ文字列が返って来る。ENUMで定義した文字列「ENUM('NEW', 'IN PROGRESS', 'FIXED')」をアプリケーションで解析して、値を抽出する。
31-Flavors/anti/information-schema.sql
SELECT column_typeFROM information_schema.columnsWHERE table_schema = 'bugtracker_schema' AND table_name = 'bugs' AND column_name = 'status';
9
新しいフレーバーの追加ENUMやCHECK制約で値を追加、削除はできない。
31-Flavors/anti/add-enum-value.sql
ALTER TABLE Bugs MODIFY COLUMN status ENUM('NEW', 'IN PROGRESS', 'FIXED', 'DUPLICATE');
10
アンチパターンは移植が困難データベース製品間で仕様が異なる。CHECK制約
ENUM
ドメイン(DOMAIN)
ユーザー定義型(UDT)
トリガー11
アンチパターンは移植が困難ふじもと調べ
アンチパターンの対応状況(○:対応、×:非対応)ANSI SQL 標準
PostgreSQL MySQL Oracle SQL
ServerDB2 UDB
CHECK制約
ENUM
DOMAIN
UDT
SQL89で標準化 ○ × ○ ○ ○なし ○ ○ × × ×SQL92で標準化 ○ × × × ×SQL99で標準化 ○ × ○ ○ ○
12
CHECK制約の実装状況ふじもと調べ
PostgreSQL特定の列の値がブーリアン(真の値)の式を満たすように指定できる。列制約またはテーブル制約として指定可能。ただし、列制約として定義してもテーブル制約として管理される。
MySQLなし。「CHECK 条項は、全てのストレージ エンジンに解析されますが、無視されます。」( マニュアルから)
Oracle特定の列の値がブーリアン(真の値)の式を満たすように指定できる。列制約またはテーブル制約として指定可能。
SQL Server特定の列の値がブーリアン(真の値)の式を満たすように指定できる。列制約またはテーブル制約として指定可能。
DB2 UDB列制約またはテーブル制約として指定可能。チェック条件は2MBのCLOBとして管理。
13
CHECK制約の実装状況ふじもと調べ
SQL Serverのマニュアルから「列に格納される値を制御するという点では、CHECK 制約は FOREIGN KEY 制約に似ています。異なる点は、有効な値を決定する方法です。FOREIGN KEY 制約では別のテーブルから有効な値の一覧が取得されますが、CHECK 制約では論理式から有効な値が決定されます。」
⇒CHECK制約(アンチパターン)の代替として 参照テーブル(解決策)が提示されている。
14
ENUMの実装状況ふじもと調べ
PostgreSQL8.3からサポート。CREATE TYPEコマンドで実現。列挙型内の値の順序はその型が作成された時に値を列挙した順番になる。
MySQL文字列タイプのひとつ。文字列タイプにはCHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM、SETがある。ENUMはテーブルを作成する際カラム仕様の中で明確に列挙された許容値リストから選択された値を持つ文字列オブジェクトである。ENUM('one', 'two', 'three') のように1つのENUMは最大65,535エレメントを持つことができる。許容値の文字列または1から始まるインデックス値(定義順)で挿入が可能。許容しない値は特別エラー値として空文字(インデックス値は0)が登録される(ストリクトSQLモードを有効にすることより挿入を禁止することも可能)。NULL値のインデックス値はNULL。
Oracle / SQL Server / DB2 UDBなし。
15
ドメイン(DOMAIN)の実装状況 ふじもと調べ
PostgreSQLCREATE DOMAINコマンドで実現。DEFAULT句、NOT NULL制約、CHECK制約などを定義する。CREATE DOMAIN name [ AS ] data_type
[ COLLATE collation ] [ DEFAULT expression ] [ constraint [ ... ] ]
constraintは、次のようになります。
[ CONSTRAINT constraint_name ] { NOT NULL | NULL | CHECK (expression) }
※ COLLATEは9.1から、CHECKは7.4から
MySQL / Oracle / SQL Server / DB2 UDBなし。
16
ユーザー定義型( UDT)の実装状況 ふじもと調べ
PostgreSQLCREATE TYPEコマンドで実現。定義が面倒。
MySQLなし。
OracleCREATE TYPE文とCREATE TYPE BODY文を使用する。CREATE TYPE BODY文でPL/SQLを使用して定義する。
SQL ServerCREATE TYPEステートメントを使用する。C#やVBの.NETプログラミング言語で、UDT の作成の仕様を満たすクラスまたは構造体を作成する。
DB2 UDBユーザー定義の構造化タイプ。CREATE DISTINCT TYPEステートメントを使用する。既存のデータ・タイプを基に定義する。UDF(ユーザー定義関数)でUDTの振る舞いを定義する。
17
アンチパターンの見つけ方「アプリケーションメニューの選択肢を1つ追加するには、データベースをオフラインにしなければならない。すべてがうまくいけば、30分もかからないはずだ」⇒列の定義時に値が指定されている。サービス中断は本来必要ない。
「status列は、以下の値のうちの1つを持つことができる。このリストの修正が必要になることは、まずないはずだ」⇒曖昧な表現。状況が変われば修正が必要になるかも。
「アプリケーションコード側の値リストが、ビジネスルールと同期していない。これで何度目だろう」⇒異なる場所で情報を二重管理していると生じるリスク。
18
アンチパターンを用いてよい場合有効値の変更が不要だと断言できる場合はアンチパターンを用いてよい。
例えば、相互排他的な2つの値(左/右、有効/無効、オン/オフ、内部/外部)を指定する場合など。
19
解決策:限定する値をデータで指定する
参照するテーブルBugStatusを作成し、許可する値をstatus列に格納する。Bugs.statusにBugStatusを参照する外部キー制約を宣言する。
31-Flavors/soln/create-lookup-table.sql
CREATE TABLE BugStatus ( status VARCHAR(20) PRIMARY KEY);
INSERT INTO BugStatus (status) VALUES ('NEW'), ('IN PROGRESS'), ('FIXED');
CREATE TABLE Bugs ( -- 他の列. . .
status VARCHAR(20), FOREIGN KEY (status) REFERENCES BugStatus(status) ON UPDATE CASCADE);
20
解決策での値セットの取得シンプルなクエリで値リストを取得できる。
⇒全てのバグのステータスがBugsでNEWの場合 でも全ての値が取得可能。
31-Flavors/soln/query-canonical-values.sql
SELECT status FROM BugStatus ORDER BY status;
21
解決策での値追加INSERT文で値を追加できる。テーブルへのアクセス遮断も不要。
31-Flavors/soln/insert-value.sql
INSERT INTO BugStatus (status) VALUES ('DUPLICATE');
22
解決策での値削除Bugsテーブルから参照されている場合、削除できない。BugStatusにactive列(有効/無効)を追加する。
DELETEではなくUPDATEを使う。
有効なものだけ検索する。
31-Flavors/soln/inactive.sql
ALTER TABLE BugStatus ADD COLUMN active ENUM('INACTIVE', 'ACTIVE') NOT NULL DEFAULT 'ACTIVE';
31-Flavors/soln/update-inactive.sql
UPDATE BugStatus SET active = 'INACTIVE' WHERE status = 'DUPLICATE';
31-Flavors/soln/select-active.sql
SELECT status FROM BugStatus WHERE active = 'ACTIVE';
23
31のフレーバーなのか?ふじもと調べ
バスキン・ロビンス社が毎月日替わりで違う種類のアイスクリームが食べられる。キャッチフレーズ「31のフレーバー」
現在、21種類の定番フレーバー、12種類のシーズン限定フレーバー、16種類の地域限定フレーバー、今月のフレーバーと可変になっている。(上記は『SQLアンチパターン』の記述から)⇒(日本)22種類の定番フレーバー、6種類のシーズン限定フレーバー、4種類の今月のフレーバー⇒合計32種類のフレーバー?⇒(アメリカ)24種類の定番フレーバー、28種類のシーズン限定フレーバー、5種類のチョイスフレーバー⇒合計57種類のフレーバー?(それぞれ日本とUSのWebサイトから)
24
31のフレーバーか?サーティワンアイスクリーム行ってみた
南砂町店(江東区)
32種類でした!4×4(写真)×2
25
バスキン・ロビンス社のサーティワンアイスクリームの豆知識 ふじもと調べ(Wikipediaを参考)
創業者がバートン・バスキンとアーヴィン・ロビンス。 バートン・バスキンはアーヴィン・ロビンスの義理の兄(姉の夫)。
アーヴィン・ロビンスの父親がアイスクリーム店を経営し、二人にアイスクリーム店開業を勧める。その後、その二つの店が合併した。
アメリカでは「バスキン・ロビンス(Baskin-Robbins)」で通じるが、「サーティワン(thirty-one)」では通じない(thirty-oneの呼称自体無い)。日本と台湾では「サーティワン」で通じるが、「バスキン・ロビンス」では通じにくい。「サーティーワン」ではなく「サーティワン」。
日本ではアメリカのバスキン・ロビンス社日本法人と不二家(各43.27%の出資比率)の合弁会社が経営。年間売上200億円(平均単価400円として5000万個の売上。1100店として一日当たり125個の売上)。サーティワンアイスクリームが不二家との合弁であることが知られていないため、「不二家の期限切れ原材料使用問題」による影響は比較的小さかった。
フレーバーの種類は1000種類以上。フレーバーは世界どこでも大抵32種類から選べる(高速道路施設などの小規模店は除く)。
26
アンチパターンを31のフレーバーに用いてはいけない理由 ふじもと考察
1000種類以上の開発済みのフレーバーから32種類のフレーバーが販売される。
今月のフレーバーは月単位で入れ替えあり。
シーズン限定フレーバーは季節単位で入れ替えあり。
定番フレーバーすら入れ替えが想定される。
32種類より少ないフレーバーを販売する店もある。
種別(定番・シーズン限定・今月)も管理したいだろう。
27
31のフレーバーは解決策参照テーブルで実現するべき
どうやって実現するかは宿題!
28
31のフレーバーは解決策参照テーブルで実現するべきヒント(≒答え)
販売明細テーブルにフレーバーを示すカラムがある。これをアンチパターン(フレーバーを示すカラムに値リストを設定)を用いないで解決する。
1000種類以上の開発済みのフレーバーを管理するフレーバーテーブル。フレーバー種別カラムを持つ。
地域フレーバーテーブル。地域テーブルとフレーバーテーブルと従属関係にある子テーブル。年月を主キーとして加える(有効開始年月日でもよい)。
店舗フレーバーテーブル。地域フレーバーテーブルと店舗テーブルと従属関係にある子テーブル。年月を主キーとして加える(有効開始年月日でもよい)。
販売明細は地域フレーバーテーブル(通常店の場合)か店舗フレーバーテーブル(小規模店の場合)と参照関係を持つ。
29
おまけ:SQLデモ本のサンプルコードを基に ENUMとCHECK制約のアンチパターンのSQL作成。
本のサンプルコードは実行可能でないものも含まれるため。http://www.oreilly.co.jp/books/9784873115894/
PostgreSQLで実行。MySQLは(サンプルコードにある)CHECK制約が実行できないため。
30