如何发现影响应用性能的数据库查询
你的仪表板在本地加载只需 200 毫秒,但在生产环境中却需要 8 秒。用户开始抱怨,产品经理问你出了什么问题。你怀疑是数据库的问题,但无法证明。
本文将向你展示如何从用户可见的症状追溯到慢数据库查询,识别实际的数据库性能瓶颈,并在开始随意添加索引之前,区分不同的根本原因。
核心要点
- 从用户可见的症状开始诊断,而不是数据库内部机制——在调查查询之前,先确认时间实际花在了哪里。
- 使用
EXPLAIN ANALYZE读取查询计划,识别顺序扫描、嵌套循环和行数不匹配问题。 - 跟踪 p95 和 p99 延迟而不是平均值,以捕获影响真实用户的异常值。
- 区分 CPU 密集型、I/O 密集型和锁竞争问题——每种问题需要不同的优化方法。
从用户可见的症状开始
页面加载慢并不总是意味着查询慢。在深入研究数据库内部之前,先确认时间实际花在了哪里。
注意以下模式:
- 持续性缓慢:每次对
/api/orders的请求都需要 3 秒以上。可能是单个昂贵的查询。 - 瀑布式请求:页面发起了 47 个连续的 API 调用。典型的 N+1 查询问题。
- 不一致的延迟:同一端点的响应时间在 50 毫秒到 5 秒之间波动。可能是锁竞争或缓存未命中。
- 超时峰值:请求偶尔完全失败。检查表锁或连接池耗尽。
浏览器开发者工具和 API 日志会给你第一个线索。如果网络时间很短但响应时间很长,说明后端是瓶颈。如果后端日志显示大部分时间花在数据库调用上,你就找到了目标。
查询性能分析的核心检测技术
读取查询计划
每个主流数据库都会解释它如何执行你的查询。这是你的主要诊断工具。
PostgreSQL:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
MySQL (8.0.18+):
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
SQLite:
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 123;
查找大表上的顺序扫描、高行数的嵌套循环,以及预估行数与实际行数的不匹配。当你期望使用索引查找时,查询计划却显示对百万行表进行”顺序扫描(Seq Scan)“,这就立即暴露了问题。
注意:不同数据库版本之间的查询规划器会有所变化。在 PostgreSQL 14 中运行良好的计划在 PostgreSQL 16 中可能会不同。不要假设升级会自动修复糟糕的查询。
启用慢查询日志
配置数据库记录超过阈值的查询。
PostgreSQL 使用 pg_stat_statements 获取聚合的运行时统计信息。对于单个查询的日志记录,使用 log_min_duration_statement。
MySQL 的 Performance Schema 和 sys schema 可以显示慢查询,无需使用已弃用的查询缓存。SQLite 缺乏内置的慢查询日志功能,需要在应用层面进行计时。
在生产环境中保守地设置阈值。从 100 毫秒开始,然后在修复明显问题后逐步收紧。
测量百分位数,而非平均值
平均查询时间会掩盖异常值。平均 50 毫秒的查询在 p99 可能达到 2 秒——持续影响 1% 的用户。
跟踪 p95 和 p99 延迟。如果你的 p99 是中位数的 10 倍,说明你有值得调查的尾部延迟问题。
Discover how at OpenReplay.com.
区分 CPU、I/O 和锁竞争
高查询时间意味着不同的问题:
- CPU 密集型:复杂计算、大量排序或正则表达式操作。查询计划在计算密集型节点上显示高”成本”。
- I/O 密集型:从磁盘读取大量数据。注意顺序扫描和缺失的索引。
- 锁竞争:查询等待其他事务。检查
pg_stat_activity(PostgreSQL)或SHOW PROCESSLIST(MySQL)中被阻塞的查询。
每种问题需要不同的 SQL 查询优化方法。添加索引无法帮助 CPU 密集型的聚合操作。
基于可观测性的分布式追踪检测
OpenTelemetry 让你能够将慢请求与特定的数据库跨度关联起来。追踪 /api/dashboard 端点可以准确显示哪些查询消耗了时间。
配置你的 ORM 或数据库驱动程序发出带有查询计时的跨度。你会清楚地看到 N+1 问题:50 个相同的 SELECT 跨度,而本应只有一个。
安全警告:不要在生产环境的追踪中记录带参数的完整 SQL。用户 ID、邮箱和其他个人身份信息(PII)会进入你的可观测性平台。应该记录查询指纹(参数化模板)。
常见陷阱
- 盲目添加索引:索引加速读取但会减慢写入。验证查询计划是否实际使用了你的新索引。
- 依赖平均值:50 毫秒的平均值掩盖了影响真实用户的 5 秒异常值。
- 一刀切的调优:“增加
work_mem”不是通用建议。先测量再调整。 - 记录原始 SQL:存在安全和合规风险。使用参数化指纹。
结论
发现数据库性能瓶颈从症状开始,而不是猜测。将慢页面追溯到慢端点,将慢端点追溯到慢查询,将慢查询追溯到它们的执行计划。测量百分位数,区分 CPU/IO/锁问题,并使用分布式追踪将前端症状与后端原因联系起来。
只有在那之后才应该进行优化。
常见问题
检查你的 API 日志中的响应时间分解。如果网络延迟低但后端响应时间高,说明服务器是瓶颈。在后端内部,测量数据库调用与应用逻辑所花费的时间。大多数框架和 APM 工具可以显示这种分解,帮助你确认查询是否是真正的罪魁祸首。
EXPLAIN 显示数据库打算使用的查询计划,但不运行查询。EXPLAIN ANALYZE 实际执行查询并报告真实的时间和行数。使用 EXPLAIN 进行快速检查,当你需要准确的性能数据时使用 EXPLAIN ANALYZE。对写操作使用 EXPLAIN ANALYZE 时要谨慎,因为它会执行更改操作。
索引会占用存储空间,并减慢 INSERT、UPDATE 和 DELETE 操作,因为数据库必须维护它们。此外,如果查询规划器估计顺序扫描会更快,它可能不会使用你的索引。始终使用 EXPLAIN ANALYZE 验证你的新索引是否真正改善了你针对的特定查询。
启用查询日志或使用分布式追踪查看每个请求执行的所有查询。寻找相同查询模板使用不同参数多次运行的模式。当逐个加载相关记录时,ORM 经常会导致这个问题。修复方法通常是使用预加载(eager loading)或使用 IN 子句将查询批量处理为单个请求。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.