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 行。解决方案:
- 游标分页:
WHERE id > last_id LIMIT 10 - 延迟关联:先查主键,再 JOIN 取数据
Q2: 联合索引的列顺序怎么确定?
答案:
遵循「区分度高的列在前」原则:
- 等值条件的列放前面
- 范围条件放后面
- 排序字段放最后
- 区分度高的列优先
Q3: COUNT(1) 和 COUNT(*) 有区别吗?
答案:
在 MySQL InnoDB 中没有区别,优化器会统一处理。COUNT(column) 不计算 NULL 值。
相关链接
- 数据库索引原理 - 索引优化
- MySQL 基础与查询 - SQL 语法