我们正在对具有高 CPU 使用率的服务器进行故障排除。在发现查询并不是真正导致它之后,我们开始研究编译。
性能监视器显示少于 50 次编译/秒和少于 15 次重新编译/秒。
在运行寻找编译的 XE 会话后,我们每秒看到数千次编译。
该系统使用触发器来审核更改。大多数编译都是由于触发器。触发器引用 sys.dm_tran_active_transactions。
我们的第一个想法是,也许在触发器中引用 DMV 会导致它每次都编译,或者可能只是这个特定的 DMV 会导致它。所以我开始测试这个理论。它确实每次都编译,但我没有检查触发器是否在每次触发时都编译,因为它不引用 DMV 而是硬编码一个值。每次触发时它仍在编译。放下触发器会停止编译。
复制脚本:
CREATE TABLE t1 (transaction_id int, Column2 varchar(100)); CREATE TABLE t2 (Column1 varchar(max), Column2 varchar(100)); GO CREATE TRIGGER t2_ins ON t2 AFTER INSERT AS INSERT INTO t1 SELECT (SELECT TOP 1 transaction_id FROM sys.dm_tran_active_transactions), Column2 FROM inserted; GO --Both of these show compilation events INSERT INTO t2 VALUES ('row1', 'value1'); INSERT INTO t2 VALUES ('row2', 'value2'); GO ALTER TRIGGER t2_ins ON t2 AFTER INSERT AS INSERT INTO t1 SELECT 1000, Column2 FROM inserted; GO --Both of these show compilation events INSERT INTO t2 VALUES ('row3', 'value3'); INSERT INTO t2 VALUES ('row4', 'value4'); DROP TRIGGER t2_ins;
使用的 XE 事件使您错误地认为触发器实际上是在编译每次执行。有两个扩展事件query_pre_execution_showplan和query_post_compilation_showplan,描述相似,但有一个重要的词不同:
query_pre_execution_showplan
编译 SQL 语句后发生。此事件返回优化查询时生成的估计查询计划的 XML 表示形式。使用此事件可能会产生显着的性能开销,因此仅应在短时间内对特定问题进行故障排除或监视时使用它。
query_post_compilation_showplan
编译 SQL 语句后发生。此事件返回编译查询时生成的估计查询计划的 XML 表示形式。使用此事件可能会产生显着的性能开销,因此仅应在短时间内对特定问题进行故障排除或监视时使用它。
这些事件在描述上并不完全相同,并且发生在与使用您的重现进行进一步测试的不同时间。使用更大的事件会话定义,很容易看出编译实际发生的位置。
在这里,您可以看到插入语句的第一次编译是作为准备好的计划在绿色框中自动参数化的。触发器在红色框中编译,计划插入到缓存中,如 sp_cache_insert 事件所示。然后在橙色框中,触发器执行获得缓存命中并为批处理中的第二个 INSERT 语句重用触发器计划,因此它不会编译 INSERT 命令的每次执行并且计划确实得到重用,正如您在 sp_cache_hit 事件中看到的那样为触发器。
如果我们在第一次执行后再次单独运行两个 INSERT 语句,触发器将不会再次编译,如下面的事件所示:
在这里,第一个语句遇到缓存中语句的准备好的自动参数化版本的缓存命中,但未命中提交的临时批处理。触发器获得缓存命中并且不会再次编译,如红色事件块所示。对于作为单独批处理运行的第二个 INSERT 语句,绿色事件块重复此行为。但是,在每种情况下,您仍然会看到 query_pre_execution_showplan 事件触发,我只能将其归因于事件描述中优化与编译的差异,但触发器并未针对这些系列事件所示的每次执行进行编译。