调试

调试

本指南涵盖当 SQL 查询出现问题时你可以使用的排查技术。你将学会如何:

  • 订阅 db:query 事件并读取其有效载荷
  • 在开发期间格式化打印查询
  • 将查询管道化到应用日志器,并可选择慢查询过滤
  • 调试单个查询而无需修改连接级配置
  • 追踪查询在应用代码中的发起位置
  • 检查查询的执行计划
  • 将查询转发到可观测系统

概述

Lucid 中的调试以 db:query 事件为核心。本指南中的其他每个技术要么在该事件上发出,要么监听它。心智模型很简单:

  • 当查询调试启用时,Lucid 为每个执行的查询发出 db:query 事件。
  • 内置的格式化打印器、你的应用日志器以及任何可观测性集成都是该事件的监听器。
  • 两个标志控制事件是否触发:连接级的 debug 标志用于启用发出,以及一个监听器订阅用于接收事件。

这些标志本身在配置指南中有文档说明。本指南展示如何使用它们。

db:query 事件

Lucid 为每个通过框架运行的查询、迁移和原始 SQL 语句发出 db:query 事件。从 start/events.ts 订阅它以在查询执行时接收通知。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import logger from '@adonisjs/core/services/logger'
emitter.on('db:query', (query) => {
logger.debug(query)
})

事件仅在两个条件都满足时触发:至少有一个监听器已订阅,并且调试输出已在连接级或逐查询级启用。订阅 db:query 但未同时在连接上设置 debug: true(或在特定查询上使用 .debug(true))将不会产生任何输出。对于典型的开发设置,两者一起启用。

config/database.ts
import app from '@adonisjs/core/services/app'
const dbConfig = defineConfig({
prettyPrintDebugQueries: app.inDev,
connection: 'postgres',
connections: {
postgres: {
client: 'pg',
connection: env.get('DATABASE_URL'),
debug: app.inDev,
},
},
})

每个事件携带一个结构化的有效载荷,包含查询的详细信息。

sql

SQL 语句字符串,带有绑定值的占位符。

bindings

绑定到 sql 中占位符的值数组。将此项与 sql 一起记录,以便手动重现查询。

method

Knex 报告的查询方法,例如 selectinsertupdatedelraw

duration

查询执行所花费的时间,表示为 process.hrtime 返回的 [seconds, nanoseconds] 元组。使用 seconds * 1000 + nanoseconds / 1e6 转换为毫秒。

connection

查询运行的连接名称。当你的应用使用多个连接时很有帮助。

model

发出查询的 Lucid 模型类名称,如果查询是通过模型发出的。对于直接使用 db 服务进行的查询,此项为 undefined。

ddl

对于数据定义语句(迁移、schema 变更)为 true。对于常规查询为 false 或不存在。

inTransaction

当查询在使用 db.transaction()client.transaction() 创建的事务客户端中运行时为 true

在开发期间格式化打印查询

Lucid 附带了一个格式化打印器,它格式化查询以用于终端输出,带有颜色、计时和已插值的绑定。通过在配置的顶级设置 prettyPrintDebugQueries 来启用它。

config/database.ts
import app from '@adonisjs/core/services/app'
import { defineConfig } from '@adonisjs/lucid'
const dbConfig = defineConfig({
prettyPrintDebugQueries: app.inDev,
connection: 'postgres',
connections: {
postgres: {
client: 'pg',
connection: env.get('DATABASE_URL'),
debug: app.inDev,
},
},
})

该标志将 Lucid 的内置格式化打印器注册为 db:query 事件的监听器。结合连接上的 debug: app.inDev,开发中执行的每个查询都会打印到终端。

你也可以通过 db.prettyPrint 以编程方式调用格式化器,例如在自定义监听器内部,格式化某些查询并将其他查询路由到日志文件。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import db from '@adonisjs/lucid/services/db'
emitter.on('db:query', (query) => {
if (query.ddl) {
return
}
db.prettyPrint(query)
})

通过应用日志器记录查询

start/events.ts 订阅 db:query 并将每个事件转发到 AdonisJS 日志器。这是生产环境的推荐路径,终端格式化打印没有用,但结构化日志是有用的。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import logger from '@adonisjs/core/services/logger'
emitter.on('db:query', (query) => {
logger.debug({
sql: query.sql,
bindings: query.bindings,
duration: query.duration,
connection: query.connection,
}, 'database query')
})

将每个查询转发到日志器可能会产生大量噪音。一个常见的模式是只记录超过持续时间阈值的查询,这样可以在不淹没日志的情况下发现慢查询。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import logger from '@adonisjs/core/services/logger'
emitter.on('db:query', (query) => {
const [seconds, nanoseconds] = query.duration ?? [0, 0]
const ms = seconds * 1000 + nanoseconds / 1e6
if (ms > 100) {
logger.warn({ sql: query.sql, bindings: query.bindings, ms }, 'slow query')
}
})

调试单个查询

当你想检查一个查询而不在连接级打开调试时,在查询构建器上调用 .debug(true)。该标志会覆盖该查询的连接设置,并在有监听器附加时触发 db:query 事件。

app/controllers/posts_controller.ts
import db from '@adonisjs/lucid/services/db'
export default class PostsController {
async index() {
return db
.from('posts')
.where('is_published', true)
.debug(true)
}
}

在实践中,当你在开发中已经启用了 prettyPrintDebugQueries 或手动监听器,但关闭了连接级 debug 标志以减少噪音时,这最有用。.debug(true) 调用允许你将特定查询重新选择到日志流中。

追踪查询的来源

当查询失败或行为异常时,堆栈跟踪通常指向 Knex 内部而不是发出查询的应用代码。asyncStackTraces 标志告诉 Knex 在构建查询时捕获原始调用点,并将其附加到执行期间抛出的任何错误上。

config/database.ts
import app from '@adonisjs/core/services/app'
const dbConfig = defineConfig({
connection: 'postgres',
connections: {
postgres: {
client: 'pg',
connection: env.get('DATABASE_URL'),
asyncStackTraces: app.inDev,
},
},
})

启用该标志后,失败查询的堆栈跟踪现在指向发起查询的控制器、服务或命令,这使追踪变得更快。该功能在查询创建时捕获堆栈会产生少量运行时开销,因此仅在开发中启用。

检查查询的执行计划

Knex 和 Lucid 都没有在查询构建器上暴露 .explain() 方法。要查看查询的执行计划,使用查询构建器构建查询,用 .toSQL() 提取其 SQL,然后通过以 EXPLAIN 前缀的 db.rawQuery 运行它。

app/services/posts_service.ts
import db from '@adonisjs/lucid/services/db'
export default class PostsService {
async explainPublishedPosts() {
const query = db
.from('posts')
.where('is_published', true)
.orderBy('created_at', 'desc')
.limit(10)
.toSQL()
const plan = await db.rawQuery(`EXPLAIN ANALYZE ${query.sql}`, query.bindings)
return plan.rows
}
}

.toSQL() 调用返回一个 { sql, bindings } 形状的对象。将两者都传给 rawQuery,以便预处理语句使用与原始查询相同的参数运行。

每种方言暴露了略微不同的语法。

方言仅计划带实时计时的计划
PostgreSQLEXPLAIN <sql>EXPLAIN ANALYZE <sql>
MySQLEXPLAIN <sql>EXPLAIN ANALYZE <sql> (MySQL 8.0.18+)
SQLiteEXPLAIN QUERY PLAN <sql>不可用

MSSQL 使用不同的工作流。计划捕获由会话级命令(如 SET SHOWPLAN_XML ON)切换,这改变了后续查询返回的内容,而不是给单个语句添加前缀。上面展示的 .toSQL() + 前缀方法不适用。请参阅 SQL Server 文档了解正确的模式。

将查询转发到可观测系统

db:query 事件是可观测性平台的集成点。在 start/events.ts 的监听器内部连接 OpenTelemetry span、Datadog trace 或任何其他收集器,使用事件有效载荷的 sqldurationconnection 字段作为维度。在监听器内部应用采样或持续时间阈值,以在繁忙服务上保持可管理的摄取量。

后续步骤

  • 配置指南了解 debugprettyPrintDebugQueriesasyncStackTraces 标志的参考。
  • 数据库服务指南了解在上下文中逐查询使用 .debug(true)