说实话,系统慢这种事,十个里有八个第一反应是"哪个接口写烂了""是不是该加索引了",然后一头扎进代码里翻半天。但我的经验恰恰相反:绝大多数时候,那个拖垮数据库的SQL,数据库自己早就把它的"犯罪记录"写进日志里了。你翻代码是在猜凶手,翻日志是看监控录像,方式完全不一样。
这篇文章就围绕一件事——怎么从数据库日志里,把藏在系统深处的慢SQL揪出来。我会把MySQL、SQL Server、Oracle三套主流库的各自做法串起来讲,重点说清楚"日志里哪些信息值得看""怎么看""有哪些坑",最后用一个我实际跟过的故障案例,把整个排查链路走一遍。
1. 日志不是用来"出问题才看"的,它本身就是慢SQL的第一案发现场
很多人对"数据库日志"的理解停留在"记录错误信息"这个层面,平时根本不管,等到报警了才想起来翻一眼。这个思维得反过来:数据库日志是你排查性能问题时最忠实、最原始的证据链。
1.1 为什么说日志比监控平台更接近真相
现在稍微正规点的团队都会上监控平台,什么Prometheus加Grafana、云数据库自带的性能洞察,图表拉出来一堆。但监控指标有个天然短板——它是"聚合结果"而不是"原始记录"。你看到CPU 99%、QPS掉了一半,但你不知道这半小时里具体是哪几条SQL在作妖;你看到锁等待飙升,但监控面板不会替你圈出那个持锁的事务到底是哪条语句引发的。
日志不一样。拿MySQL慢查询日志来说,它记录的是SQL的真实执行时间、真实扫描行数、真实锁等待时间。这些数字不是抽样出来的,不是推算出来的,是每一次数据库实际执行后写下来的。换句话说,你看到的不是"系统大概慢了",而是"这条SQL在那一刻、那个数据分布下,实际跑了8.2秒"。
用个生活化的类比:监控平台是小区门口的大屏——告诉你今天车流拥堵指数87;慢查询日志是每辆车的行车记录仪——能精确到哪辆车在哪个路口踩了多久的刹车。排查数据库性能问题,你需要的恰恰是后者。性能视图(比如MySQL的performance_schema、SQL Server的DMV)其实也属于"实时监控",但它们依赖快照或者内存累积,进程一重启、缓冲区一淘汰,历史就没了。日志是落盘的,只要磁盘没坏,它就在那里等着你翻。
1.2 一条慢SQL从产生到被记录,中间发生了什么
搞清楚慢SQL是怎么进日志的,比背参数重要得多。以MySQL为例,整个过程是这样的:
- 客户端发过来一条SQL,MySQL先做语法解析、权限校验;
- 优化器生成执行计划,存储引擎开始干活;
- SQL执行完之后,MySQL把实际执行耗时和
long_query_time这个阈值做对比; - 如果超过了阈值,并且这条SQL没有被日志过滤规则挡掉,它就带着执行时间、锁等待时间、扫描行数、返回行数等信息,写进慢查询日志文件;
- 如果开着
log_queries_not_using_indexes,那即使执行得飞快、但没走索引的SQL,也会被额外记一笔。
注意一个细节:"执行完才记录"意味着慢查询日志只包含执行完成的SQL。如果一条SQL因为锁等待或者大事务一直卡着没结束,它在慢查询日志里是"缺席"的,你得去SHOW PROCESSLIST或者performance_schema里找正在跑的线程。这一点特别容易让人踩坑,后面讲到SQL Server的9002错误时还会再遇到类似的认知问题。
1.3 各数据库日志体系的差异决定了排查路径不同
没有哪两家的日志长得一样。
- MySQL的体系最直白:慢查询日志记录慢SQL,通用查询日志记录所有连接和SQL(一般不建议开,性能损耗大),binlog记录数据变更逻辑。
- SQL Server走的是错误日志(Error Log)加系统视图的路子:错误日志记录严重错误、备份恢复、DBCC结果等信息,但它默认不记录每条SQL;慢SQL的原始信息需要通过
sys.dm_exec_query_stats、sys.dm_exec_sql_text这些DMV去捞。 - Oracle最依赖Alert Log和AWR/ASH报告:Alert Log记录实例层面的严重问题,AWR给出的是快照区间内的TOP SQL汇总,ASH能精确到会话级的等待事件。
所以"从数据库日志获取慢SQL"这个需求,落到不同数据库上,实操手法天差地别。下面我按库分开讲,每套都给可以直接用的命令和脚本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MySQL:慢查询日志的开启、轮转与真实分析姿势
MySQL这边大概是最容易上手的,毕竟慢查询日志就是专门干这个的。但"开了慢查询日志"跟"会从慢查询日志捞SQL",中间还隔着一堆细节。
2.1 参数到底怎么配才合理
先看经典的慢查询日志相关参数:
sql复制-- 查看当前慢查询日志相关配置
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_queries_not_using_indexes';
SHOW VARIABLES LIKE 'log_slow_admin_statements';
SHOW VARIABLES LIKE 'min_examined_row_limit';
动态开启的写法:
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/data/mysql/slowlog/slow-202402.log';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET GLOBAL log_slow_admin_statements = 'ON';
这里我要强调几个新手几乎必踩的坑:
第一,long_query_time的单位是秒,支持小数。测试环境设个1秒没问题,线上如果业务本来就重,建议从2秒或5秒起步,先看一个稳定周期再往下调。很多DBA一上来就设0.1秒,结果慢查询日志半小时写几个G,反而把性能拖垮了——记录日志本身也有开销。
第二,log_queries_not_using_indexes这个开关很值得开,但它有个副作用:明明全表扫描只花了0.01秒的查询也可能被记下来,因为"没走索引"这个条件独立于执行时间。日志量会变大,但换来的是你能发现那些"目前数据量小所以还能忍"的隐患SQL。等数据涨上去,这些SQL就是第一批慢SQL。我个人建议开,但要做好日志量翻几倍的预期管理。
第三,log_slow_admin_statements控制的是ALTER TABLE、ANALYZE TABLE这类管理语句要不要进慢日志。很多人不知道这个开关默认是OFF的,所以ALTER TABLE把表锁了五分钟,慢查询日志里干干净净,你排查的时候完全摸不着头脑。必须开。
2.2 怎么从原始慢日志文件里提炼有效信息
慢查询日志落到磁盘后是纯文本,长这样:
text复制# Time: 2024-02-18T10:23:45.123456Z
# User@Host: app_user[app_user] @ [192.168.1.15] Id: 884211
# Query_time: 8.257364 Lock_time: 0.000183 Rows_sent: 1 Rows_examined: 9876543
USE `mall_order_db`;
SET timestamp=1708244625;
SELECT id, order_no, user_id, status, total_amount
FROM t_order
WHERE order_no = 'NO20240218101234987';
手工一行行看?除非日志量极小,否则别干这种蠢事。直接用工具。
方案一:mysqldumpslow(MySQL自带的粗筛工具)
bash复制# 按平均查询时间排序,看前10条
mysqldumpslow -s at -t 10 /data/mysql/slowlog/slow-202402.log
# 按执行次数排序,看哪些SQL出现频率最高
mysqldumpslow -s c -t 10 /data/mysql/slowlog/slow-202402.log
mysqldumpslow的查结果会自动把条件里的具体值归一化成N,比如WHERE order_no = 'N',这样同类SQL就能聚到一条里统计。这是它最大的价值——帮你聚合,而不是让你看单条记录。
方案二:pt-query-digest(Percona Toolkit里的王牌工具)
bash复制pt-query-digest /data/mysql/slowlog/slow-202402.log > slow_report.txt
它输出的报告里,最核心的是"Profile"部分,会按总响应时间给你排出一个清单,每条SQL附上执行次数、平均耗时、百分比占比。我用这个工具的体感是,排查线上问题第一步就干这一件事就够了,通常Top 5的SQL就占据了80%以上的慢查询总时长。
安装方式各系统不同,多数Linux发行版可以直接yum/apt装percona-toolkit,不过要注意:新版MySQL 8.0.35之后官方把mysqldumpslow挪到了mysql-server包的一些子包中(部分发行版会出现命令找不到的情况),Percona Toolkit的兼容性则一直维护得不错,线上环境我更推荐直接上pt-query-digest。
2.3 慢日志的落盘目录规划,别让它撑爆磁盘
"数据库怎么建日志文件夹"这个热词,估计没少人碰到——慢查询日志刚开几天,发现根目录满了。这里给个稳妥的建目录思路:
bash复制# 建议单独挂一个数据盘给日志类文件用
mkdir -p /data/mysql_logs/{slowlog,binlog,errorlog}
chown -R mysql:mysql /data/mysql_logs
# 在my.cnf里固定配置
[mysqld]
slow_query_log = 1
slow_query_log_file = /data/mysql_logs/slowlog/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
log_slow_admin_statements = 1
注意,动态SET GLOBAL改的文件路径,在重启后会被my.cnf覆盖回去。所以要永久生效必须写进配置文件,不然你线上开着开着,重启一次,慢查询日志又写回默认的datadir了,排查时翻不到历史文件,那才叫一个抓狂。
另外,日志轮转必须要做。慢查询日志不像binlog有自动清理策略,写满了就是写满了。建议用logrotate或者自己写crontab:
bash复制# 每天0点重命名慢日志,并让MySQL重新打开新文件
0 0 * * * mv /data/mysql_logs/slowlog/slow.log /data/mysql_logs/slowlog/slow-$(date -d "yesterday" +\%Y\%m\%d).log
0 0 * * * mysqladmin -uroot -p flush-logs slow
flush-logs这个动作很关键,否则MySQL句柄还指着旧文件,你mv完之后新日志照样往旧inode里写,磁盘空间还是不会被释放。
2.4 一个让慢日志"信息量翻倍"的小习惯
只记SQL文本不够,很多时候你还需要知道这条SQL对应的执行计划。MySQL 5.7+提供了EXPLAIN FORMAT=JSON,但慢日志里并不会自动带执行计划。我习惯的做法是:用pt-query-digest筛出Top SQL后,挑出2到3条重点的,手工去库里执行:
sql复制EXPLAIN ANALYZE
SELECT id, order_no, user_id, status, total_amount
FROM t_order
WHERE order_no = 'NO20240218101234987'\G
EXPLAIN ANALYZE(MySQL 8.0.18+)会真实执行SQL并输出每一步的耗时、扫描行数,比传统EXPLAIN的估算值准确得多。慢日志负责告诉你"谁是凶手",EXPLAIN ANALYZE负责告诉你"它为什么是凶手"。两件事接上,优化动作才算有依据。
3. SQL Server:错误日志里的9002与DMV捕获慢SQL的完整套路
SQL Server和MySQL的思路完全不一样。MySQL天生就有慢查询日志这个"专用设备",而SQL Server没有"慢查询日志"这个独立文件——它的错误日志(Error Log)里都是警告和错误级别的事件,而慢SQL要靠DBA自己通过DMV去"即时捕获"。
3.1 错误日志里到底哪些信息能定位慢SQL相关故障
SQL Server错误日志文件默认路径类似:
text复制C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\Log\ERRORLOG
在SSMS里也可以直接"SQL Server日志"节点下看。日志文件会滚动编号:ERRORLOG、ERRORLOG.1、ERRORLOG.2……最多保留6个(可以配置)。我重点说两个经常被当成"慢SQL导火索"的日志事件。
事件一:错误号9002(数据库日志已满)
你在错误日志里看到类似这样的记录:
text复制Error: 9002, Severity: 17, State: 4.
The transaction log for database 'orders_db' is full due to 'LOG_BACKUP'.
9002错误直译就是"数据库'%1!'的日志已满",这个错误跟慢SQL的关系是这样的:事务日志(Transaction Log)记录的是所有数据修改操作。如果你的某个会话开启了一个大事务,比如一次性UPDATE一百万行,或者一个长事务迟迟不提交,事务日志就会持续增长。日志文件增长到上限(MAXSIZE),而你又没做日志备份来截断它(LOG_BACKUP就提示这个),新的事务就再也写不进日志了,整个数据库进入只读状态,所有写操作集体报9002。
这种场景下,"慢SQL"和"9002错误"往往互为因果:慢SQL产生了大量日志记录把日志文件撑爆了,或者日志文件撑爆后所有写操作都在等待日志空间,表现为每条写SQL都"慢到超时"。排查方向是:
sql复制-- 查看日志文件大小和已用空间百分比
DBCC SQLPERF(LOGSPACE);
然后用下面这组DMV找到持锁最久、日志占用最大的会话:
sql复制SELECT
s.session_id,
s.login_name,
s.status,
DB_NAME(s.database_id) AS db_name,
s.total_elapsed_time / 1000 AS elapsed_sec,
t.text AS sql_text
FROM sys.dm_exec_sessions s
JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE s.session_id > 50
ORDER BY s.total_elapsed_time DESC;
事件二:823、824、825 I/O错误
日志里如果出现I/O error (torn page)、Error: 823、The operating system returned error 21这类记录,意味着磁盘子系统出问题了。这种场景下SQL"慢"不是SQL本身的问题,而是读写的底层IO延迟爆炸。你从DMV里捞出来的慢SQL其实是被冤枉的——它只是恰好在那段时间跑了,碰上磁盘抽风而已。这类问题要往存储层面排查(磁盘健康、SAN/云盘延迟、网络文件系统抖动),别把时间浪费在改SQL上。
3.2 用扩展事件实时捕获慢SQL,替代不存在的慢查询日志
在SQL Server里真正用来捕获慢SQL的现代方案是扩展事件(Extended Events),它比老旧的SQL Trace(SQL Profiler)开销小得多,而且性能影响基本可以忽略。
下面这段脚本我直接复制到你的运维文档里,个人认为它已经是生产环境最推荐的"慢查询日志平替":
sql复制-- 创建扩展事件会话:捕获执行时间超过3秒的SQL
CREATE EVENT SESSION [SlowQueryCapture] ON SERVER
ADD EVENT sqlserver.sql_statement_completed
(
ACTION
(
sqlserver.sql_text,
sqlserver.session_id,
sqlserver.username,
sqlserver.database_name,
sqlserver.client_host_name
)
WHERE sqlserver.database_name = N'orders_db'
AND duration >= 3000000 -- 单位是微秒,3000000微秒=3秒
)
ADD TARGET package0.event_file
(
SET filename = N'D:\XELogs\SlowQueryCapture.xel',
max_file_size = 50, -- 单位MB
max_rollover_files = 10
);
GO
-- 启动会话
ALTER EVENT SESSION [SlowQueryCapture] ON SERVER STATE = START;
GO
捕获到.xel文件后,用这个查询把内容读出来:
sql复制SELECT
xed.event_data.value('(event/@name)[1]', 'varchar(100)') AS event_name,
xed.event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') / 1000000.0 AS duration_sec,
xed.event_data.value('(event/data[@name="statement"]/value)[1]', 'nvarchar(max)') AS sql_text,
xed.event_data.value('(event/action[@name="session_id"]/value)[1]', 'int') AS session_id,
xed.event_data.value('(event/action[@name="username"]/value)[1]', 'nvarchar(100)') AS username,
xed.event_data.value('(event/action[@name="client_host_name"]/value)[1]', 'nvarchar(100)') AS app_host
FROM
(
SELECT CAST(event_data AS XML) AS event_data
FROM sys.fn_xe_file_target_read_file(N'D:\XELogs\SlowQueryCapture*.xel', NULL, NULL, NULL)
) AS xed
ORDER BY duration_sec DESC;
这套思路就是在SQL Server上手工造出了一个慢查询日志。而且扩展事件支持按duration、physical_reads、logical_reads等维度做过滤,比固定阈值的慢日志更灵活。
3.3 没有提前开捕获,怎么事后追溯历史慢SQL
扩展事件是"从现在开始录",但如果故障已经发生过了,你需要的是一台时光机。SQL Server的DMV里缓存着自上次重启以来执行过的SQL统计信息,这就是你的"历史行车记录仪"。
sql复制SELECT TOP 20
qs.total_elapsed_time / qs.execution_count / 1000000.0 AS avg_elapsed_sec,
qs.total_elapsed_time / 1000000.0 AS total_elapsed_sec,
qs.execution_count,
qs.total_logical_reads,
qs.total_logical_writes,
SUBSTRING(st.text, (qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1) AS sql_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_elapsed_time DESC;
还有一个实用组合:sys.dm_exec_requests和sys.dm_exec_sessions配合。故障发生时(或者事后复盘时),把当时会话的状态、等待类型、阻塞链捞出来:
sql复制SELECT
blocking.session_id AS blocking_session,
blocked.session_id AS blocked_session,
blocked.wait_type,
blocked.wait_time / 1000 AS wait_sec,
blocked_text.text AS blocked_sql
FROM sys.dm_exec_requests blocked
LEFT JOIN sys.dm_exec_requests blocking
ON blocked.blocking_session_id = blocking.session_id
CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) blocked_text
WHERE blocked.blocking_session_id > 0;
这套查询在SQL Server出现大规模阻塞的时候特别有用。你直接能看到:谁堵了谁,被堵的SQL在等什么类型(LCK_M_X、LCK_M_S这种),等了多久。配合上错误日志里的9002警告,基本上能把"日志满→写阻塞→SQL集体超时"这条链路钉死。
3.4 关于SQL Server日志文件与"满日志"的几点冷知识
在实际跟踪9002错误的过程中,有几个知识不在官方文档里高亮,却非常要命:
SQL Server的日志文件增长策略默认是"自动增长",但每次增长的增量如果设得太小(比如默认10%),大事务瞬间产生的日志量会触发频繁的文件扩展。而日志文件扩展是串行操作,扩展的那段时间内,所有需要写日志的会话全部被卡住,表现为"明明数据库没死,但写SQL全部超时"。从慢SQL排查视角看,你在sys.dm_exec_query_stats里看到的慢SQL可能本身并不慢,它是被日志文件自动增长的等待拖累的。判断方法很简单,看等待类型里有没有LOGBUFFER或者WRITELOG。
另外,DBCC SQLPERF(LOGSPACE)只能看当前日志已用百分比,看不出是哪个事务把日志撑大的。要定位具体的事务,可以用:
sql复制-- 查看当前活动事务及其开启时间
SELECT
s.session_id,
s.login_name,
t.transaction_begin_time,
DB_NAME(t.database_id) AS db_name,
DATEDIFF(SECOND, t.transaction_begin_time, GETDATE()) AS open_sec
FROM sys.dm_tran_session_transactions s
JOIN sys.dm_tran_active_transactions t
ON s.transaction_id = t.transaction_id
ORDER BY open_sec DESC;
open_sec时间最长的那个事务,十有八九就是日志膨胀的元凶。顺着session_id去dm_exec_connections拿most_recent_sql_handle,再解析出SQL文本,整条证据链就齐了。
4. Oracle:Alert Log信号、AWR/ASH的慢SQL定位三板斧
Oracle在国内存量系统里依然不少(尤其是金融、政务的老系统)。它的日志体系跟前面两家差别挺大的:如果你用MySQL的习惯去翻Oracle的慢SQL,第一步就蒙圈——因为Oracle没有一个文件叫"slow_query_log"。
4.1 Alert Log里哪些消息在暗示慢SQL问题
Oracle的Alert Log路径一般是:
text复制$ORACLE_BASE/diag/rdbms/<SID>/<SID>/trace/alert_<SID>.log
这东西相当于Oracle的"总控台日志",实例启动关闭、表空间扩展、ORA错误都会写进去。跟慢SQL有关的,最典型的有几种:
ORA-01555 snapshot too old:这条通常跟undo表空间大小有关,但本质上是某条查询跑了太久,读一致性需要的undo镜像被覆盖了。看到这个错误,基本可以断定当时存在长时间运行的慢查询,或者大事务没提交。ORA-01652 unable to extend temp segment:临时表空间耗尽,通常是某个大排序或大哈希连接导致的,也是慢SQL的典型特征。ORA-00060 deadlock detected:死锁在Oracle里通常涉及两条以上的SQL互相持锁等待。Alert Log会把死锁的会话信息和对应的SQL语句片段写出来。
这些错误出现的前几分钟到几十分钟,往往就是这个慢SQL的执行区间。有了时间锚点,再去AWR或者ASH里精确捞。
4.2 AWR报告:区间级别的慢SQL汇总
AWR(Automatic Workload Repository)是Oracle自带的工作负载仓库,它按固定间隔(默认60分钟)对数据库做一次快照,把快照之间的统计信息存起来。从日志里判断出故障时间段后,用下面的SQL找对应快照号:
sql复制SELECT snap_id, begin_interval_time, end_interval_time
FROM dba_hist_snapshot
WHERE begin_interval_time > SYSDATE - 3
ORDER BY snap_id DESC;
拿到快照号之后,可以直接人工取报告:
bash复制# 生成指定快照区间的AWR报告(HTML格式)
@$ORACLE_HOME/rdbms/admin/awrrpt.sql
按提示输入报表类型(html)、快照起始号和结束号,就会在当前目录生成一个awrrpt_1_xxx_xxx.html。打开后直接拉到"SQL ordered by Elapsed Time"这个部分,它就是该时间段内最耗时的SQL排行榜。
各字段解读里我最看重三个:
Elapsed Time per Exec:单次执行平均耗时,超过2秒就要警惕;Executions:执行次数乘以单次耗时才是总消耗;%Total DB Time:这条SQL占整个数据库总时间的比例,超过30%基本就是头号嫌疑犯了。
4.3 ASH与v$sql_monitor:秒级的历史会话追溯
AWR的粒度有一个问题——它是分钟级别的汇总。如果慢SQL只跑了30秒,恰好夹在两个快照之间,AWR能把它的总量统计进去,但你不知道它具体是哪一秒开始的,中间卡在哪个等待事件上。这时候得上ASH(Active Session History)。
ASH默认也是每小时采样一次,但它是秒级采样,每一个活动会话的当前状态都会记下来。查ASH时你要做的就是把时间窗口收紧:
sql复制SELECT
TO_CHAR(sample_time, 'HH24:MI:SS') AS sample_time,
session_id,
session_serial#,
sql_id,
event,
wait_class,
time_waited
FROM gv$active_session_history
WHERE sample_time BETWEEN
TO_TIMESTAMP('2024-02-18 10:20:00', 'YYYY-MM-DD HH24:MI:SS')
AND TO_TIMESTAMP('2024-02-18 10:30:00', 'YYYY-MM-DD HH24:MI:SS')
ORDER BY sample_time;
如果这个时间窗口内某条sql_id反复出现,而且event列的值落在db file sequential read、enq: TX - row lock contention这类常见慢SQL等待事件里,那几乎可以锁定了。拿着sql_id去查SQL全文:
sql复制SELECT sql_text
FROM v$sql
WHERE sql_id = '你的sql_id';
v$sql_monitor是另一个很香的视图,Oracle 11g以后默认对执行超过5秒的SQL开启实时监控。它甚至会给出执行计划的每一行实际行数、实际耗时:
sql复制SELECT dbms_sqltune.report_sql_monitor(sql_id => '你的sql_id', type => 'TEXT') AS report
FROM dual;
那个报告文本一拉出来,哪一步全表扫描扫了一亿行,哪一步嵌套循环循环了80万次,一目了然。优化动作都省得猜了。
5. 一个真实案例:日志文件狂涨背后的9002,线索引向一条7秒UPDATE
讲完三套库的操作手法,我完整复盘一个我支持过的SQL Server生产故障。整个排查链路中,日志既是起点(9002),也是终点(慢SQL),很有参考价值。
5.1 故障现像:写请求批量失败,错误日志刷出9002
那天下午客户系统反馈订单模块无法提交新订单,大批接口超时。先上服务器看SQL Server错误日志,滚动记录里大面积出现:
text复制Date 2024-02-18 14:32:07
Log SQL Server (当前 - 2024年2月18日 14:32:00)
错误: 9002,严重性: 17,状态: 4。
数据库 'order_center' 的事务日志已满,原因 'LOG_BACKUP'。
9002直接告诉我们:事务日志满到无法接受新写入。此时很多DBA会直接条件反射式地做日志收缩或者加日志文件大小。但这只治标,真正的源头是谁产生了海量日志?如果找不到根源,加了空间明天照样爆。
5.2 排查链路:从日志满反查活动事务,顺藤摸瓜命中慢SQL
第一步,确认事务日志当前占用情况,以及哪个库最危险:
sql复制DBCC SQLPERF(LOGSPACE);
结果里order_center库的Log Space Used高达99.9%,日志文件被塞得死死的。
第二步,定位当前活动事务里谁"持续最久、写入最多":
sql复制SELECT
s.session_id,
s.login_name,
DB_NAME(t.database_id) AS db_name,
t.transaction_begin_time,
DATEDIFF(SECOND, t.transaction_begin_time, GETDATE()) AS open_sec
FROM sys.dm_tran_session_transactions s
JOIN sys.dm_tran_active_transactions t
ON s.transaction_id = t.transaction_id
WHERE t.database_id = DB_ID('order_center')
ORDER BY open_sec DESC;
果不其然,排最前面的会话session_id=127,事务已经开了快40分钟没提交。用这个session_id去抓它到底在跑什么SQL:
sql复制SELECT
r.session_id,
r.status,
r.command,
r.wait_type,
r.wait_time,
t.text AS sql_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id = 127;
命令列显示UPDATE,wait_type是NULL(说明它没在等待别人,是在真刀真枪地干活,不过它一直在执行就是没返回),SQL文本就是一条针对订单明细表的大范围更新:
sql复制UPDATE t_order_detail
SET settle_status = 3,
settle_time = GETDATE()
WHERE order_date >= '2024-01-01'
AND order_date < '2024-02-01';
这条SQL一次性更新全一个月的订单明细,几百万行的UPDATE,每一个行版本变化都要写事务日志。事务日志从一开始就没停过增长,最终撞上MAXSIZE上限,触发9002,全库写操作被冻结。从慢SQL的"慢"的视角看,它是一条典型的"低效SQL+低效业务逻辑"——应该分批更新却选择了一把梭。实际上,扩展事件里记录到这条UPDATE单次执行耗时约7.4秒根本不慢;真正让它和"慢SQL"扯上关系的,是它在一次40分钟的未提交大事务中把日志空间干爆,殃及池鱼。
第三步,处理步骤。这个场景不能贸然KILL事务,因为回滚几百万行的UPDATE相比继续执行可能需要更多时间,操作不当反而把系统拖得更狠。我当时的处理顺序是:
- 先把
order_center日志文件紧急增加空间到足够大(ALTER DATABASE order_center MODIFY FILE (NAME = order_center_log, SIZE = 30GB)),让系统立刻恢复写入能力,把业务止血; - 联系应用负责人,确认能接受截断后才终止掉那个会话:
sql复制KILL 127; - 等回滚完成后做一次事务日志备份,把日志文件内部空间释放出来:
sql复制BACKUP LOG order_center TO DISK = N'D:\Backup\order_center_log.bak'; - 回归到代码层面,把存量数据处理改成按天分批+每批一提交的形式,例如每批只更新10000行就COMMIT,避免长事务再次出现。
5.3 这个案例给我们的逆向启发
复盘这套流程你会发现:9002错误日志是"果",慢SQL是"因",而中间桥接的是事务日志的增长。如果只看错误日志就去收缩文件、加空间,等于每次都在清理垃圾,却从不关闭垃圾制造机。真正有价值的排查顺序是:
- 先看错误日志里有什么(这里是9002的LOG_BACKUP);
- 再用事务到DMV反查当前活动会话;
- 顺着会话抓到正在执行的慢SQL或长事务;
- 分析这条SQL为什么产生巨大日志量(扫描行数、更新行数、是否缺索引/缺过滤条件);
- 最后才是处理手段。
6. 日志捞慢SQL的边界,以及那些容易误判的"假慢SQL"
文章最后必须泼几盆冷水。日志法不是万能的,而且日志里的"慢"有时候会骗人。
6.1 日志里显示慢,但不该优化的几种情况
情况一:等待型慢SQL。 一条UPDATE在日志里显示执行了8秒,但Lock_time就占了7.9秒。这不是SQL本身执行慢,是它一直被别的会话堵着,拿不到锁。你光看Query_time去优化索引,等于给一个排队排了8分钟的人换双跑鞋,毫无意义。MySQL看Lock_time,SQL Server看wait_type,Oracle看event——先分清是"在跑"还是"在等"。
情况二:大事务回滚时的慢SQL误伤。 前面SQL Server的例子里,7秒的UPDATE本身并不慢,但某条只有0.1秒的小INSERT如果卡在一个已经膨胀到几十GB的事务日志里,它的"慢"是大事务带来的次生灾害。日志里抓到的慢SQL,未必是根源SQL。判断技巧是看它出现的时间段是不是跟大事务重叠,以及它的运行状态是等待还是执行。
情况三:统计信息偏差导致的执行计划变化。 如果一条SQL平时跑0.05秒,某天突然在慢日志里出现一次8秒,但之后又消失了,先别急着改SQL。很可能是当天的统计信息过期,优化器选了一个错误的执行计划。这种场景优化SQL文本没有意义,得去更新统计信息或者固化执行计划。
6.2 日志里不慢,但实际很危险的SQL
反过来,慢查询日志没记录不代表万事大吉。MySQL里如果你开着long_query_time=2,一条DELETE FROM t WHERE status=0跑1.8秒不满足慢日志阈值,但它一次性删除20万行,产生大量binlog,主从复制延迟照样拉满。这类操作不是"慢SQL",但它是"重SQL"。
所以我日常巡检的逻辑是双轨并行:慢日志逮"跑得慢的",binlog/审计日志逮"写得重的"。后者可以用SHOW BINARY LOGS结合binlog解析工具去看各时段的写入量,也可以翻performance_schema里按rows_affected维度排序找TOP语句。
6.3 日志法排查的标准流程建议
最后把我个人习惯的排查流程沉淀成清单,供你参考。
| 阶段 | 动作 | 关键点 |
|---|---|---|
| 确认现象 | 业务反馈慢还是监控告警 | 记录精确到分钟的时间窗口 |
| 锚定时间 | 从错误日志/应用日志找首个异常 | 先确认是不是数据库问题,别被应用日志带偏 |
| 抓取SQL | MySQL看慢日志文件,SQL Server看扩展事件/DMV,Oracle看AWR/ASH | 尽量缩小时间范围,减少噪音 |
| 排序定位 | 按总耗时、平均耗时、执行次数排序 | 优先处理"总耗时占比高"的,不是"单次最慢"的 |
| 分析根因 | EXPLAIN看执行计划,DMV看等待类型,ASH看等待事件 | 分清执行慢还是等待慢 |
| 实施优化 | 加索引、改SQL、分批处理、调整参数 | 一次只改一处,保留优化前后的日志对比 |
| 验证回归 | 对比慢日志记录数量与耗时分布 | 没有回归就是成功的优化 |
写到这里,回头看"通过数据库日志获取慢SQL"这件事,核心并不是记住哪个参数、哪条命令——那些随时可以查文档。真正的价值在于建立一条故障发生时快速还原真相的路径:错误日志给方向,慢日志/DMV/AWR给子弹,等待类型/执行计划给答案。这套方法论在你的数据库环境里多演练几次,等下一次线上告警来临的时候,你就不会慌着刷新监控面板,而是非常自然地打开那个日志文件,知道自己要找什么。
