H2Databaseを追っかけていたりしたブログ

H2 database のリリースノートを読んだりとか。

H2のEXPLAINを試してみる。

データベースのパフォーマンスチューニングで、SQLのチューニング(インデックスの設定含む)は一番効果が大きい。

それでいて、SQLのチューニングはポータビリティが高い。メモリの設定、ディスクの設定、パラメータの設定はデータベースごとにまったく異なる(基礎的な考え方は同じではあるけれども)。それに比べると、SQLのチューニングはどのデータベースでも同じようなもの(Oracleのヒント句を使ったチューニングなんかは独特だと思うけど)

で、SQLのチューニングを行う上で、重要なのが実行計画。だいたいどのデータベースも実行計画を確認するためのExplainコマンドが用意されている。H2にもある。

まず、以下のようなテーブルを作る。担当者、商品、売り上げ。

create table staff (id int not null primary key, lname varchar, fname varchar, item_id int);
create table item(id int not null primary key, code char(10), type int, name varchar, price decimal(5));
create table sales(id int not null primary key, sales_date date, item_id int, amount int);

-- index
create index ind_staff on staff(lname);
create index ind_item on item(code,type);

-- test
create table noprimary(id int, name varchar);

h2のオプティマイザはコストベースらしいので、データ量やカーディナリティを見るんだと思うが、とりあえず、データなしで。

sql> explain select * from staff;
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.STAFF.tableScan */
(1 row, 108 ms)

h2では内部的には通常の文の部分はUpperCaseされる。あと、スキーマを指定していないので、スキーマはデフォルトのPUBLICが補完されていて、カラム名も展開されている。まずこの様に正規化されるようだ、で、実行計画的には、STAFFテーブルに対して、テーブルスキャンが実行されているのがわかる。

sql> explain select * from staff where id = 1;
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.PRIMARY_KEY_4: ID = 1 */
WHERE ID = 1
(1 row, 11 ms)

単純にプライマリキーを指定した場合。PUBLIC.PRIMARY_KEY_4 はおそらくPrimary Keyに対して自動で生成された索引の名称。

sql> explain select * from staff join item on staff.item_id = item.id;
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID, ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.STAFF /* PUBLIC.STAFF.tableScan */
INNER JOIN PUBLIC.ITEM /* PUBLIC.PRIMARY_KEY_2: ID = STAFF.ITEM_ID AND ID = STAFF.ITEM_ID */ ON 1=1
WHERE STAFF.ITEM_ID = ITEM.ID
(1 row, 18 ms)

内部結合。担当者と商品を結合。担当者側のitem_idが外部キーにもなっておらず、索引も張られていないので、担当者はテーブルスキャン(※後で気がついたが、そもそも担当者の絞り込み条件がないのでテーブルスキャンなのは当たり前)。商品側はPUBLIC.PRIMARY_KEY_2なので、索引が使用されている。正規化の部分で、joinがinnner joinになっていたり、on句の部分が、1=1で置き換えられ、結合条件が、where句の方に回っているのがちょっと面白い。

そういえば、create tableのマニュアルみてたら、h2はDDLを発行するとコミットがかかる。DB2はdrop tableしてしまったあとでも、rollbackでテーブルが戻ってくるという素敵仕様で、あれはいいなぁ、と思う。あんまり使わないけど。他にそういうデータベースあるんだろうか。Derbyとか?

じゃ、外部キーを付けてみる。

alter table staff add constraint fk_staff_item foreign key (item_id) references item(id);

外部キーに限らず制約の名称は省略不可みたい。仕事でやるときにはきちんとつけた方がいいと思うけど、省略できるようになっていてもいいと思う。

sql> explain select * from staff join item on staff.item_id = item.id;
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID, ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.STAFF /* PUBLIC.STAFF.tableScan */
INNER JOIN PUBLIC.ITEM /* PUBLIC.PRIMARY_KEY_2: ID = STAFF.ITEM_ID AND ID = STAFF.ITEM_ID */ ON 1=1
WHERE STAFF.ITEM_ID = ITEM.ID
(1 row, 10 ms)

あれ、特に変わらない...と思ったら、staff側に条件ないんだからそりゃそうか。

sql> explain select * from staff join item on staff.item_id = item.id where staff.item_id = 1;
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID, ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.ITEM /* PUBLIC.PRIMARY_KEY_2: ID = 1 */ /* WHERE ITEM.ID = 1 */
INNER JOIN PUBLIC.STAFF /* PUBLIC.FK_STAFF_ITEM_INDEX_4: ITEM_ID = 1 AND ITEM_ID = ITEM.ID */ ON 1=1
WHERE ((STAFF.ITEM_ID = 1) AND (STAFF.ITEM_ID = ITEM.ID)) AND (ITEM.ID = 1)
(1 row, 32 ms)

結構変わってる。商品に担当者を結合する形になり、staff.item_id = 1 と staff.item_id = item.idから導きだされたのか、ITEM.ID = 1なんてのが追加?されている。

さっきの外部キーを落としてみると、担当者に商品を結合する形になるんだろうか。

sql> alter table staff drop constraint fk_staff_item;
sql> explain select * from staff join item on staff.item_id = item.id where staff.item_id = 1;
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID, ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.ITEM /* PUBLIC.PRIMARY_KEY_2: ID = 1 */ /* WHERE ITEM.ID = 1 */
INNER JOIN PUBLIC.STAFF /* PUBLIC.STAFF.tableScan */ ON 1=1
WHERE ((STAFF.ITEM_ID = 1) AND (STAFF.ITEM_ID = ITEM.ID)) AND (ITEM.ID = 1)
(1 row, 6 ms)

ならない。主キーが強い、ということなんだろうか。fk_staff_itemの索引を見ていた部分が、テーブルスキャンに変わった(そりゃそうだ)だけで、特に変わりはない。

今度は複合索引。

sql> explain select * from item where code = 'AB123' and type = 1; 
PLAN
SELECT ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.ITEM /* PUBLIC.IND_ITEM: CODE = 'AB123' AND TYPE = 1 */
WHERE (CODE = 'AB123') AND (TYPE = 1)
(1 row, 9 ms)

ちゃんと使われている。じゃ、地味に複合索引に指定されたのと逆にする。

sql> explain select * from item where type = 1 and code = 'AB123';
PLAN
SELECT ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.ITEM /* PUBLIC.IND_ITEM: TYPE = 1 AND CODE = 'AB123' */
WHERE (TYPE = 1) AND (CODE = 'AB123')
(1 row, 1 ms)

お。賢い。ちゃんと使われてる。一昔前はOracleもダメだったりしたのに。

複合索引の一部分(先頭)を指定。

sql> explain select * from item where code = 'AB123';
PLAN
SELECT ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.ITEM /* PUBLIC.IND_ITEM: CODE = 'AB123' */
WHERE CODE = 'AB123'
(1 row, 1 ms)

使われてる。

複合索引の一部分(先頭でないもの)を指定。

sql> explain select * from item where type = 1;
PLAN
SELECT ITEM.ID, ITEM.CODE, ITEM.TYPE, ITEM.NAME, ITEM.PRICE
FROM PUBLIC.ITEM /* PUBLIC.ITEM.tableScan */
WHERE TYPE = 1
(1 row, 2 ms)

さすがに使われない。この場合は索引見ないほうがよさそうだから、まぁ、そうなんだろう。

文字列に対するレンジスキャンはどうか。betweenは正規化されたら消えそうだな。

sql> explain select * from staff where lname between 'ABC' and 'XYZ';
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.IND_STAFF: LNAME >= 'ABC' AND LNAME <= 'XYZ' */
WHERE (LNAME >= 'ABC') AND (LNAME <= 'XYZ')
(1 row, 9 ms)

やっぱりbetweenは書き換えられている。そして、レンジスキャンであっても、索引は使われている。

likeだとどうか。前方一致、後方一致、部分一致を見てみる。まず前方一致。

sql> explain select * from staff where lname like 'ABC%';
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.IND_STAFF: LNAME >= 'ABC' AND LNAME < 'ABD' */
WHERE LNAME LIKE 'ABC%'
(1 row, 4 ms)

あ。likeの前方一致だとこうなるのか。考えた事なかった。はー。なるほど。ちゃんと索引が使われてる。

とすると、日本語だとどうか、というと...

sql> explain select * from staff where lname like 'あ%';
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.IND_STAFF: LNAME >= STRINGDECODE('\u3042') AND LNAME < STRINGDECODE('\u3043') */
WHERE LNAME LIKE STRINGDECODE('\u3042%')
(1 row, 1 ms)

まぁ、そうですよね、という。


後方一致と、部分一致はどうか。

sql> explain select * from staff where lname like '%ABC';
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.STAFF.tableScan */
WHERE LNAME LIKE '%ABC'
(1 row, 8 ms)
sql> explain select * from staff where lname like '%ABC%';
PLAN
SELECT STAFF.ID, STAFF.LNAME, STAFF.FNAME, STAFF.ITEM_ID
FROM PUBLIC.STAFF /* PUBLIC.STAFF.tableScan */
WHERE LNAME LIKE '%ABC%'
(1 row, 5 ms)

まぁ、使われない、と。

データが入っていない状態なので、実用上どうか、という話に対する答えにはならないのですが、ある程度思った通りの動きはしそうだな、という感じ。