Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

MySQLテーブル設計入門

42 100 vues

Publié le

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

Publié dans : Technologie
  • Soyez le premier à commenter

MySQLテーブル設計入門

  1. 1. MySQL⼊門 7限目: テーブル設計について 2015/04/30 GMOメディア株式会社 yoku0825 schoo WEB-campus
  2. 2. \こんにちは/ yoku0825@GMOメディア株式会社 オラクれない- ポスグれない- マイエスキューエる- 家に帰ると 妻の夫- せがれの⽗- 娘の⽗- Twitter: @yoku0825 Blog: ⽇々の覚書 1/144
  3. 3. アジェンダ 事前知識 DDLで使われる⽤語についてざっと説明します- データ型について説明します- カラム設計 デモケースとして掲⽰板⽤のデータモデリングをします- 非正規形のテーブルからスタートし、正規形にするまでの考え⽅を学 びます - インデックス設計 インデックスの動作の仕⽅についてトランプの例を⽤いて簡単に解説 します - 先の掲⽰板⽤のデータモデルにインデックスを作成します- 2/144
  4. 4. 質問 3/144
  5. 5. 今のあなたのレベル感を教えてください SQLもまだいまいち。。1. SQLならほぼわかるけど実務経験ガー2. テーブル設計経験者。でも破綻しちゃって。。3. バリバリサービスでやっててノウハウはあるけど、ちょっと理屈を聞いて みたくなった。 4. お前にマサカリを投げに来た5. 4/144
  6. 6. 1. SQLもまだいまいち。。 の⽅ そんなにたくさんSQLをゴリゴリ書くような授業ではないですので、気楽 に最後まで眺めてみてください。 暗記すること、憶えなければいけないことは基本何もありません。そうい えばこんな話聞いたな…というのが後から⽣きてくる、というのを弊社研 修⽣からは⽿にしています :) SQLが判ればもっと⾯⽩くなると思いますので、SQLが⼿についてき たころに観返してみると違った発⾒があるかも知れません。 - 5/144
  7. 7. 2. SQLならほぼわかるけど実務経験ガー の⽅ 前半の正規形と設計のプロセスについて、疑問に思ったところはどんどん 聞いてください。 後半かなりアーキテクチャーに寄りますので、そちらはまた慣れてきたこ ろに観返していただけると幸いです。 6/144
  8. 8. 3. テーブル設計経験者。でも破綻しちゃって。。 の⽅ まずは前半で “正規形” について誤解がないかどうか⼀緒に考えましょ う。 そして、 正規化 した/していない のに何故テーブルが破綻したのかの答 え(への⾜掛かり)を後半で探っていきます。 7/144
  9. 9. 4. バリバリサービスでやっててノウハウはある⽅ そんなあなたにこの授業の内容をそれっぽい⾔葉で説明すると 3NFまで1段階ずつ正規化していきます。- インデックスの使われ⽅(WHERE狙いのキー, ORDER BY狙いのキー) の話をします。 - JOIN時における内部表と外部表の話とWHERE狙い, ORDER BY狙い の話が絡んできます。 - 楽しんでいただけそうですか︖ 8/144
  10. 10. 5. お前にマサカリを投げに来た お⼿柔らかにお願いします。。 9/144
  11. 11. ⽤語について インデックス (主に)データを検索するために使われる ソート済みのデータのサブセ ット - MySQLでは “キー” と “インデックス” は同じもの(特に、セカンダリ ーキー)を指すことが多い リレーショナルモデル上では “キー” は “候補キー” - 10/144
  12. 12. ⽂字コードについて sjis 悪いことは⾔わないのでやめましょう。バグが多いです。- WindowsのSJISと 互換性がない ので、WindowsのSJISを使いたい のであればcp932を使ってください - ujis オススメはしませんがsjisよりはマシです。eucjpmsの⽅がいいで す。 - utf8 基本的にオススメですが、4バイト⽂字(絵⽂字)が⼊らないので、 utf8mb4にするべきかと思います。 - latin1 暗黙のデフォルト。- ⽇本語を⼊れると闇が⾒えます。- 11/144
  13. 13. ⽂字列型 CHAR(n), VARCHAR(n) n ⽂字 (not byte)のデータを格納するための型- デフォルトのまま使っていると、⼤⽂字と⼩⽂字を区別 しない (Case Insensitive) - 現在のMySQLにおいてCHAR型を選ぶメリットはほぼない。迷ったら VARCHAR。 - データサイズの側⾯でいうと、1⽂字に必要なバイト数は⽂字コード ごとに違うので、n⽂字 * 1⽂字あたりのバイト数 + αが1⾏あたりの 最⼤サイズになります。 - 12/144
  14. 14. ⽂字列型 TEXT, LONGTEXT TEXT型は2^16⽂字くらいまで、LONGTEXT型は2^64⽂字くらいま で。 - ただしmax̲allowed̲packetがあるので、LONGTEXT型でも2^64 ⽂字くらいまで全て詰め込むことはできません。 - バイト型もあるけど、あまり使うことはない PHPとかから使う時はどっちも⽂字列っぽく⾒えるけれど、Javaとか から使うと型が違ってごちゃごちゃする。 - 13/144
  15. 15. 数値型 TINYINT, INT, BIGINT それぞれ1バイト, 4バイト, 8バイトの整数型。- UNSIGNED属性をつけると符号なし整数になって1ビット余計に使え る。 - FLOAT, DOUBLE 単精度, 倍精度浮動⼩数点数型。- 浮動⼩数点型なので丸め誤差に注意。ほぼ使わない。- DECIMAL, NUMERIC 固定⼩数点数型。演算が伴うときはこれを使うか、100倍してINT型 に⼊れるなんてことも。 - 14/144
  16. 16. ⽇付時刻型 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
  17. 17. その他テーブルの属性 AUTO̲INCREMENT 数値型(⼩数点型でもOK)そのカラムにインデックスがある場合にのみ 設定可能な属性。 - ⾃動でカウントアップされるDEFAULT属性みたいな感じ。- AUTO̲INCREMENTの次の値は int(そのカラムの最⼤値) + 1- DEFAULT そのカラムに値が指定されなかった時に、暗黙にこの値を設定する。- CURRENT̲TIMESTAMPの例外を除いてリテラルのみ指定可能で、関 数の結果はDEFAULTに指定できない。 - ストレージエンジン, 圧縮, その他.. 16/144
  18. 18. 本編 カラム設計のデモケースとして掲⽰板⽤のデータモデリングをします 非正規形のテーブルからスタートし、正規形にするまでの考え⽅を⾒てい きます 17/144
  19. 19. ⼀番最初に考えること あなたの 掲⽰板システムに必要なデータは何か︖ 18/144
  20. 20. たとえば 掲⽰板にはスレッド単位で話題が上がって それに対してコメントが付く形- 1スレッドの上限コメント数は制限したい- スレッドを⽴てるにはメールアドレスを必須にしたい- スレッドの⼀覧出⼒機能をつけたい スレの作成順にソートしたい- 最終書き込み時刻でソートしたい- スレの書き込み数でもソートしたい- スレッドのタイトルの横にコメント数を表⽰したい- 19/144
  21. 21. 考え⽅の順番 まず最初に全部⼊りで考えてみる 正規化とか考えずに必要なものを全部テーブルに詰め込んでみる 正規化⼿順に沿って5NFまで持っていく もちろん慣れていれば3NFくらいからいきなり書き始められる 20/144
  22. 22. 最初の全部⼊りテーブル設計 カラム データ例 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
  23. 23. 参照系クエリー 新しく作成された順にスレッド⼀覧を表⽰するクエリー SELECT thread_title FROM bbs ORDER BY thread_created DESC 22/144
  24. 24. 参照系クエリー コメント数の多い順にスレッド⼀覧を表⽰するクエリー SELECT thread_title FROM bbs ORDER BY comment_count DESC 23/144
  25. 25. 参照系クエリー 新しくコメントされた順にスレッド⼀覧を表⽰するクエリー SELECT thread_title FROM bbs ORDER BY last_posted DESC 24/144
  26. 26. 参照系クエリー スレッドを詳細表⽰するクエリー SELECT thread_title, thread_owner, thread_owner_email, thread_created, comment_count, last_posted, comments FROM bbs WHERE thread_title = 'new_thread' 25/144
  27. 27. SELECTは特 に問題なさそ うに⾒える︖ 26/144
  28. 28. 更新系クエリー スレッドを追加するためのクエリー 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
  29. 29. 更新系クエリー コメントを追加するクエリー 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
  30. 30. 更新系クエリー それとも、こう︖ 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
  31. 31. 雲⾏きが怪し くなってきま したね :) 30/144
  32. 32. コメントの削除 どうやる︖ アプリ側で⽂字列探査して目的のコメントを削除してUPDATEする (last̲postedもがんばる) そんなばかな 31/144
  33. 33. 第1非正規形 SELECTする分には特に問題なく⾒える というかとてもシンプルで良いようにすら⾒える JOINもGROUP BYも出てこない - 更新処理が破綻している ⽂字列操作で追加、削除- ⽂字列操作はSQL外の範疇になるので、RDBMSの利点をほとんど受 けられない - 32/144
  34. 34. それでも正規化せ ずに使いますか︖ [No/いいえ] 33/144
  35. 35. 正規化とは テーブルを リレーショナルモデルのお作法に基づいた形 に作り変える作 業 SQLはリレーショナルモデルに基づいた操作を提供する⾔語 RDBMSの機能を(フルに)使うためにはSQLを使わなければいけない SQL(だけ)で綺麗に操作するためには正規化が必要 34/144
  36. 36. なんて⾔ってても わかりづらいので 実際に正規化して みましょう 35/144
  37. 37. 第1正規形 テーブルの中には個々の⾏を⼀意に識別できる候補キーが存在すること テーブルの全ての要素は NULLでないこと 宗教問題に近いので、軽く流します - アトミックである- アトミックでない場合はアトミックになるように分割する 36/144
  38. 38. 全ての⾏は⼀意に識別されなければいけない カラム データ例 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
  39. 39. 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
  40. 40. 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
  41. 41. 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
  42. 42. どれが正解︖ 同じタイトルのスレッドは存在できない タイトルが同じでもいくらでも投稿できる ある程度の情報が⼀致したら重複として扱って投稿できない 正解はどれでも良くて、これは要件との兼ね合いになる。 41/144
  43. 43. 要件を思い出す 掲⽰板にはスレッド単位で話題が上がって それに対してコメントが付く形- 1スレッドの上限コメント数は制限したい- スレッドを⽴てるにはメールアドレスを必須にしたい- スレッドの⼀覧出⼒機能をつけたい スレの作成順にソートしたい- 最終書き込み時刻でソートしたい- スレの書き込み数でもソートしたい- スレッドのタイトルの横にコメント数を表⽰したい- 42/144
  44. 44. スレッドの重複に関する要件はない でもどれかのプライマリーキーを選ばないと、同じタイトルのスレッドが 存在した時に機能が破綻する 要件を追加しなければならない 43/144
  45. 45. 追加されるであろう要件 重複したスレッドの作成を 禁⽌する タイトルが同じなら重複として作成させない => PRIMARY KEY (thread̲title) タイトルよりは複雑だけど重複判定はする => PRIMARY KEY (thread̲title, thread̲owner, thread̲owner̲email) - 禁⽌しない => PRIMARY KEY(thread̲id)- テーブル設計と要件定義は密接に関わっている 44/144
  46. 46. 今回は重複スレッ ドを禁⽌しない (thread̲id⽅式) を選んでみる 45/144
  47. 47. 第1正規形の話に戻ると テーブルの中には個々の⾏を⼀意に識別できる候補キーが存在すること テーブルの全ての要素は NULLでないこと 宗教問題に近いので、軽く流します - アトミックである- アトミックでない場合はアトミックになるように分割する 46/144
  48. 48. テーブルの要素 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
  49. 49. テーブルの要素 アトミックであること 1つの⾏の1つのカラムに複数の要素を詰め込まない こと。- これからしばらく気持ち悪いテーブル構成が並びますが、正規化の途 中ということで⼤目に⾒てください。 - 48/144
  50. 50. 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
  51. 51. ⾏を分割して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
  52. 52. プライマリーキーを伸ばす カラム データ例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
  53. 53. この段階での参照クエリー 新しく作成された順にスレッド⼀覧を表⽰するクエリー 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
  54. 54. この段階での更新系クエリー スレッドを追加するためのクエリー 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
  55. 55. この段階での更新系クエリー コメントを追加するクエリー 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
  56. 56. この段階での更新系クエリー コメントの削除 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
  57. 57. これで第1正規形︖ まだ書式によって区切られた複数の意味(コメント番号, コメント主, email, コメント⽇付, コメントの本⽂)が1つの要素に詰め込まれている = アトミックではない とはいえこの時点でコメントの投稿はINSERT, コメントの削除は DELETEで直観的に操作できるようになった 56/144
  58. 58. カラムを分割してアトミックになるようにする カラム データ例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
  59. 59. 変化のあったクエリー コメントを追加するクエリー 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
  60. 60. これで第1正規形 レコードが⼀意に識別できる 要素にNULLを含まない 要素がアトミックである 59/144
  61. 61. 第1正規化することで変わったこと コメントの削除が(SQLで)できるようになった コメントの投稿がシンプルなINSERT⽂に その気になればコメントの内容をUPDATEで編集することもできる コメント追加のINSERTのために、スレッド本体の情報も持たせないとい けないの︖ スレッドのタイトルが編集できたとしたら、たくさんの⾏を更新しないと いけなくない︖ 60/144
  62. 62. 正規化とは(再掲) テーブルを リレーショナルモデルのお作法に基づいた形 に作り変える作 業 SQLはリレーショナルモデルに基づいた操作を提供する⾔語 RDBMSの機能を(フルに)使うためにはSQLを使わなければいけない SQL(だけ)で綺麗に操作するためには正規化が必要 61/144
  63. 63. RDBMSの機能 トランザクションによるACID保証 制約によるデータの保証 集計関数 RDBMSの恩恵を しっかり受けるために 正規化する RDBMS の外側 でやるところには正規化という概念はない 62/144
  64. 64. ひとやす み 63/144
  65. 65. 正規形の種類 非正規形 (?) 第1正規形 第2正規形 第3正規形 BC正規形 第4正規形 第5正規形 第6正規形 64/144
  66. 66. 第2正規形と第3正 規形の違いを知っ ていますか︖ [Yes/No] 65/144
  67. 67. 第2正規形と第3正規形の違い 第2正規形 プライマリーキーの ⼀部 だけで決まる要素が存在しない- 第3正規形 非キー要素から⼀意に決まる要素が存在しない- どちらも存在する場合はテーブルを分割する 66/144
  68. 68. プライマリーキーの ⼀部 #とは プライマリーキーが単⼀カラムで構成されている場合、 プライマリーキ ーの”⼀部” は存在しないので⾃動的に第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
  69. 69. 分割 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
  70. 70. 分割 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
  71. 71. 参照クエリー 新しく作成された順にスレッド⼀覧を表⽰するクエリー 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
  72. 72. 更新クエリー スレッドを追加するためのクエリー 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
  73. 73. 更新クエリー コメントを追加するクエリー 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
  74. 74. これで第2正規形 プライマリーキーの⼀部から⼀意に決まる値は別のテーブルに分割する その時の”プライマリーキーの⼀部”が分割先テーブルのプライマリー キーになる - 73/144
  75. 75. 第2正規化することで変わったこと スレッドはスレッド、コメントはコメントで独⽴したテーブルに⼊ったの で コメントを追加するためだけにスレッドの情報を何度も何度も INSERTしなくてよくなった - コメントのないスレッドを作ることが出来てしまう- スレッドのタイトルを変えるようなことがあっても、コメントのテー ブルには影響を及ぼさない - 新しいスレッドを作成するために、スレッドの作成と最初のコメント を別々にINSERTする必要がある。 - 新しいコメントを投稿するたびに、スレッドの更新時刻なども更新し ないといけない。 - 74/144
  76. 76. 第2正規形と第3正規形の違い 第2正規形 プライマリーキーの ⼀部 だけで決まる要素が存在しない- 第3正規形 非キー要素から⼀意に決まる要素が存在しない- どちらも存在する場合はテーブルを分割する 75/144
  77. 77. 非キー属性から⼀意に決まる #とは thread̲ownerが決まればthread̲owner̲emailが決まる︖ comment̲ownerが決まればcomment̲owner̲emailが決まる︖ ここでも要件との兼ね合い(ユーザー登録させるの︖ とか)になってくる 決まることにして進めます 決まらない場合、これは非キー属性から ⼀意に決まらない ので、分 割せずに第3正規形ということになります。 - 76/144
  78. 78. 分割 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
  79. 79. 分割 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
  80. 80. 分割 userテーブル カラム データ例1 データ例2 user̲id yoku0825 名無しさん user̲email yoku0825@gmail.com sage 79/144
  81. 81. これで第3正規形 プライマリーキー以外のカラムが決まると⾃動的に決まるカラムがあった 場合はテーブルを分割する この時の変数になるカラムが、分割先テーブルのプライマリーキー- 80/144
  82. 82. 第3正規化することで変わったこと スレッドを⽴てたことのない、コメントを投稿したことのないユーザーが 存在できるようになった 先にユーザー登録をしておかなくてはいけなくなった- ユーザーがメールアドレスを変更してもスレッドテーブルやコメントテー ブルを操作する必要はなくなった 81/144
  83. 83. BC正規形 非キー要素からプライマリーキーの⼀部が⼀意に決まる場合はテーブル分 割 第2正規化の⽮印が逆になったパターン この掲⽰板テーブルは第3正規化した時点でBC正規形でもある 82/144
  84. 84. 第4正規形, 第5正規形 複合プライマリーキー のみ で構成されたテーブルの分割をする あんまりお目にかからなくて済む(第3正規形を作って、複合プライマリー キーのみのテーブルができることは少ない) このスキーマは第3正規化した時点で第5正規形(複合プライマリーキーだ けのテーブルがないから) 83/144
  85. 85. 個⼈的テーブル設計まとめ 必要なものを全部洗いだしてから分割していく⽅が楽 プライマリーキーに着目して、 このテーブルでは”何を主体として扱うのか”- その属性は”どの主体の属性なのか”- でテーブルを振り分ける- 84/144
  86. 86. 正規化の話おしまい ここからこの掲⽰板の 話はしばらく忘れます 85/144
  87. 87. だいじなこと ⼀番⼤事なのは 第1正規化 カラムの中⾝を関数処理して取り出さないといけないようなものはあらか じめカラムを分けて設計すること 関数処理を通すとインデックスが最適な形で使えない- アトミックでないものを⼊れるなら、検索条件にしたり集計対象にしたり してはいけない 単にDurabilityを保証できるBLOBとして使うのも(オススメしないけ ど)ありといえばあり - 常にアプリケーション側でデシリアライズするならJSONとかが⼊っ ててもいい - 86/144
  88. 88. ひとやす み 87/144
  89. 89. 都市伝説 MySQLを使うなら 遅くなるから 正規化してはいけない 確かに遅くなる ケースも ある(特に、5.5より前の古いバージョン) 何故 RDBMSを扱う上で正しい はずの正規化でRDBMSであるMySQLの レスポンスが悪くなるのか︖ MySQLはJOINが遅い︖ 正規化のプロセスを⾒てきましたけど、この通りに操作していたら プライマリーキーで結合しますよね︖ プライマリーキーを使った結合なら ⼤概の場合 ちゃんとした速度 で動きますが、NLJなので数百万件に達すると遅くなります。 JOINすると遅くなるのは多くの場合 ソート 、きれいにインデック スを使いきれていないケースです。 - 88/144
  90. 90. 前提 判りやすく説明するために多少の嘘を含んでいます。 MySQLは原則 1つのテーブルにつき同時に1つのインデックスしか使えま せん。 MySQLはおばかさんだと⾔われる所以。。- 他のDBMSは知りませんが、使えるらしいです。- 説明のモデルは5.5がベースです。 5.6, 5.7とオプティマイザーはかなりよくなっています。- 89/144
  91. 91. インデックスの設計 インデックスとは ソート済みの- ある程度のメタデータを持たせた- データの複製(サブセット)- データの複製であるため、 インデックスだけで必要な情報が集まる場合は、テーブル本体をスキ ャンしなくても良い(covering index) - 更新系のDML(INSERT, UPDATE, DELETE)の際には常にオーバーヘ ッドが加わる - テーブル本体の容量とは別に容量を必要とする- 90/144
  92. 92. カーディナリティーとは そのインデックスに含まれる”取りうる値のバリエーション”の数 均等に分散した都道府県のカーディナリティーは47 都府なら3 - ⼤きければ⼤きいほど、そのインデックスを使って絞り込んだ時に返 ってくる結果セットが⼩さくなることが 期待できる 均等に分散しているなら、カーディナリティーが47のインデック スで抽出した場合の期待値は 総レコード件数 / 47 ただし、レコードが本当に均等に分散しているとは限らない 1億3000万レコードが都道府県カラムを持っていたとしても、東京 と島根ではレコード件数に差が出る - 91/144
  93. 93. インデックスを張っていないカラムでWHEREした時の動作をトラ ンプでたとえてみると ここに100枚のトランプの束があります 何が何枚あるかはわかりません ⼊ってない札があるかもしれません この中からハートのAを全て⾒つけ出してください SQLで書くと、SELECT * FROM card WHERE suite= 'heart' AND number= 1 92/144
  94. 94. どうや る︖ 93/144
  95. 95. こうやる トランプの⼀番上の札をめくって それがハートのAかどうか確認して ハートのAなら左⼿にとっておいて 違ったらテーブルに伏せておいて それを100枚目まで繰り返す 94/144
  96. 96. これがテーブルスキャン 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
  97. 97. テーブルスキャン 全ての⾏をフェッチして WHERE条件にマッチするかどうか判定して マッチしたらバッファに詰めて 違ったらスキップして それをテーブルの末尾まで繰り返す テーブルに格納されたデータに応じて線形に負荷が増える 96/144
  98. 98. インデックスを例えると トランプの束とは別に、こんな表をあらかじめ作っておくイメージ。 KEY(suite, number)に相当- suite number 上から clover 1 25, 93 clover 2 66 .. heart 1 91, 100 .. この表を⾒ると、上から91枚目と100枚目がハートのAであることがわか るので、 あとは上から91枚目, 100枚目をめくって裏を返せばいいだけ。 97/144
  99. 99. 実際のインデックス 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
  100. 100. 検索条件の⼀部しか満たせないインデックスの場合(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
  101. 101. 検索条件の⼀部しか満たせないインデックスの場合(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
  102. 102. 検索条件の⼀部しか満たせないインデックスの場合(numberのみ) KEY(number)に相当する索引だと number 上から 1 18,25,26,57,65,82,87,91,93,100 2 7,15,43,66,74,84 3 2,9,13,24,28,30,68,69 4 16,34,40,54,90 5 1,36,51,64,72,78,96 6 3,11,19,27,44,70,92 7 4,12,49,52,59,60,83,88,98 8 17,37,38,42,47,48,62,63,67,79,95,99 9 8,14,21,29,41,56,58,71,73,77,85 10 6,20,23,39,50,81 11 22,35,46,61,86,97 12 10,31,32,33,53,75,80,94 13 5,45,55,76,89 上から18, 25, 26, ..枚目を順番にひっくり返し、ハートかどうかを確認 してバッファに詰める。 101/144
  103. 103. 検索条件の⼀部しか満たせないインデックスの場合(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
  104. 104. またトランプに戻る ここに100枚のトランプの束があります 何が何枚あるかはわかりません ⼊ってない札があるかもしれません この中から全てのハートのカードをKからAまで順番に並べてください。 SQLで書くと、SELECT * FROM cards WHERE suite= 'heart' ORDER BY number DESC; 103/144
  105. 105. どうや る︖ 104/144
  106. 106. こうやる トランプの⼀番上の札をめくって それがハートかどうか確認して ハートのAなら左⼿にとっておいて それを100枚目まで繰り返したあと 左⼿に取っておいたハートだけの束を 先頭から1枚ずつ繰って並べ替える 105/144
  107. 107. これがスキャンソート 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
  108. 108. WHEREもORDER BYもインデックスだけで解決できるパターン KEY(suite, number)の場合 suite number 上から heart 1 91,100 heart 2 84 heart 3 30 heart 4 40,90 heart 5 51,64 heart 6 44,92 heart 7 4,12,98 heart 8 37,47 heart 9 8,85 heart 12 33,53,94 heart 13 45 ⼭の上から45枚目のKを左⼿に詰め、33, 53, 94枚目のQをその上に置き Jと10がないのはこの表だけでわかり 8, 85枚目の9をその上に置き…というのを繰り返して 最後にAを上に置けばいい 107/144
  109. 109. インデックスは ソート済みの デ ータの複製 108/144
  110. 110. 実⾏計画上はこうなる 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
  111. 111. WHEREのみインデックスで解決できるパターン 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, ..枚目を左⼿に詰めて 全部ハートのカードを取り終わったら ハートの束をKから順にAまで並べ替える 110/144
  112. 112. 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
  113. 113. ORDER BYのみインデックスで解決できるパターン KEY(number)の場合 number 上から 1 18,25,26,57,65,82,87,91,93,100 2 7,15,43,66,74,84 3 2,9,13,24,28,30,68,69 4 16,34,40,54,90 5 1,36,51,64,72,78,96 6 3,11,19,27,44,70,92 7 4,12,49,52,59,60,83,88,98 8 17,37,38,42,47,48,62,63,67,79,95,99 9 8,14,21,29,41,56,58,71,73,77,85 10 6,20,23,39,50,81 11 22,35,46,61,86,97 12 10,31,32,33,53,75,80,94 13 5,45,55,76,89 5, 45, 55, .. 枚目のKをめくり、ハートだったら左⼿に、そうでなければ 机に伏せて、10, 31, 32, 33枚目のQをめくり、ハートだったら左⼿に、 そうでなければ机に伏せて…を繰り返してAまでやればおしまい 112/144
  114. 114. 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
  115. 115. インデックスでソートのみ無効化して⾼速化が⾒込めるケース 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
  116. 116. 複合インデックスの弱点 左側のカラムだけ使おうとした場合 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
  117. 117. よくある「複合インデックスが思った通りに使えないパターン」 OR演算⼦ IN演算⼦ 不等号演算⼦ ASC, DESCの混在 JOINの結合順序 116/144
  118. 118. JOINのケースに絞って引き続きトランプで 裏⾯が⾚いトランプと 裏⾯が⿊いトランプが 何が何枚⼊ってるのか全くわからない状態で 100枚ずつの束になっています 裏⾯が⾚いトランプの⼭からハートのカードだけを取り出して 裏⾯が⿊いトランプの⼭に同じカードが含まれるか確認して 両⽅に含まれていることが判ったら、裏⾯が⿊いトランプを机の上に並べ て 最後にA〜Kの順に並べた上で 若い⽅から順番に5枚返す SQLで書くと、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 トランプだと物理的な枚数に制約があるので不適だったかもしれな い。 - 117/144
  119. 119. どうするか 裏⾯が⾚いトランプの⼭の⼀番上のカードをめくってハートかどうか確認 ハートでなければ、次の1枚をめくる- 裏⾯が⿊いトランプの⼭の⼀番上から1枚ずつ順番に裏⾯が⾚いトランプ のものと同じカードを探し机に置いておいて 最後まで探し終えたら、裏⾯が⾚いトランプの⼭の次のカードをめくって 裏⾯が⿊いトランプの⼭の⼀番上から1枚ずつ順番に… これを、裏⾯が⾚いトランプの⼭の最後の1枚まで繰り返したあと、 机に置いてある裏⾯が⿊いトランプを並べ替えて、 若い⽅から5枚取って返す 118/144
  120. 120. スキャンジョイン 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
  121. 121. インデックス︖ WHERE red̲card.suite = ʻheartʼ を解決するためにred_card ADD KEY idx_suite(suite) black̲card側のsuite, numberの結合条件を解決するためにblack_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 | 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
  122. 122. ⾚い裏⾯のトランプが並んでいる順に取り出して結合すると ⾚いトランプのカード ⿊いトランプのカード ⿊いトランプ上から heart 3 NULL NULL heart 1 NULL NULL heart 1 NULL NULL heart 4 heart 4 21, 38 heart 10 heart 10 96 heart 11 heart 11 25, 52 heart 12 heart 12 36, 73 heart 5 heart 5 10, 18, 33, 75 heart 11 heart 11 25, 52 heart 3 NULL NULL heart 7 heart 7 27, 43 heart 7 heart 7 27, 43 heart 7 heart 7 27, 43 heart 8 heart 8 46, 54, 70 heart 5 heart 5 10, 18, 33, 75 heart 9 heart 9 3, 32, 77, 82 heart 4 heart 4 21, 38 121/144
  123. 123. インデックスで取り出しても、既に並びが崩れている ⾚いトランプのカード ⿊いトランプのカード ⿊いトランプ上から heart 3 NULL NULL heart 1 NULL NULL heart 1 NULL NULL heart 4 heart 4 21, 38 heart 10 heart 10 96 heart 11 heart 11 25, 52 heart 12 heart 12 36, 73 heart 5 heart 5 10, 18, 33, 75 heart 11 heart 11 25, 52 heart 3 NULL NULL heart 7 heart 7 27, 43 heart 7 heart 7 27, 43 heart 7 heart 7 27, 43 heart 8 heart 8 46, 54, 70 heart 5 heart 5 10, 18, 33, 75 heart 9 heart 9 3, 32, 77, 82 heart 4 heart 4 21, 38 122/144
  124. 124. ソートが終わってからで ないと、”若い⽅から5 枚”が判定できないので、 結局最後までソートせざ るをえない。 123/144
  125. 125. インデックスでソートまで解決するには 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 BY black̲card.numberはORDER BY red̲card.numberと等価だと気付い てそれを使っている。 124/144
  126. 126. これだと ⾚いトランプのカード ⿊いトランプのカード ⿊いトランプ上から heart 1 NULL NULL heart 1 NULL NULL heart 3 NULL NULL heart 3 NULL NULL heart 4 heart 4 21, 38 heart 4 heart 4 21, 38 heart 5 heart 5 10, 18, 33, 75 heart 5 heart 5 10, 18, 33, 75 heart 7 heart 7 27, 43 heart 7 ⽐較不要 ⽐較不要 heart 7 ⽐較不要 ⽐較不要 heart 8 ⽐較不要 ⽐較不要 heart 9 ⽐較不要 ⽐較不要 heart 10 ⽐較不要 ⽐較不要 heart 11 ⽐較不要 ⽐較不要 heart 11 ⽐較不要 ⽐較不要 heart 12 ⽐較不要 ⽐較不要 125/144
  127. 127. ⾚いトランプのカードを取り出した時点で既にソートが終わってい る 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
  128. 128. というのを知っていると、JOINで遅くなるケースはまあまあ納得が いく そもそもスキャンジョインしている ソートまでインデックスだけで解決できていない 17⾏くらい⼤したことないけれど、1000万⾏あったらソートするの はだいぶ骨が折れる。 - MySQL 5.6でオプティマイザーが賢くなったので、新しいバージョンを 使っていればこういう内部動作を気にしなくてもいいかも知れないけれど 内部的な動作原理を知っているとどんなインデックスを作ると速くなるか (あるいは、ならないか)が想像しやすい 127/144
  129. 129. 取り敢えずの動作としては”まずWHERE句を処理するためのカラ ム” “次にORDER BY句を処理するためのカラム”の順に並べる WHERE句を処理するためのカラムにINやANYが⼊っていると、それより 後ろに列挙されたぶんが使えなくなるので注意 カラムに対して演算を⾏っているとインデックスは効かない 更に⾼速化を狙うならselect̲listに⼊っているものを含めてcovering indexを狙う GROUP BYはWHEREのあと、ORDER BYの前に処理されるので、 GROUP BY⽤のカラムを追加するなら(WHERE, GROUP BY, ORDER BY)の順番になる。 HAVINGやその後のORDER BYで集計関数の結果カラムを指定してい ると、それは”演算を⾏っている”のでその部分はインデックスではス キップできない。 - 128/144
  130. 130. 掲⽰板に戻る 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 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⽇のストラトス 129/144
  131. 131. 掲⽰板に戻る userテーブル カラム データ例1 データ例2 user̲id yoku0825 名無しさん user̲email yoku0825@gmail.com sage 130/144
  132. 132. 参照⽤クエリー 新しく作成された順にスレッド⼀覧を表⽰するクエリー SELECT thread_id, thread_title FROM thread ORDER BY thread_created DESC WHEREはないがthread̲createdで逆順ソートしているので、 idx̲threadcreated (thread̲created)はあるとクイックソートを⾶ばせ る。 先頭100件のみ、などとやる時に末尾までソートしなくていいので⾼速化 できる。 131/144
  133. 133. 参照⽤クエリー コメント数の多い順にスレッド⼀覧を表⽰するクエリー SELECT thread_id, thread_title FROM thread ORDER BY comment_count DESC idx̲commentcount(comment̲count)があった⽅がいい。 132/144
  134. 134. 参照⽤クエリー 新しくコメントされた順にスレッド⼀覧を表⽰するクエリー SELECT thread_title FROM bbs ORDER BY last_posted DESC idx̲lastposted(last̲posted)があった⽅がいい 133/144
  135. 135. 参照⽤クエリー スレッドを詳細表⽰するクエリー(メタデータを取り出すところ) SELECT thread.thread_title, thread.thread_owner, user.user_email AS thread_owner_email, thread.thread_created, thread.comment_count, thread.last_posted FROM thread JOIN user ON thread.thread_owner = user.user_name WHERE thread.thread_id = 1 threadテーブルのthread̲idは既にプライマリーキーなので1つ目の WHERE句はOK userテーブルのuser̲nameもプライマリーキーなのでOK 134/144
  136. 136. 参照⽤クエリー スレッドを詳細表⽰するクエリー(つづき) SELECT comment.comment_number, comment.comment_owner, user.user_email AS comment_owner_email, comment.comment_posted, comment.comment_body FROM comment JOIN user ON comment.comment_owner = user.user_name WHERE comment.thread_id = 1 ORDER BY comment.comment_number userテーブルのuser̲nameもプライマリーキーなのでOK commentテーブルの(thread̲id, comment̲number)はプライマリーキ ーで順番もこの通りでOK 135/144
  137. 137. 他にもたとえば コメント数の多いユーザー10⼈を抽出するクエリー SELECT user.user_name, COUNT(*) AS comment_count FROM user JOIN comment ON user.user_name = comment.comment_owner GROUP BY user.user_name ORDER BY comment_count DESC LIMIT 10 commentテーブルの結合のためにidx̲commentowner (comment̲owner) userテーブルのGROUP BYのためにuser̲nameはプライマリーキーなの でこのままでOK ORDER BYのcomment̲countは集計関数の結果カラムなのでソートは 避けられない 5.6以降ならばソート⽤の⼀時インデックスを⾃動で作るかも。- 136/144
  138. 138. 他にもたとえば ユーザーの最終コメント時刻を抽出するクエリー SELECT user.user_name, MAX(comment.comment_posted) AS last_posted FROM user JOIN comment ON user.user_name = comment_comment_owner GROUP BY user.user_name どういう風に結合されるのかがイメージできると、 idx̲commentowner̲commentposted(comment̲owner, comment̲posted)があった⽅がいい気がしてきませんか︖ 137/144
  139. 139. 指標的なもの EXPLAIN Extra: Using filesortでORDER BYやGROUP BYがインデックスで 解決できていない - Extra: Using tempraryでGROUP BYやJOINがインデックスで解決で きていない - Extra: Using join bufferでスキャンジョイン- 138/144
  140. 140. 指標的なもの SHOW GLOBAL STATUS created̲tmp̲%tables .. テンポラリーテーブルが作成された (Extra: Using temporaryな)クエリーでカウントアップされる - handler̲read% .. 実際に⾏をフェッチしているとカウントアップさ れる - Select̲full̲join, Select̲scan- Sort̲merge̲passes, Sort̲scan- 139/144
  141. 141. 指標的なもの performance̲schema events̲statements̲summary̲by̲digestテーブルなど- 140/144
  142. 142. インデックスまとめ 検索が増えれば必要なインデックスも増える インデックスが増えれば更新性能, ファイルサイズにオーバーヘッドが加 わる 更新のデメリットに対して参照のメリットは⼗分⼤きい covering indexで仕留めればクエリーの速度が数百倍になることも 珍しくない - Disk容量が⾜りているなら、SELECTタイプごとに複合インデックス 作った⽅がいい - 余談: InnoDBは⾏ロックじゃなくてネクストキーロック 適切なインデックスがないとロックで死ねる- MySQLは5.6でオプティマイザーに⼤きな改良が加えられている これから利⽤なら5.6、可能なら5.7も- MariaDBなら5.3以降を- 141/144
  143. 143. おまけ thread.comment̲countってcommentから計算できるから消さないと いけないんじゃないの︖ “one fact in one place”っていうとそうなんですが- C.J.Dateの本を読んでる限りでは、正規化のプロセスとしてはそこを 切り離すのは⾒当たらなかった ご存知の⽅いたら本の名前を教えてください。 - 個⼈的には集約関数の結果をソートするのはオーバーヘッドが⼤きい し、threadテーブルから⾒るとcomment̲countは個々のスレッドが 持つ “属性” で、たまたま外部で計算したものと値が⼀致するだけ、 と考えるようにしている。 - 142/144
  144. 144. おまけ なんでuserテーブルだけナチュラルキーにしたの︖ comment̲numberもナチュラルのつもりです(n番目のコメント、と いう情報がきちんとコメントを識別する意味を持っている) thread̲idは完全にサロゲートキーです - 基本的にナチュラルキーにしたいんですけど、 InnoDBのセカンダリーキーのリーフにはPRIMARY KEYの値が格 納される InnoDBのデータ本体はPRIMARY KEYのリーフにぶら下がる InnoDBのチェンジバッファは、セカンダリーキーにしか効かない - このあたりの制約から、ある程度以上のボリュームがあったり更新が あるのが目に⾒えているものはサロゲートキーを使うことが多いで す。 - 143/144
  145. 145. それでは、素 敵なMySQLラ イフを 144/144

×