Movatterモバイル変換


[0]ホーム

URL:


yoku0825, profile picture
Uploaded byyoku0825
PDF, PPTX48,387 views

MySQLテーブル設計入門

2015/04/30 schoo WEB-campushttps://schoo.jp/class/2218

Embed presentation

Download as PDF, PPTX
MySQL⼊門7限目: テーブル設計について2015/04/30GMOメディア株式会社 yoku0825schoo WEB-campus
\こんにちは/yoku0825@GMOメディア株式会社オラクれない-ポスグれない-マイエスキューエる-家に帰ると妻の夫-せがれの⽗-娘の⽗-Twitter: @yoku0825Blog: ⽇々の覚書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)のデータを格納するための型-デフォルトのまま使っていると、⼤⽂字と⼩⽂字を区別 しない (CaseInsensitive)-現在のMySQLにおいてCHAR型を選ぶメリットはほぼない。迷ったらVARCHAR。-データサイズの側⾯でいうと、1⽂字に必要なバイト数は⽂字コードごとに違うので、n⽂字 * 1⽂字あたりのバイト数 + αが1⾏あたりの最⼤サイズになります。-12/144
⽂字列型TEXT, LONGTEXTTEXT型は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 yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12comments 1: 名無しさん < sage > 2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage > 2015-11-30 22:39:0413⽇のストラトス21/144
参照系クエリー新しく作成された順にスレッド⼀覧を表⽰するクエリーSELECT thread_titleFROM bbsORDER BY thread_created DESC22/144
参照系クエリーコメント数の多い順にスレッド⼀覧を表⽰するクエリーSELECT thread_titleFROM bbsORDER BY comment_count DESC23/144
参照系クエリー新しくコメントされた順にスレッド⼀覧を表⽰するクエリーSELECT thread_titleFROM bbsORDER BY last_posted DESC24/144
参照系クエリースレッドを詳細表⽰するクエリーSELECT thread_title,thread_owner,thread_owner_email,thread_created,comment_count,last_posted,commentsFROM bbsWHERE 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:01nThis is my first thread!nn2: yoku0825 <age> 2015-04-30 19:01:00n2 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 yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12comments 1: 名無しさん < sage > 2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage > 2015-11-30 22:39:0413⽇のストラトスこのテーブルに格納された⾏を⼀意に識別するものは︖37/144
thread̲title︖カラム データ例thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12comments 1: 名無しさん < sage > 2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage > 2015-11-30 22:39:0413⽇のストラトス同じタイトルのスレッドは存在できなくなる。38/144
thread̲id的なもの︖カラム データ例thread̲id 1thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12comments 1: 名無しさん < sage > 2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage > 2015-11-30 22:39:0413⽇のストラトスタイトルが同じだろうといくらでも投稿できる。実質制限なし。39/144
PRIMARY KEYを複雑化する︖カラム データ例thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12comments 1: 名無しさん < sage > 2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage > 2015-11-30 22:39:0413⽇のストラトスタイトル, 作成者, 作成者のメールアドレスまで⼀致したら重複スレッドとして扱う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 => NULLFALSE and NULL => FALSETRUE or NULL => TRUEFALSE or NULL => NULL-47/144
テーブルの要素アトミックであること1つの⾏の1つのカラムに複数の要素を詰め込まない こと。-これからしばらく気持ち悪いテーブル構成が並びますが、正規化の途中ということで⼤目に⾒てください。-48/144
1つの要素に複数の要素を詰め込んではいけないカラム データ例thread̲id 1thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12comments 1: 名無しさん < sage > 2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage > 2015-11-30 22:39:0413⽇のストラトス” n  n”で区切られた 複数の コメントがcommentsカラムの1つの要素に詰め込まれている49/144
⾏を分割して1⾏1コメントになるようにするカラム データ例1 データ例2thread̲id 1 1thread̲title コードギアス亡国の巨⼈ コードギアス亡国の巨⼈thread̲owner yoku0825 yoku0825thread̲owner̲email yoku0825@gmail.com yoku0825@gmail.comthread̲created 2015-11-30 22:39:02 2015-11-30 22:39:02comment̲count 10 10last̲posted 2015-11-30 22:39:12 2015-11-30 22:39:12comments 1: 名無しさん < sage >2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage >2015-11-30 22:39:0413⽇のストラトスこのプライマリーキーだと1スレッドに複数コメントがつけられない。50/144
プライマリーキーを伸ばすカラム データ例1 データ例2thread̲id 1 1thread̲title コードギアス亡国の巨⼈ コードギアス亡国の巨⼈thread̲owner yoku0825 yoku0825thread̲owner̲email yoku0825@gmail.com yoku0825@gmail.comthread̲created 2015-11-30 22:39:02 2015-11-30 22:39:02comment̲count 10 10last̲posted 2015-11-30 22:39:12 2015-11-30 22:39:12one̲comment 1: 名無しさん < sage >2015-11-30 22:39:03殺⼈教室2: 名無しさん < sage >2015-11-30 22:39:0413⽇のストラトスこれだとプライマリーキーが⼀意に識別するのは “あるスレッドの” “あるコメント”になる意味が変わってしまっているので、本当はこの時点でテーブル分割が望ましい(が、説明のためこの時点では分割せずいきます)51/144
この段階での参照クエリー新しく作成された順にスレッド⼀覧を表⽰するクエリーSELECT thread_id /* アンカータグでリンクを張るのに使う */, thread_titleFROM bbsORDER 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 commentsFROM bbsWHERE 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:00nThis 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:00n2 get!'コメントを追加するためだけにスレッドの情報もINSERT⽂に⼊れないといけないの︖54/144
この段階での更新系クエリーコメントの削除DELETEFROM bbsWHERE 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 データ例2thread̲id 1 1thread̲title コードギアス亡国の巨⼈ コードギアス亡国の巨⼈thread̲owner yoku0825 yoku0825thread̲owner̲email yoku0825@gmail.com yoku0825@gmail.comthread̲created 2015-11-30 22:39:02 2015-11-30 22:39:02comment̲count 10 10last̲posted 2015-11-30 22:39:12 2015-11-30 22:39:12comment̲number 1 2comment̲owner 名無しさん 名無しさんcomment̲owner̲email sage sagecomment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04comment̲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!'コメントの削除DELETEFROM bbsWHERE thread_id= 1 AND comment_number= 258/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テーブルカラム データ例1thread̲id 1thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲owner̲email yoku0825@gmail.comthread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12thread̲idから決まるスレッドごとの属性情報をthreadテーブルとして分離。68/144
分割commentテーブルカラム データ例1 データ例2thread̲id 1 1comment̲number 1 2comment̲owner 名無しさん 名無しさんcomment̲owner̲email sage sagecomment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04comment̲body 殺⼈教室 13⽇のストラトスthread̲id, comment̲numberから決まるコメント単位の情報をcommentテーブルに分離。69/144
参照クエリー新しく作成された順にスレッド⼀覧を表⽰するクエリーSELECT thread_id /* アンカータグでリンクを張るのに使う */, thread_titleFROM threadORDER 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_bodyFROM thread JOIN comment USING (thread_id)WHERE thread.thread_id = 1 /* スレッド一覧からthread_idをパラメーターでもらう */ORDER BY comment.comment_number70/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 = 172/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テーブルカラム データ例1thread̲id 1thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:1277/144
分割commentテーブルカラム データ例1 データ例2thread̲id 1 1comment̲number 1 2comment̲owner 名無しさん 名無しさんcomment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04comment̲body 殺⼈教室 13⽇のストラトス78/144
分割userテーブルカラム データ例1 データ例2user̲id yoku0825 名無しさんuser̲email yoku0825@gmail.com sage79/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' ANDnumber= 192/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, 93clover 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,75,88,93,96,99diamond 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,97heart 4,8,12,30,33,37,40,44,45,47,51,53,64,84,85,90,91,92,94,98,100spade 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
検索条件の⼀部しか満たせないインデックスの場合(suiteのみ)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 | idx_suite | 50 | const | 21 | Using where |+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------------+さっきと概ね同じだが、rows: 21 なので、21枚ひっくり返した(=⾏をフェッチした)ことが読み取れる。100/144
検索条件の⼀部しか満たせないインデックスの場合(numberのみ)KEY(number)に相当する索引だとnumber 上から1 18,25,26,57,65,82,87,91,93,1002 7,15,43,66,74,843 2,9,13,24,28,30,68,694 16,34,40,54,905 1,36,51,64,72,78,966 3,11,19,27,44,70,927 4,12,49,52,59,60,83,88,988 17,37,38,42,47,48,62,63,67,79,95,999 8,14,21,29,41,56,58,71,73,77,8510 6,20,23,39,50,8111 22,35,46,61,86,9712 10,31,32,33,53,75,80,9413 5,45,55,76,89上から18, 25, 26, ..枚目を順番にひっくり返し、ハートかどうかを確認してバッファに詰める。101/144
検索条件の⼀部しか満たせないインデックスの場合(numberのみ)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_number | idx_number | 1 | const | 10 | Using where |+----+-------------+-------+------+---------------+------------+---------+-------+------+-------------+key_len: 1になっているのは、suiteはUTF-8なvarchar型でサイズが⼤きいのに対し、numberはtinyint型なのでサイズが⼩さいため。rows: 10なので、 どちらか⽚⽅だけしか作れないとしたら 、こっちの⽅が さっきのクエリーに対しては 優秀ということもできる︖102/144
またトランプに戻るここに100枚のトランプの束があります何が何枚あるかはわかりません⼊ってない札があるかもしれませんこの中から全てのハートのカードをKからAまで順番に並べてください。SQLで書くと、SELECT * FROM cards WHERE suite= 'heart' ORDER BYnumber DESC;103/144
どうやる︖104/144
こうやるトランプの⼀番上の札をめくってそれがハートかどうか確認してハートのAなら左⼿にとっておいてそれを100枚目まで繰り返したあと左⼿に取っておいたハートだけの束を先頭から1枚ずつ繰って並べ替える105/144
これがスキャンソートmysql55> explain SELECT * FROM card WHERE suite= 'heart' ORDER BY number DESC;+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+| 1 | SIMPLE | card | ALL | NULL | NULL | NULL | NULL | 100 | Using where; Using filesort |+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+Extra: Using filesortが⾏フェッチ後にクイックソートしていることを表す表⽰。106/144
WHEREもORDER BYもインデックスだけで解決できるパターンKEY(suite, number)の場合suite number 上からheart 1 91,100heart 2 84heart 3 30heart 4 40,90heart 5 51,64heart 6 44,92heart 7 4,12,98heart 8 37,47heart 9 8,85heart 12 33,53,94heart 13 45⼭の上から45枚目のKを左⼿に詰め、33, 53, 94枚目のQをその上に置きJと10がないのはこの表だけでわかり8, 85枚目の9をその上に置き…というのを繰り返して最後にAを上に置けばいい107/144
インデックスはソート済みの データの複製108/144
実⾏計画上はこうなるmysql55> explain SELECT * FROM card WHERE suite= 'heart' ORDER BY number DESC;+----+-------------+-------+------+------------------+------------------+---------+-------+------+--------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+------------------+------------------+---------+-------+------+--------------------------+| 1 | SIMPLE | card | ref | idx_suite_number | idx_suite_number | 50 | const | 21 | Using where; Using index |+----+-------------+-------+------+------------------+------------------+---------+-------+------+--------------------------+Using filesortが消えた。ハートの21枚だけをフェッチ、ソートもしていないので快適。109/144
WHEREのみインデックスで解決できるパターンKEY(suite)の場合suite 上からclover 10,11,22,24,25,29,35,39,49,52,54,55,61,63,66,68,75,88,93,96,99diamond 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,97heart 4,8,12,30,33,37,40,44,45,47,51,53,64,84,85,90,91,92,94,98,100spade 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,954, 8, 12, 30, ..枚目を左⼿に詰めて全部ハートのカードを取り終わったらハートの束をKから順にAまで並べ替える110/144
WHEREのみインデックスで解決できるパターンmysql55> explain SELECT * FROM card WHERE suite= 'heart' ORDER BY number DESC;+----+-------------+-------+------+---------------+-----------+---------+-------+------+-----------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+---------------+-----------+---------+-------+------+-----------------------------+| 1 | SIMPLE | card | ref | idx_suite | idx_suite | 50 | const | 21 | Using where; Using filesort |+----+-------------+-------+------+---------------+-----------+---------+-------+------+-----------------------------+rows: 21で21枚フェッチした(ひっくり返した)ことがわかるUsing filesortされているので、フェッチした21枚をその後並べ替えている。111/144
ORDER BYのみインデックスで解決できるパターンKEY(number)の場合number 上から1 18,25,26,57,65,82,87,91,93,1002 7,15,43,66,74,843 2,9,13,24,28,30,68,694 16,34,40,54,905 1,36,51,64,72,78,966 3,11,19,27,44,70,927 4,12,49,52,59,60,83,88,988 17,37,38,42,47,48,62,63,67,79,95,999 8,14,21,29,41,56,58,71,73,77,8510 6,20,23,39,50,8111 22,35,46,61,86,9712 10,31,32,33,53,75,80,9413 5,45,55,76,895, 45, 55, .. 枚目のKをめくり、ハートだったら左⼿に、そうでなければ机に伏せて、10, 31, 32, 33枚目のQをめくり、ハートだったら左⼿に、そうでなければ机に伏せて…を繰り返してAまでやればおしまい112/144
ORDER BYのみインデックスで解決できるパターンmysql55> explain SELECT * FROM card WHERE suite= 'heart' ORDER BY number DESC;+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+| 1 | SIMPLE | card | ALL | NULL | NULL | NULL | NULL | 100 | Using where; Using filesort |+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+テーブルスキャンになっちゃった(´・ω・`)並べ替えはかっ⾶ばせても、100枚全部フェッチしなきゃいけないなら、セカンダリーキーを使わずに最初からテーブルスキャンした⽅が速いとオプティマイザーが判断。わざわざ表をチェックして開く順番を⾒てから100枚もひっくり返さないといけないのは確かに⾺⿅⾺⿅しい。。-113/144
インデックスでソートのみ無効化して⾼速化が⾒込めるケースmysql55> explain SELECT * FROM card FORCE INDEX(idx_number) WHERE suite= 'heart' ORDER BY number DESC LIMIT 5;+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+| 1 | SIMPLE | card | index | NULL | idx_number | 1 | NULL | 5 | Using where |+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+並べ替えの結果5件だけ戻すような場合。先頭から順番に取っていって、WHERE句にマッチするものが5件あつまった時点で残りの束を⾒る必要がなくなるため、⾏のフェッチ(ひっくり返して中を⾒る)動作はその時点でブレークできる。なのでEXPLAINで⾒ると微妙な感じがするが、件数が多くなれば多くなるほど実感できるほど⾼速化する。5.6以降のオプティマイザーは⾃動で選ぶことがあるが、5.5以前のオプティマイザーはまず間違いなくこの実⾏計画を選んでくれないLIMIT句はexecutorの範疇で、optimizerを通過した後のステージだから…という。-114/144
複合インデックスの弱点左側のカラムだけ使おうとした場合mysql55> explain SELECT * FROM card WHERE suite= 'heart';+----+-------------+-------+------+------------------+------------------+---------+-------+------+--------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+------------------+------------------+---------+-------+------+--------------------------+| 1 | SIMPLE | card | ref | idx_suite_number | idx_suite_number | 50 | const | 21 | Using where; Using index |+----+-------------+-------+------+------------------+------------------+---------+-------+------+--------------------------+右側のカラムだけ使おうとした場合mysql55> explain SELECT * FROM card WHERE number = 1;+----+-------------+-------+-------+---------------+------------------+---------+------+------+--------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+-------+---------------+------------------+---------+------+------+--------------------------+| 1 | SIMPLE | card | index | NULL | idx_suite_number | 51 | NULL | 100 | Using where; Using index |+----+-------------+-------+-------+---------------+------------------+---------+------+------+--------------------------+右側のカラムだけ使おうとしたのはtype: indexでインデックスの全体をスキャンしている。SELECT suite, number, GROUP_CONCAT(seq ORDER BY seq) AS 上からFROM card GROUP BY 1, 2; の結果を⾒ると何となく想像がつくはず。115/144
よくある「複合インデックスが思った通りに使えないパターン」OR演算⼦IN演算⼦不等号演算⼦ASC, DESCの混在JOINの結合順序116/144
JOINのケースに絞って引き続きトランプで裏⾯が⾚いトランプと 裏⾯が⿊いトランプが何が何枚⼊ってるのか全くわからない状態で100枚ずつの束になっています裏⾯が⾚いトランプの⼭からハートのカードだけを取り出して裏⾯が⿊いトランプの⼭に同じカードが含まれるか確認して両⽅に含まれていることが判ったら、裏⾯が⿊いトランプを机の上に並べて最後にA〜Kの順に並べた上で若い⽅から順番に5枚返すSQLで書くと、SELECT black_card.suite, black_card.number FROMred_card JOIN black_card USING (suite, number) WHEREred_card.suite= 'heart' ORDER BY black_card.number LIMIT 5トランプだと物理的な枚数に制約があるので不適だったかもしれない。-117/144
どうするか裏⾯が⾚いトランプの⼭の⼀番上のカードをめくってハートかどうか確認ハートでなければ、次の1枚をめくる-裏⾯が⿊いトランプの⼭の⼀番上から1枚ずつ順番に裏⾯が⾚いトランプのものと同じカードを探し机に置いておいて最後まで探し終えたら、裏⾯が⾚いトランプの⼭の次のカードをめくって裏⾯が⿊いトランプの⼭の⼀番上から1枚ずつ順番に…これを、裏⾯が⾚いトランプの⼭の最後の1枚まで繰り返したあと、机に置いてある裏⾯が⿊いトランプを並べ替えて、若い⽅から5枚取って返す118/144
スキャンジョインmysql55> explain SELECT black_card.suite, black_card.number FROM red_card JOIN black_card USING (suite, number) WHERE red_card.suite= 'heart' ORDER BY black_card.number LIMIT 5;+----+-------------+------------+------+---------------+------+---------+------+------+----------------------------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+------------+------+---------------+------+---------+------+------+----------------------------------------------+| 1 | SIMPLE | red_card | ALL | NULL | NULL | NULL | NULL | 100 | Using where; Using temporary; Using filesort || 1 | SIMPLE | black_card | ALL | NULL | NULL | NULL | NULL | 100 | Using where; Using join buffer |+----+-------------+------------+------+---------------+------+---------+------+------+----------------------------------------------+119/144
インデックス︖WHERE red̲card.suite = ʻheartʼ を解決するためにred_card ADD KEYidx_suite(suite)black̲card側のsuite, numberの結合条件を解決するためにblack_cardADD KEY idx_suite_number(suite, number)mysql55> explain SELECT black_card.suite, black_card.number FROM red_card JOIN black_card USING (suite, number) WHERE red_card.suite= 'heart' ORDER BY black_card.number LIMIT 5;+----+-------------+------------+------+------------------+------------------+---------+--------------------------+------+----------------------------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+------------+------+------------------+------------------+---------+--------------------------+------+----------------------------------------------+| 1 | SIMPLE | red_card | ref | idx_suite | idx_suite | 50 | const | 17 | Using where; Using temporary; Using filesort || 1 | SIMPLE | black_card | ref | idx_suite_number | idx_suite_number | 51 | const,d1.red_card.number | 1 | Using where; Using index |+----+-------------+------------+------+------------------+------------------+---------+--------------------------+------+----------------------------------------------+WHERE句のみをインデックスで解決した。ORDER BYに使われているblack̲card.numberもインデックスに含まれているのに、ソートは無効化できていない︖120/144
⾚い裏⾯のトランプが並んでいる順に取り出して結合すると⾚いトランプのカード ⿊いトランプのカード ⿊いトランプ上からheart 3 NULL NULLheart 1 NULL NULLheart 1 NULL NULLheart 4 heart 4 21, 38heart 10 heart 10 96heart 11 heart 11 25, 52heart 12 heart 12 36, 73heart 5 heart 5 10, 18, 33, 75heart 11 heart 11 25, 52heart 3 NULL NULLheart 7 heart 7 27, 43heart 7 heart 7 27, 43heart 7 heart 7 27, 43heart 8 heart 8 46, 54, 70heart 5 heart 5 10, 18, 33, 75heart 9 heart 9 3, 32, 77, 82heart 4 heart 4 21, 38121/144
インデックスで取り出しても、既に並びが崩れている⾚いトランプのカード ⿊いトランプのカード ⿊いトランプ上からheart 3 NULL NULLheart 1 NULL NULLheart 1 NULL NULLheart 4 heart 4 21, 38heart 10 heart 10 96heart 11 heart 11 25, 52heart 12 heart 12 36, 73heart 5 heart 5 10, 18, 33, 75heart 11 heart 11 25, 52heart 3 NULL NULLheart 7 heart 7 27, 43heart 7 heart 7 27, 43heart 7 heart 7 27, 43heart 8 heart 8 46, 54, 70heart 5 heart 5 10, 18, 33, 75heart 9 heart 9 3, 32, 77, 82heart 4 heart 4 21, 38122/144
ソートが終わってからでないと、”若い⽅から5枚”が判定できないので、結局最後までソートせざるをえない。123/144
インデックスでソートまで解決するにはred_card ADD KEY idx_suite_number(suite, number)してやるmysql55> explain SELECT black_card.suite, black_card.number FROM red_card JOIN black_card USING (suite, number) WHERE red_card.suite= 'heart' ORDER BY black_card.number LIMIT 5;+----+-------------+------------+------+----------------------------+------------------+---------+----------------------------+------+--------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+------------+------+----------------------------+------------------+---------+----------------------------+------+--------------------------+| 1 | SIMPLE | black_card | ref | idx_suite_number | idx_suite_number | 50 | const | 27 | Using where; Using index || 1 | SIMPLE | red_card | ref | idx_suite,idx_suite_number | idx_suite_number | 51 | const,d1.black_card.number | 1 | Using where; Using index |+----+-------------+------------+------+----------------------------+------------------+---------+----------------------------+------+--------------------------+内部表と外部表が⼊れ替わっているのにお気づきだろうか。red̲card.suite == black̲card.suite なので、ORDER BYblack̲card.numberはORDER BY red̲card.numberと等価だと気付いてそれを使っている。124/144
これだと⾚いトランプのカード ⿊いトランプのカード ⿊いトランプ上からheart 1 NULL NULLheart 1 NULL NULLheart 3 NULL NULLheart 3 NULL NULLheart 4 heart 4 21, 38heart 4 heart 4 21, 38heart 5 heart 5 10, 18, 33, 75heart 5 heart 5 10, 18, 33, 75heart 7 heart 7 27, 43heart 7 ⽐較不要 ⽐較不要heart 7 ⽐較不要 ⽐較不要heart 8 ⽐較不要 ⽐較不要heart 9 ⽐較不要 ⽐較不要heart 10 ⽐較不要 ⽐較不要heart 11 ⽐較不要 ⽐較不要heart 11 ⽐較不要 ⽐較不要heart 12 ⽐較不要 ⽐較不要125/144
⾚いトランプのカードを取り出した時点で既にソートが終わっているred̲card.idx̲suite̲numberが並んでいる順番にハートだけを抽出してblack̲card.idx̲suite̲numberは”=”演算にだけ使いORDER BY black̲card.numberはORDER BY red̲card.numberと等価で既に並べ替えなのでソートは終わっていると判断してheart 4, heart 4, heart 5, heart 5, heart 7が揃った段階で”若い⽅から5枚”が確定するので、それ以降は⿊いトランプのカードを探す必要がない。126/144
というのを知っていると、JOINで遅くなるケースはまあまあ納得がいくそもそもスキャンジョインしているソートまでインデックスだけで解決できていない17⾏くらい⼤したことないけれど、1000万⾏あったらソートするのはだいぶ骨が折れる。-MySQL 5.6でオプティマイザーが賢くなったので、新しいバージョンを使っていればこういう内部動作を気にしなくてもいいかも知れないけれど内部的な動作原理を知っているとどんなインデックスを作ると速くなるか(あるいは、ならないか)が想像しやすい127/144
取り敢えずの動作としては”まずWHERE句を処理するためのカラム” “次にORDER BY句を処理するためのカラム”の順に並べるWHERE句を処理するためのカラムにINやANYが⼊っていると、それより後ろに列挙されたぶんが使えなくなるので注意カラムに対して演算を⾏っているとインデックスは効かない更に⾼速化を狙うならselect̲listに⼊っているものを含めてcoveringindexを狙うGROUP BYはWHEREのあと、ORDER BYの前に処理されるので、GROUP BY⽤のカラムを追加するなら(WHERE, GROUP BY, ORDERBY)の順番になる。HAVINGやその後のORDER BYで集計関数の結果カラムを指定していると、それは”演算を⾏っている”のでその部分はインデックスではスキップできない。-128/144
掲⽰板に戻るthreadテーブルカラム データ例1thread̲id 1thread̲title コードギアス亡国の巨⼈thread̲owner yoku0825thread̲created 2015-11-30 22:39:02comment̲count 10last̲posted 2015-11-30 22:39:12commentテーブルカラム データ例1 データ例2thread̲id 1 1comment̲number 1 2comment̲owner 名無しさん 名無しさんcomment̲posted 2015-11-30 22:39:03 2015-11-30 22:39:04comment̲body 殺⼈教室 13⽇のストラトス129/144
掲⽰板に戻るuserテーブルカラム データ例1 データ例2user̲id yoku0825 名無しさんuser̲email yoku0825@gmail.com sage130/144
参照⽤クエリー新しく作成された順にスレッド⼀覧を表⽰するクエリーSELECT thread_id, thread_titleFROM threadORDER BY thread_created DESCWHEREはないがthread̲createdで逆順ソートしているので、idx̲threadcreated (thread̲created)はあるとクイックソートを⾶ばせる。先頭100件のみ、などとやる時に末尾までソートしなくていいので⾼速化できる。131/144
参照⽤クエリーコメント数の多い順にスレッド⼀覧を表⽰するクエリーSELECT thread_id, thread_titleFROM threadORDER BY comment_count DESCidx̲commentcount(comment̲count)があった⽅がいい。132/144
参照⽤クエリー新しくコメントされた順にスレッド⼀覧を表⽰するクエリーSELECT thread_titleFROM bbsORDER BY last_posted DESCidx̲lastposted(last̲posted)があった⽅がいい133/144
参照⽤クエリースレッドを詳細表⽰するクエリー(メタデータを取り出すところ)SELECT thread.thread_title,thread.thread_owner,user.user_email AS thread_owner_email,thread.thread_created,thread.comment_count,thread.last_postedFROM thread JOIN user ON thread.thread_owner = user.user_nameWHERE thread.thread_id = 1threadテーブルのthread̲idは既にプライマリーキーなので1つ目のWHERE句はOKuserテーブルのuser̲nameもプライマリーキーなのでOK134/144
参照⽤クエリースレッドを詳細表⽰するクエリー(つづき)SELECT comment.comment_number,comment.comment_owner,user.user_email AS comment_owner_email,comment.comment_posted,comment.comment_bodyFROM comment JOIN user ON comment.comment_owner = user.user_nameWHERE comment.thread_id = 1ORDER BY comment.comment_numberuserテーブルのuser̲nameもプライマリーキーなのでOKcommentテーブルの(thread̲id, comment̲number)はプライマリーキーで順番もこの通りでOK135/144
他にもたとえばコメント数の多いユーザー10⼈を抽出するクエリーSELECT user.user_name,COUNT(*) AS comment_countFROM user JOIN comment ON user.user_name = comment.comment_ownerGROUP BY user.user_nameORDER BY comment_count DESC LIMIT 10commentテーブルの結合のためにidx̲commentowner(comment̲owner)userテーブルのGROUP BYのためにuser̲nameはプライマリーキーなのでこのままでOKORDER BYのcomment̲countは集計関数の結果カラムなのでソートは避けられない5.6以降ならばソート⽤の⼀時インデックスを⾃動で作るかも。-136/144
他にもたとえばユーザーの最終コメント時刻を抽出するクエリーSELECT user.user_name,MAX(comment.comment_posted) AS last_postedFROM user JOIN comment ON user.user_name = comment_comment_ownerGROUP BY user.user_nameどういう風に結合されるのかがイメージできると、idx̲commentowner̲commentposted(comment̲owner,comment̲posted)があった⽅がいい気がしてきませんか︖137/144
指標的なものEXPLAINExtra: Using filesortでORDER BYやGROUP BYがインデックスで解決できていない-Extra: Using tempraryでGROUP BYやJOINがインデックスで解決できていない-Extra: Using join bufferでスキャンジョイン-138/144
指標的なものSHOW GLOBAL STATUScreated̲tmp̲%tables .. テンポラリーテーブルが作成された(Extra: Using temporaryな)クエリーでカウントアップされる-handler̲read% .. 実際に⾏をフェッチしているとカウントアップされる-Select̲full̲join, Select̲scan-Sort̲merge̲passes, Sort̲scan-139/144
指標的なものperformance̲schemaevents̲statements̲summary̲by̲digestテーブルなど-140/144
インデックスまとめ検索が増えれば必要なインデックスも増えるインデックスが増えれば更新性能, ファイルサイズにオーバーヘッドが加わる更新のデメリットに対して参照のメリットは⼗分⼤きいcovering indexで仕留めればクエリーの速度が数百倍になることも珍しくない-Disk容量が⾜りているなら、SELECTタイプごとに複合インデックス作った⽅がいい-余談: InnoDBは⾏ロックじゃなくてネクストキーロック適切なインデックスがないとロックで死ねる-MySQLは5.6でオプティマイザーに⼤きな改良が加えられているこれから利⽤なら5.6、可能なら5.7も-MariaDBなら5.3以降を-141/144
おまけthread.comment̲countってcommentから計算できるから消さないといけないんじゃないの︖“one fact in one place”っていうとそうなんですが-C.J.Dateの本を読んでる限りでは、正規化のプロセスとしてはそこを切り離すのは⾒当たらなかったご存知の⽅いたら本の名前を教えてください。-個⼈的には集約関数の結果をソートするのはオーバーヘッドが⼤きいし、threadテーブルから⾒るとcomment̲countは個々のスレッドが持つ “属性” で、たまたま外部で計算したものと値が⼀致するだけ、と考えるようにしている。-142/144
おまけなんでuserテーブルだけナチュラルキーにしたの︖comment̲numberもナチュラルのつもりです(n番目のコメント、という情報がきちんとコメントを識別する意味を持っている)thread̲idは完全にサロゲートキーです-基本的にナチュラルキーにしたいんですけど、InnoDBのセカンダリーキーのリーフにはPRIMARY KEYの値が格納されるInnoDBのデータ本体はPRIMARY KEYのリーフにぶら下がるInnoDBのチェンジバッファは、セカンダリーキーにしか効かない-このあたりの制約から、ある程度以上のボリュームがあったり更新があるのが目に⾒えているものはサロゲートキーを使うことが多いです。-143/144
それでは、素敵なMySQLライフを144/144

Recommended

PDF
MySQLと正規形のはなし
PDF
速習!論理レプリケーション ~基礎から最新動向まで~(PostgreSQL Conference Japan 2022 発表資料)
PDF
雑なMySQLパフォーマンスチューニング
PDF
MySQLerの7つ道具 plus
PDF
MySQLを割と一人で300台管理する技術
PDF
MySQLレプリケーションあれやこれや
PDF
データベース09 - データベース設計
PDF
サーバーが完膚なきまでに死んでもMySQLのデータを失わないための表技
PDF
Memoizeの仕組み(第41回PostgreSQLアンカンファレンス@オンライン 発表資料)
PDF
PostgreSQL16新機能紹介 - libpq接続ロード・バランシング(第41回PostgreSQLアンカンファレンス@オンライン 発表資料)
PDF
より速く より運用しやすく 進化し続けるJVM(Java Developers Summit Online 2023 発表資料)
PDF
トランザクション処理可能な分散DB 「YugabyteDB」入門(Open Source Conference 2022 Online/Fukuoka 発...
PDF
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
PDF
MySQLバックアップの基本
PDF
楽天プロジェクトX:基幹DB移設 編
PPTX
エンジニアライクにドキュメントを作成してみた件
PDF
MySQLで論理削除と正しく付き合う方法
PDF
MySQL 5.7の罠があなたを狙っている
PPTX
iostat await svctm の 見かた、考え方
PDF
XIDを周回させてみよう
PPTX
負荷分散だけじゃないELBのメリット
PDF
GPU仮想化最前線 - KVMGTとvirtio-gpu -
 
PDF
MySQLチューニング
PDF
Sql query patterns, optimized
PDF
Xenとzfsで作る家庭内VDIサーバ
 
PDF
SQLアンチパターン - 開発者を待ち受ける25の落とし穴 (拡大版)
PDF
binary log と 2PC と Group Commit
PPTX
PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~
PDF
SQLアンチパターン 幻の第26章「とりあえず削除フラグ」
PDF
運用に自動化を求めるのは間違っているだろうか

More Related Content

PDF
MySQLと正規形のはなし
PDF
速習!論理レプリケーション ~基礎から最新動向まで~(PostgreSQL Conference Japan 2022 発表資料)
PDF
雑なMySQLパフォーマンスチューニング
PDF
MySQLerの7つ道具 plus
PDF
MySQLを割と一人で300台管理する技術
PDF
MySQLレプリケーションあれやこれや
PDF
データベース09 - データベース設計
PDF
サーバーが完膚なきまでに死んでもMySQLのデータを失わないための表技
MySQLと正規形のはなし
速習!論理レプリケーション ~基礎から最新動向まで~(PostgreSQL Conference Japan 2022 発表資料)
雑なMySQLパフォーマンスチューニング
MySQLerの7つ道具 plus
MySQLを割と一人で300台管理する技術
MySQLレプリケーションあれやこれや
データベース09 - データベース設計
サーバーが完膚なきまでに死んでもMySQLのデータを失わないための表技

What's hot

PDF
Memoizeの仕組み(第41回PostgreSQLアンカンファレンス@オンライン 発表資料)
PDF
PostgreSQL16新機能紹介 - libpq接続ロード・バランシング(第41回PostgreSQLアンカンファレンス@オンライン 発表資料)
PDF
より速く より運用しやすく 進化し続けるJVM(Java Developers Summit Online 2023 発表資料)
PDF
トランザクション処理可能な分散DB 「YugabyteDB」入門(Open Source Conference 2022 Online/Fukuoka 発...
PDF
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
PDF
MySQLバックアップの基本
PDF
楽天プロジェクトX:基幹DB移設 編
PPTX
エンジニアライクにドキュメントを作成してみた件
PDF
MySQLで論理削除と正しく付き合う方法
PDF
MySQL 5.7の罠があなたを狙っている
PPTX
iostat await svctm の 見かた、考え方
PDF
XIDを周回させてみよう
PPTX
負荷分散だけじゃないELBのメリット
PDF
GPU仮想化最前線 - KVMGTとvirtio-gpu -
 
PDF
MySQLチューニング
PDF
Sql query patterns, optimized
PDF
Xenとzfsで作る家庭内VDIサーバ
 
PDF
SQLアンチパターン - 開発者を待ち受ける25の落とし穴 (拡大版)
PDF
binary log と 2PC と Group Commit
PPTX
PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~
Memoizeの仕組み(第41回PostgreSQLアンカンファレンス@オンライン 発表資料)
PostgreSQL16新機能紹介 - libpq接続ロード・バランシング(第41回PostgreSQLアンカンファレンス@オンライン 発表資料)
より速く より運用しやすく 進化し続けるJVM(Java Developers Summit Online 2023 発表資料)
トランザクション処理可能な分散DB 「YugabyteDB」入門(Open Source Conference 2022 Online/Fukuoka 発...
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
MySQLバックアップの基本
楽天プロジェクトX:基幹DB移設 編
エンジニアライクにドキュメントを作成してみた件
MySQLで論理削除と正しく付き合う方法
MySQL 5.7の罠があなたを狙っている
iostat await svctm の 見かた、考え方
XIDを周回させてみよう
負荷分散だけじゃないELBのメリット
GPU仮想化最前線 - KVMGTとvirtio-gpu -
 
MySQLチューニング
Sql query patterns, optimized
Xenとzfsで作る家庭内VDIサーバ
 
SQLアンチパターン - 開発者を待ち受ける25の落とし穴 (拡大版)
binary log と 2PC と Group Commit
PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~

Viewers also liked

PDF
SQLアンチパターン 幻の第26章「とりあえず削除フラグ」
PDF
運用に自動化を求めるのは間違っているだろうか
PDF
ソーシャルゲームのためのデータベース設計
PDF
実践イカパケット解析
PPTX
ちゃんとした C# プログラムを書けるようになる実践的な方法~ Visual Studio を使った 高品質・低コスト・保守性の高い開発
PDF
オンラインゲームの仕組みと工夫
PDF
SSL/TLSの基礎と最新動向
PDF
「内積が見えると統計学も見える」第5回 プログラマのための数学勉強会 発表資料
PDF
「スプラトゥーン」リアルタイム画像解析ツール 「IkaLog」の裏側
PDF
フーリエ変換と画像圧縮の仕組み
PPTX
技術選択とアーキテクトの役割
PDF
プログラム組んだら負け!実はHTML/CSSだけでできること2015夏
PPTX
ゲームエンジニアのためのデータベース設計
PPTX
Webアプリケーション負荷試験実践入門
PDF
インフラエンジニアがUnityをやるべきたった一つの理由
PDF
エンジニアのための経営学
PDF
中の下のエンジニアを脱出するための仕事術
PDF
偶然にも500万個のSSH公開鍵を手に入れた俺たちは
PPTX
Appium を使って iOS / Android の UI テストを共通化
PDF
ウェブパフォーマンスの基礎とこれから
SQLアンチパターン 幻の第26章「とりあえず削除フラグ」
運用に自動化を求めるのは間違っているだろうか
ソーシャルゲームのためのデータベース設計
実践イカパケット解析
ちゃんとした C# プログラムを書けるようになる実践的な方法~ Visual Studio を使った 高品質・低コスト・保守性の高い開発
オンラインゲームの仕組みと工夫
SSL/TLSの基礎と最新動向
「内積が見えると統計学も見える」第5回 プログラマのための数学勉強会 発表資料
「スプラトゥーン」リアルタイム画像解析ツール 「IkaLog」の裏側
フーリエ変換と画像圧縮の仕組み
技術選択とアーキテクトの役割
プログラム組んだら負け!実はHTML/CSSだけでできること2015夏
ゲームエンジニアのためのデータベース設計
Webアプリケーション負荷試験実践入門
インフラエンジニアがUnityをやるべきたった一つの理由
エンジニアのための経営学
中の下のエンジニアを脱出するための仕事術
偶然にも500万個のSSH公開鍵を手に入れた俺たちは
Appium を使って iOS / Android の UI テストを共通化
ウェブパフォーマンスの基礎とこれから

Similar to MySQLテーブル設計入門

PDF
データベース03 - SQL(CREATE, INSERT, DELETE, UPDATEなど)
PDF
Sqlの書き方セミナー資料
PDF
削除フラグのはなし
PDF
データベース設計の基本編.pdf
PDF
PDF
【第3回初心者勉強会】データベースを使おう
PPT
PhpでMySqlを使う
 
PPTX
Sql learning
データベース03 - SQL(CREATE, INSERT, DELETE, UPDATEなど)
Sqlの書き方セミナー資料
削除フラグのはなし
データベース設計の基本編.pdf
【第3回初心者勉強会】データベースを使おう
PhpでMySqlを使う
 
Sql learning

More from yoku0825

PDF
片手間MySQLチューニング戦略
PDF
MySQLerの7つ道具
PDF
イルカさんチームからゾウさんチームに教えたいMySQLレプリケーション
PDF
わかった気になるMySQL
PDF
MySQL 5.7の次のMySQL 8.0はどんなものになるだろう
PDF
MySQLアンチパターン
PDF
MySQL Fabricでぼっこぼこにされたはなし
PDF
MySQL 8.0で憶えておいてほしいこと
PDF
わたしを支える技術
PDF
MySQLおじさんの逆襲
PDF
Dockerイメージで誰でも気軽にMroonga体験
PDF
ペパボ de MySQL
PDF
MySQL 5.7の次のMySQLは
PDF
MHAの次を目指す mikasafabric for MySQL
PDF
とあるイルカの近況報告
PDF
mikasafabric for MySQL
PDF
地雷職人の朝は早い
PDF
5.7の次のMySQL
PDF
逝くぞ最新版、罠の貯蔵は十分か
PDF
MySQLステータスモニタリング
片手間MySQLチューニング戦略
MySQLerの7つ道具
イルカさんチームからゾウさんチームに教えたいMySQLレプリケーション
わかった気になるMySQL
MySQL 5.7の次のMySQL 8.0はどんなものになるだろう
MySQLアンチパターン
MySQL Fabricでぼっこぼこにされたはなし
MySQL 8.0で憶えておいてほしいこと
わたしを支える技術
MySQLおじさんの逆襲
Dockerイメージで誰でも気軽にMroonga体験
ペパボ de MySQL
MySQL 5.7の次のMySQLは
MHAの次を目指す mikasafabric for MySQL
とあるイルカの近況報告
mikasafabric for MySQL
地雷職人の朝は早い
5.7の次のMySQL
逝くぞ最新版、罠の貯蔵は十分か
MySQLステータスモニタリング

MySQLテーブル設計入門


[8]ページ先頭

©2009-2025 Movatter.jp