下面SQL1000W行数据,列的选择性很低,只有两个值('1'和'11')都是字符串类型,'1'只有一条数据,'11'有9999999行数据。
慢SQL:
select distinct col from tt; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------- HashAggregate (cost=169247.11..169247.12 rows=1 width=3) (actual time=5082.733..5082.735 rows=2 loops=1) Group Key: col -> Seq Scan on tt (cost=0.00..144247.29 rows=9999929 width=3) (actual time=0.005..275.906 rows=10000000 loops=1) Planning Time: 0.365 ms Execution Time: 5082.772 ms (5 行记录)
CTE递归优化:
WITH RECURSIVE t AS ( (SELECT col FROM tt ORDER BY col LIMIT 1) UNION ALL SELECT (SELECT col FROM tt WHERE col > t.col ORDER BY col LIMIT 1) FROM t WHERE t.col IS NOT NULL ) SELECT col FROM t WHERE col IS NOT NULL; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------------- CTE Scan on t (cost=50.84..52.86 rows=100 width=38) (actual time=0.024..0.079 rows=2 loops=1) Filter: (col IS NOT NULL) Rows Removed by Filter: 1 CTE t -> Recursive Union (cost=0.43..50.84 rows=101 width=38) (actual time=0.022..0.076 rows=3 loops=1) -> Limit (cost=0.43..0.46 rows=1 width=3) (actual time=0.021..0.021 rows=1 loops=1) -> Index Only Scan using idx_1_2_tt on tt tt_1 (cost=0.43..260443.37 rows=9999929 width=3) (actual time=0.020..0.020 rows=1 loops=1) Heap Fetches: 0 -> WorkTable Scan on t t_1 (cost=0.00..4.84 rows=10 width=38) (actual time=0.017..0.017 rows=1 loops=3) Filter: (col IS NOT NULL) Rows Removed by Filter: 0 SubPlan 1 -> Limit (cost=0.43..0.46 rows=1 width=3) (actual time=0.024..0.024 rows=0 loops=2) -> Index Only Scan using idx_1_2_tt on tt (cost=0.43..95149.36 rows=3333310 width=3) (actual time=0.024..0.024 rows=0 loops=2) Index Cond: (col > (t_1.col)::text) Heap Fetches: 0 Planning Time: 0.096 ms Execution Time: 0.096 ms (18 行记录)
里面的逻辑是:
(SELECT col FROM tt ORDER BY col LIMIT 1)
根节点通过order by 升序 找到最小的一条数据作为起点。
递归查询:
SELECT (SELECT col FROM tt WHERE col > t.col ORDER BY col LIMIT 1) FROM t WHERE t.col IS NOT NULL
在第一次迭代中,CTE t 包含值'1'。这个查询将在tt表中寻找col大于'1'的最小值。在数据集中,这将是'11'。
在第二次迭代,CTE t 将包含'11'。此时,查询将尝试找到大于'11'的最小值,但没有这样的值,所以返回NULL。
递归结束:
当递归查询返回NULL时,递归结束。这时,CTE t 将包含'1'和'11',返回和distinct 一样逻辑的数据。
慢SQL(执行时间 8s ,限制返回 30 行)
explain analyze SELECT GI.ID, GI.MODULE_ID, GI.BT, GI.WH, GI.JJCD_TEXT, GI.CREATE_DEPTNAME, GI.CREATE_TIME, GI.MODULE_NAME FROM gifgifgif GI INNER JOIN gufgufguf GUF ON (GUF.ifid = GI.ID) WHERE GI.ROWSTATE > - 1 AND (GUF.usid = '0' OR GUF.usid = '210317100256if6gVcTb3Ado1o2ytLs') AND ((GI.BT LIKE '%签%') OR (GI.MODULE_NAME LIKE '%签%') OR (GI.WH LIKE '%签%') OR (GI.JJCD_TEXT LIKE '%签%') OR (GI.CREATE_DEPTNAME LIKE '%签%')) ORDER BY GI.CREATE_TIME DESC LIMIT 30;
慢SQL执行计划
QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------- Limit (cost=1001.05..17578.06 rows=30 width=240) (actual time=6458.263..8763.733 rows=7 loops=1) -> Gather Merge (cost=1001.05..3879467.79 rows=7019 width=240) (actual time=6458.261..8763.728 rows=7 loops=1) Workers Planned: 4 Workers Launched: 4 -> Nested Loop (cost=0.99..3877631.71 rows=1755 width=240) (actual time=2843.144..8274.217 rows=1 loops=5) -> Parallel Index Scan Backward using gifgifgif_CREATE_TIME1 on gifgifgif GI (cost=0.43..1158925.09 rows=433728 width=240) (actual time=0.043..2159.037 rows=350466 loops=5) Filter: ((ROWSTATE > '-1'::numeric) AND (((BT)::text ~~ '%签%'::text) OR ((MODULE_NAME)::text ~~ '%签%'::text) OR ((WH)::text ~~ '%签%'::text) OR ((JJCD_TEXT)::text ~~ '%签%'::text) OR ((CREATE_DEPTNAME)::text ~~ '% %'::text))) Rows Removed by Filter: 423271 -> Index Only Scan using idx_gufgufguf_1_2_3 on gufgufguf GUF (cost=0.56..6.26 rows=1 width=32) (actual time=0.017..0.017 rows=0 loops=1752329) -- 慢:(1752329/5) * 0.017 / 1000 = 5.95s Index Cond: (ifid = (GI.ID)::text) Filter: (((usid)::text = '0'::text) OR ((usid)::text = '210317100256if6gVcTb3Ado1o2ytLs'::text)) Rows Removed by Filter: 3 Heap Fetches: 0 Planning Time: 0.832 ms Execution Time: 8763.803 ms (15 行记录)
我看到这计划简直无语,这种SQL不能 300 ms以内出来就绝对有问题,而且这么简单的语句都能用上并行,真的服。
Index Only Scan using idx_gufgufguf_1_2_3 on gufgufguf GUF 每个并行进程执行 5.95s 这也太拉跨了。
看执行计划基本都是用 Index Scan 或者是 Index Only Scan,但是本SQL 谓词过滤条件很多 or ,其实优化器如果执行位图扫描才是最优解计划,但是CBO偏偏没执行!!!
SQL去掉 LIMIT 30限制条件:
explain analyze SELECT GI.ID, GI.MODULE_ID, GI.BT, GI.WH, GI.JJCD_TEXT, GI.CREATE_DEPTNAME, GI.CREATE_TIME, GI.MODULE_NAME FROM gifgifgif GI INNER JOIN gufgufguf GUF ON (GUF.ifid = GI.ID) WHERE GI.ROWSTATE > - 1 AND (GUF.usid = '0' OR GUF.usid = '210317100256if6gVcTb3Ado1o2ytLs') AND ((GI.BT LIKE '%签%') OR (GI.MODULE_NAME LIKE '%签%') OR (GI.WH LIKE '%签%') OR (GI.JJCD_TEXT LIKE '%签%') OR (GI.CREATE_DEPTNAME LIKE '%签%')) ORDER BY GI.CREATE_TIME DESC ;
去掉 LIMIT 30限制条件SQL执行计划:
https://www.iqiyi.com/v_2clubpj56m4.html https://m.iqiyi.com/v_2clubpj56m4.html https://www.iqiyi.com/v_2bh6kmrmq9w.html https://m.iqiyi.com/v_2bh6kmrmq9w.html QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------- Gather Merge (cost=98222.89..99026.61 rows=6792 width=240) (actual time=33.640..35.974 rows=7 loops=1) Workers Planned: 3 Workers Launched: 3 -> Sort (cost=97222.85..97228.51 rows=2264 width=240) (actual time=26.724..26.725 rows=2 loops=4) Sort Key: GI.CREATE_TIME DESC Sort Method: quicksort Memory: 25kB Worker 0: Sort Method: quicksort Memory: 25kB Worker 1: Sort Method: quicksort Memory: 25kB Worker 2: Sort Method: quicksort Memory: 26kB -> Nested Loop (cost=510.90..97096.70 rows=2264 width=240) (actual time=11.118..26.693 rows=2 loops=4) -> Parallel Bitmap Heap Scan on gufgufguf GUF (cost=510.35..59045.81 rows=5049 width=32) (actual time=0.480..3.498 rows=1178 loops=4) Recheck Cond: (((usid)::text = '0'::text) OR ((usid)::text = '210317100256if6gVcTb3Ado1o2ytLs'::text)) Heap Blocks: exact=1464 -> BitmapOr (cost=510.35..510.35 rows=15652 width=0) (actual time=1.567..1.568 rows=0 loops=1) -> Bitmap Index Scan on gufgufguf_usid (cost=0.00..251.26 rows=7826 width=0) (actual time=0.022..0.022 rows=0 loops=1) Index Cond: ((usid)::text = '0'::text) -> Bitmap Index Scan on gufgufguf_usid (cost=0.00..251.26 rows=7826 width=0) (actual time=1.545..1.545 rows=4713 loops=1) Index Cond: ((usid)::text = '210317100256if6gVcTb3Ado1o2ytLs'::text) -> Index Scan using gifgifgif_PKEY1 on gifgifgif GI (cost=0.56..7.54 rows=1 width=240) (actual time=0.019..0.019 rows=0 loops=4713) Index Cond: ((ID)::text = (GUF.ifid)::text) Filter: ((ROWSTATE > '-1'::numeric) AND (((BT)::text ~~ '%签%'::text) OR ((MODULE_NAME)::text ~~ '%签%'::text) OR ((WH)::text ~~ '%签%'::text) OR ((JJCD_TEXT)::text ~~ '%签%'::text) OR ((CREATE_DEPTNAME)::text ~~ '% %'::text))) Rows Removed by Filter: 1 Planning Time: 0.815 ms Execution Time: 36.060 ms (24 行记录)
可以看到去掉LIMIT 30 以后,CBO能正常使用上 Bitmap Index Scan + BitmapOr 的查询策略,SQL只需要 36ms就能跑出结果。
PG比较牛逼的地方是B+树索引能基于SQL的查询条件,自动能转换成位图索引的查询策略。
像这种情况就简单了,只需要改变下限制SQL返回条数的逻辑即可,kingbase也兼容Oracle rownum 的语法,我们可以将上面SQL等价改成 rownum 来优化。
LIMIT 改写成 rownum :
explain analyze SELECT * FROM ( SELECT GI.ID, GI.MODULE_ID, GI.BT, GI.WH, GI.JJCD_TEXT, GI.CREATE_DEPTNAME, GI.CREATE_TIME, GI.MODULE_NAME FROM gifgifgif GI INNER JOIN gufgufguf GUF ON (GUF.ifid = GI.ID) WHERE GI.ROWSTATE > - 1 AND (GUF.usid = '0' OR GUF.usid = '210317100256if6gVcTb3Ado1o2ytLs') AND ((GI.BT LIKE '%签%') OR (GI.MODULE_NAME LIKE '%签%') OR (GI.WH LIKE '%签%') OR (GI.JJCD_TEXT LIKE '%签%') OR (GI.CREATE_DEPTNAME LIKE '%签%')) ORDER BY GI.CREATE_TIME DESC) WHERE ROWNUM <= 30;
LIMIT 改写成 rownum 执行计划:
QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- Count (cost=98222.89..99162.45 rows=0 width=240) (actual time=31.418..33.691 rows=7 loops=1) Stop Keys: (ROWNUM <= 30) -> Gather Merge (cost=98222.89..99026.61 rows=6792 width=240) (actual time=31.415..33.686 rows=7 loops=1) Workers Planned: 3 Workers Launched: 3 -> Sort (cost=97222.85..97228.51 rows=2264 width=240) (actual time=26.497..26.498 rows=2 loops=4) Sort Key: GI.CREATE_TIME DESC Sort Method: quicksort Memory: 25kB Worker 0: Sort Method: quicksort Memory: 25kB Worker 1: Sort Method: quicksort Memory: 27kB Worker 2: Sort Method: quicksort Memory: 25kB -> Nested Loop (cost=510.90..97096.70 rows=2264 width=240) (actual time=14.246..26.465 rows=2 loops=4) -> Parallel Bitmap Heap Scan on gufgufguf GUF (cost=510.35..59045.81 rows=5049 width=32) (actual time=0.513..3.401 rows=1178 loops=4) Recheck Cond: (((usid)::text = '0'::text) OR ((usid)::text = '210317100256if6gVcTb3Ado1o2ytLs'::text)) Heap Blocks: exact=1373 -> BitmapOr (cost=510.35..510.35 rows=15652 width=0) (actual time=1.664..1.664 rows=0 loops=1) -> Bitmap Index Scan on gufgufguf_usid (cost=0.00..251.26 rows=7826 width=0) (actual time=0.024..0.024 rows=0 loops=1) Index Cond: ((usid)::text = '0'::text) -> Bitmap Index Scan on gufgufguf_usid (cost=0.00..251.26 rows=7826 width=0) (actual time=1.639..1.639 rows=4713 loops=1) Index Cond: ((usid)::text = '210317100256if6gVcTb3Ado1o2ytLs'::text) -> Index Scan using gifgifgif_PKEY1 on gifgifgif GI (cost=0.56..7.54 rows=1 width=240) (actual time=0.019..0.019 rows=0 loops=4713) Index Cond: ((ID)::text = (GUF.ifid)::text) Filter: ((ROWSTATE > '-1'::numeric) AND (((BT)::text ~~ '%签%'::text) OR ((MODULE_NAME)::text ~~ '%签%'::text) OR ((WH)::text ~~ '%签%'::text) OR ((JJCD_TEXT)::text ~~ '%签%'::text) OR ((CREATE_DEPTNAME)::text ~~ '%签%'::text))) Rows Removed by Filter: 1 Planning Time: 0.897 ms Execution Time: 33.778 ms (26 行记录)
可以看到SQL通过将LIMIT 改写成 rownum 以后,原来执行时间 8s 降低到 33ms 就能跑出结果了,本条SQL到此已经优化完毕。