GaussDB 基础 SQL 优化:从执行计划读懂慢查询

举报
yd_288136036 发表于 2026/11/03 09:15:54 2026/11/03
【摘要】 以订单查询为例,讲清 GaussDB 慢查询的排查顺序:先看执行计划,再改函数包裹、深分页等慢写法,最后建索引并复查效果,附可运行 SQL 示例。

GaussDB 基础 SQL 优化:从执行计划读懂慢查询

很多慢查询不是数据库变慢了,而是写法让优化器走了一条很贵的路。GaussDB 优化的第一步永远不是凭感觉加索引,而是先看执行计划,再改写法,最后才考虑结构调整。本文用一个订单查询的例子,把这条排查链路讲清楚。

一、先看执行计划,不要先猜

在 GaussDB 中,EXPLAIN 会告诉你优化器打算怎么执行,而 EXPLAIN ANALYZE 会告诉你实际花了多少时间、返回了多少行。两者差别很大:前者是计划,后者是实测。排查时先看三个关键信息:扫描方式是不是全表扫描,连接方式是不是嵌套循环,返回行数和预估行数是否差距过大。

EXPLAIN ANALYZE
SELECT o.order_id, o.order_time, c.customer_name, o.amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_time >= DATE '2026-10-01'
  AND o.order_time < DATE '2026-11-01'
  AND o.status = 'PAID'
ORDER BY o.order_time DESC
LIMIT 20;

如果计划里出现 Seq Scan on orders,同时表里有几十万甚至上百万行,那就说明时间主要花在逐行扫描上。这时不要急着改业务逻辑,先检查 order_time 和 status 上有没有可用的索引,以及条件写法有没有破坏索引。

二、最常见的五种慢写法

第一种是函数包裹索引列。例如 WHERE to_char(order_time, 'YYYY-MM') = '2026-10' 会让索引失效,因为优化器无法直接用索引定位范围。改成范围条件,效果立刻不同。第二种是隐式类型转换,例如把数字列和字符串比较,优化器可能放弃索引。第三种是 LIKE '%关键词%',前导百分号无法走 B 树索引,适合改用全文检索或业务侧收敛。第四种是 OR 连接不同列的条件,常常导致多路扫描,必要时拆成 UNION ALL。第五种是深分页,OFFSET 100000 LIMIT 20 仍要先扫描并丢弃前面十万行。

-- 不推荐:函数包裹列,索引难以生效
SELECT order_id FROM orders
WHERE to_char(order_time, 'YYYY-MM') = '2026-10';

-- 推荐:改写为范围条件
SELECT order_id FROM orders
WHERE order_time >= DATE '2026-10-01'
  AND order_time < DATE '2026-11-01';

-- 不推荐:深分页
SELECT order_id FROM orders ORDER BY order_id LIMIT 20 OFFSET 100000;

-- 推荐:键集分页,记住上一页最后一条的主键
SELECT order_id FROM orders
WHERE order_id > 100000
ORDER BY order_id
LIMIT 20;

三、索引要建在查询路径上

索引不是越多越好。每一个索引都会让写入变慢、占用空间,还可能干扰优化器选择。建索引的原则是:先服务高频查询的过滤条件,再服务排序和连接列;复合索引注意最左前缀,查询条件要从左到右连续使用;区分度低的列单独建索引意义有限,例如只有两种取值的状态列,单独建索引往往不如与时间列组成复合索引。

-- 示例表与数据(GaussDB / openGauss 语法可直接运行)
CREATE TABLE customers (
  customer_id BIGINT PRIMARY KEY,
  customer_name VARCHAR(100) NOT NULL,
  region VARCHAR(50)
);

CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers(customer_id),
  order_time TIMESTAMP NOT NULL,
  status VARCHAR(20) NOT NULL,
  amount NUMERIC(12, 2) NOT NULL DEFAULT 0
);

INSERT INTO customers VALUES (1, '张三', '华北'), (2, '李四', '华东');
INSERT INTO orders VALUES
  (100001, 1, TIMESTAMP '2026-10-05 10:20:00', 'PAID', 299.00),
  (100002, 1, TIMESTAMP '2026-10-06 11:00:00', 'PAID', 159.50),
  (100003, 2, TIMESTAMP '2026-10-06 12:30:00', 'UNPAID', 89.00);

-- 服务“按时间查已支付订单”的复合索引
CREATE INDEX idx_orders_time_status ON orders(order_time, status);
CREATE INDEX idx_orders_customer ON orders(customer_id);

-- 优化后的查询:只取需要的列,条件可走索引
SELECT o.order_id, o.order_time, o.amount
FROM orders o
WHERE o.order_time >= DATE '2026-10-01'
  AND o.order_time < DATE '2026-11-01'
  AND o.status = 'PAID'
ORDER BY o.order_time DESC
LIMIT 20;

建好索引后,一定要再跑一次 EXPLAIN ANALYZE,确认扫描方式从全表扫描变成了索引扫描,实际耗时也下降了。只建索引不复查,等于没有优化。

四、只取需要的列和行

SELECT * 在连接查询里尤其贵:它会把不需要的大字段也读出来,增加 I/O 和网络传输,还可能阻止覆盖索引的使用。只列出业务真正需要的列,往往就能让查询走覆盖索引。其次是尽早过滤,先在 WHERE 里把数据量降下来,再做连接和排序;能用 EXISTS 做半连接判断时,不要用 IN 子查询返回大量结果。

-- 判断某客户是否有已支付订单:EXISTS 在找到第一条后即可停止
SELECT customer_id, customer_name
FROM customers c
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.customer_id = c.customer_id
    AND o.status = 'PAID'
);

-- 按月统计已支付金额:先过滤,再聚合
SELECT date_trunc('month', order_time) AS month,
       COUNT(*) AS order_count,
       SUM(amount) AS total_amount
FROM orders
WHERE status = 'PAID'
GROUP BY date_trunc('month', order_time)
ORDER BY month;

五、统计信息过期也会拖慢查询

优化器做选择依赖统计信息。如果表经过大批量导入、删除或更新,统计信息过期,优化器可能误判行数,从而选错连接顺序和索引。遇到“昨天还快、今天突然变慢”的查询,先检查最近是否有大批量数据变化,再考虑更新统计信息,并重新查看执行计划。不要一上来就改 SQL,更不要同时改多个地方,否则无法确认哪一步真正起了作用。

六、动手检查清单

第一,慢查询先跑 EXPLAIN ANALYZE,记录扫描方式和实际耗时。第二,检查条件列是否被函数包裹、是否存在隐式转换。第三,检查过滤列和连接列是否有索引,复合索引是否符合最左前缀。第四,改写深分页、收敛 OR 条件、去掉 SELECT *。第五,改完再跑一次执行计划,对比前后耗时和扫描方式。第六,定期复查高频查询,而不是只在出问题时才优化。

总结一句话:SQL 优化的本质,是让优化器用最小的代价找到最少的数据。先看计划,再改写法,最后调结构,这个顺序不要颠倒。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。