블로그 이미지
보미아빠

카테고리

보미아빠, 석이 (528)
밥벌이 (16)
싸이클 (1)
일상 (1)
Total
Today
Yesterday

달력

« » 2025.8
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

공지사항

최근에 올라온 글

 

스레드 구조 차이
Database Mirroring: 데이터베이스마다 독립적인 전용 워커 스레드를 사용하여 데이터 전송, 로그 적용, 세션 관리 등을 수행한다.
AlwaysOn AG: 모든 데이터베이스가 HADR Thread Pool(hadr_thread_pool_worker)을 공유하여 스레드를 사용한다. 따라서 데이터베이스 개수가 증가하더라도 스레드 수가 1:1로 비례 증가하지 않는다.

 

DB 수 증가에 따른 영향
Mirroring은 데이터베이스 수가 늘어날수록 필요한 워커 스레드 수도 선형적으로 증가한다. 스레드 수가 많아지면 SQL Server의 max worker threads 한계에 영향을 줄 수 있으며, CPU 스케줄링과 병렬 처리에도 부하가 커진다.

 

메모리 사용량
SQL Server에서 워커 스레드 하나당 약 2MB의 스택 메모리가 예약된다. 이 메모리는 max server memory 설정에 포함되므로 buffer pool 이나 plan cache 등 메모리 용량 산정 시 고려해야 한다. 1000개의 max worker thread를 설정하면 max server mermory 가 4GB 이라면 2GB 가 thread 메모리로 사용될 수 있다. (최대값으로 가정 하는게 좋고 실제 동작을 보면, worker thread 개수는 자동으로 올라가고 내려간다.)

 

결론 및 설계 포인트
Mirroring 환경에서는 DB 개수가 많을수록 워커 스레드 증가 → 메모리 사용량 증가 → max server memory를 설정할 때 이 부분을 감안해야 한다. AG 환경은 Thread Pool을 사용하므로 DB 개수 증가의 영향이 상대적으로 적다. 따라서 이중화 솔루션 설계 시 스레드 구조와 메모리 소비 패턴을 반영한 용량 계획이 필요하다.

 

 

추가 고려사항

* 데이터베이스 미러링(DM)은 DB당 단일 스레드만 쓰지 않음. 일반적으로 DB당 약 2개 이상의 스레드를 소비하고, 미러(secondary) 쪽에서는 (Enterprise의 경우) 병렬 REDO(roll‑forward) 스레드가 추가로 할당될 수 있음
* 스레드 1개당 스택 메모리(64비트 환경) 는 대략 2 MB 이므로(스택/MTL 관련), 스레드 증가가 메모리(및 max server memory 계산)에 직접 영향을 줌 
* DM 미러에서의 병렬 REDO(Enterprise) 는 “CPU 4개당 1개의 REDO 스레드(대략)” 로 동작하는 식으로 최적화 됨

 

스크린샷1 (142개 미러링 데이터베이스가 있을때 Primary)

 

스크린샷2 (142개 미러링 데이터베이스가 있을때 Secondary)

 

결론?

* DM에서 미러링 대상서버의 max workerthread 숫자 조절 필요 

* 제발 데이터베이스를 너무 많이 만들지 말자 (살려주세요 이건 아니잖아요....)

 

추가 전문 아티클 

https://www.dbi-services.com/blog/sql-server-alwayson-number-of-databases-and-the-hadr-worker-pool/

 

SQL Server AlwaysOn: number of databases and the hadr worker pool

SQL Server AlwaysOn: number of databases and the hadr worker pool

www.dbi-services.com

 

 

Often, when I’m in charge to implement an SQL Server AlwaysOn infrastructure with availability groups, customers ask me if it exists some best practices and recommendations about the maximum number of databases in an availability group planning. In fact, we have to keep in mind the relation between databases and the SQL Server worker pool in this architecture. Remember that the SQL Server AlwaysOn and availability group feature is an extension of the SQL Server mirroring and we retrieve the same concept of worker thread usage with a different architecture: Mirroring feature dedicates worker threads for each database compared to SQL Server AlwaysOn that uses requests queue and worker pool (aka HADR pool). Let’s see how the HADR worker pool behaves and its tight relationship with the databases.

First of all, let’s take a look at the Microsoft documentation here – “Prerequisites, Restrictions, and Recommendations for AlwaysOn Availability Groups – and let’s have a focus on the “Thread Usage by Availability Groups” section. After reading the content we can notice that the number of databases is an important part of the minimum hadr pool size calculation among others factors. Others factors are the following:

  • Number of replicas
  • Log capture thread
  • Log send queue thread
  • Message handler thread

Another point to keep in mind is that the size of the HADR worker pool is capped by the “max worker thread” parameter as shown below:

Hadr pool size max = max worker threads – 40

I also encourage you to read the excellent article of Bob Dorr that provides a formula to estimate the minimum size of the HADR worker pool. Once again we can notice that the number of databases is still relevant here. As explained by Bob Dorr, the max databases parameter concerns only the “active” databases where “active”.

Let’s perform a quick calculation of the min pool size in the worst case (all databases are active) required on the primary replica with several configurations based on the Bob Dorr’s formula:

Min Pool Size = Max Databases * (Log Capture Thread + (Log Send Thread * Nb secondary replicas)) + Message Handler Thread

  One secondary replica Two secondary replicas
100 databases 100 * 2 + 1 = 201 100 * 3 + 1 = 301
500 databases 500 * 2 + 1 = 1001 500 * 3 + 1 = 1501
1000 databases 1000 * 2 + 1 = 2001 1000 * 3 + 1 = 3001

 

Then let me show you a more real picture with my lab that includes a SQL Server AlwaysOn 2014 (64 bits) with one availability group and two replicas (basically one primary and one secondary). Each replica is configured with 4 VCPU and no affinity mask, so all the CPU are visible from each replica.

Next, according to the Microsoft documentation here, we will be capped by the “max worker threads” parameters equal to 512 in my case. Thus, the maximum size of the HADR worker pool size will be 512 – 40 = 472.

Now let’s play with different scenarios:

1- First scenario (no availability groups)

The first scenario includes an environment with no availability groups. The global number of worker threads is as following:

1
2
3
4
5
select
    scheduler_id,current_tasks_count, current_workers_count,active_workers_count,work_queue_count
from sys.dm_os_schedulers
where status = 'VISIBLE ONLINE'
go

This view is not perfect in our case because it includes all the worker threads of the SQL Server instance (hadr worker threads are included to this number). But we will use it as a starting point because there is no activity on my lab and we can admit that the active_workers_count column value will be relatively close than the number of HADR worker threads.

 

2 – Second scenario (availability group with 100 idle databases)

The second scenario consists in adding 100 databases to my newly availability group but there is no activity. Let’s have a look at the global number of worker threads:

The number of worker threads has increased but this is not a big deal here because the availability databases are not very active. At this point I want to introduce another way to get the number of hadr worker threads by using the extended events and the hadr_thread_pool_worker_start event:

The extended event session I used during my test …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create event session HadrThreadPoolWorkerStart
on server
add event sqlserver.hadr_thread_pool_worker_start
add target package0.event_file
(
    set filename = N'E:\SQLSERVER\SQL14\backup\HadrThreadPoolWorkerStart.xel'
)
with
(
    max_memory = 4096 KB,
    event_retention_mode = allow_single_event_loss,
    max_dispatch_latency = 30 seconds,
    max_event_size = 0 KB,
    memory_partition_mode = none,
    track_causality = off,
    startup_state = on
);
go

… the data extraction script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
declare @top_count int;
 
set @top_count = 100;
 
;with xe_cte as
(
    select
            object_name,
            cast(event_data as xml) as event_data
    from sys.fn_xe_file_target_read_file ( 'E:\SQLSERVER\SQL14\backup\HadrThreadPoolWorkerStart*.xel', null, null,  null)
)
 
select top (@top_count)
       DATEADD(hh, DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP), event_data.value('(/event/@timestamp)[1]', 'datetime2')) AS [timestamp],
       event_data.value('(/event/data/value)[3]', 'int') as active_workers,
       event_data.value('(/event/data/value)[2]', 'int') as idle_workers,
       event_data.value('(/event/data/value)[1]', 'int') as worker_limit,
       event_data.value('(/event/data/value)[4]', 'varchar(5)') as worker_start_success
from xe_cte
order by [timestamp] desc;

… and the result:

 

3- Third scenario (availability group with 200 idle databases)

Let’s add 100 additional databases and let’s have a look at the sys.dm_os_schedulers DMV:

Here, the result from the extended event session:

As the previous scenario, the number of active worker threads is still low because there is no activity on the availability databases.

 

4- Fourth scenario (availability group with 300 “active” databases)

In this scenario, let’s add 100 additional databases (for a total of 300 databases) but this time we will simulate a workload that will perform randomly on different availability databases.

Let’s take a look at the extended event session to see the number of active workers:

As you can notice, the story is not the same. A big increase of the number of HADR active workers (roughly 258) can be noticed here. However, if I refer to Bob Dorr’s formula in his article, we may be disappointed by the number of threads that we get from the extended event session that is theoretically lower than we can expect for a total number of 300 databases (300 *2 + 1 = 601). At this point, my guess is that my workload and my lab configuration didn’t allow to reach out the HADR worker thread starvation scenario but in fact, this is not so important here because the goal was simply to highlight the difference between the number of active worker threads between an idle and active situation.

 

5- Fifth scenario : case of a worker threads exhaustion scenario

In the previous scenarios, we saw that increasing the number of databases can have an impact on an availability group. From my point of view, facing this situation is probably the worst scenario but we have to take into account in your design regarding your context.

In this scenario, I will voluntary increase the number of databases up to 500 to reach out more quickly the number of allowed active worker threads.

Here’s a picture of such scenario from the extended event session:

We are in a situation of HADR worker pool starvation (active workers = work limit = 472). Moreover, the work_start_success column value (= false) indicates the SQL Server’s inability to start a new HADR worker.

After a big coffee break, here what I saw from the SQL Server error log. This message indicates an excessive worker thread contention after 15 minutes.

If you see this error message in your availability groups environment, it’s time to check the number of availability databases. You may face this situation more often than you may think. I can see a lot of customer situations where availability groups are tied to consolidated databases environments. In such situation, adding a database can become a semi-automatic process and the number of HADR worker threads can be out quickly of the control.

What is the solution in this case? Add more worker threads? More processors? Reduce the number of databases? I guess you know what I will answer: it depends on your context.

So, in this blog post we’ve seen that the number of databases in an availability group may be a problem regarding your infrastructure and the number of databases hosted in your availability group. Therefore, this topic can be probably one of your concern if you plan to consolidate a lot of databases in your availability groups. In this case, I would probably recommend to my customers to include the hadr worker pool in his monitoring tool.

Hope this helps.

By David Barbarin

 

 

Posted by 보미아빠
, |

메시지 3201, 수준 16, 상태 2, 줄 1
Cannot open backup device '\\10.0.1.11\backup\a.bak'. Operating system error 1272(You can't access this shared folder because your organization's security policies block unauthenticated guest access. These policies help protect your PC from unsafe or malicious devices on the network.).
메시지 3013, 수준 16, 상태 1, 줄 1
RESTORE HEADERONLY is terminating abnormally.

 

 

Windows 2019 이상에서는 아래 옵션이 적용되어 있어야 한다. 

Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters" -Name AllowInsecureGuestAuth -Value 1 -Type DWord
gpupdate /force

 

Posted by 보미아빠
, |

MSDTC 설정

카테고리 없음 / 2025. 8. 6. 17:16

아래와 같이 Distributed Transaction을 사용하려면 ?

EXEC master.dbo.sp_addlinkedserver @server = N'a', @srvproduct=N'',@provider = 'sqlncli',@datasrc = '10.10.60.17,1433'
EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname=N'a',@useself=N'False',@locallogin=NULL,@rmtuser=N'admin',@rmtpassword='adminP@ss0rd'




SET XACT_ABORT ON
GO
BEGIN DISTRIBUTED TRANSACTION
-- 서버A 작업
INSERT INTO bdb.dbo.tbly (idx) VALUES (1);
-- 서버B 작업
INSERT INTO a.adb.dbo.tblx (idx) VALUES (2);
COMMIT TRANSACTION

 

 

아래 설정이 양쪽에 동일하게 잘 설정 되어야 동작함

 

service process

C:\>sc query msdtc

 

hosts

admin notepad c:\windows\system32\drivers\etc\hosts

 

dcomcnfg

net stop msdtc
net start msdtc

 

port

Posted by 보미아빠
, |

C:\Program Files\Smart Storage Administrator\ssacli\bin>ssacli ctrl slot=0 ld all show status
   logicaldrive 1 (10.17 TB, RAID 5): OK

 

C:\Program Files\Smart Storage Administrator\ssacli\bin>ssacli ctrl slot=0 pd all show status
   physicaldrive 1I:1:1 (port 1I:box 1:bay 1, 1.9 TB): OK
   physicaldrive 1I:1:2 (port 1I:box 1:bay 2, 1.9 TB): OK
   physicaldrive 1I:1:3 (port 1I:box 1:bay 3, 1.9 TB): OK
   physicaldrive 1I:1:4 (port 1I:box 1:bay 4, 1.9 TB): OK
   physicaldrive 1I:1:5 (port 1I:box 1:bay 5, 1.9 TB): OK

Posted by 보미아빠
, |
declare @dbName nvarchar(1000) ='storageTest'
declare @incSizeGigaBytes int = 10

-- internal variable 
declare @currentGigaBytes bigint 
, @tobeGigaBytes bigint 
, @message nvarchar(4000) = N''
, @mdfFileName nvarchar(1000) = N''
, @fileName nvarchar(1000) = N''
, @sql nvarchar(max)


select @currentGigaBytes = (size * 8) / power(2,20) , @fileName = name 
from master.dbo.sysaltfiles
where dbid = db_id(@dbName)
	and fileid = 1 

select @tobeGigaBytes = @currentGigaBytes + @incSizeGigaBytes 

select @message = N'currentGigaBytes : ' + cast (@currentGigaBytes as nvarchar(4000)) + ', tobeGigaBytes : ' +  cast (@tobeGigaBytes as nvarchar(4000)) 
select @message 

-- log
RAISERROR(@message, 1,1 ) WITH LOG, NOWAIT
set @sql = 'ALTER DATABASE '+@dbName+' MODIFY FILE ( NAME = N'''+@fileName+''', SIZE = '+cast(@tobeGigaBytes as nvarchar(100))+'GB, FILEGROWTH = 512MB)'
print @sql 

-- cmd 
exec (@sql)
Posted by 보미아빠
, |

https://www.sqlservercentral.com/blogs/disable-or-turn-off-sql-server-telemetry-service

Disable or Turn-off SQL Server Telemetry Service

Dharmendra Keshari, 2018-08-13 (first published: 2018-07-30)

With SQL Server 2016, Microsoft is installing SQL Server Telemetry or CEIP (Customer Experience Improvement Program) Services by default. It sends feature usage info back to Microsoft. You cannot skip the installation of these services. I find most of the DBAs even don’t aware that they have installed the service on their host. In case, you are wondering how to disable it. You can follow the below steps;

How to disable SQL Server Telemetry or CEIP Services?

Let’s follow the  below steps to disable the Telemetry;

  • Log on to the host where you have installed the SQL Server
  • Go to Start > Programs > Select SQL Server 2016 Error and Usage Reporting
    • Unselect “Send Windows Error and SQL Error Reports…”
    • Unselect “Send Feature Usage Reports…”
    • Click on the options, make sure all components are unselected
    • Click OK

Posted by 보미아빠
, |

예전에는 CU (누적 업데이트 라인), GDR (보안 업데이트 라인) 이렇게 2개만 있었다. 지금도 여전히 GDR 라인만 적용해 보안 업데이트만 할 수 있고, CU가 한번이라도 적용된 서버에는 순수 GDR을 설치할 수 없다. (GDR 최상위 버전도 항상 CU 버전보다 낮았다.) 그러나, MU 정책을 바꾸면서, CU+GDR이 나오고 있다. MS 업데이트 버그로 인해 고생을 좀 해보고 나니, CU+GDR 로 한개 정도 오래된 버전을 설치하는게 가장 좋은 정책이지 않을까 생각한다. (너무 최신버전을 적용하면 벤더도 자기네 버그인지 모르니 분석에 오래 걸린다.) 

 

https://learn.microsoft.com/ko-kr/troubleshoot/sql/releases/sqlserver-2019/build-versions

 

CU 라인을 보면 아래와 같다. 

 

Posted by 보미아빠
, |

멀티부팅 환경이라면, 암호를 알고 있는 OS로 부팅해 관리자 모드 cmd로 부팅 뒤 아래 두 개의 실행파일을 바꿔두면, 다음 부팅 화면에서 접근성 메뉴(utilman.exe)나 shift 5번 연타(sethc.exe)를 하면, OS계정의 암호를 변경하거나 Administrator 계정을 활성화 암호를 변경할 수 있다. 멀티 부팅이 아니면 윈도우 설치 USB를 만들어 같은 작업을 할 수 있다. 

 

삽질 여담으로, utilman.exe의 경우 cmd 창이 바로 닫히는 경우가 있더라, sethc.exe로 shift 5연타는 잘 동작함. 그리고 cmd 창이 오류가 있다고 보이는데, 실행이 다 되더라 그냥 빠르게 닫히는 거더라. 그냥 sethc.exe를 이용하는게 좋고 둘 다 바꾸어 하는게 좋다. 그리고, 바로 shift 리부팅으로는 들어가 터미널 열려고 하면, 다시 암호를 물어보더라(그래서 암호를 알고있는 멀티부팅 서버로 들어가 터미널을 열고 암호를 모르는 서버의 파일을 바꿔치기 함)....그러니, 터미널로 들어갈 방법이 없더라. 윈도우 설치용 USB가 있는게 가장 좋고, 나는 운좋게 멀티 부팅 환경이라 따로 설치 usb 만들지 않고 성공했다. (암호를 계속 바꾸라 하니....기억이 안난다...ㅋ)

 

https://geekorea.com/how-to-reset-password-when-you-forgot-windows/

https://blog.naver.com/rj45/220296999874

 

[Windows] 윈도우 비밀번호 분실

간혹 윈도우 비밀번호를 분실 할 경우가 발생한다. 그것도 직장 상사인 경우에는 비밀번호를 변경하거나,&n...

blog.naver.com

 

<백업>

utilman.exe -> utilman.exe.bak

sethc.exe -> sethc.exe.bak

 

<변경>

cmd.exe -> utilman.exe 로 복사 (로그온 화면에서 사용자 환경 버튼 실행, 오른쪽 아로 있음)

cmd.exe -> sethc.exe 로 복사 (로그온 화면에서 shift 5번 연타시 실행)

 

net user 계정 패스워드

net user 계정 /active:yes

Posted by 보미아빠
, |

파일 삭제가 안되는 경우, 점유한 프로세스를 찾아 종료 후 파일을 지워야 한다. 

그런데, 이런 방법도 있다. (프로세스를 어떤 이유에서든 찾을수 없을때 아래 방법이 유용하더라) 

 

https://learn.microsoft.com/en-us/sysinternals/downloads/handle

 

Handle - Sysinternals

This handy command-line utility will show you what files are open by which processes, and much more.

learn.microsoft.com

 

# 세션 1 [프로세스가 파일을 읽기쓰기로 잡았다 라고 시뮬레이션]
$fs = [System.IO.File]::Open("C:\Users\minsouk\Downloads\Handle\a.txt", 'Open', 'ReadWrite', 'None')

 

핸들확인

 

파일삭제

Posted by 보미아빠
, |

https://www.sql.kiwi/2014/06/filtered-index-side-effect/

 

filtered index 해결하기 힘든 문제가 있다. 

update(insert update delete merge 모두를 그냥 update 라 한다)에서 narrow 와 wide 플랜을 선택할 때 traceflag를 이용하지 말고 top 절과 optimize for 를 이용하는 방법도 있다. 

 

-- All qualifying rows will be deleted
DECLARE @Rows bigint = 9223372036854775807;

-- Optimize the plan for deleting 100 rows
DELETE TOP (@Rows)
FROM dbo.[Data]
OUTPUT
    Deleted.RowID,
    Deleted.SomeValue,
    Deleted.StartDate,
    Deleted.CurrentFlag
WHERE StartDate = CONVERT(date, '20140106', 112)
OPTION (OPTIMIZE FOR (@Rows = 100));

 

 

CTE 를 쓰면 어떤 잇점이 있을까?

쿼리 가독성과 대상의 확인 select 가 하나의 쿼리로 할 수 있다. (select 만 실행 가능하다. )

옵티마이저가 이상한 플랜 만들수 있으니 성능 테스트 해보고 쓰자~

 

WITH 
    LastRowPerDay AS
    (
        SELECT 
            D.CurrentFlag
        FROM dbo.[Data] AS D
        WHERE 
            D.RowID =
            (
                SELECT MAX(D2.RowID)
                FROM dbo.[Data] AS D2
                WHERE D2.StartDate = D.StartDate
            )
    )
UPDATE LastRowPerDay
SET CurrentFlag = 'true';


-- VS

UPDATE D
SET CurrentFlag = 'true'
FROM dbo.[Data] AS D
WHERE D.RowID = (
    SELECT MAX(D2.RowID)
    FROM dbo.[Data] AS D2
    WHERE D2.StartDate = D.StartDate
);

 

 

 

An Unexpected Side-Effect of Adding a Filtered Index

Adding a filtered index can have surprising side-effects on existing queries, even where it seems the new filtered index is completely unrelated.

www.sql.kiwi

 

Monday, 02 June 2014

An Unexpected Side-Effect of Adding a Filtered Index

Adding a filtered index can have surprising side-effects on existing queries, even where it seems the new filtered index is completely unrelated. This post looks at an example affecting DELETE statements that results in poor performance and an increased risk of deadlock.

Note: The behaviour described in this article is current as of November 2024.
(tested on SQL Server 2022 CU15)

Test Environment

The following table will be used throughout this post:

CREATE TABLE dbo.[Data]
(
    RowID integer IDENTITY NOT NULL,

    CONSTRAINT PK_Data_RowID
        PRIMARY KEY CLUSTERED (RowID),

    SomeValue integer NOT NULL,
    StartDate date NOT NULL,
    CurrentFlag bit NOT NULL,
    Padding char(50) NOT NULL
        DEFAULT REPLICATE('ABCDE', 10)
);

This next statement creates 499,999 rows of sample data:

INSERT dbo.[Data] 
    WITH (TABLOCKX)
(
    SomeValue, 
    StartDate, 
    CurrentFlag
)
SELECT
    SomeValue = CONVERT(integer, RAND(n) * 1e6) % 1000,
    StartDate = DATEADD(DAY, (N.n - 1) % 31, CONVERT(date, '20140101', 112)),
    CurrentFlag = CONVERT(bit, 0)
FROM dbo.Numbers AS N
WHERE 
    N.n >= 1 
    AND N.n < 500 * 1000;

That uses a Numbers table as a source of consecutive integers from 1 to 499,999. In case you do not have one of those in your test environment, the following code can be used to efficiently create one containing integers from 1 to 1,000,000:

WITH
    N1 AS (SELECT N1.n FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (n)),
    N2 AS (SELECT L.n FROM N1 AS L CROSS JOIN N1 AS R),
    N3 AS (SELECT L.n FROM N2 AS L CROSS JOIN N2 AS R),
    N4 AS (SELECT L.n FROM N3 AS L CROSS JOIN N2 AS R),
    N AS (SELECT ROW_NUMBER() OVER (ORDER BY n) AS n FROM N4)
SELECT
    -- Destination column type integer NOT NULL
    ISNULL(CONVERT(integer, N.n), 0) AS n
INTO dbo.Numbers
FROM N
OPTION (MAXDOP 1);

ALTER TABLE dbo.Numbers
ADD CONSTRAINT PK_Numbers_n
PRIMARY KEY (n)
WITH (SORT_IN_TEMPDB = ON, MAXDOP = 1);

The basis of the later tests will be to delete rows from the test table for a particular StartDate. To make the process of identifying rows to delete more efficient, add this nonclustered index:

CREATE NONCLUSTERED INDEX 
    IX_Data_StartDate
ON dbo.[Data]
    (StartDate);

The sample data

Once those steps are completed, the data will look like this:

SELECT TOP (100)
    D.RowID,
    D.SomeValue,
    D.StartDate,
    D.CurrentFlag,
    D.Padding
FROM dbo.[Data] AS D
ORDER BY
    D.RowID;

The SomeValue column data may be slightly different due to the pseudo-random generation, but this difference is not important. Overall, the sample data contains 16,129 rows for each of the 31 StartDate dates in January 2014:

SELECT 
    D.StartDate, 
    NumRows = COUNT_BIG(*)
FROM dbo.[Data] AS D
GROUP BY
    D.StartDate
ORDER BY 
    D.StartDate;

The last step we need to perform to make the data somewhat realistic, is to set the CurrentFlag column to true for the highest RowID for each StartDate. The following script accomplishes this task:

WITH 
    LastRowPerDay AS
    (
        SELECT 
            D.CurrentFlag
        FROM dbo.[Data] AS D
        WHERE 
            D.RowID =
            (
                SELECT MAX(D2.RowID)
                FROM dbo.[Data] AS D2
                WHERE D2.StartDate = D.StartDate
            )
    )
UPDATE LastRowPerDay
SET CurrentFlag = 'true';

The execution plan for this update features a Segment-Top combination to efficiently locate the highest RowID per day:

Notice how the execution plan bears little resemblance to the written form of the query. This is a great example of how the optimizer works from the logical SQL specification, rather than implementing the SQL directly. In case you are wondering, the Eager Table Spool in that plan is required for Halloween Protection.

Deleting a Day of Data

With the preliminaries completed, the task at hand is to delete rows for a particular StartDate. This is the sort of query you might routinely run on the earliest date in a table, where the data has reached the end of its useful life.

Taking 1 January 2014 as our example, the test delete query is simple:

DELETE FROM
    dbo.[Data]
WHERE 
    1 = 1
    AND StartDate = CONVERT(date, '20140101', 112);

The execution plan is likewise pretty simple, though worth looking at in a bit of detail:

Note: If you don’t see a separate Index Delete operator, try changing the maximum memory available to SQL Server to around 4GB. You may then need to repopulate the test table or clear the plan cache to get the desired plan shape.

Plan analysis

The Index Seek on the far right uses the nonclustered index to find rows for the specified StartDate value. It returns just the RowID values it finds, as the operator tooltip confirms:

If you are wondering how the StartDate index manages to return the RowID, remember that RowID is the unique clustered index for the table, so it is automatically included in the StartDate nonclustered index.

The next operator in the plan is the Clustered Index Delete. This uses the RowID value found by the Index Seek to locate rows to remove.

The final operator is an Index Delete. This removes rows from the nonclustered index IX_Data_StartDate that are related to the RowID removed by the Clustered Index Delete. To locate these rows in the nonclustered index, the query processor needs the StartDate (the key for the nonclustered index).

Remember the original Index Seek did not return the Start Date, just the RowID. So how does the query processor get the StartDate for the index delete? In this particular case, the optimizer might have noticed that the StartDate value is a constant and optimized it away, but this is not what happened. The answer is that the Clustered Index Delete operator reads the StartDate value for the current row and adds it to the stream. Compare the Output List of the Clustered Index Delete shown below, with that of the Index Seek just above:

It might seem surprising to see a Delete operator reading data, but this is the way it works. The query processor knows it will have to locate the row in the clustered index in order to delete it, so it might as well defer reading columns needed to maintain nonclustered indexes until that time, if it can.

Adding a Filtered Index

Now imagine someone has a crucial query against this table that is performing badly. The helpful DBA performs an analysis and adds the following filtered index:

CREATE NONCLUSTERED INDEX
    FIX_Data_SomeValue_CurrentFlag
ON dbo.[Data] 
    (SomeValue)
INCLUDE 
    (CurrentFlag)
WHERE 
    CurrentFlag = 'true';

The new filtered index has the desired effect on the problematic query and everyone is happy. Notice that the new index does not reference the StartDate column at all, so we do not expect it to affect our day-delete query at all.

Deleting a day with the filtered index in place

We can test that expectation by deleting data for a second time:

DELETE FROM
    dbo.[Data]
WHERE 
    1 = 1
    AND StartDate = CONVERT(date, '20140102', 112);

Suddenly, the execution plan has changed to a parallel Clustered Index Scan:

If you don’t see a parallel plan, ensure your cost threshold for parallelism is set to the default value of 5 for these tests.

Notice there is no separate Index Delete operator for the new filtered index. The optimizer has chosen to maintain this index inside the Clustered Index Delete operator. This is highlighted in Plan Explorer as shown above (“+1 non-clustered indexes”) with full details in the tooltip:

If the table is large, this change to a parallel scan might be very significant. What happened to the nice Index Seek on StartDate, and why did a completely unrelated filtered index change things so dramatically?

Finding the problem

The first clue comes from looking at the properties of the Clustered Index Scan:

As well as finding RowID values for the Clustered Index Delete operator to delete, this operator is now reading CurrentFlag values. The need for this column is unclear, but it does at least begin to explain the decision to scan: The CurrentFlag column is not part of our StartDate nonclustered index.

We can confirm this by rewriting the delete query to force the use of the StartDate nonclustered index:

DELETE FROM D
FROM dbo.[Data] AS D
    WITH (INDEX(IX_Data_StartDate))
WHERE 
    1 = 1
    AND StartDate = CONVERT(date, '20140103', 112);

The execution plan is closer to its original form, but it now features a Key Lookup:

The Key Lookup properties confirm this operator is retrieving CurrentFlag values:

You might also have noticed the warning triangles in the last two plans. These are missing index warnings:

This is further confirmation that SQL Server would like to see the CurrentFlag column included in the nonclustered index. The reason for the change to a parallel Clustered Index Scan is now clear: The query processor decides that scanning the table will be cheaper than performing the Key Lookups.

Yes, but why?

This is all very weird. In the original execution plan, SQL Server was able to read extra column data needed to maintain nonclustered indexes at the Clustered Index Delete operator. The CurrentFlag column value is needed to maintain the filtered index, so why does SQL Server not just handle it in the same way?

The short answer is that it can, but only If the filtered index is maintained in a separate Index Delete operator. We can force this for the current query using undocumented trace flag 8790. Without this trace flag, the optimizer chooses whether to maintain each index in a separate operator or as part of the base table operation.

-- Forced wide update plan
DELETE FROM 
    dbo.[Data]
WHERE 
    1 = 1
    AND StartDate = CONVERT(date, '20140105', 112)
OPTION 
    (QUERYTRACEON 8790);

The execution plan is back to seeking the StartDate nonclustered index:

The Index Seek returns just RowID values (no CurrentFlag):

The Clustered Index Delete now reads the columns needed to maintain the nonclustered indexes, including CurrentFlag:

This data is eagerly written to a table spool, which is replayed for each index that needs maintaining. Notice also the explicit Filter operator before the Index Delete operator for the filtered index.

Another pattern to watch out for

This problem does not always result in a table scan instead of an index seek. To see an example of this, add another index to the test table:

CREATE NONCLUSTERED INDEX
    IX_Data_SomeValue_CurrentFlag
ON dbo.[Data] 
    (SomeValue, CurrentFlag);

Note this index is not filtered, and does not involve the StartDate column. Now try a day-delete query again:

DELETE FROM
    dbo.[Data]
WHERE 
    1 = 1
    AND StartDate = CONVERT(date, '20140104', 112);

The optimizer now comes up with this monster:

This query plan has a high surprise factor, but the root cause is the same. The CurrentFlag column is still needed, but now the optimizer chooses an index intersection strategy to get it instead of a table scan. Using the trace flag forces a per-index maintenance plan and sanity is once again restored (the only difference is an extra spool replay to maintain the new index):

Only Filtered Indexes Cause This

This issue only occurs if the optimizer chooses to maintain a filtered index in a Clustered Index Delete operator. Non-filtered indexes are not affected, as the following example shows. The first step is to drop the filtered index:

DROP INDEX 
    FIX_Data_SomeValue_CurrentFlag
ON dbo.[Data];

Now we need to write the query in a way that convinces the optimizer to maintain all the indexes in the Clustered Index Delete. My choice for this is to use a variable and a hint to lower the optimizer’s row count expectations:

-- All qualifying rows will be deleted
DECLARE @Rows bigint = 9223372036854775807;

-- Optimize the plan for deleting 100 rows
DELETE TOP (@Rows)
FROM dbo.[Data]
OUTPUT
    Deleted.RowID,
    Deleted.SomeValue,
    Deleted.StartDate,
    Deleted.CurrentFlag
WHERE StartDate = CONVERT(date, '20140106', 112)
OPTION (OPTIMIZE FOR (@Rows = 100));

The execution plan is:

Both nonclustered indexes are maintained by the Clustered Index Delete:

The Index Seek returns only the RowID:

The columns needed for the index maintenance are retrieved internally by the delete operator. These details are not exposed in show plan output (so the output list of the delete operator would be empty). I added an OUTPUT clause to the query to show the Clustered Index Delete once again returning data it did not receive on its input:

Final Thoughts

This is a tricky limitation to work around. On the one hand, we generally do not want to use undocumented trace flags in production systems.

The natural ‘fix’ is to add the columns needed for filtered index maintenance to all nonclustered indexes that might be used to locate rows to delete. This is not a very appealing proposition, from a number of points of view. Another alternative is to just not use filtered indexes at all, but that is hardly ideal either.

My feeling is that the query optimizer ought to consider a per-index maintenance alternative for filtered indexes automatically, but its reasoning appears to be incomplete in this area right now (and based on simple heuristics rather than properly costing per-index/per-row alternatives).

To put some numbers around that statement, the parallel clustered index scan plan chosen by the optimizer came in at 5.5 units in my test. The same query with the trace flag estimates a cost of 1.4 units. With the third index in place, the parallel index-intersection plan chosen by the optimizer had an estimated cost of 4.9, whereas the trace flag plan came in at 2.7 units (all tests on SQL Server 2014 RTM CU1 build 12.0.2342 under the 120 cardinality estimation model and with trace flag 4199 enabled).

I regard this as behaviour that should be improved. You can vote to agree with me on this Azure Feedback item.

Thanks for reading.

Posted by 보미아빠
, |

최근에 달린 댓글

최근에 받은 트랙백

글 보관함