一文读懂analyze使用【这次高斯不是数学家】
1. 前言
- 适用版本:【基线功能】
analyze用于收集表的统计信息,新版本的analyze在底层算法和性能上都有了较大改进:
- 将列存行数估算方法从“采集样本估算”改为“直接从CUDesc精准读取”,样本分配更加合理。
- 百分比analyze性能优化,行存提升44%,列存提升73%。
- 全新的列存采样算法,随机性更好,性能提升30%。
- 行存“蓄水池采样算法”随机性增强,统计信息更准。
- 将采样算法与存储格式分离。
- 解决删除元组多时,统计信息估算不准的问题。
- 解决小CU多时,统计信息不准的问题。
- 深度重构代码逻辑,增强代码可读性,健壮性。
通过7处性能优化,4处算法改进,9次重点重构,在执行性能和统计信息准确性方面有了全面的提升。
此外,analyze执行的是否及时,在一定程度上直接决定了SQL执行的快慢。因此,GaussDB(DWS)引入了自动统计信息收集,可以做到让用户不再担心统计信息是否过期。
2. 统计信息作用
一条SQL语句的执行会经过如下过程:
(1) 查询解析:SQL文本--(“词法分析和语义分析”)-->“原始语法树(Raw Abstract Syntax Tree)”
(2) 查询分析:原始语法树--(查询分析和重写)-->查询语法树
(3) 查询优化:查询语法树--(优化器逻辑优化和物理优化)-->执行计划
(4) 查询执行:执行计划--(执行器执行)-->执行结果
查询的分析和重写,主要进行表的合法性检查,视图的展开等等。
优化器的逻辑优化,主要进行一些等价的代数变换,例如:关系表达式推导,子查询上拉,约束条件下推,约束条件合并,无效条件的移除等待。
优化器的物理优化,主要任务就是在所有可能的执行路径中,选择出一条代价最优的路径。
物理优化时需要根据表的统计信息,估算查询表达式的选择率,从而计算路径的CPU代价和IO代价。表统计信息的收集主要依赖于analyze来完成。
3. 统计信息分类
统计信息分为表级统计信息和列级统计信息。
表级统计信息存储在pg_class中,主要涉及relpages和reltuples。
列级统计信息PG_STATISTIC中,可以通过pg_stats视图查看。
ID | 类型 | 名称 | 说明 |
---|---|---|---|
1 | 表级 | relpages | 表占用的物理页面数 |
2 | 表级 | reltuples | 表中的记录数 |
3 | 列级 | nullfrac | NULL值占比 |
4 | 列级 | width | 列的平均宽度,主要用于文本类型 |
5 | 列级 | distinct | 唯一值的个数或占比 |
6 | 列级 | MCV | 高频值,一列中频繁出现的值的数组(按频率排序),同时生成一个一一对应的频率数组。 |
7 | 列级 | HISTOGRAM | 直方图,用等频直方图描述一列中数据的分布情况,高频值不会出现在直方图中。 |
8 | 列级 | CORRELATION | 相关系数,列的物理顺序和排序后逻辑顺序的相关系数。统计值范围从-1到1, 趋向于-1表示逆向相关, 趋向于1表示正向相关, 趋向于0表示不相关。通常用来估算索引扫描的代价,相关系数越高, 索引扫描时离散块扫描更少, 代价也就越低。 |
表级统计信息影响表大小的估算;列级统计信息影响查询表达式代价的估算。
distinct影响JOIN时内外表选择,width影响内存大小估算,MCV影响等值表达式,HISTOGRAM影响范围表达式。
4. 统计信息收集
统计信息收集可以借助analyze命令完成,语法如下:
{ ANALYZE | ANALYSE } [ VERBOSE ] [ [schema_name.] table_name [ ( column_name [, ...] ) ] ];
4.1 全库统计信息收集
analyze; --收集全库的统计信息
4.2 按schema进行统计信息收集
默认不支持按schema进行analyze,可以先借助SQL来拼接出所有表的analyze命令,再批量执行。
select 'analyze '||nspname||'.'||relname||';' from pg_class c, pg_namespace n where c.relnamespace=n.oid and n.nspname = 'public';
4.3 整表统计信息收集
analyze t1; --收集t1表所有列的统计信息
4.4 单列统计信息收集 {ANALYZE} [ table_name [ ( column_name [, ...] ) ] ];
analyze t1(a, b, c); --仅收集t1表a,b,c这几列的单列统计信息。推荐这种方式,避免全列收集,性能好些。
4.5 多列统计信息收集
对于表中多个列有相关性且查询中有同时基于这些列的条件或分组操作的情况,可尝试收集多列统计信息。
多列统计信息要求使用百分比采样方式收集,需default_statistics_target为负值,例如:-2。
因百分比采样多,耗时长。仅当单列确实无法满足时再采用,不建议普遍使用。
ANALYZE tablename ((column_1, column_2));
--收集tablename表的column_1、column_2列的组合多列统计信息
ALTER TABLE tablename ADD STATISTICS ((column_1, column_2));
--添加tablename表的column_1、column_2列的多列统计信息声明
ANALYZE tablename;
--收集单列统计信息,并收集已声明的多列统计信息
ALTER TABLE tablename DELETE STATISTICS ((column_1, column_2));
--删除tablename表的column_1、column_2列的多列统计信息或其声明
5. 采样方法
我们支持了两种采样模式,分别适用于不同样本量的场景。
(1) 固定值采样 采集样本放在内存,通过内置算法函数计算统计信息。适用于较小样本量时。
(2) 百分比采样 采集样本放入临时表,通过SQL计算统计信息。适用于按百分比指定采样大小,和多列统计信息收集。
如果计算的采样样本数大于等于总数据量的2%,且用户表的数据量小于1600000时,ANALYZE所需时间相比guc参数为默认值的时间会有所增加。
采样率为正数,表示使用固定值采样。
采样率为负数,表示使用百分比采样。
6. 采样率
目前默认收集统计信息的采样大小是30000行(default_statistics_target*300),如果表的统计信息估算不准,可以尝试增大采样率。
6.1 设置全局默认的采样大小
通过guc参数default_statistics_target可以设置全局默认的analyze采样大小。
a. default_statistics_target设置为正数,表示按固定值的方式进行采样。取值范围:[100, 10000]
analyze采样大小 = default_statistics_target * 300;
b. default_statistics_target设置为负数,表示按百分比的方式进行采样。取值范围:[-100, 0)
analyze采样大小 = (-1) * default_statistics_target * 表的估算大小。例如:default_statistics_target为-2,即按2%收集样本估算统计信息。
6.2 设置表级的采样大小
通过单独设置列属性attstattarget,可以设置表级的采样大小。表采样大小取所有列的最大值。
ALTER TABLE table_name ALTER [ COLUMN ] column_name SET STATISTICS [PERCENT] integer;
为随后的ANALYZE操作设置针对每个字段的统计收集目标。目标的范围可以在0到10000之内设置。设置为-1时表示重新恢复到使用系统缺省的统计目标。
ALTER TABLE table_name ALTER column_name SET STATISTICS 200; --把采样大小调整为60000
ALTER TABLE table_name ALTER column_name SET STATISTICS PERCENT 2; --把采样大小调整为2%
6.3 增大CU的选择数量
为了避免解压太多CU,我们按采样率计算里需要采集的CU数量,并且样本都集中落在这些随机选择的CU中。
有时样本数不少但都比较集中可能导致批量重复场景的数据特征抓不准,这是可以通过cstore_cu_sample_ratio参数,增大CU的选择倍数。
alter table lineitem alter l_orderkey set (cstore_cu_sample_ratio=10);
7. 采样模型
数据的数据特征各种各样,通过采集小部分数据来估算全局的数据特征分布,通常很难做的很准确。我们只能尽可能的保证采样足够均匀,以期望估算准确。
所以,我们自研了新的样本采集模型,相对传统“蓄水池算法”的采样模型,样本抽取更加随机,执行效率更快。
现在通过cost_model_version来控制模型选择,=1使用旧模型,=2使用新模型。
由于新模型会将distinct估算更准确,可能会导致业务场景的执行计划变动,所以现在默认依然使用旧模型。
8. analzye权限
我们认为analyze表的统计信息计算,应该数据表数据资产管理的内容。因此应该由owner或者有analyze权限的人,负责完成统计信息收集任务。
表的owner或者有analyze权限的人,可以对表进行analzye。
库的owner,可以对全库的所有表进行analyze。
9. 支持的表类型
行存表,列存表,分区表,继承表,OBS表,外表。
10. autoanalyze的使用
10.1 自动收集场景
需要进行自动统计信息收集的场景通常有五个:批量DML结束时,增量DML结束时,DDL结束时,查询开始时和后台定时任务。
场景 | 语句 | 触发理由 | 是否支持 |
---|---|---|---|
批量DML结束时 | INSERT,UPDATE,DELETE,UPSERTCOPY,MERGE | 批量的数据变化导致统计信息失效 | 不支持。批量变化最需要及时收集统计信息,但为避免并发灌数场景多次重复触发统计信息收集,我们并未支持。 |
增量DML结束时 | INSERT,UPDATE,DELETE,UPSERT | 一次变化不大,但多次累积后依然会导致统计信息失效 | 不支持。增量的变化通常很频繁,每次都去检查是否需要收集统计信息会拖慢DML语句的执行,所以我们并未支持。 |
DDL语句结束时 | TRUNCATE,ALTER PARTITION | 批量的数据变化导致统计信息失效 | 不支持。analzye会给所有分区加锁,扩大加锁范围导致分布式死锁。 |
查询开始时 | SELECT | 查询执行前保证统计信息有效 | 支持。失效的统计信息大概率导致很差的执行计划。通过统计信息收集的代价换来更优的执行计划,通常还是比较合适的。 |
后台定时任务 | autovacuum线程 | 后台线程定时轮询检查所有表 | 支持。 |
所以,为了避免对DML,DDL带来不必要的性能开销和死锁风险,我们选择了在查询开始前触发analzye。
10.2 自动收集原理
GaussDB(DWS)在SQL执行过程中,会记录表增删改查相关的运行时统计信息,并在事务提交或回滚后记录到共享的内存种。
这些信息可以通过 “pg_stat_all_tables视图” 查询,也可以通过下面函数进行查询。
pg_stat_get_tuples_inserted --表累积insert条数
pg_stat_get_tuples_updated --表累积update条数
pg_stat_get_tuples_deleted --表累积delete条数
pg_stat_get_tuples_changed --表自上次analyze以来,修改的条数
pg_stat_get_last_analyze_time --查询最近一次analyze时间
因此,根据共享内存中 "表自上次analyze以来修改过的条数" 是否超过一定阈值,就可以判定是否需要做analyze了。
10.3 自动收集阈值
10.3.1 全局阈值
autovacuum_analyze_threshold #表触发analyze的最小修改量
autovacuum_analyze_scale_factor #表触发analyze时的修改百分比
当"表自上次analyze以来修改的条数" >= autovacuum_analyze_threshold + 表估算大小 * autovacuum_analyze_scale_factor时,需要自动触发analyze。
10.3.2 表级阈值
--设置表级阈值
ALTER TABLE item SET (autovacuum_analyze_threshold=50);
ALTER TABLE item SET (autovacuum_analyze_scale_factor=0.1);
--查询阈值
postgres=# select pg_options_to_table(reloptions) from pg_class where relname='item';
pg_options_to_table
---------------------------------------
(autovacuum_analyze_threshold,50)
(autovacuum_analyze_scale_factor,0.1)
(2 rows)
--重置阈值
ALTER TABLE item RESET (autovacuum_analyze_threshold);
ALTER TABLE item RESET (autovacuum_analyze_scale_factor);
不同表的数据特征不一样,需要触发analyze的阈值可能有不同的需求。表级阈值优先级高于全局阈值。
10.3.3 查看表的修改量是否超过了阈值(仅当前CN)
postgres=# select pg_stat_get_local_analyze_status('t_analyze'::regclass);
pg_stat_get_local_analyze_status
----------------------------------
Analyze not needed
(1 row)
10.4 自动收集方式
GaussDB(DWS)提供了三种场景下表的自动分析。
- 当查询中存在“统计信息完全缺失”或“修改量达到analyze阈值”的表,且执行计划不采取FQS (Fast Query Shipping)执行时,则通过autoanalyze控制此场景下表统计信息的自动收集。此时,查询语句会等待统计信息收集成功后,生成更优的执行计划,再执行原查询语句。
- 当autovacuum设置为on时,系统会定时启动autovacuum线程,对“修改量达到analyze阈值”的表在后台自动进行统计信息收集。
|
触发方式 | 触发条件 | 触发频率 | 控制参数 | 备注 |
---|---|---|---|---|
同步 | 无统计信息 | 查询时 | autoanalyze | truncate主表时会清空统计信息 |
同步 | 数据修改超阈值 | 查询时 | autoanalyze | 先触发analyze, 后选择最优计划 |
异步 | 数据修改超阈值 | autovacuum线程轮询检查 | autovacuum_mode&&autovacuum_naptime | 2s等锁超时5min执行超时 |
10.5 冻结统计信息
10.5.1 冻结表的distinct值
当一个表的distinct总是估算不准,例如:数据扎堆儿重复场景。如果表的distinct值固定,可以通过以下方式冻结表的distinct值。
postgres=# alter table lineitem alter l_orderkey set (n_distinct=0.9);
ALTER TABLE
postgres=# select relname,attname,attoptions from pg_attribute a,pg_class c where c.oid=a.attrelid and attname='l_orderkey';
relname | attname | attoptions
----------+------------+------------------
lineitem | l_orderkey | {n_distinct=0.9}
(1 row)
postgres=# alter table lineitem alter l_orderkey reset (n_distinct);
ALTER TABLE
postgres=# select relname,attname,attoptions from pg_attribute a,pg_class c where c.oid=a.attrelid and attname='l_orderkey';
relname | attname | attoptions
----------+------------+------------
lineitem | l_orderkey |
(1 row)
10.5.2 手动查看表是否需要做analyze
如果表的数据特征基本不变,还可以冻结表的统计信息,来避免重复进行analyze。
alter table table_name set frozen_stats=true;
10.6 冻结统计信息
a. 不想在业务高峰期时触发数据库后台任务,所以不愿意打开autovacuum来触发analyze,怎么办?
b. 业务修改了一批表,想立即对这些表马上做一次analyze,又不知道都有哪些表,怎么办?
c. 业务高峰来临前想对临近阈值的表都做一次analyze,怎么办?
我们将autovacuum检查阈值判断是否需要analyze逻辑,抽取成了函数,帮助用户灵活主动的检查哪些表需要做analyze。
10.6.1 判断表是否需要analyze(串行版,适用于所有历史版本)
-- the function for get all pg_stat_activity information in all CN of current cluster.
CREATE OR REPLACE FUNCTION pg_catalog.pgxc_stat_table_need_analyze(in table_name text)
RETURNS BOOl
AS $$
DECLARE
row_data record;
coor_name record;
fet_active text;
fetch_coor text;
relTuples int4;
changedTuples int4:= 0;
rel_anl_threshold int4;
rel_anl_scale_factor float4;
sys_anl_threshold int4;
sys_anl_scale_factor float4;
anl_threshold int4;
anl_scale_factor float4;
need_analyze bool := false;
BEGIN
--Get all the node names
fetch_coor := 'SELECT node_name FROM pgxc_node WHERE node_type=''C''';
FOR coor_name IN EXECUTE(fetch_coor) LOOP
fet_active := 'EXECUTE DIRECT ON (' || coor_name.node_name || ') ''SELECT pg_stat_get_tuples_changed(oid) from pg_class where relname = ''''|| table_name ||'''';''';
FOR row_data IN EXECUTE(fet_active) LOOP
changedTuples = changedTuples + row_data.pg_stat_get_tuples_changed;
END LOOP;
END LOOP;
EXECUTE 'select pg_stat_get_live_tuples(oid) from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into relTuples;
EXECUTE 'show autovacuum_analyze_threshold;' into sys_anl_threshold;
EXECUTE 'show autovacuum_analyze_scale_factor;' into sys_anl_scale_factor;
EXECUTE 'select (select option_value from pg_options_to_table(c.reloptions) where option_name = ''autovacuum_analyze_threshold'') as value
from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into rel_anl_threshold;
EXECUTE 'select (select option_value from pg_options_to_table(c.reloptions) where option_name = ''autovacuum_analyze_scale_factor'') as value
from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into rel_anl_scale_factor;
--dbms_output.put_line('relTuples='||relTuples||'; sys_anl_threshold='||sys_anl_threshold||'; sys_anl_scale_factor='||sys_anl_scale_factor||'; rel_anl_threshold='||rel_anl_threshold||'; rel_anl_scale_factor='||rel_anl_scale_factor||';');
if rel_anl_threshold IS NOT NULL then
anl_threshold = rel_anl_threshold;
else
anl_threshold = sys_anl_threshold;
end if;
if rel_anl_scale_factor IS NOT NULL then
anl_scale_factor = rel_anl_scale_factor;
else
anl_scale_factor = sys_anl_scale_factor;
end if;
if changedTuples > anl_threshold + anl_scale_factor * relTuples then
need_analyze := true;
end if;
return need_analyze;
END; $$
LANGUAGE 'plpgsql';
10.6.2 判断表是否需要analyze(并行版,适用于支持并行执行框架的版本)
-- the function for get all pg_stat_activity information in all CN of current cluster.
--SELECT sum(a) FROM pg_catalog.pgxc_parallel_query('cn', 'SELECT 1::int FROM pg_class LIMIT 10') AS (a int); 利用并发执行框架
CREATE OR REPLACE FUNCTION pg_catalog.pgxc_stat_table_need_analyze(in table_name text)
RETURNS BOOl
AS $$
DECLARE
relTuples int4;
changedTuples int4:= 0;
rel_anl_threshold int4;
rel_anl_scale_factor float4;
sys_anl_threshold int4;
sys_anl_scale_factor float4;
anl_threshold int4;
anl_scale_factor float4;
need_analyze bool := false;
BEGIN
--Get all the node names
EXECUTE 'SELECT sum(a) FROM pg_catalog.pgxc_parallel_query(''cn'', ''SELECT pg_stat_get_tuples_changed(oid)::int4 from pg_class where relname = ''''|| table_name ||'''';'') AS (a int4);' into changedTuples;
EXECUTE 'select pg_stat_get_live_tuples(oid) from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into relTuples;
EXECUTE 'show autovacuum_analyze_threshold;' into sys_anl_threshold;
EXECUTE 'show autovacuum_analyze_scale_factor;' into sys_anl_scale_factor;
EXECUTE 'select (select option_value from pg_options_to_table(c.reloptions) where option_name = ''autovacuum_analyze_threshold'') as value
from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into rel_anl_threshold;
EXECUTE 'select (select option_value from pg_options_to_table(c.reloptions) where option_name = ''autovacuum_analyze_scale_factor'') as value
from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into rel_anl_scale_factor;
dbms_output.put_line('relTuples='||relTuples||'; sys_anl_threshold='||sys_anl_threshold||'; sys_anl_scale_factor='||sys_anl_scale_factor||'; rel_anl_threshold='||rel_anl_threshold||'; rel_anl_scale_factor='||rel_anl_scale_factor||';');
if rel_anl_threshold IS NOT NULL then
anl_threshold = rel_anl_threshold;
else
anl_threshold = sys_anl_threshold;
end if;
if rel_anl_scale_factor IS NOT NULL then
anl_scale_factor = rel_anl_scale_factor;
else
anl_scale_factor = sys_anl_scale_factor;
end if;
if changedTuples > anl_threshold + anl_scale_factor * relTuples then
need_analyze := true;
end if;
return need_analyze;
END; $$
LANGUAGE 'plpgsql';
10.6.3 判断表是否需要analyze(自定义阈值)
-- the function for get all pg_stat_activity information in all CN of current cluster.
CREATE OR REPLACE FUNCTION pg_catalog.pgxc_stat_table_need_analyze(in table_name text, int anl_threshold, float anl_scale_factor)
RETURNS BOOl
AS $$
DECLARE
relTuples int4;
changedTuples int4:= 0;
need_analyze bool := false;
BEGIN
--Get all the node names
EXECUTE 'SELECT sum(a) FROM pg_catalog.pgxc_parallel_query(''cn'', ''SELECT pg_stat_get_tuples_changed(oid)::int4 from pg_class where relname = ''''|| table_name ||'''';'') AS (a int4);' into changedTuples;
EXECUTE 'select pg_stat_get_live_tuples(oid) from pg_class c where c.oid = '''|| table_name ||'''::REGCLASS;' into relTuples;
if changedTuples > anl_threshold + anl_scale_factor * relTuples then
need_analyze := true;
end if;
return need_analyze;
END; $$
LANGUAGE 'plpgsql';
通过“优化器触发的实时analyze”和“后台autovacuum触发的轮询analyze”,GaussDB(DWS)已经可以做到让用户不再关心表是否需要analyze。建议在最新版本中试用。
11. 总结
统计信息的计算基于随机的数据采样,因此每次analyze计算的统计信息会有小幅度的正常波动。
如果波动较大,说明数据特征值的分布并不随机,可能存在特征数据扎堆儿的情况。此时,可以加大采样率,更多的样本来覆盖更细的数据特征。
- 点赞
- 收藏
- 关注作者
评论(0)