【SQL】実行計画の読み方

テーブル作成

create table shops (
  shop_id    char(5) not null,
  shop_name  varchar(64),
  area       varchar(64),
   constraint pk_shops primary key (shop_id));

create table items (
  item_id       char(5)  not null, 
  shop_id       char(5) not null,
  item_name     char(32) not null, 
  price         integer  not null, 
  primary key (item_id, shop_id));

データ投入

insert into shops values('10000', 'オレンジストア', '東京');
insert into shops values('20000', 'コンビニ作之助', '埼玉');

insert into items values('10001', '10000', 'pc', 40000);
insert into items values('10002', '10000', 'モニタ', 12000);
insert into items values('10003', '10000', 'マウス', 5000);

insert into items values('20001', '20000', 'ジュース', 120);
insert into items values('20002', '20000', 'あんまん', 100);
insert into items values('20003', '20000', 'チョコ', 150);

こんな感じ。

hogedb=> select * from shops;
 shop_id |   shop_name    | area 
---------+----------------+------
 10000   | オレンジストア | 東京
 20000   | コンビニ作之助 | 埼玉
(2 rows)
hogedb=> select * from items;
 item_id | shop_id |              item_name               | price 
---------+---------+--------------------------------------+-------
 10001   | 10000   | pc                                   | 40000
 10002   | 10000   | モニタ                               | 12000
 10003   | 10000   | マウス                               |  5000
 20001   | 20000   | ジュース                             |   120
 20002   | 20000   | あんまん                             |   100
 20003   | 20000   | チョコ                               |   150
(6 rows)

実行計画確認

統計情報

スキーマ確認

hogedb=> select current_schema;
 current_schema 
----------------
 public
(1 row)

統計情報更新

hogedb=> analyze;

或いはスキーマとテーブルを指定

analyze public.shops;

統計情報がいつ更新されたか確認

select relname
     , n_live_tup
     , n_dead_tup
     , last_autovacuum
     , last_analyze
     , last_autoanalyze
  from pg_stat_all_tables
 where schemaname = 'public' -- ここにスキーマ
 order by
       relname
;

以下にて確認

 relname | n_live_tup | n_dead_tup | last_autovacuum |         last_analyze          | last_autoanalyze 
---------+------------+------------+-----------------+-------------------------------+------------------
 items   |          6 |          0 |                 | 2019-11-10 15:02:05.138966+09 | 
 shops   |          2 |          0 |                 | 2019-11-10 15:02:05.122272+09 | 
(2 rows)

実行計画確認

フルスキャン

hogedb=> explain select * from shops;
                      QUERY PLAN                      
------------------------------------------------------
 Seq Scan on shops  (cost=0.00..1.02 rows=2 width=35)
(1 row)

以下の結合ではHashとなる。Nested Loops が見たかったが。

hogedb=> select s.shop_name
hogedb->      , i.item_name
hogedb->   from shops s
hogedb->  inner join
hogedb->        items i
hogedb->     on s.shop_id = i.shop_id;
   shop_name    |              item_name               
----------------+--------------------------------------
 オレンジストア | pc                              
 オレンジストア | モニタ                             
 オレンジストア | マウス                             
 コンビニ作之助 | ジュース                            
 コンビニ作之助 | あんまん                            
 コンビニ作之助 | チョコ                             
(6 rows)

実行計画

hogedb=> explain select s.shop_name
hogedb->      , i.item_name
hogedb->   from shops s
hogedb->  inner join
hogedb->        items i
hogedb->     on s.shop_id = i.shop_id;
                             QUERY PLAN                             
--------------------------------------------------------------------
 Hash Join  (cost=1.04..2.15 rows=6 width=60)
   Hash Cond: (i.shop_id = s.shop_id)
   ->  Seq Scan on items i  (cost=0.00..1.06 rows=6 width=44)
   ->  Hash  (cost=1.02..1.02 rows=2 width=28)
         ->  Seq Scan on shops s  (cost=0.00..1.02 rows=2 width=28)
(5 rows)

実行計画は以下の順に処理される事をあらわしています。

  • shops をフルスキャン

    • ネストの深いところから先に実行

    • 小さい方(rows=2)からHashを作る。

  • Hash を作る。

  • items をフルスキャン し、shop_id でHash Join

この例では無いですが、

ツリー表示で同じインデントの階層の上に位置するテーブルが先に実行されます。

また、Nested Loops の場合は、内部表に結合キーで一意になるようにする。

駆動表が小さく。

内部表が大きいと効率が良い。

内部表に、結合キーで一意にアクセスできるなら、内部表が何億件レコードであろうと一発で返せるからです。

do read tableA -- 駆動表

  do read tableB -- 内部表

    ここで一発で内部表のループが終わる。内部表がどんなに大きくても。

  end do

end do

実行計画の比較

hogedb=> select * from items;
 item_id | shop_id |              item_name               | price 
---------+---------+--------------------------------------+-------
 10001   | 10000   | pc                                   | 40000
 10002   | 10000   | モニタ                               | 12000
 10003   | 10000   | マウス                               |  5000
 20001   | 20000   | ジュース                             |   120
 20002   | 20000   | あんまん                             |   100
 20003   | 20000   | チョコ                               |   150
(6 rows)

このテーブルから、shop_idが20000の店舗に値上げを指示します。

select item_id
     , shop_id
     , item_name
     , price
  from items
 where shop_id = '10000'
 union all
 select item_id
     , shop_id
     , item_name
     , price * 1.5
  from items
 where shop_id = '20000'
;
 item_id | shop_id |              item_name               | price 
---------+---------+--------------------------------------+-------
 10001   | 10000   | pc                                   | 40000
 10002   | 10000   | モニタ                               | 12000
 10003   | 10000   | マウス                               |  5000
 20001   | 20000   | ジュース                             | 180.0
 20002   | 20000   | あんまん                             | 150.0
 20003   | 20000   | チョコ                               | 225.0
(6 rows)

このクエリの実行計画は以下の通りです。

                               QUERY PLAN                               
------------------------------------------------------------------------
 Append  (cost=0.00..2.23 rows=6 width=212)
   ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..1.11 rows=3 width=82)
         ->  Seq Scan on items  (cost=0.00..1.07 rows=3 width=54)
               Filter: (shop_id = '10000'::bpchar)
   ->  Seq Scan on items items_1  (cost=0.00..1.09 rows=3 width=82)
         Filter: (shop_id = '20000'::bpchar)
(6 rows)

Seq というのがフルスキャンの事なのですが、2回発生していることがわかります。

ディスクI/Oはコストになるので以下のクエリに変えてみます。

select item_id
     , shop_id
     , item_name
     , case when shop_id = '20000' then price * 1.5
            else price end
  from items
;

最初のクエリと同じ結果が得られます。

実行計画は以下の通りです。

                      QUERY PLAN                      
------------------------------------------------------
 Seq Scan on items  (cost=0.00..1.12 rows=6 width=82)
(1 row)

フルスキャンが1回に減っています。

これは簡単な例なので実行計画を見なくてもunionを選択することはないと思うのですが、実行計画を読めると嬉しいことの1つはこういう事なのかなと思いました。

スキャン多いよね、、減らす方法を考えようと思うことができます。

参考書籍