对于sql优化,除了索引之外,执行计划和统计信息是无法绕开的一个话题,如果sql优化(所有的RDBMS)脱离了统计信息的话就少了一个为什么的过程,味道就感觉少了一大半。
刚接触Postgresql,粗浅地学习总结一下Postgresql相关的统计信息。
postgresql 进程模型
postgresql中的两种“统计信息”
SQLServer的性能计数器和各种系统表和DMV,或者MySQL中的performance_schema中的的信息
这一类统计信息基本上等价于SQLServer或者MySQL中用来指导执行计划的统计信息
参考这里:https://blog.pgaddict.com/posts/the-two-kinds-of-stats-in-postgresql
-
postgresql中的负载指标“统计信息”(Monitoring stats)
负载指标由上文提到的stats collector进程来实时收集更新。PostgreSQL的统计数据收集器是一个支持收集和报告服务器活动信息的子系统。
目前,收集器可以计算对磁盘块和单行项中的表和索引的访问次数。
它还跟踪每个表中的总行数,以及关于vacuum的信息,并分析每个表的操作。
同时还可以记录基于sql语句执行的代价信息。这部分与该主题关系不大,就不展开详述。
-
postgresql中的数据分布状态描述“统计信息”(Data distribution stats)
比如update statistics *** (***) with sample 30 percent,或者MySQL中的analyze table table_name
有两种VACUUM的变体:标准VACUUM和VACUUM FULL。
1,标准形式的VACUUM可以和生产数据库操作并行运行(SELECT、INSERT、UPDATE和DELETE等命令将继续正常工作,但在清理期间你无法使用ALTER TABLE等命令来更新表的定义)。
2,VACUUM FULL可以收回更多磁盘空间但是运行起来更慢,且vacuum full不会有后台进程主从触发(只能手动执行)。
另外,VACUUM FULL类似于表的重建或者说碎片整理,同时需要一个大小相当于原始表的额外空间。
要求在其工作的表上得到一个排他锁,因此无法和对此表的其他使用并行。因此,通常管理员应该努力使用标准VACUUM并且避免VACUUM FULL。
大概列举一下相关的系统表以及相关参数,简述相关的含义。
create table myschema.table_test ( c1 serial primary key, c2 int, c3 varchar(100), c4 varchar(100), c5 timestamp ) create or replace function random_string(integer) returns text as $body$ select upper(array_to_string(array(select substring('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' FROM (ceil(random()*62))::int FOR 1) FROM generate_series(1, $1)), '')); $body$ language sql volatile;
insert into myschema.table_test (c2,c3,c4,c5) select cast(random()*500000 as int),random_string(10),random_string(10), cast( now()-'1 min'::interval * random()*500000 as timestamp ) from generate_series(1,1000000)
postgresql中关于数据库负载的统计信息
-
库级别信息摘要
在库级别,pg_stat_databases用来描述描述"库"级别的摘要信息,包括库名,当前库事务提交次数,回滚次数,读写次数,死锁等等信息。
这些信息可以观察到到一个库的负载情况和健康状况。
-
表级别的信息摘要
在表级别,由pg_stat_user_tables来描述某个具体的表中的信息,包括增删查改的次数,数据行等摘要信息。
这些信息可以衡量一个表的冷热程度,活跃性,以及体量以及一些analyze时间相关的信息。
pg_class 来描述表的物理存储信息,包括数据行数,数据页的个数。
关于postgresql中的描述数据分布统计信息
pg_stats用来描述一个表中所有的字段的数据分布信息,为执行计划决策提供依据,与SQLServer的直方图类似,熟悉的配方熟悉的味道,只有管理员账号能够访问
类似于SQLServer的统计信息+直方图,也即执行dbcc show_statistics(***,***)的效果,或者MySQL中的information_schema.column_statistics表中的信息(简直一模一样)。
这一点,SQLServer的直方图用一种相对比较直观的方式展示了出来。
select * from pg_stats WHERE tablename = 'table_test'; schemaname | myschema tablename | table_test attname | c1 inherited | f null_frac | 0 avg_width | 4 n_distinct | -1 most_common_vals | most_common_freqs | histogram_bounds | {15,9799,20037,30372,40276,……………………,990687,999949} correlation | 1 most_common_elems | most_common_elem_freqs | elem_count_histogram | schemaname | myschema tablename | table_test attname | c2 inherited | f null_frac | 0 avg_width | 4 n_distinct | -0.330106 most_common_vals | most_common_freqs | histogram_bounds | {23,4712,9677,14189,19403,………………490576,495541,499975} correlation | -0.00480835 most_common_elems | most_common_elem_freqs | elem_count_histogram | schemaname | myschema tablename | table_test attname | c5 inherited | f null_frac | 0 avg_width | 8 n_distinct | -0.993476 most_common_vals | most_common_freqs | histogram_bounds | {"2019-08-05 14:29:35.515329","2019-08-08 19:17:14.628418",……"2020-07-14 18:25:47.515329","2020-07-17 19:40:48.015329"} correlation | -0.00216757 most_common_elems | most_common_elem_freqs | elem_count_histogram | schemaname | myschema tablename | table_test attname | c4 inherited | f null_frac | 0 avg_width | 11 n_distinct | -1 most_common_vals | most_common_freqs | histogram_bounds | {0035UXPI6A,0N8JC5OIER,1BZZAU76H5,…………ZQSMJJRFWE,ZZZYYV9TKJ} correlation | -0.00186405 most_common_elems | most_common_elem_freqs | elem_count_histogram |
schemaname | myschema tablename | table_test attname | c3 inherited | f null_frac | 0 avg_width | 11 n_distinct | -1 most_common_vals | most_common_freqs | histogram_bounds | {000XXEZ4HN,0N3GEAC1QS,…………,Z5ANIIBHDO,ZH6ZYR94CJ,ZQW7M2HZ4I,ZZZENAC3OQ} correlation | 0.00391295 most_common_elems | most_common_elem_freqs | elem_count_histogram | Time: 1.259 ms
关于pg_stat系统表的详细描述如下
名字 | 类型 | 引用 | 描述 |
schemaname | name | pg_namespace.nspname | 包含此表的模式名字 |
tablename | name | pg_class.relname | 表的名字 |
attname | name | pg_attribute.attname | 这一行描述的字段的名字 |
inherited | bool | 如果为真,那么这行包含继承的子字段,不只是指定表的值。 | |
null_frac | real | 记录中字段为空的百分比 | |
avg_width | integer | 字段记录以字节记的平均宽度 | |
n_distinct | real | 如果大于零,就是在字段中独立数值的估计数目。如果小于零, 就是独立数值的数目被行数除的负数。 用负数形式是因为ANALYZE 认为独立数值的数目是随着表增长而增长; 正数的形式用于在字段看上去好像有固定的可能值数目的情况下。比如, -1 表示一个唯一字段,独立数值的个数和行数相同。 |
|
most_common_vals | anyarray | 一个字段里最常用数值的列表。如果看上去没有啥数值比其它更常见,则为 null | |
most_common_freqs | real[] | 一个最常用数值的频率的列表,也就是说,每个出现的次数除以行数。 如果most_common_vals是 null ,则为 null。 | |
histogram_bounds | anyarray | 一个数值的列表,它把字段的数值分成几组大致相同热门的组。 如果在most_common_vals里有数值,则在这个饼图的计算中省略。
如果字段数据类型没有<操作符或者most_common_vals 列表代表了整个分布性,则这个字段为 null。 |
|
correlation | real | 统计与字段值的物理行序和逻辑行序有关。它的范围从 -1 到 +1 。 在数值接近 -1 或者 +1
的时候,在字段上的索引扫描将被认为比它接近零的时候开销更少, 因为减少了对磁盘的随机访问。 如果字段数据类型没有<操作符,那么这个字段为null。 |
|
most_common_elems | anyarray | 经常在字段值中出现的非空元素值的列表。(标量类型为空。) | |
most_common_elem_freqs | real[] | 最常见元素值的频率列表,也就是,至少包含一个给定值的实例的行的分数。 每个元素频率跟着两到三个附加的值;它们是在每个元素频率之前的最小和最大值,
还有可选择的null元素的频率。 当most_common_elems 为null时,为null) |
|
elem_count_histogram | real[] | 该字段中值的不同非空元素值的统计直方图,跟着不同非空元素的平均值。(标量类型为空。) |
统计信息的更新
1,统计信息的手动更新:analyze table_name
2,统计信息的自动更新
开始之前,对比SQLServer和MySQL中统计信息的自动更新的出发情况,统计信息更新是一个非常有意思的话题。
SQLServer是表中的输入写入(增删改)超过阈值500 + (20 %×表数据总量)之后会自动触发更新,以为默认情况下可以认为这是一个写死的参数。
因为SQLServer统计信息的更新会有非常多的问题,虽然SQLServer有一个trace flag 2371
可以改变改规则,但也属于半遮半掩的一个非开放功能对于MySQL或者postgresql,类似所有的参数都是可配置化的,因此非常透明
MySQL是innodb_stats_auto_recalc打开的情况下,增删改的次数大于表中已有数据的10%之后主动触发更新。
2.1 自动更新的开关
首先autovacuum开关需要打开,也即上文中提到的autovacuum lancher进程实现,在表中的数据满足一定条件之后的定时更新
这里的autovacuum是这个自动化更新的开关。默认打开。
2.2关于自动更新的触发机制
也类似于MySQL,子线程会根据上下文,存在一个工作频率,postgresql在打开autovacuum基础上,
autovacuum进程执行统计信息更新的唤醒频率,以及工作线程数,依次对各个表执行并发清理,
autovacuum_naptime唤醒频率默认为1min,autovacuum_max_workers工作线程默认为3个,被唤醒的工作线程会并发对库中的满足更新条件的表进行统计信息更新。
2.3关于自动更新统计信息的阈值
这里会涉及两个参数autovacuum_vacuum_threshold和autovacuum_vacuum_scale_factor
anl_base_thresh默认值时50,anl_scale_factor默认值时0.2,这都是可配置的,而且是每个表可以独立配置的,这里难免会想到SqlServer的这个阈值也是类似变化数量超过500 + (20 %×表数据总量)
autovacuum进程进行 vacuum 触发条件表上增删改的行数 >= autovacuum_vacuum_scale_factor* reltuples(表上记录数) + autovacuum_vacuum_threshold
2.4关于自动更新的采样范围
参考这里,提了一个非常好的问题:https://dba.stackexchange.com/questions/200136/postgresql-what-the-default-statistics-target-value-really-means
采样容量:采用300×default_statistics_target=30000作为采样的样本默认容量,default_statistics_target是一个可配置化的参数,默认值变为100
这一点类似于MySQL中的STATS_SAMPLE_PAGES,也就是更新统计信息时候采样的比例,只不过这里是page页面的个数,也是可以基于表来配置的。
难免又想到Server的类似配置项,SqlServer没有该配置项,不过一切都不是问题,可以换种方式间接使实现。
多扯一句,很多人在讲什么SQL优化,执行计划,索引之类的,如果抛开统计信息不谈,基本上还是那种初级的手段,或者根本就没遇到过复杂的场景。
上面说了,对于采样容量,300×default_statistics_target=30000作为采样的样本默认容量,default_statistics_target可配置化,这一切看起来都没有问题,其实可以仔细考虑一下。
对于某些大表或者数据分布不均匀的请,300×default_statistics_target是不一定够的,对于10W行的表,采用30000的采样容量,跟1000W行的表采用同样的容量,统计信息得到的数据准确性差别是非常大的。
上面说了,MySQL和SQLServer可以在表级别定义一个STATS_SAMPLE_PAGES或者default_statistics_target的取样范围。
sqlserver有一个类似于与此的统计采样百分比,update statistics *** (***) with sample 30 percent,就是这里的30,这个值可以从0~100%,随意指定。
可见,很多东西是殊途同归的,只是外面的一层马甲不一样而已。(为什么往往要从一定范围内采样而不是100%采样?其实脚后跟就能想明白)
这个字段级统计信息的属性记录在pg_attribute这个系统表中。
2.5 统计信息最后一次更新之后的变化
统计信息更新日志,pg_stat_all_tables表存储了所有表的最后一次更新历史信息(last_analyza),以及最后一次更新之后数据发生的变化情况(n_mod_since_analyze),这是一个非常因吹斯汀的数据。
上面把统计信息各种阈值,各种触发条件七七八八地列举的差不多了,有没有表再回头关注最后一次统计信息更新之后表的基数的变化?
肯定是有必要的,上面说了,即便是default_statistics_target是一个可以调整的参数,但不一定知道具体哪个值是合理的或者说是可行的。
那么,就可以观察,在执行计划使用统计信息做预估,出现偏差的临界点,就需要重新收集更新统计信息了,
那么此时就可以结合pg_stat_all_tables此时举上次收集完统计信息变化的情况,来反推autovacuum_vacuum_scale_factor这个值,从而更加科学地去设置autovacuum_vacuum_scale_factor这个因子。
3 手工创建统计信息以及多列统计信息
开始之前现提出一个问题:为什么需要手动创建统计信息?
通常情况下,统计信息在满足一定条件,且取样达到一定程度之后,可以得到一个相对准确的统计信息,一切看起来都是水到渠成。
但是不排除一些个特殊情况,需要手动创建统计信息来实现预估的准确性,比如数据倾斜严重的情况下,又难以100%取样(即便100%取样,统计信息还有一个“步长”的限制),
此时手动创建统计信息,从而更好指导执行计划的生成。
这一点SQLServer和Postgresql都是支持的,MySQL这一点是不支持的。
这个就稍微扯远一点,SQLServer中对于select * from table where c1 = m and c2 = n这种语句,返回行数是如何预估的?
如之前提到过的,假如c1的选择性为p1,c2的选择性为p2,表中的总行数为table_rowcount,暂忽略索引自身以及统计信息准确性带来的影响,以此为前提。
在SQLServer 2012中是预估返回函数是p1*p2*table_rowcount,
在SQLServer 2014或者更高版本中,这个算法发生了变化,是P1*P21/2*table_rowcount
简单demo一下,假设在c2和c3字段上某些条件值分布的特别不均匀(严重倾斜)的情况下,创建这么一个统计信息之后,可以指导执行计划在遇到类似的查询条件之后,做出更加准确的预估。
详细信息参考笔者的上一篇译文:https://www.cnblogs.com/wy123/p/13306673.html
需要注意的是,在创建完统计信息之后,且执行analyze table,且指定的列数与统计信息中的一致的时候,才会生成该统计信息。
db01=# drop statistics if exists statis_c2_c5; DROP STATISTICS db01=# create statistics if not exists statis_c2_c5(dependencies)on c2,c5 from myschema.table_test; CREATE STATISTICS db01=# create statistics if not exists statis_c2_c5(dependencies)on c2,c5 from myschema.table_test; NOTICE: statistics object "statis_c2_c5" already exists, skipping CREATE STATISTICS db01=# analyze myschema.table_test(c2,c5); ANALYZE db01=# select * from pg_statistic_ext where stxname like '%statis_c2_c5%'; -[ RECORD 1 ]---+----------------------------------------- stxrelid | 18730 stxname | statis_c2_c5 stxnamespace | 2200 stxowner | 10 stxkeys | 2 5 stxkind | {f} stxndistinct | stxdependencies | {"2 => 5": 0.817946, "5 => 2": 0.998274}
这其中的算法到底是如何实现的,原理是什么,计算出来结果的准确性如何等等一系列问题,笔者目前也尚不清楚,需要进一步的挖掘(备注:20200722已放弃,如下)。
20200722更新:
因为统计信息作为关系数据库系统中最复杂的组件之一,看到相关系度的计算原理和公式之后,就开始想念我的高数老师……,神似贝叶斯定理???
已经开始放弃原理的探索了……,开始无条件相信源码中这部分功能实现的人。
感慨,在非关系数据库中,相比关系数据是是没有也不需要如此复杂的组件的,因此在某些场景下,简单就是美。
Vacuum和Analyze
参考链接:
https://blog.pgaddict.com/posts/the-two-kinds-of-stats-in-postgresql
http://www.postgres.cn/docs/10/routine-vacuuming.html
https://www.postgresql.org/docs/12/planner-stats.html
https://www.postgresql.org/docs/11/runtime-config-query.html
https://pganalyze.com/blog/postgresql-log-monitoring-101-deadlocks-checkpoints-blocked-queries
https://www.cybertec-postgresql.com/en/setting-postgresql-configuration-parameters/
https://coderbook.com/@marcus/postgres-autovacuum-vacuum-and-analyze-explained/
https://wiki.postgresql.org/wiki/Introduction_to_VACUUM,_ANALYZE,_EXPLAIN,_and_COUNT