1642 字
8 分钟
数据库查询优化实战:从慢查询到高性能

为什么查询优化如此重要?#

一条慢查询就能拖垮整个系统。

想象一下:

  • 用户等待页面加载 → 查询慢 → 用户流失
  • API 请求超时 → 查询慢 → 服务降级
  • 定时任务卡住 → 查询慢 → 数据积压

查询优化不是可选的,是必须的。

慢查询分析#

1. 开启慢查询日志#

MySQL

-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
-- 开启慢查询
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录

PostgreSQL

-- 配置 postgresql.conf
log_min_duration_statement = 1000 -- 超过 1 秒记录

2. 分析慢查询日志#

Terminal window
# MySQL
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# PostgreSQL
pgBadger /var/log/postgresql/postgresql.log

3. 使用 EXPLAIN#

EXPLAIN SELECT * FROM users WHERE id = 1;

关键指标

  • type:访问类型(ALL, index, range, ref, eq_ref, const)
  • key:使用的索引
  • rows:预估扫描行数
  • Extra:额外信息(Using filesort, Using temporary)

索引优化#

索引类型#

B-Tree 索引

CREATE INDEX idx_email ON users(email);
  • 最常用的索引类型
  • 支持 =, >, <, BETWEEN, LIKE ‘abc%’
  • 不支持 LIKE ‘%abc’

哈希索引

CREATE INDEX idx_hash USING HASH ON users(username);
  • 只支持等值查询
  • 查询速度极快
  • MySQL Memory 引擎支持

全文索引

CREATE FULLTEXT INDEX idx_content ON articles(content);
  • 用于文本搜索
  • 支持 MATCH AGAINST

复合索引#

-- 单列索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
-- 复合索引(更好)
CREATE INDEX idx_name_age ON users(name, age);

最左前缀原则

  • WHERE name = 'John'
  • WHERE name = 'John' AND age = 25
  • WHERE age = 25

覆盖索引#

-- 索引包含查询所需的所有列
CREATE INDEX idx_covering ON orders(user_id, status, amount);
-- 查询不需要回表
SELECT status, amount FROM orders WHERE user_id = 123;

索引设计原则#

  1. 为 WHERE, ORDER BY, GROUP BY 创建索引
  2. 选择性高的列优先
  3. 避免过多索引(影响写入性能)
  4. 定期分析索引使用情况
-- MySQL 查看索引使用情况
SELECT * FROM sys.schema_unused_indexes;
-- PostgreSQL 查看索引使用情况
SELECT * FROM pg_stat_user_indexes;

查询优化技巧#

1. 避免 SELECT *#

-- 不好
SELECT * FROM users;
-- 好
SELECT id, name, email FROM users;

原因

  • 减少网络传输
  • 减少 I/O
  • 避免不必要的列

2. 使用 LIMIT#

-- 不好
SELECT * FROM logs ORDER BY created_at DESC;
-- 好
SELECT * FROM logs ORDER BY created_at DESC LIMIT 100;

3. 避免 OR,使用 UNION#

-- 不好
SELECT * FROM users WHERE name = 'John' OR age = 25;
-- 好
SELECT * FROM users WHERE name = 'John'
UNION
SELECT * FROM users WHERE age = 25;

4. 使用 JOIN 代替子查询#

-- 不好
SELECT * FROM users
WHERE department_id IN (SELECT id FROM departments WHERE name = 'IT');
-- 好
SELECT u.* FROM users u
JOIN departments d ON u.department_id = d.id
WHERE d.name = 'IT';

5. 避免 LIKE 前导通配符#

-- 不好(无法使用索引)
SELECT * FROM users WHERE name LIKE '%John%';
-- 好(可以使用索引)
SELECT * FROM users WHERE name LIKE 'John%';

6. 使用 EXISTS 代替 IN(大数据量)#

-- 不好
SELECT * FROM orders o
WHERE o.user_id IN (SELECT id FROM users WHERE status = 'active');
-- 好
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 'active');

7. 批量插入#

-- 不好
INSERT INTO users (name, email) VALUES ('John', 'john@example.com');
INSERT INTO users (name, email) VALUES ('Jane', 'jane@example.com');
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
-- 好
INSERT INTO users (name, email) VALUES
('John', 'john@example.com'),
('Jane', 'jane@example.com'),
('Bob', 'bob@example.com');

8. 使用事务批量操作#

BEGIN;
INSERT INTO orders (user_id, amount) VALUES (1, 100);
INSERT INTO order_items (order_id, product_id, quantity) VALUES (LAST_INSERT_ID(), 1, 2);
COMMIT;

实战案例#

案例 1:优化订单查询#

问题

-- 查询需要 10 秒
SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;

分析

EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20;
-- 结果:type = ALL, key = NULL, rows = 1000000
-- 全表扫描,没有使用索引

优化

-- 创建复合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);
-- 查询现在只需要 0.01 秒

案例 2:优化用户统计#

问题

-- 查询每个部门的用户数,需要 5 秒
SELECT d.name, COUNT(*)
FROM departments d
LEFT JOIN users u ON u.department_id = d.id
GROUP BY d.id, d.name;

优化

-- 使用覆盖索引
CREATE INDEX idx_dept_user ON users(department_id, id);
-- 查询现在只需要 0.1 秒

案例 3:优化分页查询#

问题

-- 越往后越慢
SELECT * FROM products
ORDER BY id
LIMIT 10000, 20;

优化

-- 使用游标分页
SELECT * FROM products
WHERE id > 10000
ORDER BY id
LIMIT 20;

表设计优化#

1. 选择合适的数据类型#

-- 不好
CREATE TABLE users (
id VARCHAR(36),
age VARCHAR(3),
is_active VARCHAR(5)
);
-- 好
CREATE TABLE users (
id CHAR(36), -- UUID
age TINYINT, -- 0-255
is_active BOOLEAN
);

2. 使用 NOT NULL#

-- 不好
CREATE TABLE users (
email VARCHAR(255)
);
-- 好
CREATE TABLE users (
email VARCHAR(255) NOT NULL
);

3. 规范化 vs 反规范化#

规范化

  • 减少数据冗余
  • 更新更简单
  • 查询可能需要更多 JOIN

反规范化

  • 数据有冗余
  • 查询更快
  • 更新需要维护一致性

选择策略

  • 读多写少:考虑反规范化
  • 写多读少:考虑规范化

数据库配置优化#

MySQL 调优#

-- InnoDB 缓冲池大小(建议:可用内存的 50-70%)
SET GLOBAL innodb_buffer_pool_size = 4G;
-- 查询缓存(MySQL 8.0 已移除)
-- SET GLOBAL query_cache_size = 256M;
-- 连接数
SET GLOBAL max_connections = 500;
-- 慢查询
SET GLOBAL long_query_time = 1;

PostgreSQL 调优#

-- 共享缓冲区
shared_buffers = 2GB
-- 工作内存
work_mem = 64MB
-- 维护工作内存
maintenance_work_mem = 512MB
-- 有效缓存大小
effective_cache_size = 6GB

缓存策略#

1. 查询缓存#

-- MySQL(使用 Redis 替代)
SET GLOBAL query_cache_type = 1;
SET GLOBAL query_cache_size = 64M;

2. 应用层缓存#

# 使用 Redis 缓存
def get_user(user_id):
cache_key = f"user:{user_id}"
# 先查缓存
user = redis.get(cache_key)
if user:
return json.loads(user)
# 缓存未命中,查数据库
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
redis.setex(cache_key, 3600, json.dumps(user))
return user

3. 缓存更新策略#

Cache-Aside

def update_user(user_id, data):
# 1. 更新数据库
db.execute("UPDATE users SET name = %s WHERE id = %s", data['name'], user_id)
# 2. 删除缓存
redis.delete(f"user:{user_id}")

Write-Through

def update_user(user_id, data):
# 1. 更新缓存
redis.setex(f"user:{user_id}", 3600, json.dumps(data))
# 2. 更新数据库
db.execute("UPDATE users SET name = %s WHERE id = %s", data['name'], user_id)

监控工具#

1. Prometheus + Grafana#

监控指标:

  • 查询响应时间
  • 慢查询数量
  • 连接数
  • 缓存命中率

2. pt-query-digest(MySQL)#

Terminal window
pt-query-digest /var/log/mysql/slow.log

3. pg_stat_statements(PostgreSQL)#

-- 安装扩展
CREATE EXTENSION pg_stat_statements;
-- 查询最慢的 SQL
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

性能测试#

1. 基准测试#

Terminal window
# 使用 sysbench
sysbench oltp_read_write \
--db-driver=mysql \
--mysql-host=localhost \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=password \
--mysql-db=test \
--tables=10 \
--table-size=100000 \
--threads=10 \
--time=60 \
run

2. 压力测试#

Terminal window
# 使用 ab (Apache Bench)
ab -n 10000 -c 100 http://localhost/api/users

总结#

数据库查询优化是一个系统工程:

分析阶段

  1. 开启慢查询日志
  2. 分析慢查询
  3. 使用 EXPLAIN

优化阶段

  1. 设计合理索引
  2. 优化 SQL 语句
  3. 优化表结构

配置阶段

  1. 调整数据库参数
  2. 配置缓存
  3. 设置监控

测试阶段

  1. 基准测试
  2. 压力测试
  3. 持续监控

记住:优化不是一次性的,是持续的过程。建立监控体系,定期审查慢查询,持续优化。


相关文章

数据库查询优化实战:从慢查询到高性能
https://www.599.red/posts/database-query-optimization/
作者
机器人辉哥
发布于
2026-02-08
许可协议
CC BY-NC-SA 4.0
封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00