name: 数据库优化 description: 用于PostgreSQL和MySQL的查询优化、索引策略和数据库性能调优
数据库优化
EXPLAIN分析
在优化之前始终运行EXPLAIN ANALYZE。从下往上阅读输出。
-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...;
-- MySQL
EXPLAIN ANALYZE SELECT ...;
需要关注的关键指标:
- Seq Scan 在大表上 = 缺少索引
- Nested Loop 有高行数 = 考虑哈希/合并连接
- Sort 没有索引 = 在排序列上添加索引
- Rows estimated vs actual 差异 = 过时统计信息,运行
ANALYZE
索引策略
B树(默认,大多数情况)
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_orders_user_date ON orders (user_id, created_at DESC);
用于:等值查询、范围查询、排序。在复合索引中,列顺序重要:首先放置等值列,然后是范围/排序列。
部分索引(PostgreSQL)
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';
当查询总是基于特定条件过滤时使用。比完整索引小得多。
GIN(PostgreSQL - 数组、JSONB、全文搜索)
CREATE INDEX idx_products_tags ON products USING GIN (tags);
CREATE INDEX idx_docs_search ON documents USING GIN (to_tsvector('english', content));
GiST(PostgreSQL - 空间、范围类型)
CREATE INDEX idx_locations_point ON locations USING GiST (coordinates);
CREATE INDEX idx_events_period ON events USING GiST (tsrange(start_at, end_at));
覆盖索引(仅索引扫描)
-- PostgreSQL
CREATE INDEX idx_users_email_name ON users (email) INCLUDE (name);
-- MySQL
CREATE INDEX idx_users_email_name ON users (email, name);
N+1 查询检测
症状:1个查询获取父级 + 每个子级N个查询。
# 不好:N+1
users = db.query(User).all()
for user in users:
print(user.orders) # 每个用户触发查询
# 好:急切加载
users = db.query(User).options(joinedload(User.orders)).all()
// 不好:N+1
const users = await User.findAll();
for (const user of users) {
const orders = await Order.findAll({ where: { userId: user.id } });
}
// 好:批量加载
const users = await User.findAll({ include: [Order] });
检测:启用查询日志,计算每个请求的查询数。单个端点超过10个查询是警告信号。
连接池
经验法则:pool_size = (核心数 * 2) + 磁盘数
典型Web应用:每个应用实例10-20个连接
PostgreSQL:
- 在高连接场景中使用PgBouncer事务模式
- 设置
idle_in_transaction_session_timeout = '30s' - 使用
pg_stat_activity监控
MySQL:
- 根据可用RAM设置
max_connections(每个连接使用约10MB) - 使用ProxySQL进行连接复用
- 使用
SHOW PROCESSLIST监控
读取副本
- 路由所有
SELECT查询到副本 - 路由所有写入到主库
- 考虑复制延迟(通常10-100ms)
- 不要在副本上进行读后写;对于一致性关键的读取使用主库
- 使用连接级别路由,而非查询级别
# SQLAlchemy 读取副本路由
class RoutingSession(Session):
def get_bind(self, mapper=None, clause=None):
if self._flushing or self.is_modified():
return engines["primary"]
return engines["replica"]
分区策略
范围分区(时间序列数据)
-- PostgreSQL
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
created_at timestamptz NOT NULL,
data jsonb
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2025_q1 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
CREATE TABLE events_2025_q2 PARTITION OF events
FOR VALUES FROM ('2025-04-01') TO ('2025-07-01');
哈希分区(均匀分布)
CREATE TABLE sessions (
id uuid PRIMARY KEY,
user_id bigint NOT NULL
) PARTITION BY HASH (user_id);
CREATE TABLE sessions_0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
分区当表超过50-100GB或需要快速删除旧数据时。
查询优化检查清单
- 运行
EXPLAIN ANALYZE并阅读计划 - 检查在大表(>10K行)上的顺序扫描
- 验证索引使用(检查
pg_stat_user_indexes中的idx_scan) - 查找阻止索引使用的隐式类型转换
- 用特定列替换
SELECT * - 添加
LIMIT到仅需要子集的查询 - 使用
EXISTS代替COUNT(*) > 0 - 批量
INSERT/UPDATE操作(每批500-1000行) - 避免在
WHERE子句中对索引列使用函数 - 监控慢查询日志(pg:
log_min_duration_statement = 100)
危险模式
LIKE '%term%'在未索引列上(使用全文搜索替代)ORDER BY RANDOM()(使用TABLESAMPLE或应用级别随机化)SELECT DISTINCT掩盖连接问题UPDATE/DELETE上缺少WHERE(首先用SELECT验证)- 长时间运行的事务持有锁
- 使用
OFFSET进行深度分页(使用键集/游标分页替代)