MySQL的slow_query_log仅记录SQL在服务端实际执行超时的部分,无法捕获API网关层端到端耗时(如PHP执行、网络延迟、连接池等待、锁竞争等),故需在应用层(如PDO)主动打点采样并关联请求ID,才能准确定位真实慢因。

为什么慢SQL日志不能只靠MySQL的slow_query_log
因为API网关层看到的“慢”,是端到端耗时(含PHP执行、网络、DB连接池等待、锁竞争等),而MySQL slow log只记录语句在服务端实际执行超时的部分。你可能发现一个接口整体耗时800ms,但slow log里一条SQL都没记——它可能卡在PDO::prepare()阻塞了700ms,或ORM生成了N+1查询却每条都不到阈值。
所以必须在PHP代码中主动拦截、采样、记录:在PDO或MySQLi执行前打点,在fetch后计算耗时,再按阈值过滤。
如何在PDO中无侵入地捕获慢SQL(含参数绑定)
不能简单包装query()或execute(),否则会丢失预处理语句的参数绑定上下文。正确做法是继承PDOStatement并重写execute(),同时用PDO::ATTR_STATEMENT_CLASS全局注入:
class TracedPDOStatement extends PDOStatement
{
protected $sql;
protected $start;
<pre class="brush:php;toolbar:false;">protected function __construct($sql) {
$this->sql = $sql;
$this->start = microtime(true);
}
public function execute($params = null): bool
{
$result = parent::execute($params);
$duration = microtime(true) - $this->start;
if ($duration >= 0.2) { // 阈值设为200ms
$bound = $this->debugDumpParams(); // 注意:仅调试用,生产需手动解析$params
error_log(sprintf(
"[SLOW-SQL] %.3fs | %s | params=%s",
$duration,
$this->sql,
json_encode($params ?: [])
));
}
return $result;
}
}
$pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_STATEMENT_CLASS => ['TracedPDOStatement', []], ]);
- 注意
debugDumpParams()在生产环境不可用(会触发警告且不返回结构化数据),真实项目应改用getAttributes()+ 手动映射参数位置 - 不要在
execute()里做I/O(如写文件),避免拖慢主流程;建议发到Redis队列或通过syslog()交由rsyslog异步落盘 - 若用Laravel,优先改写
PDOConnector或监听illuminate.database.query事件,而非直接继承PDOStatement
如何关联SQL耗时与API请求ID(避免日志错乱)
单个HTTP请求可能并发执行多条SQL,若只记时间戳,日志会散落在不同行,无法归因。必须把当前请求唯一标识(如X-Request-ID头)透传进SQL日志:
在中间件中设置全局上下文:
if ($requestId = $request->header('X-Request-ID')) {
define('CURRENT_REQUEST_ID', $requestId);
}
然后在TracedPDOStatement::execute()中补上:
error_log(sprintf(
"[SLOW-SQL][%s] %.3fs | %s | params=%s",
defined('CURRENT_REQUEST_ID') ? CURRENT_REQUEST_ID : 'unknown',
$duration,
$this->sql,
json_encode($params ?: [])
));
- 不要依赖
$_SERVER['REQUEST_ID'](仅Apache mod_unique_id支持,Nginx需额外配置) - 若用Swoole,
CURRENT_REQUEST_ID需改为Co::getcid()或协程上下文存储,否则多协程会覆盖 - 日志行首固定加
[SLOW-SQL]标签,方便ELK或grep快速过滤
哪些SQL明明很慢却不会被捕获?怎么补漏
有三类常见漏网之鱼:
-
PDO::beginTransaction()、commit()、rollback()本身不走PDOStatement,但事务持有锁期间的等待会被计入后续SQL——需单独对这些方法打点 - 使用
PDO::quote()拼接SQL(绕过预处理)的场景,此时$sql是字符串拼接结果,需在调用处手动包裹计时 - Doctrine DBAL或Eloquent的
toSql()不执行,只有get()/first()才触发;务必确保日志钩子挂载在真正执行的位置,而非构建Query Builder时
最稳妥的做法:在所有数据库操作统一入口(如封装的DB::query()方法)里加计时,而不是依赖底层驱动钩子。
慢SQL日志真正的难点不在采集,而在区分“是SQL真慢”还是“被其他环节拖累”。务必同步采集连接获取耗时、PHP执行耗时、网络RTT,否则单看SQL时间容易误判。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











