#title Parallelism Enhancements in SQL Server 2008
[[TableOfContents]]
sql server 2016 sp1(cu3) 이후 버전은 ENABLE_PARALLEL_PLAN_PREFERENCE 힌트를 쓰면 된다.
https://www.mssqltips.com/sqlservertip/4939/how-to-force-a-parallel-execution-plan-in-sql-server-2016/
이 문서의 내용은 SQL Server Magazine 2010년 7월호에 개재된 내용 및 필자가 풀어 쓴 것이다. 각각의 2005(sp3), 2008(sp1) 버전의 SQL Server가 설치된 테스트 환경이 필요하다. (이 문서는 2008R2에서 테스트함.) 다음의 코드를 2005, 2008 각 버전의 인스턴스에서 실행시킨다.
==== 쿼리플랜비교1:Join ====
'''테스트 준비'''
{{{
-- Sample datatabase
SET NOCOUNT ON;
IF DB_ID('testparallel') IS NULL CREATE DATABASE testparallel;
GO
USE testparallel;
GO
-- Helper function GetNums
-- returns a sequence of integers of a requested size
IF OBJECT_ID('dbo.GetNums', 'IF') IS NOT NULL
DROP FUNCTION dbo.GetNums;
GO
CREATE FUNCTION dbo.GetNums(@n AS BIGINT) RETURNS TABLE
AS
RETURN
WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1),
L1 AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
L2 AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
L3 AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
L4 AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
L5 AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
SELECT TOP (@n) n FROM Nums ORDER BY n;
GO
USE testparallel;
IF OBJECT_ID('dbo.T1') IS NOT NULL DROP TABLE dbo.T1;
IF OBJECT_ID('dbo.T2') IS NOT NULL DROP TABLE dbo.T2;
GO
CREATE TABLE dbo.T1
(
col1 INT NOT NULL,
col2 INT NOT NULL,
filler BINARY(100) NOT NULL DEFAULT(0x)
);
CREATE UNIQUE CLUSTERED INDEX idx_cl_col1 ON dbo.T1(col1);
INSERT INTO dbo.T1 WITH(TABLOCK) (col1, col2)
SELECT n AS col1, n AS col2
FROM dbo.GetNums(1000000);
SELECT * INTO dbo.T2 FROM dbo.T1;
CREATE UNIQUE CLUSTERED INDEX idx_cl_col1 ON dbo.T2(col1);
GO
}}}
다음의 SQL을 2005, 2008 각 인스턴스에서 실행시켜보자.
{{{
SELECT *
FROM dbo.T1
JOIN T2
ON T1.col1 = T2.col1
WHERE T1.col2 <= 100;
}}}
'''2005 버전'''
attachment:ParallelismEnhancementsInSQLServer2008/p01.jpg?width=850
'''2008 버전'''
attachment:ParallelismEnhancementsInSQLServer2008/p02.jpg?width=850
2005버전의 경우는 Thread 4를 제외하고는 작업을 하지 않을 테지만, 2008의 경우는 첫 번째 작업을 끝내고 난 결과를 다시 여러 Thread에 분배함으로써 다음 작업을 수월하게 해준다.
==== 쿼리플랜비교2:Star Join ====
'''테스트 준비'''
{{{
USE testparallel;
IF OBJECT_ID('dbo.Fact', 'U') IS NOT NULL DROP TABLE dbo.Fact;
IF OBJECT_ID('dbo.Dim1', 'U') IS NOT NULL DROP TABLE dbo.Dim1;
IF OBJECT_ID('dbo.Dim2', 'U') IS NOT NULL DROP TABLE dbo.Dim2;
IF OBJECT_ID('dbo.Dim3', 'U') IS NOT NULL DROP TABLE dbo.Dim3;
GO
CREATE TABLE dbo.Dim1
(
key1 INT NOT NULL CONSTRAINT PK_Dim1 PRIMARY KEY,
attr1 INT NOT NULL,
filler BINARY(100) NOT NULL DEFAULT (0x)
);
CREATE TABLE dbo.Dim2
(
key2 INT NOT NULL CONSTRAINT PK_Dim2 PRIMARY KEY,
attr1 INT NOT NULL,
filler BINARY(100) NOT NULL DEFAULT (0x)
);
CREATE TABLE dbo.Dim3
(
key3 INT NOT NULL CONSTRAINT PK_Dim3 PRIMARY KEY,
attr1 INT NOT NULL,
filler BINARY(100) NOT NULL DEFAULT (0x)
);
CREATE TABLE dbo.Fact
(
key1 INT NOT NULL CONSTRAINT FK_Fact_Dim1 FOREIGN KEY
REFERENCES dbo.Dim1,key2 INT NOT NULL CONSTRAINT FK_Fact_Dim2 FOREIGN KEY
REFERENCES dbo.Dim2,
key3 INT NOT NULL CONSTRAINT FK_Fact_Dim3 FOREIGN KEY REFERENCES dbo.Dim3,
measure1 INT NOT NULL,
measure2 INT NOT NULL,
measure3 INT NOT NULL,
filler BINARY(100) NOT NULL DEFAULT (0x),
CONSTRAINT PK_Fact PRIMARY KEY(key1, key2, key3)
);
INSERT INTO dbo.Dim1(key1, attr1)
SELECT n, ABS(CHECKSUM(NEWID())) % 20 + 1
FROM dbo.GetNums(100);
INSERT INTO dbo.Dim2(key2, attr1)
SELECT n, ABS(CHECKSUM(NEWID())) % 10 + 1
FROM dbo.GetNums(50);
INSERT INTO dbo.Dim3(key3, attr1)
SELECT n, ABS(CHECKSUM(NEWID())) % 40 + 1
FROM dbo.GetNums(200);
INSERT INTO dbo.Fact WITH (TABLOCK)
(key1, key2, key3, measure1, measure2, measure3)
SELECT N1.n, N2.n, N3.n,
ABS(CHECKSUM(NEWID())) % 1000000 + 1,
ABS(CHECKSUM(NEWID())) % 1000000 + 1,
ABS(CHECKSUM(NEWID())) % 1000000 + 1
FROM dbo.GetNums(100) AS N1
CROSS JOIN dbo.GetNums(50) AS N2
CROSS JOIN dbo.GetNums(200) AS N3;
GO
}}}
다음의 SQL을 2005, 2008 각 인스턴스에서 실행시켜보자. ([Star Join] 참고)
{{{
SELECT *
FROM dbo.Fact AS F
JOIN dbo.Dim2 AS D2
ON F.key2 = D2.key2
JOIN dbo.Dim3 AS D3
ON F.key3 = D3.key3
WHERE D2.attr1 <= 3
AND D3.attr1 <= 2;
}}}
'''2005 버전'''
attachment:ParallelismEnhancementsInSQLServer2008/p03.jpg?width=850
'''2008 버전'''
attachment:ParallelismEnhancementsInSQLServer2008/p04.jpg?width=850
조인 횟수를 확 줄여주는 것이 포인트다.
==== 쿼리플랜비교3:Partitioning ====
'''기본'''
attachment:ParallelismEnhancementsInSQLServer2008/partition_2005_2008.png
'''테스트 준비'''
{{{
USE testparallel;
IF OBJECT_ID('dbo.PartitionedTable', 'U') IS NOT NULL
DROP TABLE dbo.PartitionedTable;
IF EXISTS(SELECT * FROM sys.partition_schemes WHERE name = 'PS1')
DROP PARTITION SCHEME PS1;
IF EXISTS(SELECT * FROM sys.partition_functions WHERE name = 'PF1')
DROP PARTITION FUNCTION PF1;
GO
CREATE PARTITION FUNCTION PF1 (INT)
AS RANGE LEFT FOR VALUES (250000, 500000, 750000);
CREATE PARTITION SCHEME PS1
AS PARTITION PF1 ALL TO ([PRIMARY]);
CREATE TABLE dbo.PartitionedTable
(
col1 INT NOT NULL,
col2 INT NOT NULL,
filler BINARY(100) DEFAULT(0x)
) ON PS1(col1);
CREATE UNIQUE CLUSTERED INDEX idx_col1 ON dbo.PartitionedTable(col1) ON PS1(col1);
INSERT INTO dbo.PartitionedTable WITH (TABLOCK) (col1, col2)
SELECT n, ABS(CHECKSUM(NEWID())) % 1000000 + 1
FROM dbo.GetNums(1000000);
}}}
다음의 SQL문을 2005, 2008의 각각의 인스턴스에서 실행시켜본다.
{{{
SELECT *
FROM dbo.PartitionedTable
WHERE col1 <= 500000
ORDER BY col2;
}}}
'''2005 버전'''
attachment:ParallelismEnhancementsInSQLServer2008/p05.jpg?width=850
'''2008 버전'''
attachment:ParallelismEnhancementsInSQLServer2008/p06.jpg?width=850
실제실행계획을 xml로 떠서 보면 몇개의 thread가 사용되었으며, 각각의 thread가 몇개의 row를 처리했는지 볼 수 있다. Ctrl+L을 눌러서 나오는 예상실행계획에서는 볼 수 없다. 아래의 그림처럼 {{{ ... }}} 태그부분을 보면 된다. 표시된 부분을 보면 총 5개의 thread가 사용되었으며, 각각의 thread는 대략 4만5천 row씩을 처리한 것을 볼 수 있다.
attachment:ParallelismEnhancementsInSQLServer2008/xml_plan.jpg
==== 병렬처리 강제화 ====
이 방법은 문서화되지 않은 dbcc 명령을 통해 구현할 수 있다. cpu와 io에 대한 가중치를 옵티마이저에게 알려줄 수 있다. 최종적인 쿼리 비용이 설정된 값(기본값 = 5)보다 크면 병렬처리하므로 cpu에 대한 가중치로 병렬처리를 유도할 수 있다. 먼저 설정값을 본다.
{{{
DBCC TRACEON (3604); -- Show DBCC output
DBCC SHOWWEIGHTS; -- Show the settings
/* 결과
DBCC Opt Weights CPU: 1.000000 IO: 1.000000 SPID 81
*/
}}}
병렬처리 하지 않는 쿼리의 실행 계획을 보자.
{{{
select count(*) from master.dbo.spt_values
}}}
attachment:ParallelismEnhancementsInSQLServer2008/p01.png
위 그림을 보면 알 수 있겠지만 병렬처리 하지 않는다. cpu에 가중치를 주어 쿼리 비용을 5가 넘어가도록 조정해 보자.
{{{
DBCC FREEPROCCACHE
DBCC SETCPUWEIGHT(10000);
go
select count(*) from master.dbo.spt_values
option (recompile, querytraceon 8649) -- 옵션은 꼭 필요하다.
go
DBCC SETCPUWEIGHT(1); -- 원래대로 해놓자.
DBCC SETIOWEIGHT(1); --io에도 가중치를 줄 수 있다.
go
}}}
attachment:ParallelismEnhancementsInSQLServer2008/p02.png
필자의 경우는 cpu에 10000 정도를 주었더니 쿼리 비용이 5를 넘어가 병렬처리를 하는 것을 볼 수 있었다. 그렇다면 비용이 5가 넘어가게 io도 가중치를 주면 병렬처리를 하지 않을까하는 생각을 해봤다. sql server 2008 r2에서 DBCC SETIOWEIGHT(10000); 과 같이 io에 가중치를 주어봤으나 병렬처리 실행 계획이 만들어지지 않았다. 이때 cpu 가중치는 1이었다. 하지만 DBCC SETCPUWEIGHT(10); 과 같이 가중치를 주니 병렬처리 실행계획을 세우더라. io와 cpu 가중치를 적절히 주어 실행계획을 유도시키면 강제로 병렬처리 할 수 있음을 알 수 있다.
{{{
DBCC FREEPROCCACHE
DBCC SETCPUWEIGHT(10);
DBCC SETIOWEIGHT(10000);
go
select count(*) from master.dbo.spt_values
option (recompile, querytraceon 8649)
go
DBCC SETCPUWEIGHT(1); -- Default CPU weight
DBCC SETIOWEIGHT(1);
go
}}}
뭐...TRACEON 했던거 TRACEOFF 하자.
{{{
DBCC TRACEOFF (3604);
}}}
DBCC 명령을 계속 사용할 수 없으니, play_guide(계획지침)를 이용하면 되것다. 또는 다음과 같이 병렬 실행 계획을 xml로 떠서 실행하면 되것다.
{{{
select count(*) from master.dbo.spt_values
option (use plan N'
...
...
...
--소스가 길어서 아래의 source_01.txt로 첨부하였다.
}}}
{{{#!html
source_01.txt (새창에서 열림)
}}}
attachment:ParallelismEnhancementsInSQLServer2008/p03.png
특이한 점은 예상하위트리 비용이 5를 넘지 않았다는 것이다. 어찌되었건 sql server는 힌트로 준 실행계획을 그대로 사용한 것이다. 뭐.. 가끔씩 쓸만할라나?
==== 파티션 압축 ====
{{{
set nocount on
set statistics io off
declare
@bdt char(8)
, @edt char(8)
, @p int
, @sql varchar(4000)
set @bdt = '20110928'
while (@bdt <= '20300301')
begin
set @edt = convert(char(8), dateadd(dd, 1, @bdt), 112)
set @p = $partition.파티션함수(@bdt)
set @sql = '
alter table 테이블명
rebuild partition = ' + convert(varchar, @p) + '
with (data_compression = page)'
exec (@sql)
print @sql
--print @bdt + ', ' + @edt
set @bdt = @edt
end
}}}
==== Distribute, Repartition, Gather Stream ====
sql server 옵티마이저는 serial vs parallel 비용을 비교해서 둘 중에 작은 것을 선택한다고 한다.
attachment:ParallelismEnhancementsInSQLServer2008/parallel_mathod_2.png
==== 결론 ====
2005보다 병렬처리가 좋아졌다. 알고리즘(처리방식)이 좋아진거지 실제 환경에서 좋아졌다고는 볼 수 없다. 그림을 보면 알겠지만 논리적 읽기 수가 2008 버전이 더 많은 부분이 있다. 이제까지 I/O Base의 성능튜닝 기준이 바뀌었다고 볼 수도 있는 것이다. 병렬처리는 하드웨어 자원을 최대한 많이 쓰고 최대한 효율적으로 처리하기 위한 처리방법이므로 I/O에 너무 집착하지는 말아야 할 것이다. CPU, RAM, Disk 어느 한 곳이라도 병목현상이 발생하면 시스템은 전체적으로 느려진다는 진리는 여전히 유효하다.
==== 참고자료 ====
* [http://sqlblog.com/blogs/paul_white/archive/2011/12/23/forcing-a-parallel-query-execution-plan.aspx Forcing a Parallel Query Execution Plan]
* [attachment:ParallelismEnhancementsInSQLServer2008/SQL2008R2_Parallel_QP_Understanding_and_Controlling.docx Understanding and Controlling Parallel Query Processing in SQL Server 2008]
* [http://www.simple-talk.com/sql/learn-sql-server/understanding-and-using-parallelism-in-sql-server/ Understanding and Using Parallelism in SQL Server]