SQL 查询优化

本页给出真实可运行的优化示例:EXPLAIN ANALYZE 输出解读(Seq Scan vs Index Scan 对比)、复合索引列顺序规则、N+1/全表扫描/缺失索引的具体 SQL 修复、IN vs EXISTS / 子查询 vs JOIN 性能对比,以及 PgBouncer 连接池关键参数配置;让 Agent 给出可验证的改写假设。

先归类:全表扫描、错误基数估计、隐式类型转换、OR 导致索引失效、排序与临时文件过大等;输出时按可能性排序并引用计划中的关键算子。

索引建议需包含选择性、最左前缀与写放大权衡;复合索引列顺序应贴合 WHERE + ORDER BY 的实际组合。

禁止凭空断言「加缓存即可」:若提出缓存,须说明键设计、失效策略与一致性要求,与 SQL 优化结论分开陈述。

  • 分页:深分页优先 keyset / seek 而非大 OFFSET。
  • JOIN:驱动表选择与 NL/Hash/Merge 适用场景简要说明。
  • 验证:给出 before/after 的计时或行数对比步骤(含代表性参数)。

执行计划工作流(ASCII)

从语句到可行动假设的典型阅读顺序;具体算子名随引擎而异,但层级逻辑一致。

-- EXPLAIN ANALYZE 真实输出示例(PostgreSQL)
-- 慢查询:全表扫描
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 12345 AND status = 'pending';

-- 输出示例(未建索引):
-- Seq Scan on orders  (cost=0.00..4821.00 rows=3 width=128)
--                     (actual time=0.042..23.847 rows=3 loops=1)
--   Filter: ((user_id = 12345) AND (status = 'pending'))
--   Rows Removed by Filter: 98997
-- Planning Time: 0.124 ms
-- Execution Time: 23.923 ms    ← 全表扫描 99000 行只返回 3 行
-- Buffers: shared hit=2257

-- 建复合索引后:
CREATE INDEX idx_orders_user_status ON orders (user_id, status);

-- 优化后输出:
-- Index Scan using idx_orders_user_status on orders
--              (cost=0.42..12.43 rows=3 width=128)
--              (actual time=0.024..0.031 rows=3 loops=1)
--   Index Cond: ((user_id = 12345) AND (status = 'pending'))
-- Planning Time: 0.201 ms
-- Execution Time: 0.058 ms    ← 从 23ms 降到 0.06ms,提升 400x
-- Buffers: shared hit=5

-- 关注点:
-- 1. Seq Scan + Rows Removed by Filter 大 → 缺索引
-- 2. actual rows 与 estimated rows 差异大 → 统计信息过期(ANALYZE)
-- 3. Sort / Hash / Temp → 内存不足溢出磁盘(work_mem 调整)
-- 4. Nested Loop 驱动表 rows 大 → 连接顺序可能选错
┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
│  慢查询日志  │────▶│ EXPLAIN      │────▶│ 找「最贵」子树   │
│  / 监控阈值  │     │ (ANALYZE)    │     │ 扫描行·回表·排序 │
└─────────────┘     └──────────────┘     └────────┬────────┘
                                                  │
                    ┌─────────────────────────────┼─────────────────────────────┐
                    ▼                             ▼                             ▼
            ┌───────────────┐            ┌───────────────┐            ┌───────────────┐
            │ 访问路径       │            │ 连接顺序/算法  │            │ 物化/临时      │
            │ Seq vs Index  │            │ NL / Hash /   │            │ Sort / Agg    │
            └───────┬───────┘            └───────┬───────┘            └───────┬───────┘
                    │                             │                             │
                    └─────────────────────────────┴─────────────────────────────┘
                                                  │
                                                  ▼
                                        ┌──────────────────┐
                                        │ 对照统计信息      │
                                        │ ndv / 直方图 / 相关│
                                        └────────┬─────────┘
                                                  │
                                                  ▼
                                        ┌──────────────────┐
                                        │ 提出索引/改写假设 │
                                        │ + 可重复验证步骤  │
                                        └──────────────────┘
            

索引选择启发

以下为经验法则,最终以实际计划与数据分布为准。

-- 复合索引列顺序规则:等值列优先,范围列靠后,排序列最后
-- 查询模式:WHERE user_id = ? AND created_at > ? ORDER BY created_at
-- 正确顺序(等值列在前):
CREATE INDEX idx_orders_uid_created
    ON orders (user_id, created_at DESC);
-- user_id 等值过滤后,created_at 的有序性可用于 ORDER BY(避免 Sort 节点)

-- 错误顺序(范围列在前):
-- CREATE INDEX idx_orders_created_uid ON orders (created_at, user_id);
-- 结果:created_at 范围扫描后 user_id 无法走索引有序性

-- 覆盖索引:避免回表
-- 若 SELECT 只需 user_id, status, amount,可 INCLUDE 额外列
CREATE INDEX idx_orders_cover
    ON orders (user_id, status)
    INCLUDE (amount, created_at);
-- 效果:Index Only Scan,无需回表读 heap

-- 部分索引:低基数列 + 常用过滤条件
CREATE INDEX idx_orders_pending
    ON orders (user_id)
    WHERE status = 'pending';
-- 适合:99% 行是 completed,只 1% 是 pending 的场景
案例 倾向 注意
等值过滤列多 高选择性列靠前;= 条件优先进入复合索引左侧 范围条件后的列常无法继续走索引有序性
排序 + 过滤 尝试 (过滤前缀, ORDER BY 列…) 与 SELECT * 回表成本权衡;覆盖索引可减少回表
多表 JOIN 小结果集驱动;连接键上有索引 统计过期会导致选错驱动表与算法
低选择性列 单独索引收益可能差 考虑组合索引、部分索引或改写查询
写多读少表 节制索引数量 每次写入需维护所有相关二级索引

N+1 查询

N+1:外层 1 次查询拿到 N 行后,对每一行再发 1 条子查询或关联加载,总往返 ≈ 1+N。ORM 默认懒加载时最常见。Agent 分析时应区分「单次慢 SQL」与「多次轻 SQL 叠加」;缓解方向包括 JOIN / IN 批量预取、显式 eager load、数据加载器批处理或改写为一条聚合子查询。
-- ❌ N+1 反模式(伪代码 + SQL)
-- 第 1 次查询:SELECT id FROM users WHERE active = true  → 1000 行
-- 然后对每个 user 发:SELECT * FROM orders WHERE user_id = ?  → 1000 次
-- 总计:1001 次查询

-- ✅ 修复:JOIN 一次获取
SELECT u.id, u.name, o.id AS order_id, o.amount, o.status
FROM   users u
LEFT   JOIN orders o ON o.user_id = u.id
WHERE  u.active = true;

-- ✅ 修复:IN 批量预取(数据加载器模式)
SELECT * FROM orders
WHERE  user_id = ANY(ARRAY[1,2,3,...,1000]);  -- 一次性批量

-- ✅ IN vs EXISTS 性能对比
-- IN(子查询结果集小时高效):
SELECT * FROM orders
WHERE  user_id IN (SELECT id FROM users WHERE plan = 'premium');

-- EXISTS(外层表大、子查询短路时高效):
SELECT * FROM orders o
WHERE  EXISTS (
    SELECT 1 FROM users u
    WHERE  u.id = o.user_id AND u.plan = 'premium'
);
-- 规则:子查询结果集小 → IN;外层表大且子查询有索引 → EXISTS

-- 子查询 vs JOIN 可读性与性能
-- ❌ 相关子查询(每行执行一次):
SELECT u.id, (SELECT SUM(amount) FROM orders WHERE user_id = u.id) AS total
FROM   users u;

-- ✅ 改写为 JOIN + GROUP BY(一次扫描):
SELECT u.id, COALESCE(SUM(o.amount), 0) AS total
FROM   users u
LEFT   JOIN orders o ON o.user_id = u.id
GROUP  BY u.id;
-- PgBouncer 连接池关键参数配置(pgbouncer.ini)
[databases]
payments = host=127.0.0.1 port=5432 dbname=payments

[pgbouncer]
pool_mode = transaction          ; transaction/session/statement
max_client_conn = 1000           ; 客户端最大连接数
default_pool_size = 20           ; 每个 (db, user) 对的连接池大小
min_pool_size = 5                ; 最小保持连接数(避免冷启动)
reserve_pool_size = 5            ; 应急保留连接
reserve_pool_timeout = 5         ; 等超时后启用保留连接(秒)
max_db_connections = 100         ; 对同一 DB 的总后端连接上限
server_idle_timeout = 600        ; 空闲后端连接关闭超时(秒)
client_idle_timeout = 0          ; 0=禁用客户端超时
auth_type = md5
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid

; 推导:若 QPS=500, 单查询 p95=50ms,需要并发 = 500 * 0.05 = 25
; default_pool_size 设为 25~30,留 20% 余量

SQL 关键字与粗成本序

在文本框中粘贴语句:下方同步高亮常见 SQL 关键字;并给出子句的粗略成本相对排序(仅作阅读提示,非真实代价模型)。

关键字高亮预览

---
name: sql-query-tuning
description: 分析慢 SQL,输出 EXPLAIN 解读、索引 DDL 与改写草案
---
# 诊断步骤
1. EXPLAIN (ANALYZE, BUFFERS) 获取真实计划
2. 识别:Seq Scan + Rows Removed 大 → 缺索引
3. 识别:actual rows 与 estimate 差异大 → ANALYZE 统计信息过期
4. 识别:Sort / Hash / Temp → work_mem 不足(调整会话级参数测试)
# 索引策略
等值列靠前,范围列靠后,ORDER BY 列在末尾(同向)
覆盖索引 INCLUDE 减少回表;部分索引 WHERE 过滤低选择性
建大表索引用 CONCURRENTLY;不在事务块内执行
# 常见慢查询修复
N+1: LEFT JOIN ... GROUP BY 或 IN(SELECT id ...) 批量预取
全表扫描: 建索引或改写 WHERE 避免函数包裹索引列
相关子查询: 改写为 JOIN + GROUP BY(一次扫描)
IN vs EXISTS: 子查询结果集小→IN, 外层大且有索引→EXISTS
# 连接池
PgBouncer pool_mode=transaction; default_pool_size=QPS*p95_sec*1.2
max_client_conn 与应用实例数 * 线程数对齐
# 验证
before/after Execution Time 对比;生产需代表性参数 + ANALYZE 后测试

返回技能库 更多技能入口