통계 자동 갱신 조건과 별개로 옵티마이저 판단
SQL Server는 통계가 오래되거나 변경되면 자동으로 갱신하고 그때 계획을 다시 컴파일합니다.
하지만 통계 갱신이 없더라도, 데이터 분포 변화가 심해 기존 계획이 비효율적이라고 판단되면 옵티마이저가 강제 Recompile을 트리거할 수 있습니다.
 
DML로 인한 데이터 양 변화
예를 들어, 특정 테이블에 대량의 INSERT/DELETE가 발생해 행 수가 급격히 변하면, 기존 계획의 조인 전략(예: Nested Loop vs Hash Join)이 더 이상 적합하지 않을 수 있습니다.
이 경우 옵티마이저는 계획 캐시를 무효화하고 새 계획을 생성합니다.
ü   대량 DML로 인해 통계 임계치 초과 시 자동 갱신 → 기존 계획의 카디널리티 추정이 틀려서 재컴파일 발생

옵션 및 힌트 영향
OPTION(RECOMPILE) 힌트가 있거나, 저장 프로시저에 WITH RECOMPILE이 설정된 경우 매 실행마다 새 계획을 생성합니다.
파라미터 스니핑으로 인해 특정 값에서 기존 계획이 비효율적일 때도 Recompile이 발생합니다.
 
메모리 압박 또는 캐시 제거
DBCC FREEPROCCACHE 실행, 메모리 부족으로 인한 캐시 제거 등도 재컴파일을 유발합니다.
 
스키마 변경
o    ALTER TABLE, ALTER VIEW, 인덱스 생성/삭제
o    제약조건, 열 추가/삭제 → 계획이 참조하는 객체 구조가 바뀌면 무효화

 
SET 옵션 변경
ANSI_NULLS, QUOTED_IDENTIFIER 등 세션 옵션 변경 시
 
임시 테이블 변경
Temp Table 구조 변경, 인덱스 추가/삭제
 
기타
Query Store 강제 계획 적용 실패
데이터베이스 버전 변경
파티션 뷰 변경 등

 

또한 통계 갱신이 없더라도 옵티마이저는 내부 임계값을 기준으로 판단합니다.

예를 들어 특정 테이블에서 수백만 건의 INSERT가 발생하게 될 경우, 통계는 아직 갱신되지 않았지만, 옵티마이저는 기존 계획이 잘못될 가능성이 높다고 판단하여 Recompile을 수행할 수 있고, 파라미터 값 변화로 인해 기존 Plan이 비효율적으로 판단될 경우 Recompile을 수행할 수 있습니다.

 

Recompile이 발생하는 경우는 여러가지 이유가 있으며 테이블의 컬럼 및 인덱스 변경, Set옵션의 변경, 서버 재시작, 플랜캐시의 상황으로 캐시에서 밀려난 후 Compile, RECOMPILE 힌트, 명시적인 SP_Recompile등의 요소에 의해 발생할 수 있습니다.

: Recompile a Stored Procedure - SQL Server | Microsoft Learn

 

 

 

--XEVENT모니터링 적용

 

 
--1. 파일 타겟(100MB * 10개 순환) 세션 생성
CREATE EVENT SESSION Recompiles_File
ON SERVER
ADD EVENT sqlserver.sql_statement_recompile(
    ACTION(
        sqlserver.sql_text,
        sqlserver.database_id,
        sqlserver.session_id,
        sqlserver.plan_handle
    )
)
ADD TARGET package0.event_file(
    SET filename = N'Recompiles',      -- 실제 생성: Recompiles_0_*.xel 파일명만 적을경우 기본위치(xp_readerrorlog의 로파일 생성디렉토리)에 생성
        max_file_size = 100,                       -- MB
        max_rollover_files = 10        -- 파일 숫자를 너무 많이 할 경우 조회시 수행속도 이슈가 발생할 수 있음   
)
WITH (
    MAX_MEMORY = 20MB,
    EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
    MAX_DISPATCH_LATENCY = 5 SECONDS,
    TRACK_CAUSALITY = ON,
    STARTUP_STATE = ON
);
GO
 
--2. 모니터링  Start
ALTER EVENT SESSION Recompiles_File ON SERVER STATE = START;
GO
 
 
--XEVENT모니터링  컴파일  원인확인
 
  1. XEvent저장 Default경로확인
 DECLARE @full nvarchar(4000)=CAST(SERVERPROPERTY('ErrorLogFileName') AS nvarchar(4000));
SELECT DefaultXEFolder =       LEFT(@full, LEN(@full) - CHARINDEX('\', REVERSE(@full)));  -- 이 폴더가 기본 .xel 저장 위치
 
 
--2 문제되는 쿼리 Plan 확인
DECLARE @Start datetime2 = '2025-10-23 01:57:13'; --시간필터
DECLARE @Query varchar(5000) = 'dbo'; --쿼리 필터
DECLARE @EventFileDir varchar(5000) = 'C:\DATA\MSSQL15.MSSQLSERVER\MSSQL\Log'; --XEvent 파일 경로 xp_readerror의 기본으로 생성시 로그파일 디렉토리와 동일함
DECLARE @EventFILE varchar(1000) = '\Recompiles*.xel'   --특별한 사유 없으면 그대로 사용할 것
SELECT session_id,  compile_time, recompile_cause_id, database_id, '0x'+ plan_handle as plan_handle, sql_text FROM
(
      SELECT
    --event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
   event_data.value('(event/@timestamp)[1]', 'datetime2(0)') AS compile_time,
    event_data.value('(event/data[@name="recompile_cause"]/text)[1]','VARCHAR(300)')   AS recompile_cause_id,
    event_data.value('(event/action[@name="session_id"]/value)[1]','VARCHAR(300)')     AS session_id,
    event_data.value('(event/action[@name="database_id"]/value)[1]','VARCHAR(300)')    AS database_id,
      event_data.value('(event/action[@name="plan_handle"]/value)[1]','VARCHAR(max)')     AS plan_handle,
    event_data.value('(event/action[@name="sql_text"]/value)[1]','nvarchar(max)') AS sql_text
FROM
      (
            SELECT CAST(event_data AS xml) AS event_data
            FROM sys.fn_xe_file_target_read_file(@EventFileDir + @EventFILE, NULL, NULL, NULL)
 
            --날짜필터조건
            WHERE  CAST(event_data AS xml).value('(event/@timestamp)[1]', 'datetime2(0)') >= @Start
            --SQL 필터조건
            AND CAST(event_data AS xml).value('(event/action[@name="sql_text"]/value)[1]','nvarchar(max)')  LIKE '%'+@Query+'%'
      ) AS x
)T1
ORDER BY compile_time DESC;
GO
 
 
-- 2-2. plan_handle + SQL 텍스트/DB 함께 조회
SELECT  cp.plan_handle,
            cp.usecounts,
        DB_NAME(t.dbid) AS DBName,
        t.text          AS SqlText,
        qp.query_plan
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) t
OUTER APPLY sys.dm_exec_query_plan(cp.plan_handle) qp
WHERE cp.plan_handle = 문제되는Plan확인에서확인한 plan_handle; -- '' 추가하면 안됨
 

 

--SQL지연 발생시 긴급 즉조치 
-- 3-1문제된 특정플랜 삭제( plan_handle이 확인될 경우)
DBCC FREEPROCCACHE (2 문제되는 쿼리 Plan 확인에서 확인된  plan_handle);
 
 
-- 3-2 컴파일된 Plan캐시 전체삭제(모든 실행계획이 재컴파일 되므로 이도저도 안될때 Failover 하기전에 최종방법으로 수행)
DBCC FREEPROCCACHE;

SQL Server 시간은 운영체제(Windows) 시간이므로, 정상적인 DST 전환만으로 SQL Server 서버 자체의 리부팅은 필요 없습니다.

SQL Server Windows API(GetSystemTimeAsFileTime) 시스템 시간을 그대로 사용합니다.

SYSDATETIME (Transact-SQL) - SQL Server | Microsoft Learn

 

해당 시간 전환 전/후로 아래와 같이 쿼리하여 현재 SQL Server가 인지하는 시간값을 retrieve 하실 수 있으며,

혹시 해당 시간 전환이 잘 안 될 경우 서버에서 제대로 시간 변경이 안 되는 것이므로 이 때 NTP 서버와의 통신 상태 점검 및 Windows 레벨에서의 시간 설정을 확인해 보실 수 있습니다.

--1. 현재 시간 확인
SELECT SYSDATETIMEOFFSET() AS now_with_offset;
--2. 
조회 시점 폴란드 Timezone DST 적용 여부 확인
SELECT * FROM sys.time_zone_info WHERE name LIKE 'Central European%';

 

sys.time_zone_info (Transact-SQL) - SQL Server | Microsoft Learn

WRITELOG SQL Server가 트랜잭션 로그에 변경 내용을 디스크에 기록할 때, 해당 작업이 완료될 때까지 대기하는 동안 발생하는 Wait Type입니다.
디스크 I/O 성능이 낮거나 순간적으로 트랜잭션 로그 기록 량이 많을 경우 이 대기가 증가할 수 있습니다.

로그 기록 지연은 해당 트랜잭션뿐 아니라 다른 트랜잭션에도 영향을 미쳐 전체 성능 저하를 유발할 수 있습니다.
WRITELOG 대기의 일반적인 원인은 다음 URL을 참고할 수 있습니다.

Troubleshoot slow SQL Server performance caused by I/O issues - SQL Server | Microsoft Learn

WRITELOG 대기 원인 분석을 위해 성능 모니터와 세션 정보 수집을 권장 드립니다.
성능 카운터는 전반적인 시스템 상태를 확인하는 데 유용하며, 세션 정보는 어떤 쿼리나 트랜잭션이 WRITELOG 대기를 발생시키는지 파악하고 세부 분석하는 데 도움이 됩니다.
수집 방법은 아래와 같습니다.

1. SQL 성능 카운터 수집 

Step1 cmd를 관리자권한으로 실행한 뒤 아래 명령을 실행하면 성능 수집기가 생성됩니다.
1초 주기로 최대 1024MB까지 수집되며, 수집 경로는 환경에 맞게 수정해 주시기 바랍니다.
 
WRITELOG 대기 발생 시점을 알고 있는 경우, 해당 짧은 기간 동안 1초 간격으로 수집할 것을 권장 드립니다.
발생 시점을 알 수 없는 경우에는 10~15초 간격으로 하루 정도 수집하는 것을 권장 드립니다.

Logman.exe create counter SQLPerfLog  -o "E:\sqlperflogs\PerfLog.csv" -f csv -v mmddhhmm -max 1024  -si 00:00:01 -c "\PhysicalDisk(*)\*" "\LogicalDisk(*)\*" "\Memory\*" "\Processor(_Total)\*" "\Process(*)\*" "\Network Interface(*)\*" "\Paging File(*)\*" "\System\*" "\SQLServer:Access Methods\*" "\SQLServer:Buffer Manager\*" "\SQLServer:Buffer Node(*)\*" "\SQLServer:Catalog Metadata(*)\*" "\SQLServer:Database Replica(*)\*" "\SQLServer:Databases(*)\*" "\SQLServer:Exec Statistics(*)\*" "\SQLServer:General Statistics\*" "\SQLServer:Latches\*" "\SQLServer:Locks(*)\*" "\SQLServer:Memory Manager\*" "\SQLServer:Plan Cache(*)\*" "\SQLServer:Resource Pool Stats(*)\*" "\SQLServer:SQL Errors(*)\*" "\SQLServer:SQL Statistics\*"  "\SQLServer:Transactions\*" "\SQLServer:Wait Statistics(*)\*" "\SQLServer:Workload Group Stats(*)\*

 
Step2 성능 수집을 시작합니다.
Logman.exe start SQLPerfLog
 
** 성능 수집을 중지하려면 아래 명령어를 입력합니다.
Logman.exe stop SQLPerfLog

2. 세션 정보

초단위로 DMV를 사용하여 세션 정보를 기록합니다.


Step1 로그 적재용 임시 데이터베이스 및 테이블을 생성합니다.

CREATE DATABASE tmpMonitoringDB
GO
 
use tmpMonitoringDB
GO
 
select top(1) getdate() as ctime, db_name(der.database_id) as database_name, der.*, dest.text, des.program_name
into dm_exec_requests_history
from sys.dm_exec_requests as der
inner join sys.dm_exec_sessions as des on der.session_id = des.session_id
cross apply sys.dm_exec_sql_text(der.sql_handle) as dest
GO
 
truncate table dm_exec_requests_history
GO

 
Step2 1초 단위로 루프를 수행하면서 세션 정보를 수집합니다.
무한 루프이므로 상황이 종료된 이후 쿼리를 직접 취소하시면 됩니다.

declare @startTime datetime = getdate()
while 1=1
begin
    insert into dm_exec_requests_history
    select
        getdate() as ctime,
        db_name(der.database_id) as database_name,
        der.*,
        dest.text,
        des.program_name
    from sys.dm_exec_requests as der
    inner join sys.dm_exec_sessions as des on der.session_id = des.session_id
    cross apply sys.dm_exec_sql_text(der.sql_handle) as dest
    where der.session_id <> @@spid
     waitfor delay '00:00:01'
end

+ Recent posts