getLastSql()常为空或错误,因其仅返回最近一次成功执行的SQL,而非构建的SQL;易受全局作用域、中间件、读写分离未指定连接及DB_DEBUG关闭等因素影响。

getLastSql() 为什么总是空或不对
因为 getLastSql() 只返回「最近一次成功执行的 SQL」,不是「最近一次写的 SQL」。它不关心你写了多少个 where(),只认最终调用 select()、find()、update() 这类执行方法后那一句。
常见踩坑点:
-
Db::name('user')->where('id', 1)->getLastSql()—— ❌ 中途调用,SQL 没组装完,返回空或上一条 - 事务里、模型事件中、中间件后执行查询,
getLastSql()可能被日志写入、缓存检查等操作覆盖 - 用了读写分离(如
Db::connect('read')),但没指定连接名调用,Db::getLastSql()默认取主连接,查不到从库语句 - 全局作用域(如
baseQuery)自动加了条件,但你没意识到,以为自己写的where就是全部
fetchSql(true) 返回的 SQL 为什么带 ? 占位符
这是 PDO 预处理的正常行为,不是框架 bug。fetchSql(true) 和 getLastSql() 都只返回带参数占位符(WHERE id = ?)的原始 SQL,真实参数在 PDO 层绑定,不拼进字符串——这是防 SQL 注入的核心机制。
想临时看到完整可复制的 SQL(含实际值),可以手动替换(仅限调试):
立即学习“PHP免费学习笔记(深入)”;
$sql = UserModel::where('id', 1)->fetchSql(true)->find();
// $sql 是 "SELECT * FROM `think_user` WHERE `id` = ?"
$params = [1];
foreach ($params as $param) {
$sql = preg_replace('/\?/', var_export($param, true), $sql, 1);
}
echo $sql; // 输出:SELECT * FROM `think_user` WHERE `id` = 1
注意:fetchSql(true) 必须紧挨着执行方法,比如 ->fetchSql(true)->find(),中间插了 with() 或 cache() 在某些 TP6 版本可能失效。
开了调试却看不到 SQL 日志?先查这三个配置
ThinkPHP 的 SQL 日志不是“开了 app_debug = true 就自动出来”,必须三者同时满足:
-
app_debug必须为true(通常在.env或config/app.php里) -
log.level数组里必须显式包含'sql',例如['error', 'sql', 'notice'];只设debug或info不触发 - 数据库配置中
deploy或trace开关无效,真正起作用的是日志级别和调试开关
另外两个易忽略点:
- 日志文件路径权限不足时,TP 会静默丢弃 SQL 日志,可临时把
log.path改成/tmp测试是否写入成功 - 单条 SQL 超长(比如含大 JSON 或 GROUP_CONCAT),会被截断显示为
(2048 bytes),需调大log.file_size(设为0表示不限制)
想看所有 SQL(包括关联、子查询、慢查询分析)该用什么
单靠 getLastSql() 或日志文件不够,尤其面对 with() 关联、闭包 where、视图或存储过程时:
- 页面底部的 Trace 面板最直观,能列出当前请求全部 SQL + 执行时间 + EXPLAIN 分析(需开启
sql_explain => true) - 用
Db::listen()主动监听,比被动等日志更可控:Db::listen(function ($sql, $time, $explain) { echo "[SQL] {$sql} [Time: {$time}s]\n"; if ($explain) { print_r($explain); } });注意:一旦注册了listen,默认 SQL 日志就停了,得自己写入文件或发到监控系统 -
buildSql()适合验证复杂子查询逻辑,它不执行、不走 PDO,纯字符串拼接,对selectSub()、union()支持较好,但遇到视图定义或函数字段可能不准
真正难调试的永远不是单条 SQL,而是多连接切换、全局作用域干扰、预加载 N+1、以及日志和监听之间的竞争关系——这些地方没留心,再全的配置也白搭。



















