跳到主要内容

SQL 查询优化

问题

如何分析和优化慢 SQL?EXPLAIN 怎么看?

答案

慢查询发现

slow-query.sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录

-- 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query_log_file';

EXPLAIN 分析

explain.sql
EXPLAIN SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.age > 20
GROUP BY u.id
ORDER BY order_count DESC
LIMIT 10;
说明关注点
type访问类型ALL(全表扫描) → index → range → ref → const
key使用的索引NULL 表示未使用索引
rows预估扫描行数越小越好
Extra额外信息Using filesort、Using temporary 需关注
type 性能排序

system > const > eq_ref > ref > range > index > ALL

出现 ALL(全表扫描)通常需要优化。

常见优化技巧

optimization.sql
-- 1. 避免 SELECT *
-- ❌
SELECT * FROM users WHERE id = 1;
-- ✅ 只查需要的列
SELECT name, email FROM users WHERE id = 1;

-- 2. 分页优化(深翻页问题)
-- ❌ OFFSET 越大越慢
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 100000;
-- ✅ 游标分页
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;

-- 3. 避免在索引列上使用函数
-- ❌ 索引失效
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- ✅ 范围查询,可以使用索引
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

-- 4. 用 EXISTS 替代 IN(大子查询时)
-- ❌ IN 子查询可能全表扫描
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
-- ✅ EXISTS 可以提前终止
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

-- 5. 批量操作
-- ❌ 逐条插入
INSERT INTO logs (msg) VALUES ('a');
INSERT INTO logs (msg) VALUES ('b');
-- ✅ 批量插入
INSERT INTO logs (msg) VALUES ('a'), ('b'), ('c');

-- 6. COUNT 优化
-- ❌ COUNT(*)(InnoDB 需全表扫描)
SELECT COUNT(*) FROM users;
-- ✅ 维护计数器(Redis 或 summary 表)

Node.js 中的查询优化

query-optimization.ts
// Prisma: 使用 select 替代 include 减少数据量
const users = await prisma.user.findMany({
select: { id: true, name: true }, // 只取需要的字段
where: { status: 'active' },
});

// 批量操作
await prisma.user.createMany({
data: users, // 一次插入多条
skipDuplicates: true,
});

// 原生 SQL(复杂查询)
const result = await prisma.$queryRaw`
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > ${startDate}
GROUP BY u.id
HAVING order_count > 5
`;

常见面试问题

Q1: 深翻页(大 OFFSET)为什么慢?

答案

LIMIT 10 OFFSET 100000 实际上会扫描前 100010 行,然后丢弃前 100000 行。解决方案:

  1. 游标分页WHERE id > last_id LIMIT 10
  2. 延迟关联:先查主键,再 JOIN 取数据

Q2: 联合索引的列顺序怎么确定?

答案

遵循「区分度高的列在前」原则:

  1. 等值条件的列放前面
  2. 范围条件放后面
  3. 排序字段放最后
  4. 区分度高的列优先

Q3: COUNT(1) 和 COUNT(*) 有区别吗?

答案

在 MySQL InnoDB 中没有区别,优化器会统一处理。COUNT(column) 不计算 NULL 值。

相关链接