PostgreSQL 查询优化——EXPLAIN应用

工作中有这个一个需求,开发反应search_history查询太慢了,看怎么优化一下。

data=> \timing
启用计时功能.
data=> SELECT   *
data-> FROM
data->  (
data(>    SELECT s.org_id,user_id,id_no,MIN (s.create_date) AS create_date,T.corp_type AS TYPE
data(>    FROM
data(>        search_history s,
data(>        tech_org_info T
data(>    WHERE
data(>        s.org_id != 'Credittone'
data(>        AND ID != '48459fc5-ca41-4fce-a548-f4267929453f'
data(>        AND id_no = '140104197007091720'
data(>        AND s.create_date > '2016-01-01 00:00:00'
data(>        AND s.org_id = T .org_id
data(>        GROUP BY s.org_id,user_id,id_no,LEFT (s.create_date, 10),T.corp_type LIMIT 100
data(>  ) tab
data-> ORDER BY create_date DESC;
 org_id  | user_id |       id_no        |      create_date      | type
---------+---------+--------------------+-----------------------+------
 XIAONIU | XIAONIU | 140104197007091720 | 2016-01-21 09:32:03.0 | 小贷
 XIAONIU | XIAONIU | 140104197007091720 | 2016-01-12 14:28:56.0 | 小贷
(2 行记录)


时间:1204.422 ms

我们看到用时1秒多,要等待一会儿才能看到结果,系统的处理还要花费一些时间,用户体验可想而知了。

  • 一条SQL查询语句的查询时间控制在多长时间内比较好?
    答案是:一个实例上95%的sql在1s之内。

分析一下这个SQL,看到表join,先去掉join试试:

data=> SELECT org_id,user_id,id_no,MIN(create_date) AS create_date
data-> FROM
data->     search_history
data-> WHERE
data->     org_id != 'Credittone'
data->     AND id != '48459fc5-ca41-4fce-a548-f4267929453f'
data->     AND id_no = '140104197007091720'
data->     AND create_date > '2016-01-01 00:00:00'
data->GROUP BY org_id,user_id,id_no,LEFT(create_date, 10) LIMIT 100;
 org_id  | user_id |       id_no        |      create_date
---------+---------+--------------------+-----------------------
 XIAONIU | XIAONIU | 140104197007091720 | 2016-01-12 14:28:56.0
 XIAONIU | XIAONIU | 140104197007091720 | 2016-01-21 09:32:03.0
(2 行记录)


时间:1185.436 ms

时间依然是1s多,跟join关系不太大。
看一下表的索引:

data=> \d+ search_history
                                     资料表 "public.search_history"
     栏位      |          型别          | 修饰词 |   存储   | 统计目标 |              描述
---------------+------------------------+--------+----------+----------+---------------------------------
 id            | character varying(64)  | 非空   | extended |          | 唯一ID
 user_id       | character varying(64)  |        | extended |          | 用户ID
 org_id        | character varying(64)  |        | extended |          | 机构ID
 city          | character varying(32)  |        | extended |          | 被查询人所在城市
 name          | character varying(32)  |        | extended |          | 被查询人姓名
 id_type       | character varying(18)  |        | extended |          | 证件类型
 id_no         | character varying(32)  |        | extended |          | 证件号码
 mobile_no     | character varying(32)  |        | extended |          | 手机号码
 apply_json    | jsonb                  |        | extended |          | 更新后输入的jsonb
 result_json   | jsonb                  |        | extended |          | 更新后返回jsonb
 create_date   | character varying(32)  |        | extended |          | 创建时间
 corp_name     | character varying(250) |        | extended |          |
 result_remark | jsonb                  |        | extended |          |
 time_record   | jsonb                  |        | extended |          | 各个模块调用开始时间和结束时间)
索引:
    "ahn_primary_key" PRIMARY KEY, btree (id)
    "search_history_id_ukey" UNIQUE, btree (id)
    "idx_func_search_history_create_date" btree (substr(create_date::text, 1, 10))
    "search_history_apply_index" gin (apply_json)
    "search_history_city_index" btree (city)
    "search_history_createdate_index" btree (create_date DESC)
    "search_history_mobileno_index" btree (mobile_no)
    "search_history_name_index" btree (name)
    "search_history_org_id_index" btree (org_id)
    "search_history_result_index" gin (result_json)
    "search_history_user_id_index" btree (user_id)

可以看到where的条件字段都是有索引的。那简单的问题找不到了,我们看一下执行计划:


data=> explain  (SELECT org_id,user_id,id_no,MIN(create_date) AS create_date
data(>          FROM
data(>                  search_history
data(>          WHERE
data(>                  org_id != 'Credittone'
data(>          AND id != '48459fc5-ca41-4fce-a548-f4267929453f'
data(>          AND id_no = '140104197007091720'
data(>          AND create_date > '2016-01-01 00:00:00'
data(>          GROUP BY org_id,user_id,id_no,LEFT(create_date, 10) LIMIT 100);
                                                                                                                QUERY PLAN

---------------------------------------------------------
------------------------------------------------------
 Limit  (cost=227856.15..227857.33 rows=94 width=64)
   ->  HashAggregate  (cost=227856.15..227857.33 rows=94 width=64)
         Group Key: org_id, user_id, id_no, "left"((create_date)::text, 10)
         ->  Seq Scan on search_history  (cost=0.00..227850.59 rows=445 width=64)
               Filter: (((org_id)::text <> 'Credittone'::text) AND ((id)::text <> '48459fc5-ca41-4fce-a548-f4267929453f'::text) AND ((create_date)::text > '2016-01-01 00:
xt) AND ((id_no)::text = '140104197007091720'::text))
(5 行记录)

括号中的引用(从左到右)是:

  • 预计的启动开销。在输出扫描开始之前消耗的时间,比如在一个排序节点里执行排序的时间。
  • 预计总开销。这个估算是假设计划节点运行完成做出的,即所有可用行都被检索。在实际中一个节点的父节点可能会决定不读取所有可用的行(参见LIMIT下面的例子)。
  • 预计这个规划节点输出的行数。同样,这个节点被假定执行到完成为止。
  • 预计这个规划节点的行平均宽度(以字节计算)。

Limit (cost=227856.15..227857.33 rows=94 width=64)
由这一行我们可以看到预计的总开销达到22万多,这个量级太高了。
这个量级是怎么计算得到的呢?

下面我们从头开始(先ANALYZE更新一下系统表):

data=> ANALYZE search_history;
ANALYZE

然后我们看一下全表扫描的执行计划:


data=> explain select * from search_history;
                                 QUERY PLAN
----------------------------------------------------------------------------
 Seq Scan on search_history  (cost=0.00..214887.41 rows=1302341 width=1499)
(1 行记录)

这个是全表扫描:预计的总开销是214887.41 。这个预计的总开销是怎么计算的呢?


data=> select relpages,reltuples::int from pg_class where relname='search_history';
 relpages | reltuples
----------+-----------
   201864 |   1302341
(1 行记录)

那可以看到search_history这个表有201864 (20万)个磁盘页面和 1302341(130万)行 。select count() from search_history; 得到的结果为1317799*

估计成本通过
(磁盘页面读取*seq_page_cost)+(行扫描*cpu_tuple_cost)计算。默认情况下, seq_page_cost是1.0,cpu_tuple_cost是0.01, 因此估计成本为(201864* 1.0) + (1302341* 0.01) = 214887.41。与执行计划的数值相等。

我们可以看下pg_class的字段描述:

名字 类型 描述
relpages int4 以页(大小为BLCKSZ)的此表在磁盘上的形式的大小。 它只是规划器用的一个近似值,是由VACUUM,ANALYZE 和几个 DDL 命令,比如CREATE INDEX更新。
reltuples float4 表中行的数目。只是规划器使用的一个估计值,由VACUUM,ANALYZE 和几个 DDL 命令,比如CREATE INDEX更新。

我们增加一个where条件来看一下:

data=> EXPLAIN  (SELECT *
data(>          FROM
data(>                  search_history
data(>          WHERE
data(>       create_date > '2016-01-01 00:00:00');
                                 QUERY PLAN
----------------------------------------------------------------------------
 Seq Scan on search_history  (cost=0.00..218143.26 rows=1264218 width=1499)
   Filter: ((create_date)::text > '2016-01-01 00:00:00'::text)
(2 行记录)

我们看到rows变成了1264218,减少了一部分,但是成本并没有降低。
请注意EXPLAIN输出显示WHERE子句当作一个”filter”条件附属于顺序扫描计划节点。 这意味着规划节点为它扫描的每一行检查该条件,并且只输出符合条件的行。 预计的输出行数降低了,因为有WHERE子句。不过,扫描仍将必须访问所有 1302341行, 因此开销没有降低;实际上它还增加了一些(确切的说,通过10000 * cpu_operator_cost)以反映检查WHERE条件的额外CPU时间**。

这条查询实际选择的行数是1264218,但是预计的行数只是个大概。如果你试图重复这个试验, 那么你很可能得到不同的预计。还有,这个预计会在每次ANALYZE命令之后改变, 因为ANALYZE生成的统计是从该表中随机抽取的样本计算的。

再回过头来看最开始的执行计划:


data=> explain  (SELECT org_id,user_id,id_no,MIN(create_date) AS create_date
data(>          FROM
data(>                  search_history
data(>          WHERE
data(>                  org_id != 'Credittone'
data(>          AND id != '48459fc5-ca41-4fce-a548-f4267929453f'
data(>          AND id_no = '140104197007091720'
data(>          AND create_date > '2016-01-01 00:00:00'
data(>          GROUP BY org_id,user_id,id_no,LEFT(create_date, 10) LIMIT 100);
                                                                                                                QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=227917.73..227918.91 rows=94 width=64)
   ->  HashAggregate  (cost=227917.73..227918.91 rows=94 width=64)
         Group Key: org_id, user_id, id_no, "left"((create_date)::text, 10)
         ->  Seq Scan on search_history  (cost=0.00..227911.97 rows=461 width=64)
               Filter: (((org_id)::text <> 'Credittone'::text) AND ((id)::text <> '48459fc5-ca41-4fce-a548-f4267929453f'::text) AND ((create_date)::text > '2016-01-01 00:00:00'::text) AND ((id_no)::text = '140104197007091720'::text))
(5 行记录)

我们看这个执行计划的最底层,可以看到它其实是走全表扫描的,完全没有用的索引的,这其实是规划节点为它扫描的每一行检查该条件,并且只输出符合条件的行

所以这条sql语句肯定是不好的咯。
修改它使它尽量使用到索引。

"idx_func_search_history_create_date" btree (substr(create_date::text, 1, 10))

data=> EXPLAIN  (SELECT *
data(>          FROM
data(>                  search_history
data(>          WHERE
data(>       LEFT(create_date, 10) like '2016%');
                               QUERY PLAN
-------------------------------------------------------------------------
 Seq Scan on search_history  (cost=0.00..221630.98 rows=6589 width=1499)
   Filter: ("left"((create_date)::text, 10) ~~ '2016%'::text)
(2 行记录)


data=>
data=> EXPLAIN  (SELECT *
data(>          FROM
data(>                  search_history
data(>          WHERE
data(>       create_date > '2016-01-01 00:00:00');
                                 QUERY PLAN
----------------------------------------------------------------------------
 Seq Scan on search_history  (cost=0.00..218336.49 rows=1279223 width=1499)
   Filter: ((create_date)::text > '2016-01-01 00:00:00'::text)
(2 行记录)

由上可以看到对于使用了字符串类型的日期型,用like 的行数少但是开销反而大一点儿。。。没有用到索引哎。

我们先对where的相等条件应用索引,然后貌似id_no上没有建索引。。。。



data=> create index idx_search_history_id_no ON search_history (id_no);
CREATE INDEX
data=> EXPLAIN  (SELECT *
data(>          FROM
data(>                  search_history
data(>          WHERE
data(>       id_no = '140104197007091720');
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Bitmap Heap Scan on search_history  (cost=26.90..3212.52 rows=835 width=1499)
   Recheck Cond: ((id_no)::text = '140104197007091720'::text)
   ->  Bitmap Index Scan on idx_search_history_id_no  (cost=0.00..26.69 rows=835 width=0)
         Index Cond: ((id_no)::text = '140104197007091720'::text)
(4 行记录)

这里,规划器决定使用两步的规划:最底层的规划节点访问一个索引,找出匹配索引条件的行的位置, 然后上层规划节点真实地从表中抓取出那些行。独立地抓取数据行比顺序地读取它们的开销高很多, 但是因为并非所有表的页面都要被访问,这么做实际上仍然比一次顺序扫描开销要少。 使用两层规划的原因是因为上层规划节点把索引标识出来的行位置在读取它们之前按照物理位置排序, 这样可以最小化独立抓取的开销。节点名称里面提到的“bitmap”是进行排序的机制

然后再看查询计划:

data=> explain  (SELECT s.org_id,user_id,id_no,MIN (s.create_date) AS create_date
data(>          FROM
data(>                  search_history s
data(>          WHERE
data(>                  s.org_id != 'Credittone'
data(>          AND ID != '48459fc5-ca41-4fce-a548-f4267929453f'
data(>          AND id_no = '140104197007091720'
data(>          AND s.create_date > '2016-01-01 00:00:00'
data(>          GROUP BY s.org_id,user_id,id_no,LEFT (s.create_date, 10) LIMIT 100);
                                                                                       QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=3225.69..3226.88 rows=95 width=64)
   ->  HashAggregate  (cost=3225.69..3226.88 rows=95 width=64)
         Group Key: org_id, user_id, id_no, "left"((create_date)::text, 10)
         ->  Bitmap Heap Scan on search_history s  (cost=26.81..3219.85 rows=467 width=64)
               Recheck Cond: ((id_no)::text = '140104197007091720'::text)
               Filter: (((org_id)::text <> 'Credittone'::text) AND ((id)::text <> '48459fc5-ca41-4fce-a548-f4267929453f'::text) AND ((create_date)::text > '2016-01-01 00:00:00'::text))
               ->  Bitmap Index Scan on idx_search_history_id_no  (cost=0.00..26.69 rows=835 width=0)
                     Index Cond: ((id_no)::text = '140104197007091720'::text)
(8 行记录)
data=> \timing
启用计时功能.
data=> SELECT   *
data-> FROM
data->  (
data(>          SELECT s.org_id,user_id,id_no,MIN (s.create_date) AS create_date,T.corp_type AS TYPE
data(>          FROM
data(>                  search_history s,
data(>                  tech_org_info T
data(>          WHERE
data(>                  s.org_id != 'Credittone'
data(>          AND ID != '48459fc5-ca41-4fce-a548-f4267929453f'
data(>          AND id_no = '140104197007091720'
data(>          AND s.create_date > '2016-01-01 00:00:00'
data(>          AND s.org_id = T .org_id
data(>          GROUP BY s.org_id,user_id,id_no,LEFT (s.create_date, 10),T.corp_type LIMIT 100
data(>  ) tab
data-> ORDER BY create_date DESC;
 org_id  | user_id |       id_no        |      create_date      | type
---------+---------+--------------------+-----------------------+------
 XIAONIU | XIAONIU | 140104197007091720 | 2016-01-21 09:32:03.0 | 小贷
 XIAONIU | XIAONIU | 140104197007091720 | 2016-01-12 14:28:56.0 | 小贷
(2 行记录)


时间:8.335 ms

可以看到时间减少至8ms。

总结:虽然脑残的是没有仔细检查索引,不然一开始就找到原因了。
但是我们把explain应用了一下不是很好吗?

你可能感兴趣的:(数据库)