背景
在 TiDB 中,统计信息是查询优化器用来决定最佳查询执行计划的重要依据。准确的统计信息可以显著提高查询性能,因此定期的统计信息收集是维持数据库性能的重要任务之一。
统计信息收集过程中,analyze 操作会扫描表中的数据,并计算相关的统计数据。在此过程中,采样率(samplerate)是一个关键参数,它决定了需要扫描的数据量。理想情况下,采样率应当与表的大小成反比,以平衡统计信息的准确性和资源消耗。
最近有用户在 dashboard 发现了内存消耗高的 SQL,这是一个 analyze 收集统计信息的 SQL,涉及的表平均行长只有 600 bytes,并且采用低并发度参数的情况下,内存消耗接近 10G。我在 mysql.analyze_jobs
表中发现,当 processed_rows
高达 133 万时,采样率samplerate
自适应地使用了 1。按预期,采样率应该是 11 万 / 133 万,即约 0.08,这导致实际使用的内存比预期高出许多。因此,我想了解一下 analyze
操作中的自适应采样率是如何计算出来的。
# 低并发参数下收集统计信息
set session tidb_build_stats_concurrency=1;
set session tidb_distsql_scan_concurrency=1;
set session tidb_index_serial_scan_concurrency=1;
analyze table xxx;
# mysql.analyze_jobs 查到的 job_info 信息
analyze table all columns with 256 buckets, 500 topn, 1 samplerate
源码分析过程
咨询相关产研大佬后,找到了对应的代码位置,在 getAdjustedSampleRate
模块,代码地址如下: https://github.com/pingcap/tidb/blob/v6.5.3/executor/builder.go#L2608-L2646
我们重点关注哪些情况下采样率 (sampleRate) 会被算成 1。
情形 1:stats_meta 没有值,且没有 PD 信息时,会返回默认 defaultRate (0.001)
if statsTbl == nil && !hasPD {
return defaultRate
}
情形 2:stats_meta 的 count 值为 0 且没有 PD 信息时,返回 1 ,也就是 samplerate 是 1
if statsTbl.Count == 0 && !hasPD {
return 1
}
情形 3:stats_meta 的 count 值乘以 5 ,如果仍小于存储获得的 approxiCount 值,那么:
- 如果
150000/approxiCount
小于1
,那么返回值将是150000/approxiCount
,samplerate 将是一个零点几的小数。 - 如果
150000/approxiCount
大于或等于1
,那么返回值将是1
,samplerate 是 1。
if float64(statsTbl.Count*5) < approxiCount {
return math.Min(x: 1, 150000/approxiCount)
}
情形 4:stats_meta 的 count 值为 0 ,返回 1 ,也就是 samplerate 是 1
if statsTbl.Count == 0 {
return 1
}
情形 5:和经验值 11W 行做对比,返回零点几或者 1
- 如果
110000/statsTbl.Count
小于1
,那么返回值将是110000/statsTbl.Count
,samplerate 将是一个零点几的小数。 - 如果
110000/statsTbl.Count
大于或等于1
,那么返回值将是1
,samplerate 是 1。
注:可以简单理解为表数据量小于 11W,采样率就是 1,大于 11W 就是零点几。
return math.Min(x: 1, config.DefRowsForSampleRate/float64(statsTbl.Count))
相关实验
注:为方便理解,这里准备了“情形 3”、“情形 4”和“情形 5”的 demo
情形 1-2 几乎没有或不易演示,略
情形 3
# lightning 设置 analyze = "off" 情况下导数 100w
# 此时查询 Approximate Count for table TAB1 为 200w
MySQL [test]> show stats_meta;
+---------+------------+----------------+---------------------+--------------+-----------+
| Db_name | Table_name | Partition_name | Update_time | Modify_count | Row_count |
+---------+------------+----------------+---------------------+--------------+-----------+
| test | TAB1 | | 2024-05-16 21:48:44 | 0 | 0 |
+---------+------------+----------------+---------------------+--------------+-----------+
1 row in set (0.03 sec)
MySQL [test]> analyze table tab1;
Query OK, 0 rows affected, 1 warning (3.75 sec)
# 第一次收集走“情形3”,采样率: 15w/200w=0.075
MySQL [test]> show warnings;
+-------+------+--------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------------------------+
| Note | 1105 | Analyze use auto adjusted sample rate 0.075000 for table test.tab1 |
+-------+------+--------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL [test]> show stats_meta;
+---------+------------+----------------+---------------------+--------------+-----------+
| Db_name | Table_name | Partition_name | Update_time | Modify_count | Row_count |
+---------+------------+----------------+---------------------+--------------+-----------+
| test | TAB1 | | 2024-05-16 21:49:51 | 0 | 1000000 |
+---------+------------+----------------+---------------------+--------------+-----------+
1 row in set (0.00 sec)
MySQL [test]> analyze table tab1;
Query OK, 0 rows affected, 1 warning (4.42 sec)
# 第二次收集走“情形5”,采样率: 11w/100w=0.11
MySQL [test]> show warnings;
+-------+------+--------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------------------------+
| Note | 1105 | Analyze use auto adjusted sample rate 0.110000 for table test.tab1 |
+-------+------+--------------------------------------------------------------------+
1 row in set (0.00 sec)
情形 4
MySQL [test]> create table tab2(id int);
Query OK, 0 rows affected (0.12 sec)
MySQL [test]> show stats_meta where table_name='tab2';
+---------+------------+----------------+---------------------+--------------+-----------+
| Db_name | Table_name | Partition_name | Update_time | Modify_count | Row_count |
+---------+------------+----------------+---------------------+--------------+-----------+
| test | tab2 | | 2024-05-15 16:57:34 | 0 | 0 |
+---------+------------+----------------+---------------------+--------------+-----------+
1 row in set (0.03 sec)
MySQL [test]> analyze table tab2;
Query OK, 0 rows affected, 1 warning (0.04 sec)
MySQL [test]> show warnings;
+-------+------+--------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------------------------+
| Note | 1105 | Analyze use auto adjusted sample rate 1.000000 for table test.tab2 |
+-------+------+--------------------------------------------------------------------+
1 row in set (0.00 sec)
情形 5
# tab2 插入数据到17W
MySQL [test]> show stats_meta where table_name='tab2';
+---------+------------+----------------+---------------------+--------------+-----------+
| Db_name | Table_name | Partition_name | Update_time | Modify_count | Row_count |
+---------+------------+----------------+---------------------+--------------+-----------+
| test | tab2 | | 2024-05-15 17:13:20 | 0 | 170001 |
+---------+------------+----------------+---------------------+--------------+-----------+
1 row in set (0.00 sec)
MySQL [test]> analyze table tab2;
Query OK, 0 rows affected, 1 warning (0.37 sec)
# 采样率 = 110000/170001 = 0.647055
MySQL [test]> show warnings;
+-------+------+--------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------------------------+
| Note | 1105 | Analyze use auto adjusted sample rate 0.647055 for table test.tab2 |
+-------+------+--------------------------------------------------------------------+
1 row in set (0.00 sec)
结论
我原本预计这个案例会走“情形 5”,但实际上它走了“情形 4”(通过排除法得出)。在 master 版本中,这部分代码已得到改进,并且会打印原因。目前的版本不容易排查,推测可能遇到了某些未知问题,导致统计信息被错误地覆盖成 0。很可能是遇到以下 issue 的问题,该问题将在 6.5.10 版本中得到修复:
https://github.com/pingcap/tidb/issues/52294
最后感谢 weizhen 大佬的支持!