Slide 1

Slide 1 text

MySQL と PostgreSQL と INDEX (良いタイトルが思い浮かばなかったので考えるのを諦めた) 第 18 回 PostgreSQL アンカンファレンス@オンライン  2020/11/02 まつひさ(hmatsu47)

Slide 2

Slide 2 text

自己紹介(前回とだいたい同じ) 松久裕保(@hmatsu47) https://qiita.com/hmatsu47 名古屋で Web インフラのお守り係をしています MySQL 8.0 の薄い本を作って配っています ○ Qiitaの記事: https://qiita.com/hmatsu47/items/ceb75caf46e3c761095d ○ GitHub リポジトリの他、印刷版を勉強会などで無料配布していました ○ 新型コロナウイルスの関係でオフライン勉強会ができなくなったので、 現在は BOOTH でも配布しています(100円+送料)8.0.22対応版準備中 https://booth.pm/ja/items/2062599 2

Slide 3

Slide 3 text

自己紹介(前回と同じ) 松久裕保(@hmatsu47) https://qiita.com/hmatsu47 名古屋で Web インフラのお守り係をしています MySQL 8.0 の薄い本を作って配っています ○ Qiitaの記事: https://qiita.com/hmatsu47/items/ceb75caf46e3c761095d ○ GitHub リポジトリの他、印刷版を勉強会などで無料配布していました ○ 新型コロナウイルスの関係でオフライン勉強会ができなくなったので、 現在は BOOTH でも配布しています(100円+送料)不良在庫が… https://booth.pm/ja/items/2062599 3 (ちょっとだけ)レビューに参加しました  https://booth.pm/ja/items/2398704

Slide 4

Slide 4 text

余談① ??? 4

Slide 5

Slide 5 text

余談① !!!(原因:なぜかはてブのホッテントリ入りしてた) 5

Slide 6

Slide 6 text

余談② 今秋の IPA DB スペシャリスト試験で… ● 前回のネタでの予想が一部当たって(?)、午後 Ⅱ 問 1 に IoT っぽいデータを扱う出題が !? ● クラスタリングも含まれていたが、こちらは試験問題の レベルを上げない配慮か、かなり簡略化された扱いに 6

Slide 7

Slide 7 text

余談③ Qiita で… ● 前回ネタのおまけ記事が デイリートレンドの下の ほうに… !? 7

Slide 8

Slide 8 text

今日は… ● PostgreSQL アンカンファレンスのゆるふわ枠存続の  ために、登壇枠で参加(3 回目) ● だがしかし、ネタがない ○ 10/31(土)までネタ選定に悩む… 8

Slide 9

Slide 9 text

そーだいさんの Tweet を思い出した →残念ながらカンファレンスには出れないけれど、今回のネタは  とりあえず MySQL と PostgreSQL の違いをテーマにしてみる 9

Slide 10

Slide 10 text

また、とある Twitter の FF さんが… ● 「MySQL で INDEX 作ったら SELECT が遅くなった」 ● 「MySQL で 10 秒以上掛かったのに、PostgreSQL では 1 秒で終わった」 ● 「最適な INDEX を作ったら MySQL でも 1 秒で終わった けれど、INDEX 職人するのつらい」 (すべて意訳) 10

Slide 11

Slide 11 text

というわけで、 ● FF さん事例を少しアレンジして、 ● 同じ構造のテーブルを作って同じデータを入れて、 ● 同じように INDEX を数パターン作ってみて 比較してみた 11

Slide 12

Slide 12 text

テストの内容(1/3) ● 100 台の機器(uuid で識別)から、毎日 1 時間おきに  約 7 年半の間に届いた2 ヶ月分の計測値を、機器別に  集計して平均値を求める →6,553,600 行のデータから、100 (台) × 24 (時間) × 61 (日)  =146,400 行を抽出・グループ化して平均値を取得 12

Slide 13

Slide 13 text

テストの内容(2/3) ● 比較する INDEX は以下の 6 種類 ○ ①なし ○ ②日時(date_created)のみ ○ ③日時(date_created)+uuid ○ ④日時(date_created)+uuid+計測値(value) ○ ⑤日時(date_created)+計測値(value) ○ ⑥uuid のみ 13

Slide 14

Slide 14 text

テストの内容(2/3) ● 比較する INDEX は以下の 6 種類 ○ ①なし ○ ②日時(date_created)のみ ○ ③日時(date_created)+uuid ○ ④日時(date_created)+uuid+計測値(value) ○ ⑤日時(date_created)+計測値(value) ○ ⑥uuid のみ 14 ※③~⑤については、絞り込み範囲 を日時のみからさらに限定すること を意図したものではありません。 (④はカバリングになるかの確認)

Slide 15

Slide 15 text

テストの内容(3/3) ● テスト環境は以下のとおり ○ AWS RDS(MySQL 5.7.31 / PostgreSQL 12.3 R1) ○ db.m5.xlarge(4vCPU・16GiB Mem) ○ Single AZ ○ デフォルトパラメータグループ 15

Slide 16

Slide 16 text

①INDEX なし CREATE TABLE t1noindex (id int PRIMARY KEY, uuid VARCHAR(40) NOT NULL, date_created timestamp NOT NULL, value int); ②日付(date_created)のみ CREATE TABLE t1date (id int PRIMARY KEY, uuid VARCHAR(40) NOT NULL, date_created timestamp NOT NULL, value int); CREATE INDEX indexdate ON t1date (date_created); ③日付(date_created)+uuid CREATE TABLE t1dateuuid (id int PRIMARY KEY, uuid VARCHAR(40) NOT NULL, date_created timestamp NOT NULL, value int); CREATE INDEX indexdateuuid ON t1dateuuid (date_created, uuid); テスト用のテーブル(1/2) 16

Slide 17

Slide 17 text

④日付(date_created)+uuid+計測値(value) CREATE TABLE t1dateuuidval (id int PRIMARY KEY, uuid VARCHAR(40) NOT NULL, date_created timestamp NOT NULL, value int); CREATE INDEX indexdateuuidval ON t1dateuuidval (date_created, uuid, value); ⑤日付(date_created)+計測値(value) CREATE TABLE t1dateval (id int PRIMARY KEY, uuid VARCHAR(40) NOT NULL, date_created timestamp NOT NULL, value int); CREATE INDEX indexdateval ON t1dateval (date_created, uuid, value); ⑥uuid のみ CREATE TABLE t1uuid (id int PRIMARY KEY, uuid VARCHAR(40) NOT NULL, date_created timestamp NOT NULL, value int); CREATE INDEX indexuuid ON t1dateuuidval (uuid); テスト用のテーブル(2/2) 17

Slide 18

Slide 18 text

t1=> SELECT uuid, AVG(value) FROM t1noindex WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid; uuid | avg --------------------------------------+------------------------ 41406cb0-1bef-11eb-a84c-063e4b2ff148 | 100.0000000000000000 41410373-1bef-11eb-a84c-063e4b2ff148 | 99.0000000000000000 41418bde-1bef-11eb-a84c-063e4b2ff148 | 98.0000000000000000 4141f03b-1bef-11eb-a84c-063e4b2ff148 | 97.0000000000000000 41427fd0-1bef-11eb-a84c-063e4b2ff148 | 96.0000000000000000 414314f9-1bef-11eb-a84c-063e4b2ff148 | 95.0000000000000000 (中略) 4170f36e-1bef-11eb-a84c-063e4b2ff148 | 5.0000000000000000 41716ab0-1bef-11eb-a84c-063e4b2ff148 | 4.0000000000000000 4171edeb-1bef-11eb-a84c-063e4b2ff148 | 3.0000000000000000 41726790-1bef-11eb-a84c-063e4b2ff148 | 2.0000000000000000 4172e0b8-1bef-11eb-a84c-063e4b2ff148 | 1.00000000000000000000 (100 rows) クエリの実行結果(部分)※表示の例は PostgreSQL 18

Slide 19

Slide 19 text

テストの結果(クエリ 5 回以上実行・最後の 3 回の平均) 19 INDEX の種類 MySQL 5.7 PostgreSQL 12 ①なし 2,000 ms 268 ms ②日時のみ 270 ms 26 ms ③日時+uuid 270 ms 29 ms ④日時+uuid+計測値 183 ms 30 ms ⑤日時+計測値 277 ms 29 ms ⑥uuid のみ 8,560 ms 269 ms

Slide 20

Slide 20 text

MySQL で「uuid のみ」が極端に遅いのは? ● uuid のみでは、ソートはできても絞り込みができない ● INDEX 全行スキャン→その後テーブル全行をフィルタ処理 ○ 集約時のソート効果 <<< アクセス経路が長くなるデメリット ○ 単純にフルテーブルスキャンするほうが速い 20 INDEX の種類 MySQL 5.7 PostgreSQL 12 ①なし 2,000 ms 268 ms ⑥uuid のみ 8,560 ms 269 ms

Slide 21

Slide 21 text

mysql> EXPLAIN FORMAT=JSON SELECT uuid, AVG(value) FROM t1uuid WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid\G *************************** 1. row *************************** EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1301666.80" }, "grouping_operation": { "using_filesort": false, "table": { "table_name": "t1uuid", "access_type": "index", "possible_keys": [ "indexuuid" ], "key": "indexuuid", (後略) MySQL ではソートの効果はあるものの… 21 集約時の filesort が不要に

Slide 22

Slide 22 text

mysql> EXPLAIN SELECT uuid, AVG(value) FROM t1uuid WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1uuid partitions: NULL type: index possible_keys: indexuuid key: indexuuid key_len: 42 ref: NULL rows: 6360514 filtered: 11.11 Extra: Using where 1 row in set, 1 warning (0.00 sec) MySQL では INDEX+テーブル全行スキャン→遅い 22

Slide 23

Slide 23 text

t1=> EXPLAIN SELECT uuid, AVG(value) FROM t1uuid WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid; QUERY PLAN ------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------- Finalize GroupAggregate (cost=116748.46..116774.04 rows=100 width=69) Group Key: uuid -> Gather Merge (cost=116748.46..116771.79 rows=200 width=69) Workers Planned: 2 -> Sort (cost=115748.43..115748.68 rows=100 width=69) Sort Key: uuid -> Partial HashAggregate (cost=115744.11..115745.11 rows=100 width=69) Group Key: uuid -> Parallel Seq Scan on t1uuid (cost=0.00..115433.15 rows=62192 width=41) Filter: ((date_created >= '2020-10-01 00:00:00'::timestamp without time zone) AND (date_created <= '2020-11-30 23:59:59'::timestamp without time zone)) (10 rows) ※テーブルを Parallel Seq Scan PostgreSQL では INDEX を使わない 23

Slide 24

Slide 24 text

日時が先頭の INDEX(②③④⑤)の効果の違いは? ● MySQL は④でカバリング INDEX の効果あり ● PostgreSQL は④も効果なし 24 INDEX の種類 MySQL 5.7 PostgreSQL 12 ②日時のみ 270 ms 26 ms ③日時+uuid 270 ms 29 ms ④日時+uuid+計測値 183 ms 30 ms ⑤日時+計測値 277 ms 29 ms

Slide 25

Slide 25 text

mysql> EXPLAIN SELECT uuid, AVG(value) FROM t1date WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1date partitions: NULL type: range possible_keys: indexdate key: indexdate key_len: 4 ref: NULL rows: 282244 filtered: 100.00 Extra: Using index condition; Using temporary; Using filesort 1 row in set, 1 warning (0.00 sec) MySQL では、(②日時のみ) 25

Slide 26

Slide 26 text

mysql> EXPLAIN SELECT uuid, AVG(value) FROM t1dateuuid WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1dateuuid partitions: NULL type: range possible_keys: indexdateuuid key: indexdateuuid key_len: 4 ref: NULL rows: 294202 filtered: 100.00 Extra: Using index condition; Using temporary; Using filesort 1 row in set, 1 warning (0.00 sec) MySQL では、(③日時+uuid)※⑤日時+計測値もほぼ同じ 26

Slide 27

Slide 27 text

mysql> EXPLAIN SELECT uuid, AVG(value) FROM t1dateuuidval WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1dateuuidval partitions: NULL type: range possible_keys: indexdateuuidval key: indexdateuuidval key_len: 4 ref: NULL rows: 302700 filtered: 100.00 Extra: Using where; Using index; Using temporary; Using filesort 1 row in set, 1 warning (0.00 sec) MySQL では、(④日時+uuid+計測値 / カバリング INDEX) 27

Slide 28

Slide 28 text

MySQL では、④のカバリング INDEX のみ効果あり ● EXPLAIN を見ても差がわかりづらい ○ むしろ悪化しているように見える… ● ④のカバリング INDEX では、テーブル行を見ない分高速 ○ 今回の例では差はわずか ○ 主キーの並び順≠日時順でない場合は、もっと効果が高い ■ クラスタインデックスであるため ○ カバリングでなければ無効果(アクセス経路変わらず・ソート効果低) 28

Slide 29

Slide 29 text

t1=> EXPLAIN SELECT uuid, AVG(value) FROM t1date WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid; QUERY PLAN ------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------- Finalize GroupAggregate (cost=6014.99..6040.57 rows=100 width=69) Group Key: uuid -> Gather Merge (cost=6014.99..6038.32 rows=200 width=69) Workers Planned: 2 -> Sort (cost=5014.96..5015.21 rows=100 width=69) Sort Key: uuid -> Partial HashAggregate (cost=5010.64..5011.64 rows=100 width=69) Group Key: uuid -> Parallel Index Scan using indexdate on t1date (cost=0.43..4733.69 rows=55390 width=41) Index Cond: ((date_created >= '2020-10-01 00:00:00'::timestamp without time zone) AND (date_created <= '2020-11-30 23:59:59'::timestamp without time zone)) (10 rows) ※Parallel Index Scan PostgreSQL では、(②日時のみ) 29

Slide 30

Slide 30 text

t1=> EXPLAIN SELECT uuid, AVG(value) FROM t1dateuuid WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid; QUERY PLAN ------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Finalize GroupAggregate (cost=105685.08..105710.67 rows=100 width=69) Group Key: uuid -> Gather Merge (cost=105685.08..105708.42 rows=200 width=69) Workers Planned: 2 -> Sort (cost=104685.06..104685.31 rows=100 width=69) Sort Key: uuid -> Partial HashAggregate (cost=104680.74..104681.74 rows=100 width=69) Group Key: uuid -> Parallel Bitmap Heap Scan on t1dateuuid (cost=6545.33..104368.39 rows=62470 width=41) Recheck Cond: ((date_created >= '2020-10-01 00:00:00'::timestamp without time zone) AND (date_created <= '2020-11-30 23:59:59'::timestamp without time zone)) -> Bitmap Index Scan on indexdateuuid (cost=0.00..6507.85 rows=149929 width=0) Index Cond: ((date_created >= '2020-10-01 00:00:00'::timestamp without time zone) AND (date_created <= '2020-11-30 23:59:59'::timestamp without time zone)) (12 rows) PostgreSQL では、(③日時+uuid)※⑤もほぼ同じ 30

Slide 31

Slide 31 text

t1=> EXPLAIN SELECT uuid, AVG(value) FROM t1dateuuidval WHERE date_created BETWEEN '2020-10-01 00:00:00' AND '2020-11-30 23:59:59' GROUP BY uuid; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------- ------------------- ------------------------------------------------- Finalize GroupAggregate (cost=106301.65..106327.24 rows=100 width=69) Group Key: uuid -> Gather Merge (cost=106301.65..106324.99 rows=200 width=69) Workers Planned: 2 -> Sort (cost=105301.63..105301.88 rows=100 width=69) Sort Key: uuid -> Partial HashAggregate (cost=105297.31..105298.31 rows=100 width=69) Group Key: uuid -> Parallel Bitmap Heap Scan on t1dateuuidval (cost=7065.83..104990.55 rows=61352 width=41) Recheck Cond: ((date_created >= '2020-10-01 00:00:00'::timestamp without time zone) AND (date_created <= '2020-11-30 23:59:59'::timestamp without time zone)) -> Bitmap Index Scan on indexdateuuidval (cost=0.00..7029.02 rows=147246 width=0) Index Cond: ((date_created >= '2020-10-01 00:00:00'::timestamp without time zone) AND (date_created <= '2020-11-30 23:59:59'::timestamp without time zone)) (12 rows) PostgreSQL では、(④日時+uuid+計測値) 31

Slide 32

Slide 32 text

PostgreSQL では、INDEX 列が増えると僅かに悪化 ● ②日時(date_created)のみで十分 ● ④でも高速化せずさらに(僅かながら)悪化 ○ EXPLAIN を見ると、Index Only Scan になっていない 32

Slide 33

Slide 33 text

まとめ ● MySQL は(現状では)多数の行をスキャンするのが苦手 ○ 全体的に PostgreSQL より遅い ● MySQL と PostgreSQL では INDEX 作成による挙動の  変化に違いがある ○ 一方では有効な INDEX が他方では無意味なことも ○ 今回は絞り込み+集約のパターンだったが、絞り込み+ソート (ソート+絞り込み)のパターンでもまた結果が違うかも 33