SQL実践入門
目的
実行計画を読み解け、簡単なSQLチューニングならできるようになる。
(インデックスを貼る、結合アルゴリズムを変更させるようにSQLを改変する等)
DBMSのアーキテクチャ
メモリにデータがあれば高速、ディスクにしかなければ低速。頻繁にアクセスされるデータをメモリ上に置くかで速度は変わる。
基本的にSQL文が遅くなる原因は大きく分けて2つあるが、ほとんどの場合はストレージのI/Oによるものである。
- ストレージのI/O
- CPUによる演算
※非常に小さいデータにしかアクセスしない場合(単一主キーによる検索SQLなど)、CPUによる演算が原因となるときもある。
クエリの処理とデータアクセスの実行のイメージ
カタログマネージャ(統計情報)にテーブルやインデックスの情報が格納されている。
→そのため、統計情報が古いと正常な実行計画が返却されない。
統計情報として収集される代表的なものは以下のとおり
- 各テーブルのレコード数
- 各テーブルの列数と列のサイズ
- 列値のカーディナリティ(値の個数)
- 列値のヒストグラム(各値がいくつあるのかの分布)
- 列内にあるNULLの数
- インデックス情報
大きく更新がなされた際に統計情報を更新することで、適切な実行計画を作成できるようになる。
そのため、統計情報の更新についてもしっかり考える必要がある
- 定期的に更新するジョブをジョブネットに組み込む
- 自動的に統計情報を更新してくれるデータベースもある
- Oracleならデフォルトで定期的に実行してくれる
- PostgreSQLでも閾値以上のデータ更新処理が走った後に自動実行される
B+treeインデックスについて
インデックスを有効活用するポイント
以下の列にインデックスを貼ると効果が出やすい。
- カーディナリティが高い列
- 選択率が低い(少ない行に絞り込める)列(5~10%未満)
インデックスが使えない検索条件
- 中間、後方一致のLIKE句
- カラムの値に対して演算を実行、または、関数を適用している
- IS NULL句を利用している
- 否定形(<>、!=、NOT IN)を利用している
実行計画の確認方法
PostgreSQLの場合は以下で確認できる。
EXPLAIN SQLクエリ
また、以下にすることで、実際にSQL文を投げてみた結果を取得することができる
EXPLAIN ANALYZE SQLクエリ
テーブルフルスキャンの場合の実行計画
以下のSQLを投げた時の実行計画
SELECT *
FROM Shops;
-------------------------------------------------------
Seq Scan on shops (cost=0.00..1.60 rows=60 width=22)
- on Shops
操作対象のオブジェクト(テーブル以外にもインデックス、パーティション、シーケンスなども現れる)
- Seq Scan
シーケンシャルスキャン。当該テーブルの全データにアクセスするという意味。
- rows=60
操作の対象となるレコード数(あくまで統計情報から取得した値なので実際に入っているデータとは違う場合が多い)
- cost=0.00..1.60
実行時間の目安となる値ではあるが、あくまで目安なので、大きくても早い時もあるし、小さくても遅いときもある。
ただ、経験的にはコスト1000で1秒のイメージではある。(たしか。。)
cost=XXX...YYY
ちなみに上記のXXXとYYYの2つの値の意味は、XXXがレコードの1件目を取り出し始めるのにかかるコストで、YYYがレコードの最終行目を取り出し終わるのにかかるコストという意味。
インデックススキャンの場合の実行計画
以下のSQLを投げて実行計画を取得する。
SELECT *
FROM Shops
WHERE shop_id = '00050';
-------------------------------------------------------
Index Scan using pk_shops on shops (cost=0.00..8.27 rows=1 width=320)
Filter: (shop_id = '00050'::bpchar)
- Filter: (shop_id = '00050'::bpchar)
データ量が少なければインデックススキャンでもフルスキャンでも大差はない。
ただ、多量のレコードを取得する場合(一般に7%~10%程度)やレコード総数が少ない場合にはフルスキャンを行ったほうが効率が良い場合もある。
テーブル結合の実行計画
SELECT shop_name
FROM Shops S
INNER JOIN Reservations R
ON S.shop_id = R.shop_id;
-------------------------------------------------------
Nested Loop (cost=0.14..14.80 rows=10 width=2)
-> Seq Scan on reservations r (cost=0.00..1.10 rows=10 width=6)
-> Index Scan using pk_shops on shops s (cost=0.14..1.36 rows=1 width=8)
Index Cond: (shop_id = r.shop_id)
- Nested Loop
Nested Loop結合(後述)でテーブルを結合する
- アクセスする順番
- 深い階層から処理される。
- 同じインデントなら上に位置した項目から処理される
→そのため、Seq Scan → Index Scan → Nested Loopの順で処理される。
実行計画の重要性
オプティマイザが選ぶ実行計画は動的に変わるため、基本はオプティマイザに任せるようにするのが良い。
(データベースのデータは時とともに変化するので)
ただ、絶対に使ったほうが早いはずのインデックスを使ってくれない、Nested Loop結合でJOINしたほうが早いのにMerge JOINされてしまう、など明らかにおかしいことも行ってしまうことがある。
そのため、ヒント句を用いて実行計画を手動で変えることもある。
以下はT1とT2をNested Loop結合に強制するヒント句。
/*+
* NestLoop(T1 T2)
*/
SELECT *
FROM TABLE_A T1
INNER JOIN TABLE_B T2
SQLアンチパターン
UNIONによる条件分岐はNG
SELECT item_name, year, price_tax_ex AS price
FROM Items
WHERE year <= 2001
UNION ALL
SELECT item_name, year, price_tax_in AS price
FROM Items
WHERE year >= 2002;
item_name | year | price_tax_ex | price_tax_in |
---|---|---|---|
ITEM1 | 2000 | PRICE_EX1 | |
ITEM2 | 2001 | PRICE_EX1 | |
ITEM3 | 2002 | PRICE_IN1 | |
ITEM3 | 2003 | PRICE_IN1 |
------------------------------------------------------------
Append (cost=0.00..2.42 rows=12 width=47)
-> Seq Scan on items (cost=0.00..1.15 rows=6 width=47)
Filter: (year <= 2001)
-> Seq Scan on items (cost=0.00..1.15 rows=6 width=47)
Filter: (year >= 2002)
WHERE句で条件分岐させるのは素人
SELECT item_name, year,
CASE WHEN year <= 2001 THEN price_tax_ex
WHEN year >= 2002 THEN price_tax_in END AS price
FROM Items;
---------------------------------------------------------
Seq Scan on items (cost=0.00..1.18 rows=12 width=51)
※UNIONではインデックスを利用できていたが、CASE式を利用することでインデックスが働かなくなり、性能が悪化することもあるので注意。
SQLのループはNG
for (String string : productList) {
DTO dto = select(string);
}
<select>
SELECT *
FROM PRODUCT
WHERE productCd = #{string}
</select>
List<DTO> dtoList = select(productList);
<select>
SELECT *
FROM PRODUCT
WHERE productCd IN
<foreach item="item" index="index" collection="productList"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
結合アルゴリズム
Nested Loop
結合コストはR(T1)×R(T2)
となる。
ここで、内部表にインデックスを貼ることにより、R(T2)≒1と近似できるため、コストは実質R(T1)
となる。
その際に駆動表に小さいテーブルを選ぶことで、よりコストを削減できる。
実際のSQLで確認
SELECT E.emp_id, E.emp_name, E.dept_id, D.dept_name
FROM Employees E INNER JOIN Departments D
ON E.dept_id = D.dept_id;
----------------------------------------------------------------------------
Nested Loop (cost=0.00..5005.38 rows=510 width=212)
Join Filter: (e.dept_id = d.dept_id)
-> Seq Scan on departments d (cost=0.00..16.50 rows=650 width=94)
-> Materialize (cost=0.00..17.65 rows=510 width=130)
-> Seq Scan on employees e (cost=0.00..15.10 rows=510 width=130)
---------------------------------------------------------------------------------------------
Nested Loop (cost=0.15..150.90 rows=510 width=212)
-> Seq Scan on employees e (cost=0.00..15.10 rows=510 width=130)
-> Index Scan using departments_pkey on departments d (cost=0.15..0.26 rows=1 width=94)
Index Cond: (dept_id = e.dept_id)
Hash
ハッシュ関数:入力に対してなるべく一意性と一様性を持った値を出力する関数
ハッシュアルゴリズムとして有名なものがMD5やSHA1、SHA256など
パスワードのDBへの格納等に利用されている。
'password'→5f4dcc3b5aa765d61d8327deb882cf99(md5)
- 小さいほうのテーブルの結合キー列を全行読み込みハッシュ関数にかける
- 生成したハッシュテーブルをメモリ上に読み込む
- もう片方のテーブルを上から順に読み込み、順番にハッシュ関数にかけて合致するものがないかを検索する。
------------------------------------------------------------------------
Hash Join (cost=24.63..46.74 rows=510 width=212)
Hash Cond: (e.dept_id = d.dept_id)
-> Seq Scan on employees e (cost=0.00..15.10 rows=510 width=130)
-> Hash (cost=16.50..16.50 rows=650 width=94)
-> Seq Scan on departments d (cost=0.00..16.50 rows=650 width=94)
メリット
結合する前にソートを行う必要がない。(後述するSort Merge結合との比較)
Nested Loop結合における内部表にヒット件数が多い場合でも有効に働く。
デメリット
ハッシュテーブルをメモリ上に持つため、メモリ上に収まらない場合ストレージを使用するので遅延が発生する
等値結合でしか使えない(条件に大小条件を利用できない)
Sort Merge
それぞれのテーブルに対してソートを行ってマッチングするだけ。
→ソートコストが重い。
結合アルゴリズムの利点欠点
サブクエリの弊害
- サブクエリの計算コストがかかる(中身のSELECTを実行するたびにデータを作る必要がある)
- データのI/Oコストがかかる(メモリ上に収まらない場合ストレージI/Oが発生する。)
- 最適化を受けられない(インデックス等の情報が存在しないのでクエリ解析で最適化できない)
※最近の実行計画ではサブクエリ内部のロジックと外部のロジックをマージして実行計画を立ててくれる。
しかし、2階層以上ネストしたサブクエリではその恩恵を授かれない。
サブクエリで性能問題を起こす例
SELECT R1.cust_id, R1.seq, R1.price
FROM Receipts R1
INNER JOIN (SELECT cust_id, MIN(seq) AS min_seq
FROM Receipts
GROUP BY cust_id) R2
ON R1.cust_id = R2.cust_id
AND R1.seq = R2.min_seq;
-----------------------------------------------------------------------------------
Hash Join (cost=1.34..2.57 rows=1 width=10)
Hash Cond: ((r1.cust_id = receipts.cust_id) AND (r1.seq = (min(receipts.seq))))
-> Seq Scan on receipts r1 (cost=0.00..1.13 rows=13 width=10)
-> Hash (cost=1.27..1.27 rows=4 width=6)
-> HashAggregate (cost=1.19..1.23 rows=4 width=6)
-> Seq Scan on receipts (cost=0.00..1.13 rows=13 width=6)
問題点
receiptsテーブルに2回アクセスしている。
SELECT cust_id, seq, price
FROM (SELECT cust_id, seq, price,
ROW_NUMBER()
OVER (PARTITION BY cust_id
ORDER BY seq) AS row_seq
FROM Receipts ) WORK
WHERE WORK.row_seq = 1;
----------------------------------------------------------------------------
Subquery Scan work (cost=1.37..1.79 rows=1 width=16)
Filter: (work.row_seq = 1)
-> WindowAgg (cost=1.37..1.63 rows=13 width=10)
-> Sort (cost=1.37..1.40 rows=13 width=10)
Sort Key: receipts.cust_id, receipts.seq
-> Seq Scan on receipts (cost=0.00..1.13 rows=13 width=10)
サブクエリでパフォーマンスが上がるケース
SELECT C.co_cd, MAX(C.district),
SUM(emp_nbr) AS sum_emp
FROM Companies C
INNER JOIN Shops S
ON C.co_cd = S.co_cd
WHERE main_flg = 'Y'
GROUP BY C.co_cd;
----------------------------------------------------------------------
HashAggregate (cost=53.46..53.52 rows=6 width=28)
-> Nested Loop (cost=0.00..53.43 rows=6 width=28)
-> Seq Scan on shops s (cost=0.00..23.75 rows=6 width=20)
Filter: (main_flg = 'Y'::bpchar)
-> Index Scan using companies_pkey on companies c (cost=0.00..4.93 rows=1 width=24)
Index Cond: (co_cd = s.co_cd)
サブクエリを利用した場合
SELECT C.co_cd, C.district, sum_emp
FROM Companies C
INNER JOIN (SELECT co_cd, SUM(emp_nbr) AS sum_emp
FROM Shops
WHERE main_flg = 'Y'
GROUP BY co_cd) CSUM
ON C.co_cd = CSUM.co_cd;
----------------------------------------------------------------------
Nested Loop (cost=23.78..40.38 rows=2 width=32)
-> HashAggregate (cost=23.78..23.80 rows=2 width=20)
-> Seq Scan on shops (cost=0.00..23.75 rows=6 width=20)
Filter: (main_flg = 'Y'::bpchar)
-> Index Scan using companies_pkey on companies c (cost=0.00..8.27 rows=1 width=24)
Index Cond: (co_cd = shops.co_cd)