Skip to main content
MySQL⼊門
7限目: テーブル設計について
2015/04/30
GMOメディア株式会社 yoku0825
schoo WEB-campus
\こんにちは/
yoku0825@GMOメディア株式会社
オラクれない-
ポスグれない-
マイエスキューエる-
家に帰ると
妻の夫-
せがれの⽗-
娘の⽗-
Twitter: @yoku0825
Blog: ⽇々の覚書
1/144
アジェンダ
事前知識
DDLで使われる⽤語についてざっと説明します-
データ型について説明します-
カラム設計
デモケースとして掲⽰板⽤のデータモデリングをします-
非正規形のテーブルからスタートし、正規形にするまでの考え⽅を学
びます
-
インデックス設計
インデックスの動作の仕⽅についてトランプの例を⽤いて簡単に解説
します
-
先の掲⽰板⽤のデータモデルにインデックスを作成します-
2/144
質問
3/144
今のあなたのレベル感を教えてください
SQLもまだいまいち。。1.
SQLならほぼわかるけど実務経験ガー2.
テーブル設計経験者。でも破綻しちゃって。。3.
バリバリサービスでやっててノウハウはあるけど、ちょっと理屈を聞いて
みたくなった。
4.
お前にマサカリを投げに来た5.
4/144
1. SQLもまだいまいち。。 の⽅
そんなにたくさんSQLをゴリゴリ書くような授業ではないですので、気楽
に最後まで眺めてみてください。
暗記すること、憶えなければいけないことは基本何もありません。そうい
えばこんな話聞いたな…というのが後から⽣きてくる、というのを弊社研
修⽣からは⽿にしています :)
SQLが判ればもっと⾯⽩くなると思いますので、SQLが⼿についてき
たころに観返してみると違った発⾒があるかも知れません。
-
5/144
2. SQLならほぼわかるけど実務経験ガー の⽅
前半の正規形と設計のプロセスについて、疑問に思ったところはどんどん
聞いてください。
後半かなりアーキテクチャーに寄りますので、そちらはまた慣れてきたこ
ろに観返していただけると幸いです。
6/144
3. テーブル設計経験者。でも破綻しちゃって。。 の⽅
まずは前半で “正規形” について誤解がないかどうか⼀緒に考えましょ
う。
そして、 正規化 した/していない のに何故テーブルが破綻したのかの答
え(への⾜掛かり)を後半で探っていきます。
7/144
4. バリバリサービスでやっててノウハウはある⽅
そんなあなたにこの授業の内容をそれっぽい⾔葉で説明すると
3NFまで1段階ずつ正規化していきます。-
インデックスの使われ⽅(WHERE狙いのキー, ORDER BY狙いのキー)
の話をします。
-
JOIN時における内部表と外部表の話とWHERE狙い, ORDER BY狙い
の話が絡んできます。
-
楽しんでいただけそうですか︖
8/144
5. お前にマサカリを投げに来た
お⼿柔らかにお願いします。。
9/144
⽤語について
インデックス
(主に)データを検索するために使われる ソート済みのデータのサブセ
ット
-
MySQLでは “キー” と “インデックス” は同じもの(特に、セカンダリ
ーキー)を指すことが多い
リレーショナルモデル上では “キー” は “候補キー”
-
10/144
⽂字コードについて
sjis
悪いことは⾔わないのでやめましょう。バグが多いです。-
WindowsのSJISと 互換性がない ので、WindowsのSJISを使いたい
のであればcp932を使ってください
-
ujis
オススメはしませんがsjisよりはマシです。eucjpmsの⽅がいいで
す。
-
utf8
基本的にオススメですが、4バイト⽂字(絵⽂字)が⼊らないので、
utf8mb4にするべきかと思います。
-
latin1
暗黙のデフォルト。-
⽇本語を⼊れると闇が⾒えます。-
11/144
⽂字列型
CHAR(n), VARCHAR(n)
n ⽂字 (not byte)のデータを格納するための型-
デフォルトのまま使っていると、⼤⽂字と⼩⽂字を区別 しない (Case
Insensitive)
-
現在のMySQLにおいてCHAR型を選ぶメリットはほぼない。迷ったら
VARCHAR。
-
データサイズの側⾯でいうと、1⽂字に必要なバイト数は⽂字コード
ごとに違うので、n⽂字 * 1⽂字あたりのバイト数 + αが1⾏あたりの
最⼤サイズになります。
-
12/144
⽂字列型
TEXT, LONGTEXT
TEXT型は2^16⽂字くらいまで、LONGTEXT型は2^64⽂字くらいま
で。
-
ただしmax̲allowed̲packetがあるので、LONGTEXT型でも2^64
⽂字くらいまで全て詰め込むことはできません。
-
バイト型もあるけど、あまり使うことはない
PHPとかから使う時はどっちも⽂字列っぽく⾒えるけれど、Javaとか
から使うと型が違ってごちゃごちゃする。
-
13/144
数値型
TINYINT, INT, BIGINT
それぞれ1バイト, 4バイト, 8バイトの整数型。-
UNSIGNED属性をつけると符号なし整数になって1ビット余計に使え
る。
-
FLOAT, DOUBLE
単精度, 倍精度浮動⼩数点数型。-
浮動⼩数点型なので丸め誤差に注意。ほぼ使わない。-
DECIMAL, NUMERIC
固定⼩数点数型。演算が伴うときはこれを使うか、100倍してINT型
に⼊れるなんてことも。
-
14/144
⽇付時刻型
DATE型
3バイトで年⽉⽇-
TIME型
3バイトで時分秒、地味に負値も格納できる。-
DATETIME型
5.5までは8バイト、5.6からは5バイト。年⽉⽇時分秒まで格納。-
5.6以降はTIMESTAMP型よりこちらが推奨されている。-
TIMESTAMP型
4バイトでUNIXTIMEを格納して⽇付型として解釈する。-
1970/1/1 00:00:00〜2038/1/19 03:14:07-
15/144
その他テーブルの属性
AUTO̲INCREMENT
数値型(⼩数点型でもOK)そのカラムにインデックスがある場合にのみ
設定可能な属性。
-
⾃動でカウントアップされるDEFAULT属性みたいな感じ。-
AUTO̲INCREMENTの次の値は int(そのカラムの最⼤値) + 1-
DEFAULT
そのカラムに値が指定されなかった時に、暗黙にこの値を設定する。-
CURRENT̲TIMESTAMPの例外を除いてリテラルのみ指定可能で、関
数の結果はDEFAULTに指定できない。
-
ストレージエンジン, 圧縮, その他..
16/144
本編
カラム設計のデモケースとして掲⽰板⽤のデータモデリングをします
非正規形のテーブルからスタートし、正規形にするまでの考え⽅を⾒てい
きます
17/144
⼀番最初に考えること
あなたの 掲⽰板システムに必要なデータは何か︖
18/144
たとえば
掲⽰板にはスレッド単位で話題が上がって
それに対してコメントが付く形-
1スレッドの上限コメント数は制限したい-
スレッドを⽴てるにはメールアドレスを必須にしたい-
スレッドの⼀覧出⼒機能をつけたい
スレの作成順にソートしたい-
最終書き込み時刻でソートしたい-
スレの書き込み数でもソートしたい-
スレッドのタイトルの横にコメント数を表⽰したい-
19/144
考え⽅の順番
まず最初に全部⼊りで考えてみる
正規化とか考えずに必要なものを全部テーブルに詰め込んでみる
正規化⼿順に沿って5NFまで持っていく
もちろん慣れていれば3NFくらいからいきなり書き始められる
20/144
最初の全部⼊りテーブル設計
カラム データ例
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
comments 1: 名無しさん < sage > 2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage > 2015-11-30 22:39:04
13⽇のストラトス
21/144
参照系クエリー
新しく作成された順にスレッド⼀覧を表⽰するクエリー
SELECT thread_title
FROM bbs
ORDER BY thread_created DESC
22/144
参照系クエリー
コメント数の多い順にスレッド⼀覧を表⽰するクエリー
SELECT thread_title
FROM bbs
ORDER BY comment_count DESC
23/144
参照系クエリー
新しくコメントされた順にスレッド⼀覧を表⽰するクエリー
SELECT thread_title
FROM bbs
ORDER BY last_posted DESC
24/144
参照系クエリー
スレッドを詳細表⽰するクエリー
SELECT thread_title,
thread_owner,
thread_owner_email,
thread_created,
comment_count,
last_posted,
comments
FROM bbs
WHERE thread_title = 'new_thread'
25/144
SELECTは特
に問題なさそ
うに⾒える︖
26/144
更新系クエリー
スレッドを追加するためのクエリー
INSERT INTO bbs SET thread_title= 'new_thread',
thread_owner= 'yoku0825',
thread_owner_email= 'yoku0825@gmail.com',
thread_created= '2015/04/30 19:00:00',
comment_count= 1,
last_posted= '2015/04/30 19:00:00',
comments= '1: yoku0825 <yoku0825@gmail.com> 2015-04-30 19:00:00nThis is my first thread!'
27/144
更新系クエリー
コメントを追加するクエリー
UPDATE bbs SET comment_count = comment_count + 1,
last_posted = NOW(),
comments = CONCAT(comments, 'nn',
'2: yoku0825 <yoku0825@gmail.com> 2015-04-30 19:01:00n2 get!')
WHERE thread_title= 'new_thread'
28/144
更新系クエリー
それとも、こう︖
UPDATE bbs SET comment_count = 2,
last_posted= '2015-04-30 19:01:00',
comments = '1: yoku0825 <yoku0825@gmail.com> 2015-04-30 19:00:01n
This is my first thread!nn
2: yoku0825 <age> 2015-04-30 19:01:00n
2 get!'
WHERE thread_title = 'new_thread';
29/144
雲⾏きが怪し
くなってきま
したね :)
30/144
コメントの削除
どうやる︖
アプリ側で⽂字列探査して目的のコメントを削除してUPDATEする
(last̲postedもがんばる)
そんなばかな
31/144
第1非正規形
SELECTする分には特に問題なく⾒える
というかとてもシンプルで良いようにすら⾒える
JOINもGROUP BYも出てこない
-
更新処理が破綻している
⽂字列操作で追加、削除-
⽂字列操作はSQL外の範疇になるので、RDBMSの利点をほとんど受
けられない
-
32/144
それでも正規化せ
ずに使いますか︖
[No/いいえ]
33/144
正規化とは
テーブルを リレーショナルモデルのお作法に基づいた形 に作り変える作
業
SQLはリレーショナルモデルに基づいた操作を提供する⾔語
RDBMSの機能を(フルに)使うためにはSQLを使わなければいけない
SQL(だけ)で綺麗に操作するためには正規化が必要
34/144
なんて⾔ってても
わかりづらいので
実際に正規化して
みましょう
35/144
第1正規形
テーブルの中には個々の⾏を⼀意に識別できる候補キーが存在すること
テーブルの全ての要素は
NULLでないこと
宗教問題に近いので、軽く流します
-
アトミックである-
アトミックでない場合はアトミックになるように分割する
36/144
全ての⾏は⼀意に識別されなければいけない
カラム データ例
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
comments 1: 名無しさん < sage > 2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage > 2015-11-30 22:39:04
13⽇のストラトス
このテーブルに格納された⾏を⼀意に識別するものは︖
37/144
thread̲title︖
カラム データ例
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
comments 1: 名無しさん < sage > 2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage > 2015-11-30 22:39:04
13⽇のストラトス
同じタイトルのスレッドは存在できなくなる。
38/144
thread̲id的なもの︖
カラム データ例
thread̲id 1
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
comments 1: 名無しさん < sage > 2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage > 2015-11-30 22:39:04
13⽇のストラトス
タイトルが同じだろうといくらでも投稿できる。実質制限なし。
39/144
PRIMARY KEYを複雑化する︖
カラム データ例
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
comments 1: 名無しさん < sage > 2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage > 2015-11-30 22:39:04
13⽇のストラトス
タイトル, 作成者, 作成者のメールアドレスまで⼀致したら重複スレッド
として扱う
40/144
どれが正解︖
同じタイトルのスレッドは存在できない
タイトルが同じでもいくらでも投稿できる
ある程度の情報が⼀致したら重複として扱って投稿できない
正解はどれでも良くて、これは要件との兼ね合いになる。
41/144
要件を思い出す
掲⽰板にはスレッド単位で話題が上がって
それに対してコメントが付く形-
1スレッドの上限コメント数は制限したい-
スレッドを⽴てるにはメールアドレスを必須にしたい-
スレッドの⼀覧出⼒機能をつけたい
スレの作成順にソートしたい-
最終書き込み時刻でソートしたい-
スレの書き込み数でもソートしたい-
スレッドのタイトルの横にコメント数を表⽰したい-
42/144
スレッドの重複に関する要件はない
でもどれかのプライマリーキーを選ばないと、同じタイトルのスレッドが
存在した時に機能が破綻する
要件を追加しなければならない
43/144
追加されるであろう要件
重複したスレッドの作成を
禁⽌する
タイトルが同じなら重複として作成させない => PRIMARY KEY
(thread̲title)
タイトルよりは複雑だけど重複判定はする => PRIMARY KEY
(thread̲title, thread̲owner, thread̲owner̲email)
-
禁⽌しない => PRIMARY KEY(thread̲id)-
テーブル設計と要件定義は密接に関わっている
44/144
今回は重複スレッ
ドを禁⽌しない
(thread̲id⽅式)
を選んでみる
45/144
第1正規形の話に戻ると
テーブルの中には個々の⾏を⼀意に識別できる候補キーが存在すること
テーブルの全ての要素は
NULLでないこと
宗教問題に近いので、軽く流します
-
アトミックである-
アトミックでない場合はアトミックになるように分割する
46/144
テーブルの要素
NULLではないこと
NOT NULL制約つけましょう
テストケースが膨⼤に増える
TRUE, FALSEの2値なら条件網羅テストケースは 2 ^ カラム数
TRUE, FALSE, NULLの3値だと条件網羅テストケースは 3 ^ カラ
ム数
演算に慣れていないと問題の分析に時間がかかる
パッと⾔えますか︖
TRUE and NULL => NULL
FALSE and NULL => FALSE
TRUE or NULL => TRUE
FALSE or NULL => NULL
-
47/144
テーブルの要素
アトミックであること
1つの⾏の1つのカラムに複数の要素を詰め込まない こと。-
これからしばらく気持ち悪いテーブル構成が並びますが、正規化の途
中ということで⼤目に⾒てください。
-
48/144
1つの要素に複数の要素を詰め込んではいけない
カラム データ例
thread̲id 1
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
comments 1: 名無しさん < sage > 2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage > 2015-11-30 22:39:04
13⽇のストラトス
” n  n”で区切られた 複数の コメントがcommentsカラムの1つの要素
に詰め込まれている
49/144
⾏を分割して1⾏1コメントになるようにする
カラム データ例1 データ例2
thread̲id 1 1
thread̲title コードギアス亡国の巨⼈ コードギアス亡国の巨⼈
thread̲owner yoku0825 yoku0825
thread̲owner̲email yoku0825@gmail.com yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02 2015-11-30 22:39:02
comment̲count 10 10
last̲posted 2015-11-30 22:39:12 2015-11-30 22:39:12
comments 1: 名無しさん < sage >
2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage >
2015-11-30 22:39:04
13⽇のストラトス
このプライマリーキーだと1スレッドに複数コメントがつけられない。
50/144
プライマリーキーを伸ばす
カラム データ例1 データ例2
thread̲id 1 1
thread̲title コードギアス亡国の巨⼈ コードギアス亡国の巨⼈
thread̲owner yoku0825 yoku0825
thread̲owner̲email yoku0825@gmail.com yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02 2015-11-30 22:39:02
comment̲count 10 10
last̲posted 2015-11-30 22:39:12 2015-11-30 22:39:12
one̲comment 1: 名無しさん < sage >
2015-11-30 22:39:03
殺⼈教室
2: 名無しさん < sage >
2015-11-30 22:39:04
13⽇のストラトス
これだとプライマリーキーが⼀意に識別するのは “あるスレッドの” “ある
コメント”になる
意味が変わってしまっているので、本当はこの時点でテーブル分割が望ま
しい(が、説明のためこの時点では分割せずいきます)
51/144
この段階での参照クエリー
新しく作成された順にスレッド⼀覧を表⽰するクエリー
SELECT thread_id /* アンカータグでリンクを張るのに使う */, thread_title
FROM bbs
ORDER BY thread_created DESC
スレッドを詳細表⽰するクエリー
SELECT thread_title,
thread_owner,
thread_owner_email,
thread_created,
comment_count,
last_posted,
GROUP_CONCAT(one_comment ORDER BY one_comment ASC SEPARATOR 'n') AS comments
FROM bbs
WHERE thread_id = 1 /* スレッド一覧からthread_idをパラメーターでもらう */
GROUP̲CONCATはMySQL独⾃関数なので、アプリ側でループした⽅が
良い。
52/144
この段階での更新系クエリー
スレッドを追加するためのクエリー
INSERT INTO bbs SET thread_id= 1,
thread_title= 'new_thread',
thread_owner= 'yoku0825',
thread_owner_email= 'yoku0825@gmail.com',
thread_created= '2015/04/30 19:00:00',
comment_count= 1,
last_posted= '2015/04/30 19:00:00',
one_comment= '1: yoku0825 <yoku0825@gmail.com> 2015-04-30 19:00:00n
This is my first thread!'
53/144
この段階での更新系クエリー
コメントを追加するクエリー
INSERT INTO bbs SET thread_id= 1,
thread_title= 'new_thread',
thread_owner= 'yoku0825',
thread_owner_email= 'yoku0825@gmail.com',
thread_created= '2015/04/30 19:00:00',
comment_count= 2,
last_posted= '2015/04/30 19:01:00',
one_comment= '2: yoku0825 <yoku0825@gmail.com> 2015-04-30 19:01:00n
2 get!'
コメントを追加するためだけにスレッドの情報もINSERT⽂に⼊れないと
いけないの︖
54/144
この段階での更新系クエリー
コメントの削除
DELETE
FROM bbs
WHERE thread_id= 1 AND one_comment= '2: yoku0825 <yoku0825@gmail.com> 2015-04-30 19:01:00n2 get!';
WHERE句が不恰好ではあるものの、⼀応SQLでコメントを削除できるよ
うになった。
55/144
これで第1正規形︖
まだ書式によって区切られた複数の意味(コメント番号, コメント主,
email, コメント⽇付, コメントの本⽂)が1つの要素に詰め込まれている
= アトミックではない
とはいえこの時点でコメントの投稿はINSERT, コメントの削除は
DELETEで直観的に操作できるようになった
56/144
カラムを分割してアトミックになるようにする
カラム データ例1 データ例2
thread̲id 1 1
thread̲title コードギアス亡国の巨⼈ コードギアス亡国の巨⼈
thread̲owner yoku0825 yoku0825
thread̲owner̲email yoku0825@gmail.com yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02 2015-11-30 22:39:02
comment̲count 10 10
last̲posted 2015-11-30 22:39:12 2015-11-30 22:39:12
comment̲number 1 2
comment̲owner 名無しさん 名無しさん
comment̲owner̲email sage sage
comment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04
comment̲body 殺⼈教室 13⽇のストラトス
one̲commentの代わりにcomment̲numberをプライマリーキーの⼀
部として差し替え。
57/144
変化のあったクエリー
コメントを追加するクエリー
INSERT INTO bbs SET thread_id= 1,
thread_title= 'new_thread',
thread_owner= 'yoku0825',
thread_owner_email= 'yoku0825@gmail.com',
thread_created= '2015/04/30 19:00:00',
comment_count= 2,
last_posted= '2015/04/30 19:01:00',
comment_number= 2,
comment_owner = 'yoku0825',
comment_owner_email = 'yoku0825@gmail.com',
comment_posted = '2015-04-30 19:01:00',
comment_body = '2 get!'
コメントの削除
DELETE
FROM bbs
WHERE thread_id= 1 AND comment_number= 2
58/144
これで第1正規形
レコードが⼀意に識別できる
要素にNULLを含まない
要素がアトミックである
59/144
第1正規化することで変わったこと
コメントの削除が(SQLで)できるようになった
コメントの投稿がシンプルなINSERT⽂に
その気になればコメントの内容をUPDATEで編集することもできる
コメント追加のINSERTのために、スレッド本体の情報も持たせないとい
けないの︖
スレッドのタイトルが編集できたとしたら、たくさんの⾏を更新しないと
いけなくない︖
60/144
正規化とは(再掲)
テーブルを リレーショナルモデルのお作法に基づいた形 に作り変える作
業
SQLはリレーショナルモデルに基づいた操作を提供する⾔語
RDBMSの機能を(フルに)使うためにはSQLを使わなければいけない
SQL(だけ)で綺麗に操作するためには正規化が必要
61/144
RDBMSの機能
トランザクションによるACID保証
制約によるデータの保証
集計関数
RDBMSの恩恵を しっかり受けるために 正規化する
RDBMS の外側 でやるところには正規化という概念はない
62/144
ひとやす
み
63/144
正規形の種類
非正規形 (?)
第1正規形
第2正規形
第3正規形
BC正規形
第4正規形
第5正規形
第6正規形
64/144
第2正規形と第3正
規形の違いを知っ
ていますか︖
[Yes/No]
65/144
第2正規形と第3正規形の違い
第2正規形
プライマリーキーの ⼀部 だけで決まる要素が存在しない-
第3正規形
非キー要素から⼀意に決まる要素が存在しない-
どちらも存在する場合はテーブルを分割する
66/144
プライマリーキーの ⼀部 #とは
プライマリーキーが単⼀カラムで構成されている場合、 プライマリーキ
ーの”⼀部” は存在しないので⾃動的に第2正規形
(thread̲id, comment̲number)の⼀部から⼀意に決まる値はあるか︖
thread̲idが決まればthread̲title, thread̲owner,
thread̲owner̲email, thread̲created, comment̲count,
last̲postedが決まる
-
thread̲owner, comment̲ownerが決まれば
thread̲owner̲email, comment̲owner̲emailも決まるはずだけ
ど、これは プライマリーキーの⼀部ではない のでここでは分割しな
い
-
67/144
分割
threadテーブル
カラム データ例1
thread̲id 1
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲owner̲email yoku0825@gmail.com
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
thread̲idから決まるスレッドごとの属性情報をthreadテーブルとして分
離。
68/144
分割
commentテーブル
カラム データ例1 データ例2
thread̲id 1 1
comment̲number 1 2
comment̲owner 名無しさん 名無しさん
comment̲owner̲email sage sage
comment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04
comment̲body 殺⼈教室 13⽇のストラトス
thread̲id, comment̲numberから決まるコメント単位の情報を
commentテーブルに分離。
69/144
参照クエリー
新しく作成された順にスレッド⼀覧を表⽰するクエリー
SELECT thread_id /* アンカータグでリンクを張るのに使う */, thread_title
FROM thread
ORDER BY thread_created DESC
スレッドを詳細表⽰するクエリー
SELECT thread.thread_title,
thread.thread_owner,
thread.thread_owner_email,
thread.thread_created,
thread.comment_count,
thread.last_posted,
comment.comment_number,
comment.comment_owner,
comment.comment_owner_email,
comment.comment_posted,
comment.comment_body
FROM thread JOIN comment USING (thread_id)
WHERE thread.thread_id = 1 /* スレッド一覧からthread_idをパラメーターでもらう */
ORDER BY comment.comment_number
70/144
更新クエリー
スレッドを追加するためのクエリー
INSERT INTO thread SET thread_id= 1,
thread_title= 'new_thread',
thread_owner= 'yoku0825',
thread_owner_email= 'yoku0825@gmail.com',
thread_created= '2015/04/30 19:00:00',
comment_count= 1,
last_posted= '2015/04/30 19:00:00'
INSERT INTO comment SET thread_id= 1,
comment_number= 1,
comment_owner= 'yoku0825',
comment_owner_email= 'yoku0825@gmail.com',
comment_posted= '2015/04/30 19:00:00',
comment_body= 'This is my first thread!'
71/144
更新クエリー
コメントを追加するクエリー
INSERT INTO comment SET thread_id= 1,
comment_number= 1,
comment_owner= 'yoku0825',
comment_owner_email= 'yoku0825@gmail.com',
comment_posted= '2015/04/30 19:00:00',
comment_body= 'This is my first thread!'
UPDATE thread SET comment_count= comment_count + 1,
last_posted = '2015/04/30 19:00:00'
WHERE thread_id = 1
72/144
これで第2正規形
プライマリーキーの⼀部から⼀意に決まる値は別のテーブルに分割する
その時の”プライマリーキーの⼀部”が分割先テーブルのプライマリー
キーになる
-
73/144
第2正規化することで変わったこと
スレッドはスレッド、コメントはコメントで独⽴したテーブルに⼊ったの
で
コメントを追加するためだけにスレッドの情報を何度も何度も
INSERTしなくてよくなった
-
コメントのないスレッドを作ることが出来てしまう-
スレッドのタイトルを変えるようなことがあっても、コメントのテー
ブルには影響を及ぼさない
-
新しいスレッドを作成するために、スレッドの作成と最初のコメント
を別々にINSERTする必要がある。
-
新しいコメントを投稿するたびに、スレッドの更新時刻なども更新し
ないといけない。
-
74/144
第2正規形と第3正規形の違い
第2正規形
プライマリーキーの ⼀部 だけで決まる要素が存在しない-
第3正規形
非キー要素から⼀意に決まる要素が存在しない-
どちらも存在する場合はテーブルを分割する
75/144
非キー属性から⼀意に決まる #とは
thread̲ownerが決まればthread̲owner̲emailが決まる︖
comment̲ownerが決まればcomment̲owner̲emailが決まる︖
ここでも要件との兼ね合い(ユーザー登録させるの︖ とか)になってくる
決まることにして進めます
決まらない場合、これは非キー属性から ⼀意に決まらない ので、分
割せずに第3正規形ということになります。
-
76/144
分割
threadテーブル
カラム データ例1
thread̲id 1
thread̲title コードギアス亡国の巨⼈
thread̲owner yoku0825
thread̲created 2015-11-30 22:39:02
comment̲count 10
last̲posted 2015-11-30 22:39:12
77/144
分割
commentテーブル
カラム データ例1 データ例2
thread̲id 1 1
comment̲number 1 2
comment̲owner 名無しさん 名無しさん
comment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04
comment̲body 殺⼈教室 13⽇のストラトス
78/144
分割
userテーブル
カラム データ例1 データ例2
user̲id yoku0825 名無しさん
user̲email yoku0825@gmail.com sage
79/144
これで第3正規形
プライマリーキー以外のカラムが決まると⾃動的に決まるカラムがあった
場合はテーブルを分割する
この時の変数になるカラムが、分割先テーブルのプライマリーキー-
80/144
第3正規化することで変わったこと
スレッドを⽴てたことのない、コメントを投稿したことのないユーザーが
存在できるようになった
先にユーザー登録をしておかなくてはいけなくなった-
ユーザーがメールアドレスを変更してもスレッドテーブルやコメントテー
ブルを操作する必要はなくなった
81/144
BC正規形
非キー要素からプライマリーキーの⼀部が⼀意に決まる場合はテーブル分
割
第2正規化の⽮印が逆になったパターン
この掲⽰板テーブルは第3正規化した時点でBC正規形でもある
82/144
第4正規形, 第5正規形
複合プライマリーキー のみ で構成されたテーブルの分割をする
あんまりお目にかからなくて済む(第3正規形を作って、複合プライマリー
キーのみのテーブルができることは少ない)
このスキーマは第3正規化した時点で第5正規形(複合プライマリーキーだ
けのテーブルがないから)
83/144
個⼈的テーブル設計まとめ
必要なものを全部洗いだしてから分割していく⽅が楽
プライマリーキーに着目して、
このテーブルでは”何を主体として扱うのか”-
その属性は”どの主体の属性なのか”-
でテーブルを振り分ける-
84/144
正規化の話おしまい
ここからこの掲⽰板の
話はしばらく忘れます
85/144
だいじなこと
⼀番⼤事なのは 第1正規化
カラムの中⾝を関数処理して取り出さないといけないようなものはあらか
じめカラムを分けて設計すること
関数処理を通すとインデックスが最適な形で使えない-
アトミックでないものを⼊れるなら、検索条件にしたり集計対象にしたり
してはいけない
単にDurabilityを保証できるBLOBとして使うのも(オススメしないけ
ど)ありといえばあり
-
常にアプリケーション側でデシリアライズするならJSONとかが⼊っ
ててもいい
-
86/144
ひとやす
み
87/144
都市伝説
MySQLを使うなら 遅くなるから 正規化してはいけない
確かに遅くなる ケースも ある(特に、5.5より前の古いバージョン)
何故 RDBMSを扱う上で正しい はずの正規化でRDBMSであるMySQLの
レスポンスが悪くなるのか︖
MySQLはJOINが遅い︖
正規化のプロセスを⾒てきましたけど、この通りに操作していたら
プライマリーキーで結合しますよね︖
プライマリーキーを使った結合なら ⼤概の場合 ちゃんとした速度
で動きますが、NLJなので数百万件に達すると遅くなります。
JOINすると遅くなるのは多くの場合 ソート 、きれいにインデック
スを使いきれていないケースです。
-
88/144
前提
判りやすく説明するために多少の嘘を含んでいます。
MySQLは原則 1つのテーブルにつき同時に1つのインデックスしか使えま
せん。
MySQLはおばかさんだと⾔われる所以。。-
他のDBMSは知りませんが、使えるらしいです。-
説明のモデルは5.5がベースです。
5.6, 5.7とオプティマイザーはかなりよくなっています。-
89/144
インデックスの設計
インデックスとは
ソート済みの-
ある程度のメタデータを持たせた-
データの複製(サブセット)-
データの複製であるため、
インデックスだけで必要な情報が集まる場合は、テーブル本体をスキ
ャンしなくても良い(covering index)
-
更新系のDML(INSERT, UPDATE, DELETE)の際には常にオーバーヘ
ッドが加わる
-
テーブル本体の容量とは別に容量を必要とする-
90/144
カーディナリティーとは
そのインデックスに含まれる”取りうる値のバリエーション”の数
均等に分散した都道府県のカーディナリティーは47
都府なら3
-
⼤きければ⼤きいほど、そのインデックスを使って絞り込んだ時に返
ってくる結果セットが⼩さくなることが 期待できる
均等に分散しているなら、カーディナリティーが47のインデック
スで抽出した場合の期待値は 総レコード件数 / 47
ただし、レコードが本当に均等に分散しているとは限らない
1億3000万レコードが都道府県カラムを持っていたとしても、東京
と島根ではレコード件数に差が出る
-
91/144
インデックスを張っていないカラムでWHEREした時の動作をトラ
ンプでたとえてみると
ここに100枚のトランプの束があります
何が何枚あるかはわかりません
⼊ってない札があるかもしれません
この中からハートのAを全て⾒つけ出してください
SQLで書くと、SELECT * FROM card WHERE suite= 'heart' AND
number= 1
92/144
どうや
る︖
93/144
こうやる
トランプの⼀番上の札をめくって
それがハートのAかどうか確認して
ハートのAなら左⼿にとっておいて
違ったらテーブルに伏せておいて
それを100枚目まで繰り返す
94/144
これがテーブルスキャン
mysql55> explain SELECT * FROM card WHERE suite= 'heart' AND number= 1;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | card | ALL | NULL | NULL | NULL | NULL | 100 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.02 sec)
type: ALL がテーブルスキャン(=テーブルの先頭から末尾まで全スキャン
)を,
rows: 100が100⾏フェッチしたことを⽰している
95/144
テーブルスキャン
全ての⾏をフェッチして
WHERE条件にマッチするかどうか判定して
マッチしたらバッファに詰めて
違ったらスキップして
それをテーブルの末尾まで繰り返す
テーブルに格納されたデータに応じて線形に負荷が増える
96/144
インデックスを例えると
トランプの束とは別に、こんな表をあらかじめ作っておくイメージ。
KEY(suite, number)に相当-
suite number 上から
clover 1 25, 93
clover 2 66
..
heart 1 91, 100
..
この表を⾒ると、上から91枚目と100枚目がハートのAであることがわか
るので、
あとは上から91枚目, 100枚目をめくって裏を返せばいいだけ。
97/144
実際のインデックス
mysql55> explain SELECT * FROM card WHERE suite= 'heart' AND number= 1;
+----+-------------+-------+------+------------------+------------------+---------+-------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+------------------+------------------+---------+-------------+------+--------------------------+
| 1 | SIMPLE | card | ref | idx_suite_number | idx_suite_number | 51 | const,const | 2 | Using where; Using index |
+----+-------------+-------+------+------------------+------------------+---------+-------------+------+--------------------------+
type: refがインデックスが使えていることを⽰す(他にもパターンがある
)
key: idx_suite_numberが使っているインデックスを⽰す
key_len: 51がキーの先頭51バイトを有効利⽤していることを⽰す
varchar(16) * 3bytes(UTF-8) + 2byte(padding) + 1byte
(tinyint)
-
rows: 2で、2⾏(含まれているハートのA)だけピンポイントにフェッチし
たことを⽰す
98/144
検索条件の⼀部しか満たせないインデックスの場合(suiteのみ)
KEY(suite)に相当する索引だと
suite 上から
clover 10,11,22,24,25,29,35,39,49,52,54,55,61,63,66,68,7
5,88,93,96,99
diamond 1,2,3,5,6,7,13,16,17,19,21,23,27,46,50,56,58,60,65,
71,72,73,76,78,81,82,83,86,87,97
heart 4,8,12,30,33,37,40,44,45,47,51,53,64,84,85,90,91,9
2,94,98,100
spade 9,14,15,18,20,26,28,31,32,34,36,38,41,42,43,48,57,
59,62,67,69,70,74,77,79,80,89,95
上から4, 8, 12, 30, ..枚目を順番にひっくり返し、Aかどうかを確認して
バッファに詰める。
99/144