优化器里的概率学 - 性能抖动原理分析

1 minute read

背景

数据库的优化器大量的使用了概率学的知识,例如高频词的频率,数据分布柱状图,评估某个VALUE有多少行,评估物理存储与列的线性相关性等等。

PostgreSQL 里面的统计学知识:

《用PostgreSQL了解一些统计学术语以及计算方法和表示方法 - 1》

《PostgreSQL数据库监控中的统计学 - 对象SIZE的数据分布图》

《PostgreSQL 统计信息之 - 逻辑与物理存储的线性相关性》

本文提到的CASE也和统计学有关,用户同样的SQL,更换条件后查询的响应时间差别比较大。

我们来分析一下问题出在哪里?

例子

tbl这张表,有两个相关字段,xxx, val。xxx, val都包含一个btree索引。

create table tbl (xxx int, val int);  
create index idx_tbl_xxx on tbl using btree (xxx);  
create index idx_tbl_val on tbl using btree (val);  

tbl约9.1千万记录。(来自统计信息)

postgres=# select * from pg_class where relname = 'tbl';  
-[ RECORD 1 ]--+---------------------------------------------------------------------------  
...  
relpages       | 8257667  
reltuples      | 9.18266e+07  
...  

val字段的统计信息,柱状分布

postgres=# select * from pg_stats where tablename = 'tbl' and (attname = 'val');  
-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
schemaname             | public  
tablename              | tbl  
attname                | val  
inherited              | f  
null_frac              | 0  
avg_width              | 4  
n_distinct             | 23472  
most_common_vals       | {11321542,9590084,9534670,11472095,9576467,9545491,11477451,10989530,11207289,11366499,11478550,11507267,10147621,10915696,11265877,11397691,10943926,11299197,11375631,11382656,11383293,11397909,11433126,11440696,11455808,11502873,9309452,9419251,9831208,10915491,10954298,10968050,10971601,10971856,11350206,11365070,11418452,11424692,11453932,11455400,11456117,11462578,11477841,11495983,11499188,11519466,11526843,11538523,9309313,9507528,9570697,9589957,9848424,10211296,10403265,10740134,10851181,10905772,10925615,10942712,10988994,10991227,10997731,10999386,11365698,11370643,11401522,11415268,11418400,11420980,11431494,11432202,11432813,11440231,11450771,11452106,11474915,11475542,11501297,11505822,11507072,11512290,11541832,9516436,9571509,9572190,9576510,9869955,9870066,10760447,10760491,10890296,10898933,10902740,10905208,10907293,10916731,10917591,10927390,10933657}  
most_common_freqs      | {0.000633333,0.000566667,0.000533333,0.000533333,0.000466667,0.000433333,0.000433333,0.0004,0.0004,0.0004,0.0004,0.0004,0.000366667,0.000366667,0.000366667,0.000366667,0.000333333,0.000333333,0.000333333,0.000333333,0.000333333,0.000333333,0.000333333,0.000333333,0.000333333,0.000333333,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.0003,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000266667,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333,0.000233333}  
histogram_bounds       | {-1,7558107,7828127,8112859,9046149,9241826,9379468,9541732,9576599,9672961,9846871,9860375,9870728,10060192,10351217,10740412,10856432,10893866,10902117,10906515,10913106,10918910,10925204,10931076,10938203,10944727,10950843,10958976,10963704,10969886,10972265,10975634,10980061,10985045,10988826,10993407,10997844,10999535,11000963,11087834,11109438,11118360,11127807,11282790,11299113,11321740,11341584,11355644,11359298,11363485,11365828,11367262,11371057,11372356,11376100,11382254,11383472,11387734,11398387,11403238,11414003,11419357,11420978,11421807,11424546,11428043,11430424,11434838,11438052,11440476,11442560,11450686,11453180,11455088,11455714,11461724,11472384,11473771,11474364,11474986,11476815,11480406,11483760,11486407,11489680,11492521,11497577,11503852,11507380,11509618,11512309,11515497,11518366,11522532,11525500,11526789,11529964,11534552,11540765,11544743,11559885}  
correlation            | 0.523764  
most_common_elems      |  
most_common_elem_freqs |  
elem_count_histogram   |  

一条响应时间会抖动的SQL如下(更改val的值,某些值会导致抖动)。

postgres=# explain SELECT min(xxx) from tbl where val=11546671;  
                                                           QUERY PLAN  
--------------------------------------------------------------------------------------------------------------------------------  
 Result  (cost=4164.53..4164.54 rows=1 width=0)  
   InitPlan 1 (returns $0)  
     ->  Limit  (cost=0.57..4164.53 rows=1 width=8)  
           ->  Index Scan using idx_tbl_xxx_1 on tbl  (cost=0.57..15860545.10 rows=3809 width=8)  
                 Index Cond: (xxx IS NOT NULL)  
                 Filter: (val = 11546671)  
(6 rows)  

诊断

1、15860545.10是如何评估得到的?

index scan的成本取决于索引的BLOCK的数量乘以random_page_cost再加上记录数乘以cpu_index_tuple_cost,乘以输入参数的估值比例。

(pg_class.relpages*random_page_cost + pg_class.reltuples*cpu_index_tuple_cost) * (3809/(9.18266e+07))

2、rows=3809 是如何评估得到的?

首先val=11546671,这个值不在val的高频词(most_common_vals)里面,因此多少行可能出现一次呢?

= (reltuples * (1-sum(most_common_freqs))) / (n_distinct-100) = ((9.18266e+07) * (1-0.0303)) / (23472-100)  
= 3809.8688182440527126  

3、cost=4164.53..4164.54是如何评估出来的?

由于每3809行就可能有一个val=11546671的记录,因此index scan的总成本除以它就是最终min(xxx)的成本

15860545.10 / 3809.8688182440527126  
=  
4163.0160660781062716  

4、为什么会选择xxx列的索引?

因为是基于成本的优化,所以最终会选择成本最低的。

假如使用val列的索引,并从heap table get tuples,因此需要扫描整个val列的索引,才能得到min(xxx)。还不如直接全表扫描。

5、为什么更换val条件性能会抖动?

因为前面提到的3809只是一个概率,不一定扫描3809行就能遇到一条 val=??? 的值,当 val=??? 分布在xxx索引的末端时,性能就会非常非常差。

优化方法 1

这个CASE使用复合索引就可以解决。

create index idx_tbl_val_xxx on tbl using btree (val,xxx);  

为了达到最好的效果,驱动列建议只进行 = 或者 in 的查询,不要使用范围扫描。(最开始的扫描量越少越好,和多表JOIN的优化一个道理,最开始就将结果集降到最低)

优化方法 2

实际上从优化器的角度,还有优化的余地,让优化器选择val而非xxx的单独列索引。

例如数据按xxx索引进行cluster,然后统计连续的数据块的val的边界值。(类似brin索引)

评估出按xxx列排序扫描时,需要扫多少个数据块才能扫到 val=? 的值。

参考

《数据库优化器原理 - 如何治疗选择综合症》

Flag Counter

digoal’s 大量PostgreSQL文章入口