DEV Community

WDSEGA
WDSEGA

Posted on

SQL Performance Optimization: 10 Tips to Speed Up Queries 100x

"这条SQL怎么这么慢?"这大概是后端开发中最常见的问题之一。很多开发者遇到慢查询就本能地加索引,但索引不是万能药。真正的SQL性能优化需要从查询设计、索引策略、执行计划分析等多个维度入手。这篇文章总结了10个经过实战验证的优化技巧,每个都有具体的代码示例和效果对比。

技巧1:用EXPLAIN分析执行计划

优化SQL的第一步不是改代码,而是理解查询是怎么执行的。

-- 基础EXPLAIN
EXPLAIN SELECT * FROM orders WHERE user_id = 123;

-- 详细EXPLAIN(推荐)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;

-- PostgreSQL格式化输出
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) 
SELECT u.name, COUNT(o.id) 
FROM users u 
JOIN orders o ON u.id = o.user_id 
WHERE u.created_at > '2025-01-01'
GROUP BY u.name;
Enter fullscreen mode Exit fullscreen mode

关注重点

  • Seq Scan(全表扫描)—— 通常需要优化
  • Index Scan(索引扫描)—— 理想情况
  • rows(预估行数)—— 与实际行数差距大说明统计信息过时
  • actual time(实际执行时间)—— 找出最耗时的部分
-- 如果统计信息过时,手动更新
ANALYZE orders;
ANALYZE users;
Enter fullscreen mode Exit fullscreen mode

技巧2:避免SELECT *,只查需要的列

-- 慢:查询所有列(包括大文本字段)
SELECT * FROM articles WHERE author_id = 456;

-- 快:只查询需要的列
SELECT id, title, created_at FROM articles WHERE author_id = 456;
Enter fullscreen mode Exit fullscreen mode

为什么SELECT *慢?三个原因:

  1. 网络传输:数据库到应用之间传输了大量不需要的数据
  2. 内存占用:数据库需要在内存中构造完整行
  3. 覆盖索引失效:无法利用覆盖索引(后面会讲)

技巧3:合理创建复合索引

单列索引不够用的时候,复合索引是关键。但顺序很重要。

-- 假设这个查询很频繁
SELECT * FROM orders 
WHERE user_id = 123 AND status = 'paid' 
ORDER BY created_at DESC 
LIMIT 20;

-- 创建复合索引(顺序很关键)
-- 最左前缀原则:等值条件在前,范围条件在后
CREATE INDEX idx_orders_user_status_created 
ON orders(user_id, status, created_at DESC);
Enter fullscreen mode Exit fullscreen mode

复合索引的顺序规则

  1. 等值条件的列放前面(user_id = 123, status = 'paid'
  2. 范围条件的列放后面(created_at DESC
  3. 排序的列放最后
-- 这个查询能用到上面的索引
WHERE user_id = 123 AND status = 'paid' ORDER BY created_at DESC

-- 这个查询只能用到部分索引(user_id部分)
WHERE user_id = 123 ORDER BY created_at DESC

-- 这个查询完全用不到索引
WHERE status = 'paid' ORDER BY created_at DESC
Enter fullscreen mode Exit fullscreen mode

技巧4:利用覆盖索引避免回表

覆盖索引是指索引包含了查询所需的所有列,不需要回表查询数据行。

-- 假设 users 表有 (id, name, email, bio, avatar, created_at)

-- 查询1:需要回表(bio和avatar不在索引中)
SELECT name, email, bio FROM users WHERE id = 123;

-- 查询2:可以覆盖索引(只查name和email)
SELECT name, email FROM users WHERE id = 123;

-- 创建覆盖索引
CREATE INDEX idx_users_name_email ON users(id, name, email);
Enter fullscreen mode Exit fullscreen mode

效果:覆盖索引可以把查询速度提升10-100倍,特别是在数据量大的时候。

技巧5:用JOIN替代子查询

很多数据库对JOIN的优化比子查询好得多。

-- 慢:相关子查询(每行执行一次子查询)
SELECT u.name, 
    (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count
FROM users u;

-- 快:用JOIN替代
SELECT u.name, COALESCE(o.order_count, 0) as order_count
FROM users u
LEFT JOIN (
    SELECT user_id, COUNT(*) as order_count 
    FROM orders 
    GROUP BY user_id
) o ON u.id = o.user_id;
Enter fullscreen mode Exit fullscreen mode
-- 慢:WHERE中的子查询
SELECT * FROM orders 
WHERE user_id IN (SELECT id FROM users WHERE region = 'Asia');

-- 快:用JOIN替代
SELECT DISTINCT o.* 
FROM orders o
JOIN users u ON o.user_id = u.id 
WHERE u.region = 'Asia';
Enter fullscreen mode Exit fullscreen mode

技巧6:分页优化——避免大偏移量

传统的OFFSET分页在大偏移量时性能急剧下降。

-- 慢:OFFSET越大越慢(数据库需要扫描并跳过前面的行)
SELECT * FROM articles 
ORDER BY created_at DESC 
LIMIT 20 OFFSET 100000;

-- 快:使用游标分页(基于上一页最后一条记录)
SELECT * FROM articles 
WHERE created_at < '2025-05-01 12:00:00'  -- 上一页最后一条的时间
ORDER BY created_at DESC 
LIMIT 20;

-- 或者使用WHERE id > last_id
SELECT * FROM articles 
WHERE id > 500000  -- 上一页最后一条的ID
ORDER BY id ASC 
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

性能对比(100万行数据):

  • OFFSET 0:约5ms
  • OFFSET 10000:约50ms
  • OFFSET 100000:约500ms
  • OFFSET 500000:约2500ms
  • 游标分页:始终约5ms

技巧7:批量操作替代循环查询

N+1查询问题是性能杀手。

# 慢:N+1查询(在循环中执行SQL)
users = db.query("SELECT * FROM users LIMIT 100")
for user in users:
    orders = db.query(f"SELECT * FROM orders WHERE user_id = {user['id']}")
    user['orders'] = orders
# 执行了101次查询!

# 快:批量查询
users = db.query("SELECT * FROM users LIMIT 100")
user_ids = [u['id'] for u in users]
orders = db.query(
    "SELECT * FROM orders WHERE user_id IN (%s)" % 
    ','.join(map(str, user_ids))
)
# 构建user_id到orders的映射
from collections import defaultdict
orders_map = defaultdict(list)
for order in orders:
    orders_map[order['user_id']].append(order)

for user in users:
    user['orders'] = orders_map.get(user['id'], [])
# 只执行了2次查询!
Enter fullscreen mode Exit fullscreen mode

技巧8:避免在索引列上使用函数

在索引列上使用函数会导致索引失效,触发全表扫描。


sql
-- 慢:函数导致索引失效
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
SELECT * FROM orders WHERE DATE(created_at) = '2025-06-01';
SELECT * FROM products WHERE name LIKE '%phone%';


---
*本文首发于[我的技术博客](https://wdsega.github.io),欢迎访问获取更多技术文章。*
*如果你是内容创作者或自由职业者,推荐看看我整理的[Creator Pro Bundle](https://segauser.gumroad.com/l/rrhmbb)工具包,包含AI提示词系统、内容创作工具、副业指南和自动化脚本,源码全开放。*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)