你好,数据库的使用者们!今天,我们来聊聊一个在数据库性能调优中至关重要,却又常常被忽视的“藏宝图”——执行计划。无论你使用的是哪种数据库,理解执行计划都是通往高性能SQL的必经之路。今天,我们就以阿里云的云原生数据库PolarDB为例,来一场深入浅出的执行计划解读之旅。

想象一下,你给数据库下达了一条SQL指令,比如“帮我找出上个月所有购买了某款产品且来自北京的用户信息”。数据库可不是一拍脑袋就去翻箱倒柜,它内部有一个非常聪明的“查询优化器”。这个优化器会像一位经验丰富的图书馆管理员,面对你的请求,它会快速构思出几种不同的查找方案:是先按“购买时间”这个索引快速定位到上个月的记录,再从中筛选“北京”的用户?还是先用“城市”索引找到所有北京用户,再核对他们的购买记录?抑或是干脆把整个“用户-订单”大表从头到尾扫描一遍?

这个被优化器最终选定的、最有效率的“查找方案”,就是执行计划。解读它,你就能看透数据库执行SQL的每一个心思和步骤,从而找到性能瓶颈的症结所在。

一、获取执行计划:让数据库交出它的“作战方案”

在PolarDB(这里我们以PolarDB for MySQL 兼容版为例)中,获取执行计划最常用的方法是使用 EXPLAIN 命令。只需在你要分析的SQL语句前加上 EXPLAINEXPLAIN FORMAT=JSON 即可。

示例1:获取一个简单查询的执行计划

-- 假设我们有一个订单表 orders
-- 我们先获取一个简单查询的文本格式执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND order_date > '2023-10-01';

-- 输出可能类似于:
-- +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
-- | id | select_type | table  | partitions | type | possible_keys | key     | key_len | ref   | rows | filtered | Extra       |
-- +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
-- |  1 | SIMPLE      | orders | NULL       | ref  | idx_user      | idx_user| 4       | const |   23 |    33.33 | Using where |
-- +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+

注释EXPLAIN 命令并没有真正执行后面的 SELECT 语句,它只是让优化器展示其计划。type=ref 表示使用了非唯一索引扫描,key=idx_user 显示使用了哪个索引,rows=23 是优化器预估要检查的行数,这是一个非常重要的效率指标。

示例2:获取更详细的JSON格式执行计划

-- JSON格式包含更丰富的信息,是深度分析的首选
EXPLAIN FORMAT=JSON
SELECT o.order_id, o.amount, u.user_name
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.city = '北京' AND o.status = 'completed'
ORDER BY o.order_date DESC
LIMIT 10;

注释:执行后,你会得到一个结构化的JSON对象,其中包含了完整的执行过程树、每一步的成本估算、访问方法、连接类型等详细信息。虽然看起来复杂,但它是性能分析的“富矿”。

二、解读核心字段:看懂执行计划的“密码”

面对 EXPLAIN 的输出,我们需要聚焦几个关键字段。这里我们结合一个稍复杂的例子来解读。

示例3:分析一个多表关联查询

-- 创建示例表结构(假设)
-- CREATE TABLE users (user_id INT PRIMARY KEY, user_name VARCHAR(100), city VARCHAR(50));
-- CREATE TABLE orders (order_id INT PRIMARY KEY, user_id INT, amount DECIMAL(10,2), city VARCHAR(50), order_date DATE);
-- CREATE INDEX idx_orders_user ON orders(user_id);
-- CREATE INDEX idx_orders_city_date ON orders(city, order_date);

EXPLAIN
SELECT u.user_name, SUM(o.amount) as total_amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE o.city = '上海'
  AND o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY u.user_id, u.user_name
HAVING total_amount > 10000
ORDER BY total_amount DESC;

假设执行计划输出有两行(代表两个表的访问)。我们来解读关键列:

  • id: 执行序号。id相同,执行顺序从上到下(如关联查询);id不同,序号越大越先执行(如子查询)。
  • select_type: 查询类型。SIMPLE(简单查询)、PRIMARY(最外层查询)、SUBQUERY(子查询)等。本例中驱动表(通常是users)可能是SIMPLE,被驱动表(orders)的select_type在关联查询中通常也是SIMPLE(对于JOIN)。
  • table: 当前步骤访问的表。
  • type(访问类型): 这是衡量性能最重要的指标之一! 从优到劣常见的有:
    • system / const: 通过主键或唯一索引一次就找到,最优。
    • eq_ref: 关联查询中,使用主键或唯一索引进行关联。
    • ref: 使用非唯一索引进行查找。我们示例中 o.city 上的条件可能用到此类型(如果索引是 (city, order_date))。
    • range: 索引范围扫描,如 BETWEEN, >, < 等。我们的 o.order_date 条件在联合索引中可能体现为范围扫描。
    • index: 全索引扫描(比全表扫描快,因为只读索引)。
    • ALL: 全表扫描,最差情况,需要极力避免。
  • possible_keys: 可能用到的索引。
  • key: 实际用到的索引。如果为NULL,则未用索引。在我们的例子中,orders 表应该会用到 idx_orders_city_date 索引。
  • key_len: 使用的索引长度。可以判断是否充分利用了复合索引。例如 idx_orders_city_date (city, order_date),如果 key_len 只等于 city 的长度,说明 order_date 并未完全用于索引查找,而是用于过滤。
  • rows: 优化器预估需要扫描的行数。结合type看,如果rows很大但type很差(如ALL),就是性能警报。
  • Extra: 额外信息,包含大量重要细节。
    • Using where: 表示在存储引擎检索行后,服务器层再次进行了过滤。如果typeindexrange,这很正常;如果是ALL,则说明筛选条件没用到索引。
    • Using index: 表示查询使用了覆盖索引,无需回表,性能极佳。
    • Using temporary: 表示需要创建临时表来处理查询(如GROUP BY、ORDER BY),如果数据量大可能影响性能。
    • Using filesort: 表示无法利用索引完成排序,需要额外的排序步骤。我们的示例中 ORDER BY total_amount 很可能导致此情况。
    • Using join buffer: 表示连接查询使用了连接缓冲区,通常发生在没有理想索引用于关联时。

关联技术:覆盖索引 为了优化上述示例,避免 Using filesortUsing temporary,一个高级技巧是使用覆盖索引。覆盖索引是指一个索引包含了查询所需的所有字段,使得查询只需扫描索引而无需回表取数据。

-- 如果我们为上述查询创建这样一个覆盖索引
-- CREATE INDEX idx_covering ON orders(city, order_date, user_id, amount);

-- 那么对于 orders 表的过滤和聚合部分,执行计划中的 Extra 字段可能会出现 ‘Using index’, 效率大幅提升。
-- 但注意,GROUP BY 和 ORDER BY 涉及的是 user 表的字段和聚合结果,此索引主要优化了 orders 表的扫描部分。

三、实战优化案例:从计划到性能提升

光说不练假把式,我们来看一个真实的优化案例。

问题SQL:一个分页查询在数据量变大后越来越慢。

-- 原始慢SQL
SELECT * FROM article WHERE category_id = 10 AND status = 'published' ORDER BY publish_time DESC LIMIT 100000, 20;

获取并分析计划

EXPLAIN FORMAT=JSON
SELECT * FROM article WHERE category_id = 10 AND status = 'published' ORDER BY publish_time DESC LIMIT 100000, 20;

计划分析:假设执行计划显示 type=index(使用了 (category_id, status, publish_time) 这个索引),但 rows 预估非常大,并且 Extra 中没有 Using where。这意味着,虽然走了索引排序,但它需要沿着索引扫描“100000+20”条记录,然后抛弃前10万条。LIMIT M, N 中的大偏移量 M 是罪魁祸首。

优化方案:使用“延迟关联”或“书签法”优化大偏移量分页。

-- 优化后的SQL
SELECT a.*
FROM article a
INNER JOIN (
    SELECT id -- 只选择主键
    FROM article
    WHERE category_id = 10 AND status = 'published'
    ORDER BY publish_time DESC
    LIMIT 100000, 20
) AS tmp ON a.id = tmp.id -- 通过主键快速回表获取20行完整数据
ORDER BY a.publish_time DESC; -- 维持顺序

优化后计划分析:子查询 SELECT id ... 可以利用覆盖索引((category_id, status, publish_time, id)),只需要扫描索引的100020条记录,但不需要回表,速度较快。拿到20个目标id后,外层查询通过主键id快速回表获取完整数据行。整个过程避免了大量无用的回表操作,性能提升可能达到几个数量级。

四、PolarDB的特色与进阶分析

PolarDB作为云原生数据库,在执行计划方面除了提供标准MySQL的 EXPLAIN 功能外,还有一些增强特性:

  1. 执行计划管理:PolarDB可以绑定执行计划,防止因统计信息变化等原因导致优化器选择更差的计划,非常适合稳定核心业务SQL的性能。
  2. 更准确的代价估算:PolarDB的优化器针对云上硬件和存储特点进行了优化,其代价模型可能更准确,rows 等预估值的可靠性更高。
  3. 并行查询:对于复杂的分析型查询,PolarDB可以生成并行执行计划,利用多核能力加速处理。EXPLAIN 输出中可能会显示 Parallel 相关的信息。
  4. 智能诊断:在PolarDB的控制台或性能洞察功能中,它可能直接对慢SQL的执行计划进行可视化分析和优化建议,降低了人工解读的门槛。

应用场景

  • 日常慢SQL排查:当监控到慢查询时,第一时间通过 EXPLAIN 分析其执行计划。
  • SQL上线前评审:在开发阶段,对核心或复杂的SQL进行执行计划审查,预防性能问题。
  • 索引优化:根据执行计划中的 typekeyExtra 等信息,判断现有索引是否有效,并设计更优的索引。
  • 数据库升级或迁移验证:在版本升级或数据迁移后,对比关键SQL的执行计划是否发生变化,确保性能稳定。

技术优缺点

  • 优点
    • 洞察本质:直接揭示数据库内部工作方式,是性能调优最根本的手段。
    • 成本低廉EXPLAIN 命令几乎不消耗资源。
    • 通用性强:解读执行计划的技能可迁移到大多数关系型数据库。
  • 缺点
    • 学习曲线:需要记忆和理解大量专业术语和字段含义。
    • 基于估算EXPLAIN 显示的是优化器“认为”的计划和成本,并非实际运行时数据。极端情况下,估算不准可能导致优化器选错计划。
    • 静态分析:传统的 EXPLAIN 无法反映运行时资源竞争、锁等待等情况。需要结合 SHOW PROFILEperformance_schemaEXPLAIN ANALYZE(PolarDB PostgreSQL兼容版支持)进行动态分析。

注意事项

  1. 结合统计信息:执行计划依赖于表的统计信息(如行数、索引区分度)。务必确保 ANALYZE TABLE 定期执行,保持统计信息新鲜。
  2. 关注数据分布:优化器的选择严重依赖数据分布。测试环境与生产环境数据量、数据分布不同,可能导致执行计划截然不同。
  3. 不要过度优化:索引不是越多越好。每个索引都会增加写操作成本。优化应针对核心瓶颈,追求平衡。
  4. 善用格式化工具:对于复杂的 EXPLAIN FORMAT=JSON 输出,可以借助一些在线格式化工具或IDE插件,使其更易读。

文章总结: 执行计划就像数据库医生手中的X光片,它能清晰照出SQL语句的“骨骼”与“脉络”。掌握PolarDB执行计划的解读,意味着你获得了主动掌控SQL性能的钥匙。从识别全表扫描(ALL)的警报,到理解索引覆盖(Using index)的喜悦,再到通过优化连接方式和重写SQL来扭转乾坤,每一步都建立在扎实的解读能力之上。记住,调优不是玄学,而是基于执行计划这份“蓝图”的科学分析和实践。多查看、多思考、多实践,你就能逐渐培养出对SQL性能的敏锐直觉,让数据库的每一次查询都高效如飞。