GreatSQL 为何选择全表扫描而不选索引
1. 问题背景
在生产环境中,发现某些查询即使有索引,也没有使用索引,反而选择了全表扫描。这种现象的根本原因在于优化器评估索引扫描的成本时,认为使用索引的成本高于全表扫描。
2. 场景复现
2.1 环境信息
- 机器 IP:192.168.137.120
- GreatSQL 版本:8.0.32-26
2.2 环境准备
通过脚本创建了一个包含 100 万条数据的表,并在 age
列上创建了索引 idx_age
,如下所示:
#!/bin/bash
# 数据库配置
db_host="192.168.137.120"
db_user="root"
db_pass="xxxx"
db_name="test"
db_port=3306
table_name="t1"
my_conn="greatsql -h$db_host -P$db_port -u$db_user -p$db_pass -D$db_name"
# 创建大表
create_table() {
$my_conn -e "
CREATE TABLE IF NOT EXISTS ${table_name} (
id INT primary key,
name VARCHAR(255),
age INT,
email VARCHAR(255),
address VARCHAR(255),
created_at DATETIME,
updated_at DATETIME,
key idx_age(age)
);"
}
# 批量插入数据
bulk_insert() {
values=""
for ((i=1; i<=1000000; i++)); do
values+="($i, 'name $i', $((RANDOM % 100)), 'email$i@example.com', 'address $i', NOW(), NOW()),"
if (( i % 1000 == 0 )); then
values=${values%,} # 去掉最后的逗号
# 执行插入
$my_conn -e "INSERT INTO ${table_name} (id, name, age, email, address, created_at, updated_at) VALUES $values;"
values="" # 重置values
fi
done
}
# 主执行函数
main() {
echo "开始创建表..."
create_table
echo "表创建完成!"
echo "开始批量插入数据..."
start_time=$(date +%s)
bulk_insert
end_time=$(date +%s)
echo "插入数据完成!"
echo "耗时:$((end_time - start_time)) 秒"
}
# 执行主函数
main
复制
2.3 SQL 查询
测试了两个查询:
- 查询
age > 80
的记录:
greatsql> EXPLAIN SELECT * FROM t1 WHERE age > 80;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | t1 | NULL | ALL | idx_age | NULL | NULL | NULL | 994098 | 37.05 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
复制
- 查询
age < 3
的记录:
greatsql> EXPLAIN SELECT * FROM t1 WHERE age < 3;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+-------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+-------+----------+-----------------------+
| 1 | SIMPLE | t1 | NULL | range | idx_age | idx_age | 5 | NULL | 55344 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+-------+----------+-----------------------+
复制
对于第一个查询,优化器选择了全表扫描(type = ALL
),而对于第二个查询,则使用了索引扫描(type = range
)。这是因为优化器认为,在 age > 80
的查询中,索引扫描的成本较高,而全表扫描相对较低。
3. 优化器分析
启用 optimizer_trace
查看优化器的执行细节:
greatsql> SET optimizer_trace="enabled=on";
greatsql> SELECT * FROM t1 WHERE age > 80;
+-----+----------+------+----------------------+-------------+---------------------+---------------------+
| id | name | age | email | address | created_at | updated_at |
+-----+----------+------+----------------------+-------------+---------------------+---------------------+
| 48 | name 48 | 81 | email48@example.com | address 48 | 2024-12-26 11:25:00 | 2024-12-26 11:25:00 |
| 87 | name 87 | 81 | email87@example.com | address 87 | 2024-12-26 11:25:00 | 2024-12-26 11:25:00 |
| 130 | name 130 | 81 | email130@example.com | address 130 | 2024-12-26 11:25:00 | 2024-12-26 11:25:00 |
| 201 | name 201 | 81 | email201@example.com | address 201 | 2024-12-26 11:25:00 | 2024-12-26 11:25:00 |
| 232 | name 232 | 81 | email232@example.com | address 232 | 2024-12-26 11:25:00 | 2024-12-26 11:25:00 |
......
| 999998 | name 999998 | 99 | email999998@example.com | address 999998 | 2024-12-26 11:27:22 | 2024-12-26 11:27:22 |
+--------+-------------+------+-------------------------+----------------+---------------------+---------------------+
greatsql> SELECT trace FROM information_schema.optimizer_trace\G
trace: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `t1`.`id` AS `id`,`t1`.`name` AS `name`,`t1`.`age` AS `age`,`t1`.`email` AS `email`,`t1`.`address` AS `address`,`t1`.`created_at` AS `created_at`,`t1`.`updated_at` AS `updated_at` from `t1` where (`t1`.`age` > 80)"
}
]
}
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`t1`.`age` > 80)",
"steps": [
{
"transformation": "equality_propagation", --传播等式
"resulting_condition": "(`t1`.`age` > 80)"
},
{
"transformation": "constant_propagation", --传播常量
"resulting_condition": "(`t1`.`age` > 80)"
},
{
"transformation": "trivial_condition_removal", --移除无关条件
"resulting_condition": "(`t1`.`age` > 80)"
}
]
}
},
{
"substitute_generated_columns": {
}
},
{
"table_dependencies": [ --表依赖
{
"table": "`t1`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
]
}
]
},
{
"ref_optimizer_key_uses": [
]
},
{
"rows_estimation": [ --行数估算
{
"table": "`t1`",
"range_analysis": {
"table_scan": { --全表扫描
"rows": 994078, -- 994078 行需要被扫描
"cost": 106040 -- 106040 是执行全表扫描的估算成本
},
"potential_range_indexes": [ --潜在范围索引
{
"index": "PRIMARY",
"usable": false, -- 主键索引不适用于此次查询
"cause": "not_applicable"
},
{
"index": "idx_age",
"usable": true, -- idx_age索引可用于此次查询
"key_parts": [ -- 索引基于age,id创建
"age",
"id"
]
}
],
"setup_range_conditions": [
],
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
},
"skip_scan_range": {
"potential_skip_scan_indexes": [
{
"index": "idx_age",
"usable": false,
"cause": "query_references_nonkey_column"
}
]
},
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "idx_age",
"ranges": [
"80 < age"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"in_memory": 0,
"rows": 379410, -- 使用idx_age索引估算扫描行数
"cost": 417303, -- 使用idx_age索引估算成本
"chosen": false, -- 未选择该索引
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
}
}
}
]
},
{
"considered_execution_plans": [ --考虑的执行计划
{
"plan_prefix": [
],
"table": "`t1`",
"best_access_path": { -- 最优访问方式
"considered_access_paths": [
{
"rows_to_scan": 994078,
"access_type": "scan",
"resulting_rows": 994078,
"cost": 106038,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 994078,
"cost_for_plan": 106038,
"chosen": true -- 优化器最终选择了全表扫描,行数为994708,成为106038
}
]
},
{
"attaching_conditions_to_tables": { --将条件附加到表
"original_condition": "(`t1`.`age` > 80)",
"attached_conditions_computation": [
],
"attached_conditions_summary": [
{
"table": "`t1`",
"attached": "(`t1`.`age` > 80)"
}
]
}
},
{
"finalizing_table_conditions": [ --最终确定表条件
{
"table": "`t1`",
"original_table_condition": "(`t1`.`age` > 80)",
"final_table_condition ": "(`t1`.`age` > 80)" --最终的表条件
}
]
},
{
"refine_plan": [ --优化计划细节
{
"table": "`t1`"
}
]
}
]
}
},
{
"join_execution": {
"select#": 1,
"steps": [
]
}
}
]
}
复制
从优化器的执行计划中可以看到:
- 全表扫描:优化器选择了全表扫描的估算成本为 106038。
- 索引扫描:虽然索引
idx_age
在age
列上可以被使用,但由于它并不包含查询中所有列(如id
,name
,email
等),因此需要回表操作,导致索引扫描的估算成本为 417303,远高于全表扫描。
4. 问题分析
优化器选择使用全表扫描而不是索引扫描的原因主要是因为:
- 回表开销:
idx_age
索引仅包含age
和id
列,而查询需要age
,id
,name
,email
,address
,created_at
,updated_at
等字段。因此,使用索引后需要额外的回表操作,这会增加查询的成本。特别是在数据量较大时,回表次数增多,导致整体性能下降。 - 估算成本:在某些情况下,优化器评估使用索引的成本比全表扫描高。例如,如果查询涉及的列较多,而索引并不覆盖这些列,回表的代价可能超过直接扫描整张表的代价。优化器会倾向于选择代价较低的执行计划,即全表扫描。
5. 解决方案与优化建议
- 查询优化:可以通过调整查询条件,减少涉及的列数,或者使用
EXPLAIN
分析不同查询条件的执行计划,选择最优的查询方式。 - 分析索引选择性:确保索引列的选择性较高,即索引能有效减少扫描的行数。如果某个列的选择性较低(如范围条件
age > 80
),全表扫描可能仍然是最优选择。 - 调整配置参数:根据表的数据分布和查询特征,可能需要调整 MySQL 的优化器相关参数(如
optimizer_search_depth
或optimizer_switch
),以优化查询执行计划的选择。 - 在本案例中,条件
age > 80
读取扫描的数据量太大了,在真实业务中只有很少数情况需要读取这么大量数据,因此建议加上LIMIT N
限定读取行数(N通常不高于1000)。 - 在部分其他场景中,有时可以尝试通过使用直方图来优化查询效率,可以根据实际情况选择。关于直方图可以参考文章:深入聊聊MySQL直方图的应用。
通过这些方法,可以有效减少全表扫描的发生,提高查询性能。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。
评论
相关阅读
2025年2月国产数据库中标情况一览:GoldenDB 3500+万!达梦近千万!
通讯员
878次阅读
2025-03-06 11:40:20
2月“墨力原创作者计划”获奖名单公布
墨天轮编辑部
455次阅读
2025-03-13 14:38:19
你看这红牛又慢又快,像不像侧箱上的Oracle
多明戈教你玩狼人杀
208次阅读
2025-03-28 10:12:13
Navicat 17.2 正式发布 | AI 助手、支持 Snowflake...超多新功能等你解锁
Navicat
200次阅读
2025-03-14 14:13:33
套壳论
梧桐
187次阅读
2025-03-09 10:58:17
KingbaseES V9单机扩容至RWC集群
智慧同学
35次阅读
2025-03-22 18:02:18
rac
手机用户6879
17次阅读
2025-03-14 12:52:35
九有数据库完成A轮融资,大米创投领投。
朱凌浩 LEONARDO
8次阅读
2025-03-31 11:28:04
数据库选型测试经验分享
17364520545
6次阅读
2025-03-31 18:29:14