728x90
제 1강 : 파티션드 테이블

이름: 김민석
편지: MINSOUK@HOTMAIL.COM
NHN WWW.NHNCORP.COM DBA

테이블을 만들고 데이터를 적제하고 어떻게하면 할당된 PGAE 에 많은 데이터를 넣어서 효율적으로 관리할까?

테이블 사이즈가 40G ~ 80G 가 되고 있는데, 풀스캔을 한번 타면 어떻게 될까? 생각만 해도 아찔한 순간입니다.. 모 사이트의 어떤서버를 통합하니 1 TERA 테이블이 생겼다. 인덱스 리빌딩을 해야 하는데... 이럴경우 관리자가 떠 안아야 할 부담은 거의 절정을 이루게된다. 이러한 대용량 테이블을 보다 효과적으로 관리하기 위해서 SQL 2005 에서는 PARTITIONED TABLE 이라는 기술을 선보이고 있습니다. 모두 아는 기능이고 해서 특별히 시간내기 어렵고, 기능을 실습해 보고 싶은 분을 위해서 간단하게 따라하기 식으로 글을 적어 보았습니다.

이미 다른 상용 DBMS 들은 다양한 테이블 분할방법 (RANGE PARTITIONED TABLE, LIST PARTITIONED TABLE, HASH PARTITIONED TABLE)을 선보이며, 실제로 운영하고 있습니다. 늦은 감이 없지는 않지만, 언제까지 2008년에 제품지원이 중단되는 2000만을 MSSQL 이라고 생각하고 있는 사람들도 우끼는 이야기 라고 생각합니다. 어떤 데이터베이스가 가장 멋진 데이터베이스가 아니라, 해당 제품을 가장 잘 핸들링 하는 사람이 있는 데이터베이스가 가장 좋은 데이터베이스 라고 개인적으로 생각합니다.

SQL 2000 엔터프라이즈를 쓰면서 SCHEMA BINDING INDEXED VIEW 도 모르고, MSCS 도 써보지 않았다면?? 혹자는 어려워 못쓸 것이고, 혹자는 들어보지 못해서 못 쓸 것입니다. 간단하게 실습하고 긴요하게 쓰이길 바랍니다.

우리가 꿈꾸고 갈망하던 많은 부분이 MSSQL 2005 를 통해서 해결되어 가고 있고, 더해서 차기 활화산(카트마이)도 멋지게 타고 있으니 열심히 공부해 보도록 하자.

목차

PARTITIONED TABLE
     PARTITIONED FUNCTION
     PARTITIONED SCHEME
     CREATE PARTITIONED TABLE
     CHECK PARTITIONED INFORMATION
     PARTITION MERGE
     PARTITION SPLIT
     PARTITION SWITCH

EXAMPLE
     TO IMPLEMENT AN AUTOMATIC SLIDING WINDOW IN A PARTITIONED TABLE

ON SQL 2005
     http://msdn2.microsoft.com/en-us/library/aa964122.aspx

USE MASTER
GO

-- DROP DATABASE PDB
-- GO

/*-----------------------------------------------------------------------
PARTITIONED TABLE
      PARTITIONED FUNCTION
      PARTITIONED SCHEME
      CREATE PARTITIONED TABLE
      CHECK PARTITIONED INFORMATION
      PARTITION MERGE
      PARTITION SPLIT
      PARTITION SWITCH

      * PARTITIONED FUNCTION
분할된 테이블 및 인덱스를 만드는 첫번째 단계로 CREATE PARTITION FUNCTION 이다.
-------------------------------------------------------------------------*/

CREATE PARTITION FUNCTION PARTITION_FUNCTION_NAME ( INPUT_PARAMETER_TYPE )
AS RANGE [ LEFT | RIGHT ]
FOR VALUES ( [ BOUNDARY_VALUE [ ,...N ] ] )
[ ; ]

기준이 있어야 나누지!! 어떤 기준으로 나눌지를 정의하는 단계이다.
예를 들어 나는 SEQUENCE 를 기준으로 나누기를 원한다라고 생각 했다면,

CREATE PARTITION FUNCTION MYRANGEPF1 (INT)
AS RANGE LEFT FOR VALUES (1, 100, 1000);


COL1 <= 1
COL1 > 1 AND COL1 <= 100
COL1 > 100 AND COL1 <= 1000
COL1 > 1000

CREATE PARTITION FUNCTION MYRANGEPF2 (INT)
AS RANGE RIGHT FOR VALUES (1, 100, 1000);


COL1 < 1
COL1 >= 1 AND COL1 < 100
COL1 >= 100 AND COL1 < 1000
COL1 >= 1000

/*************************
[질문] 내 맘데로 넣으면 넌 어떻게 할꺼야?
**************************/

-- 실행
CREATE DATABASE PDB
GO
USE PDB
GO

CREATE PARTITION FUNCTION MYRANGEPF3 (INT)
AS RANGE LEFT FOR VALUES ( 1, NULL,1000, 100,-100);
GO

-- DROP PARTITION FUNCTION MYRANGEPF3

결과

경고: 파티션 함수 'MYRANGEPF3'의 범위 값 목록이 값을 기준으로 정렬되어 있지 않습니다. CREATE PARTITION SCHEME 작업 동안 파일 그룹에 파티션을 매핑하면 함수 'MYRANGEPF3'이(가) CREATE PARTITION SCHEME에서 참조된 경우 정렬된 경계 값을 사용합니다.

-- 확인
SELECT *
FROM SYS.PARTITION_RANGE_VALUES
WHERE
          FUNCTION_ID IN
                   (
                   SELECT FUNCTION_ID
                   FROM SYS.PARTITION_FUNCTIONS
                   WHERE NAME = ('MYRANGEPF3')
                   )

결과

FUNCTION_ID BOUNDARY_ID PARAMETER_ID VALUE
----------- ----------- ----------- -----------
65536 1 1 NULL
65536 2 1 -100
65536 3 1 1
65536 4 1 100
65536 5 1 1000

똑똑한 SQL 서버는 이 값들을 정렬해서 순서를 재배열 하여 사용하게 된다.
또한 NULL 은 우선순위 최상으로 소팅되는 되는 것을 알수 있다.

음 나누다 보니 보기 이상한데 하나더 나누어 보고 싶다면 어떻게 할까? 바꾸어 보자!

/* 구문정보
ALTER PARTITION FUNCTION PARTITION_FUNCTION_NAME()
{
    SPLIT RANGE ( BOUNDARY_VALUE )
   | MERGE RANGE ( BOUNDARY_VALUE )
} [ ; ]
*/

생성된 모든 파티션을 확인 부터 해보자.
개별 파티션 함수에 대한 정보를 얻으려면

SELECT * FROM SYS.PARTITION_FUNCTIONS             --(파티션 함수정보)
SELECT * FROM SYS.PARTITION_PARAMETERS               --(매개 변수 정보)
SELECT * FROM SYS.PARTITION_RANGE_VALUES             --(경계구성)
SELECT * FROM SYS.PARTITION_SCHEMES               --(파티션 구성정보)
SELECT * FROM SYS.DATA_SPACES               --(파티션 구성정보)
SELECT * FROM SYS.DESTINATION_DATA_SPACES              --(개별 파티션 구성정보)
SELECT * FROM SYS.PARTITIONS             --(파티션 정보)
SELECT * FROM SYS.TABLES              --(테이블 분할정보)
SELECT * FROM SYS.INDEXES              --(인덱스 분할정보)
SELECT * FROM SYS.INDEX_COLUMNS               --(인덱스 컬럼정보)

-- SPLIT

ALTER PARTITION FUNCTION MYRANGEPF3 ()
SPLIT RANGE (-1000);
GO

SELECT *
FROM SYS.PARTITION_RANGE_VALUES
WHERE
         FUNCTION_ID IN
                 (
                 SELECT FUNCTION_ID
                 FROM SYS.PARTITION_FUNCTIONS
                 WHERE NAME = ('MYRANGEPF3')
                 )

결과

FUNCTION_ID BOUNDARY_ID PARAMETER_ID VALUE
----------- ----------- ----------- -----------
65536 1 1 NULL
65536 2 1 -1000
65536 3 1 -100
65536 4 1 1
65536 5 1 100
65536 6 1 1000

알아서 잘 배열 되는 것을 관찰할 수 있다.

-- MERGE
ALTER PARTITION FUNCTION MYRANGEPF3 ()
MERGE RANGE (-1000);

FUNCTION_ID BOUNDARY_ID PARAMETER_ID VALUE
----------- ----------- ----------- -----------
65536 1 1 NULL
65536 2 1 -100
65536 3 1 1
65536 4 1 100
65536 5 1 1000

정리

PARTITION_RANGE_VALUES + 1 개의 영역이 생기게 되고,기준값은 LEFT, RIGHT 의 지정에 따라 기준값의 위치가 다르게 속하게 되는 것을 알 수 있다. 예를들어 RIGHT 파티션을 만들고 경계값이 NULL 이 있다면, RIGHT 파티션의 경우 기준값을 포함하지 않는 더작은 영역을 만들게 된다. 그러나 NULL 보다 더 작은 값이 없으므로 첫번째 파티션은 비게 된다.

DROP PARTITION FUNCTION MYRANGEPF3
GO

CREATE PARTITION FUNCTION MYRANGEPF3 (INT)
AS RANGE RIGHT FOR VALUES ( NULL, -100, 1, 100, 1000);
GO

참고 : 기준값을 정열해서 배치하면 에러 없음
5개로 기준값으로 나누었으니 다음과 같은 6개의 영역이 생긴다.

1                                     값       <     NULL    --                                   첫번째 기준값 보다 작은 값들 (빈파티션)
2    NULL      <=       값      <       -100       -- NULL                포함 기준값보다 더 큰 값들
3    -100       <=       값      <        1                         -- -100                   포함 다음기준값 보다 작은 값들
4    1                      <=     값       <           100                     -- 1                    포함 다음기준값 보다 작은 값들
5    100                   <=     값       <           1000        -- 100                    포함 다음기준값 보다 작은 값들
6                                     값       >=         1000        -- 1000                  포함 더 큰값들 다

/*-----------------------------------------------------------------------
PARTITIONED TABLE
          PARTITIONED FUNCTION
          PARTITIONED SCHEME
          CREATE PARTITIONED TABLE
          CHECK PARTITIONED INFORMATION
          PARTITION MERGE
          PARTITION SPLIT
          PARTITION SWITCH

          * PARTITIONED SCHEME
파일그룹 매핑 구성표 만들기 (필수 선행작업: 파티션 함수 생성)
-------------------------------------------------------------------------*/

/* 구문정보
CREATE PARTITION SCHEME partition_scheme_name
AS PARTITION partition_function_name
[ ALL ] TO ( { file_group_name | [ PRIMARY ] } [ ,...n ] )
[ ; ]
*/

ALL 을 지정하면 하나의 파일그룹만을 지정 할 수 있고, 파티션 펑션의 기준값 보다 같거나 많은 개수를 정의해야 하며, 적을 경우 에러를 나타내며 종료 많은경우 NEXT USED 로 정의 됨

-- 실험
CREATE PARTITION SCHEME PSMYSCHEME3
AS PARTITION MYRANGEPF3
TO (FG1,FG2)

결과

메시지 7707, 수준 16, 상태 1, 줄 2
연결된 파티션 함수 'MYRANGEPF3'은(는) 구성표 'PSMYSCHEME3'에 언급된 파일 그룹보다 많은 파티션을 생성합니다.
음~ 파일그룹이 있는지 없는지 검사하지도 않고 바로 에러!

-- 설정하려는 파티션 펑션의 기준값 개수 확인
SELECT *
FROM SYS.PARTITION_RANGE_VALUES
WHERE
          FUNCTION_ID IN
                     (
                     SELECT FUNCTION_ID
                     FROM SYS.PARTITION_FUNCTIONS
                     WHERE NAME = ('MYRANGEPF3')
                     )

65537 1 1 NULL
65537 2 1 -100
65537 3 1 1
65537 4 1 100
65537 5 1 1000

-- 음~ 최소 6개 만들고 더 만들면 다음파티션으로 쓰겠군...
CREATE PARTITION SCHEME PSMYSCHEME3
AS PARTITION MYRANGEPF3
TO (FG1,FG2,FG3,FG4,FG5,FG6,FG7)

결과

메시지 208, 수준 16, 상태 58, 줄 1
개체 이름 'FG1'이(가) 잘못되었습니다.
개수를 만족하고 나서 파일 그룹이 실제로 있는지 찾음 해당데이터 베이스에 파일그룹이 없으면 에러가 난다.

여기서 잠깐! 스토리지 이야기도 조금 하고 파일그룹 및 데이터 파일 핸들링 하는 방법에 대해서도 조금만 알아보자.

데이터 베이스에서 파일의 숫자 만큼 쓰래드가 생성 될 수 있기 때문에 파일을 여러개 만드는 것이 좋다. 클러스터 인덱스가 있는 데이터 파일을 파일그룹, 난클러스터 인덱스는 다른 디스크에 위치시키는 것이 절대적으로 유리하다. 파일그룹을 잘게 나누면 파일그룹별로 백업 복구도 가능하고 용량산정을 위해 다른 위치로 옮길때도 옮기기 좋은 단위가 된다는 것을 상기하자!

또한 로그파일[LDF] 는 분리된 다른 위치에 독립적으로 쓰게 하는것이 좋다. I/O 패턴이 다른 파일을 같은 디스크에 배치하는 것은 어떤경우에도 좋은 방법이 되지 못한다.

개인적으로 로그는 300G 10000RPM RAID 1 을 추천하고 싶다. 최대 테이블 사이즈가 300기가를 넘는다면 다른 방법을 구상하도록 한다.

또한 데이터 디스크는 RAID 1+0 를 추천한다. MMORPG 같은 경우나 빈번하고 짧은 트랜젝션이 많은 데이터베이스에서 쓰기스피드가 느린 RAID 5 는 CheckPoint시 트랜잭션 숫자를 감소시키는 주요 범인이 된다.

로컬백업을 한다면 로컬백업이 되는 위치는 백업시간을 단축하기 위해 RAID 0 에 위치시킨다. 두장의 RAID 5 로 구성된 디스크 두장에 백업 하는것 보다 두장의 RAID 0 로 백업한다면 훨씬 더 빠른 백업을 수행 할 수 있다. 그러나 디스크 장애로 잦은 RAID 리빌딩을 하기 싫다면 디스크 에러에 강한 RAID 1+0 로 구성하는 것이 바르다고 할 수 있다.

이런 디스크 문제가 빈번하게 일어난다면 스토리지 시스템을 도입하는 것을 고려해 보기 바란다. 스토리지 시스템은 자체 리드 캐쉬 라이트 캐쉬가 있기 때문에 메모리까지만 받아주면 스토리지 컨트롤러가 알아서 디스크에 쓸것이다. 최신 엔터프라이즈 스토리지는 컨트롤러 캐쉬가 16G에 이른다.

-- 파일그룹 생성방법
ALTER DATABASE PDB
ADD FILEGROUP FG1

-- 파일그룹에 간단한 파일 추가
ALTER DATABASE PDB
ADD FILE ( NAME = PDB_DAT1, FILENAME = 'C:\FG1\PDB_DAT1.ndf') TO FILEGROUP FG1

-- 삭제방법
-- 1, 파일 삭제
ALTER DATABASE PDB
REMOVE FILE PDB_DAT1

-- 2, 파일그룹 삭제
ALTER DATABASE PDB
REMOVE FILEGROUP FG1

-- 좀더 자세하게
USE master
GO
ALTER DATABASE PDB
ADD FILEGROUP FG1;
ALTER DATABASE PDB
ADD FILEGROUP FG2;
ALTER DATABASE PDB
ADD FILEGROUP FG3;
ALTER DATABASE PDB
ADD FILEGROUP FG4;
ALTER DATABASE PDB
ADD FILEGROUP FG5;
ALTER DATABASE PDB
ADD FILEGROUP FG6;
ALTER DATABASE PDB
ADD FILEGROUP FG7;
ALTER DATABASE PDB
ADD FILEGROUP FG8;
GO

-- 해당경로를 미리 만들어 두어야 한다.
-- C:\ 드라이브에 FG1,FG2,FG3,FG4,FG5
-- G:\ 드라이브에 FG6,FG7,FG8
ALTER DATABASE PDB
ADD FILE
(
    NAME = PDB_DAT1,
    FILENAME = 'C:\FG1\PDB_DAT1.ndf', -- 테스트 이므로
    SIZE = 5MB,
    MAXSIZE = 100MB,
    FILEGROWTH = 5MB
)
--, -- 여러개의 파일로 분할 하고 싶은 경우
--(
-- NAME = PDB_DAT2,
-- FILENAME = 'C:\FG1\PDB_DAT2.ndf',
-- SIZE = 5MB,
-- MAXSIZE = 100MB,
-- FILEGROWTH = 5MB
--)
TO FILEGROUP FG1

ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT2,FILENAME = 'C:\FG2\PDB_DAT2.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG2
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT3,FILENAME = 'C:\FG3\PDB_DAT3.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG3
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT4,FILENAME = 'C:\FG4\PDB_DAT4.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG4
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT5,FILENAME = 'C:\FG5\PDB_DAT5.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG5
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT6,FILENAME = 'G:\FG6\PDB_DAT6.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG6
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT7,FILENAME = 'G:\FG7\PDB_DAT7.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG7
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT8,FILENAME = 'G:\FG8\PDB_DAT8.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG8

-- 다시 하던일 계속하자!
USE PDB
GO

CREATE PARTITION SCHEME PSMYSCHEME3
AS PARTITION MYRANGEPF3
TO (FG1,FG2,FG3,FG4,FG5,FG6,FG7,FG8)
GO

결과 R1

파티션 구성표 'PSMYSCHEME3'이(가) 작성되었습니다. 파티션 구성표 'PSMYSCHEME3'에서 'FG7'은(는) 다음에 사용되는 파일 그룹으로 표시됩니다.
다음에 사용되는 파일 그룹 다음에 지정된 1 파일 그룹은 무시됩니다.

음~ 우리는 6개의 파일그룹만 필요한데, 더 만들어 두니 바로 다음 파일그룹은 NEXT USED 로 지정되고 나머지들은 무시되는 것이구나.

-- 삭제방법
-- DROP PARTITION SCHEME PSMYSCHEME3

-- 해당디비의 상황
EXEC SP_HELPDB N'PDB'
EXEC SP_HELPFILEGROUP;
EXEC SP_HELPFILE;
SELECT * FROM SYS.SYSALTFILES WHERE DBID = DB_ID('PDB')

여기서 잠깐!
데이터 베이스 미러링 같은 경우 피지컬 영역 구성이 다를수 있고 스토리지 용량도 보장 할 수 없고 이런 파티션 스킴을 고치는 작업은 미러링중에 미러 대상서버에 적용되지 않는다.

/*-----------------------------------------------------------------------
PARTITIONED TABLE
        PARTITIONED FUNCTION
        PARTITIONED SCHEME
        CREATE PARTITIONED TABLE
        CHECK PARTITIONED INFORMATION
        PARTITION MERGE
        PARTITION SPLIT
        PARTITION SWITCH

        * CREATE PARTITIONED TABLE
준비하는데 수고했습니다. 이제 파티션드 테이블을 만들어 보겠습니다.
-------------------------------------------------------------------------*/

-- 두번째 작업에서 힘들게 만들었던 파일스키마 정의에 테이블을 매핑 시켜 만든다.
IF OBJECT_ID('DBO.PT_LOG') IS NOT NULL
DROP TABLE DBO.PT_LOG
GO

CREATE TABLE [dbo].[PT_LOG] (
             PIDX INT -- 파티션 기준값
        , IDX INT IDENTITY(1,1)
        , DATA VARCHAR(6000) NULL
) ON PSMYSCHEME3 (PIDX)
GO

/*-----------------------------------------------------------------------
PARTITIONED TABLE
        PARTITIONED FUNCTION
        PARTITIONED SCHEME
        CREATE PARTITIONED TABLE
        CHECK PARTITIONED INFORMATION
        PARTITION MERGE
        PARTITION SPLIT
        PARTITION SWITCH

        * CHECK PARTITIONED INFORMATION ,PARTITION MERGE, PARTITION SPLIT
데이터 삽입과 파티션 정보확인 및 MERGE SPLIT 해보도록 하겠습니다. NULL 은 어디에 있을까? 궁금하지 않나요?
-------------------------------------------------------------------------*/

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (NULL, REPLICATE('A',6000))
GO 601 -- 인서트 601번 실행

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (-100, REPLICATE('A',6000))
GO 602

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (1, REPLICATE('A',6000))
GO 603

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (100, REPLICATE('A',6000))
GO 604

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (1000, REPLICATE('A',6000))
GO 605

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (2000, REPLICATE('A',6000))
GO 606

SET STATISTICS IO ON
SET STATISTICS PROFILE ON
SELECT COUNT(*) FROM PT_LOG

결과

   |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1008],0)))
         |--Stream Aggregate(DEFINE:([Expr1008]=Count(*)))
                |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1007]) PARTITION ID:([PtnIds1007]))
**                    |--Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5)),((6))))
                       |--Table Scan(OBJECT:([PDB].[dbo].[PT_LOG]))

테이블 'PT_LOG'. 검색 수 6, 논리적 읽기 수 3621, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

별표 별표(**) 부분이 어떤 파티션을 읽었는지 보여주눈 부분이다. 상수 조인을 이용한 6회 읽은 것을 알 수 있다.

SET STATISTICS IO OFF
SET STATISTICS PROFILE OFF

결과

3621

SET STATISTICS IO ON
SET STATISTICS PROFILE ON
SELECT COUNT(*) FROM PT_LOG WHERE PIDX IS NULL
SET STATISTICS IO OFF
SET STATISTICS PROFILE OFF

결과

   |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1010],0)))
         |--Stream Aggregate(DEFINE:([Expr1010]=Count(*)))
               |--Table Scan(OBJECT:([PDB].[dbo].[PT_LOG]), WHERE:([PDB].[dbo].[PT_LOG].[PIDX] IS NULL) PARTITION ID:((2)))

-- 파티션 아이디 2번에서만 읽은 것을 볼 수 있다. 아무리 봐도 넘 이쁜 넘이다.
SET STATISTICS IO ON
SET STATISTICS PROFILE ON

SELECT COUNT(*)
FROM PT_LOG
WHERE
         PIDX IS NULL OR
         PIDX = 1
GROUP BY PIDX
HAVING PIDX = 1

SET STATISTICS IO OFF
SET STATISTICS PROFILE OFF

결과

   |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1010],0)))
         |--Stream Aggregate(DEFINE:([Expr1010]=Count(*)))
               |--Table Scan(OBJECT:([PDB].[dbo].[PT_LOG]), WHERE:([PDB].[dbo].[PT_LOG].[PIDX]=(1)) PARTITION ID:((4)))

SELECT OBJECT_NAME(OBJECT_ID), *
FROM SYS.PARTITIONS
WHERE OBJECT_ID = OBJECT_ID('PT_LOG')
ORDER BY PARTITION_NUMBER, INDEX_ID;

결과

partition_id   object_id      index_id      partition_number   hobt_id      rows

PT_LOG 72057594040680448 53575229 0 1 72057594040680448 0
PT_LOG 72057594040745984 53575229 0 2 72057594040745984 601
PT_LOG 72057594040811520 53575229 0 3 72057594040811520 602
PT_LOG 72057594040877056 53575229 0 4 72057594040877056 603
PT_LOG 72057594040942592 53575229 0 5 72057594040942592 604
PT_LOG 72057594041008128 53575229 0 6 72057594041008128 1211

위와같이 RIGHT TYPE 에서는 NULL 보다 논리적으로 더 작은 것이 없으므로 제일 안쪽 파티션이 항상 비는 것을 알 수 있다.
음~ 배운게 있으니 1211 을 적당하게 다시 나누어 보아야 겠다.
파티션 기준값을 어떻게 나누었는지 다음 쿼리로 확인해 보자.

SELECT *
FROM SYS.PARTITION_RANGE_VALUES
WHERE
     FUNCTION_ID IN
         (
         SELECT FUNCTION_ID
         FROM SYS.PARTITION_FUNCTIONS
         WHERE NAME = ('MYRANGEPF3')
         )

결과

function_id boundary_id parameter_id value
----------- ----------- ----------- -----
65541 1 1 NULL
65541 2 1 -100
65541 3 1 1
65541 4 1 100
65541 5 1 1000

2000으로 경계값을 더 나누어 보자 위에서 아래와 같은 데이터를 넣었다는것을 우리는 알고 있으므로

-- INSERT INTO PT_LOG (PIDX, DATA) VALUES (2000, REPLICATE('A',6000))
-- GO 606

-- 다음과 같은 쿼리를 실행했다.
ALTER PARTITION FUNCTION MYRANGEPF3 ()
SPLIT RANGE (2000);
GO

결과 잘 나누어져 들어갔음을 확인 할 수 있다.

FUNCTION_ID BOUNDARY_ID PARAMETER_ID VALUE
----------- ----------- ----------- -----
65541 1 1 NULL
65541 2 1 -100
65541 3 1 1
65541 4 1 100
65541 5 1 1000
65541 6 1 2000

ALTER PARTITION FUNCTION MYRANGEPF3 ()
SPLIT RANGE (3000);
GO

결과

메시지 7707, 수준 16, 상태 1, 줄 1
연결된 파티션 함수 'MYRANGEPF3'은(는) 구성표 'PSMYSCHEME3'에 언급된 파일 그룹보다 많은 파티션을 생성합니다.

우리는 위에서 파티션 스키마를 만들때, 결과 R1 에서 NEXT USED 로 설정된 FG7 까지만 적용이 되고 FG8에는 무시되었던 것을 기억할 것입니다. 이런 결과 값이 나오는 테이블에서 2000 보다 큰 값이 들어갈 영역을 만들려고 했으나, 역시 무시되는 것을 알 수 있습니다. 그럼 NEXT USED로 다음 파일그룹이 사용될 영역만 다시 정의해 주면 될것을 생각할 수 있겠습니다.

EXEC SP_HELP N'PT_LOG'

결과

Data_located_on_filegroup
-------------------------
PSMYSCHEME3

-- 해당 테이블의 스키마 정보를 이용해서 어떤 파일그룹을 쓰고 있는지 조사
SELECT
            A.NAME, A.DATA_SPACE_ID, A.IS_DEFAULT,
            B.DATA_SPACE_ID,
            C.NAME
FROM
            SYS.DATA_SPACES A
            INNER JOIN SYS.DESTINATION_DATA_SPACES B
                    ON A.DATA_SPACE_ID = B.DATA_SPACE_ID
            INNER JOIN SYS.PARTITION_SCHEMES C
                    ON B.PARTITION_SCHEME_ID = C.DATA_SPACE_ID
WHERE
            C.NAME = 'PSMYSCHEME3'

결과

NAME DATA_SPACE_ID IS_DEFAULT DATA_SPACE_ID NAME
---- ------------- ------------- ------------- ----
FG1 2 0 2 PSMYSCHEME3
FG2 3 0 3 PSMYSCHEME3
FG3 4 0 4 PSMYSCHEME3
FG4 5 0 5 PSMYSCHEME3
FG5 6 0 6 PSMYSCHEME3
FG6 7 0 7 PSMYSCHEME3
FG7 8 0 8 PSMYSCHEME3

-- 해당 디비의 파일그룹 조회
EXEC SP_HELPFILEGROUP

결과

groupname groupid filecount
--------- ------- ---------
PRIMARY 1 1
FG1 2 1
FG2 3 1
FG3 4 1
FG4 5 1
FG5 6 1
FG6 7 1
FG7 8 1
FG8 9 1

-- 해당 스킴의 파티션펑션 및 TYPE 정보 조회
SELECT
            A.BOUNDARY_ID, A.VALUE,
            B.NAME,B.TYPE_DESC,B.FANOUT, BOUNDARY_VALUE_ON_RIGHT
FROM SYS.PARTITION_RANGE_VALUES A
            INNER JOIN SYS.PARTITION_FUNCTIONS B
            ON A.FUNCTION_ID = B.FUNCTION_ID
            INNER JOIN SYS.PARTITION_SCHEMES C
            ON B.FUNCTION_ID = C.FUNCTION_ID
WHERE
            C.NAME = 'PSMYSCHEME3'

결과

BOUNDARY_ID VALUE NAME TYPE_DESC FANOUT BOUNDARY_VALUE_ON_RIGHT
----------- ---------- --------- ------ -----------------------
1 NULL MYRANGEPF3 RANGE 8 1
2 -100 MYRANGEPF3 RANGE 8 1
3 1 MYRANGEPF3 RANGE 8 1
4 100 MYRANGEPF3 RANGE 8 1
5 1000 MYRANGEPF3 RANGE 8 1
6 2000 MYRANGEPF3 RANGE 8 1

해당하는 스키마는 PRIMARY 파일 그룹과 FG8을 안쓰고 있다는 것을 알 수 있다.
우리는 FG8을 다음 파티션 그룹으로 쓰기로 하자.

-- 용법
ALTER PARTITION SCHEME partition_scheme_name
NEXT USED [ filegroup_name ] [ ; ]

-- 실행
ALTER PARTITION SCHEME PSMYSCHEME3
NEXT USED FG8
GO

결과

명령이 완료되었습니다.

-- 확인
SELECT
             A.NAME, A.DATA_SPACE_ID, A.IS_DEFAULT,
             B.DATA_SPACE_ID,
             C.NAME
FROM
             SYS.DATA_SPACES A
             INNER JOIN SYS.DESTINATION_DATA_SPACES B
                        ON A.DATA_SPACE_ID = B.DATA_SPACE_ID
             INNER JOIN SYS.PARTITION_SCHEMES C
                        ON B.PARTITION_SCHEME_ID = C.DATA_SPACE_ID
WHERE
            C.NAME = 'PSMYSCHEME3'

groupname groupid filecount
--------- ------- ---------

BOUNDARY_ID VALUE NAME TYPE_DESC FANOUT BOUNDARY_VALUE_ON_RIGHT
FG1 2 0 2 PSMYSCHEME3
FG2 3 0 3 PSMYSCHEME3
FG3 4 0 4 PSMYSCHEME3
FG4 5 0 5 PSMYSCHEME3
FG5 6 0 6 PSMYSCHEME3
FG6 7 0 7 PSMYSCHEME3
FG7 8 0 8 PSMYSCHEME3
FG8 9 0 9 PSMYSCHEME3

잘 적용된것을 확인 할 수 있다.

-- 펑션을 수정해 보도록 하자 !
ALTER PARTITION FUNCTION MYRANGEPF3 ()
SPLIT RANGE (3000);
GO

잘 수행되는 것을 확인 할 수 있을 것이다.
이제는 다양한 값을 넣어 보겠습니다.

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (NULL, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (-99, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (2, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (101, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (1001, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (2001, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (3000, REPLICATE('A',6000))
GO 3

INSERT INTO PT_LOG (PIDX, DATA)
VALUES (3001, REPLICATE('A',6000))
GO 3

SELECT OBJECT_NAME(OBJECT_ID), *
FROM SYS.PARTITIONS
WHERE OBJECT_ID = OBJECT_ID('PT_LOG')
ORDER BY PARTITION_NUMBER, INDEX_ID;

결과

groupname groupid filecount
--------- ------- ---------

PT_LOG 72057594041663488 117575457 0 1 72057594041663488 0
PT_LOG 72057594041729024 117575457 0 2 72057594041729024 604
PT_LOG 72057594041794560 117575457 0 3 72057594041794560 605
PT_LOG 72057594041860096 117575457 0 4 72057594041860096 606
PT_LOG 72057594041925632 117575457 0 5 72057594041925632 607
PT_LOG 72057594041991168 117575457 0 6 72057594041991168 608
PT_LOG 72057594042056704 117575457 0 7 72057594042056704 609
PT_LOG 72057594042122240 117575457 0 8 72057594042122240 6

SELECT
            $PARTITION.MYRANGEPF3(PIDX) AS 'PARTITION ID',
            MIN(PIDX) AS 'MIN',
            MAX(PIDX) AS 'MAX',
            COUNT(*) AS 'ROWCNT'
FROM
            PT_LOG
GROUP BY $PARTITION.MYRANGEPF3(PIDX)
ORDER BY 1

결과

PARTITION ID MIN MAX ROWCNT
------------ ----------- ----------- -----------
2 NULL NULL 604
3 -100 -99 605
4 1 2 606
5 100 101 607
6 1000 1001 608
7 2000 2001 608
8 3000 3001 6

-- 파티션 머지
ALTER PARTITION FUNCTION MYRANGEPF3 ()
MERGE RANGE (1000);

SELECT
            A.NAME, A.DATA_SPACE_ID, A.IS_DEFAULT,
            B.DATA_SPACE_ID,
            C.NAME
FROM
            SYS.DATA_SPACES A
            INNER JOIN SYS.DESTINATION_DATA_SPACES B
                        ON A.DATA_SPACE_ID = B.DATA_SPACE_ID
            INNER JOIN SYS.PARTITION_SCHEMES C
                        ON B.PARTITION_SCHEME_ID = C.DATA_SPACE_ID
WHERE
            C.NAME = 'PSMYSCHEME3'

결과 FG6 이 없어진 것을 확인 할 수 있다.

FG1 2 0 2 PSMYSCHEME3
FG2 3 0 3 PSMYSCHEME3
FG3 4 0 4 PSMYSCHEME3
FG4 5 0 5 PSMYSCHEME3
FG5 6 0 6 PSMYSCHEME3
FG6 7 0 7 PSMYSCHEME3
FG7 8 0 8 PSMYSCHEME3
FG8 9 0 9 PSMYSCHEME3

EXEC SP_HELPFILEGROUP

결과

PRIMARY 1 1
FG1 2 1
FG2 3 1
FG3 4 1
FG4 5 1
FG5 6 1
FG6 7 1
FG7 8 1
FG8 9 1

물리적 파일 그룹은 그대로 이다.

DBCC SHOWFILESTATS

결과

1 1 35 22 PDB C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\PDB.mdf
3 2 80 1 PDB_DAT1 C:\FG1\PDB_DAT1.ndf
4 3 80 78 PDB_DAT2 C:\FG2\PDB_DAT2.ndf
5 4 80 78 PDB_DAT3 C:\FG3\PDB_DAT3.ndf
6 5 80 78 PDB_DAT4 C:\FG4\PDB_DAT4.ndf
7 6 160 78 PDB_DAT5 C:\FG5\PDB_DAT5.ndf
8 7 160 78 PDB_DAT6 G:\FG6\PDB_DAT6.ndf
9 8 80 79 PDB_DAT7 G:\FG7\PDB_DAT7.ndf
10 9 80 2 PDB_DAT8 G:\FG8\PDB_DAT8.ndf

정확하게 예전과 같이 분할된 것을 볼 수 있다.

/*-----------------------------------------------------------------------
PARTITIONED TABLE
            PARTITIONED FUNCTION
            PARTITIONED SCHEME
            CREATE PARTITIONED TABLE
            CHECK PARTITIONED INFORMATION
            PARTITION MERGE
            PARTITION SPLIT
            PARTITION SWITCH

            * PARTITION SWITCH
이제 파티션 이동하기를 해보겠습니다. 대용량 로그 파일이 있을때 DELETE 로 지우면서
세월아 내월아 기다릴 수 없으니 한방에 날릴 수 있도록 다른 파티션으로 이동해 보도록 하겠습니다.
그리고 스키마 정보만 바꾸어서
-------------------------------------------------------------------------*/
EXEC SP_HELPFILEGROUP
GO

ALTER DATABASE PDB
ADD FILEGROUP FG3_AUX;
GO

-- 폴더를 미리 만들고
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT3_AUX,FILENAME = 'C:\FG3_AUX\PDB_DAT3_AUX.ndf',SIZE = 5MB,MAXSIZE = 100MB,FILEGROWTH = 5MB) TO FILEGROUP FG3_AUX

IF OBJECT_ID('DBO.PT_LOG_AUX') IS NOT NULL
DROP TABLE DBO.PT_LOG_AUX
GO

CREATE TABLE [dbo].[PT_LOG_AUX] (
                 PIDX INT -- 파티션 기준값
             , IDX INT IDENTITY(1,1)
             , DATA VARCHAR(6000) NULL
) ON FG3_AUX
GO

DBCC SHOWFILESTATS

ALTER TABLE PT_LOG SWITCH PARTITION 3 TO [PT_LOG_AUX] ;
GO

메시지 4939, 수준 16, 상태 1, 줄 1
ALTER TABLE SWITCH 문이 실패했습니다. 테이블 'PDB.dbo.PT_LOG_AUX'은(는) 파일 그룹 'FG3_AUX'에 있고 테이블 'PDB.dbo.PT_LOG'의 파티션 3은(는) 파일 그룹 'FG3'에 있습니다.

-- 파일 그룹이 다른 테이블은 스위칭 하지 못한다.
IF OBJECT_ID('DBO.PT_LOG_AUX') IS NOT NULL
DROP TABLE DBO.PT_LOG_AUX
GO

CREATE TABLE [dbo].[PT_LOG_AUX] (
                 PIDX INT -- 파티션 기준값
            , IDX INT IDENTITY(1,1)
            , DATA VARCHAR(6000) NULL
) ON FG3
GO

ALTER TABLE PT_LOG SWITCH PARTITION 3 TO [PT_LOG_AUX] ;
GO

-- 성공적으로 스위칭 된다.

SELECT * FROM PT_LOG WHERE PIDX = -100
-- 하나도 없다.
SELECT *
FROM SYS.PARTITION_RANGE_VALUES
WHERE
            FUNCTION_ID IN
                         (
                         SELECT FUNCTION_ID
                         FROM SYS.PARTITION_FUNCTIONS
                         WHERE NAME = ('MYRANGEPF3')
                         )

SELECT * FROM PT_LOG_AUX
-- 결과값이 나온다.

DBCC SHOWFILESTATS
-- 결국 같은 파일 그룹에서 정의만 다르게 해서 다른 테이블로 빼버리는 형태이다. 결론적으로 부하가 없다는 말입니다.

ALTER TABLE PT_LOG_AUX SWITCH TO [PT_LOG] PARTITION 3 ;
GO

결과

메시지 4982, 수준 16, 상태 1, 줄 2
ALTER TABLE SWITCH 문이 실패했습니다. 원본 테이블 'PDB.dbo.PT_LOG_AUX'의 CHECK 제약 조건에서 대상 테이블 'PDB.dbo.PT_LOG'의 파티션 3에서 정의한 범위가 허용하지 않는 값을 허용합니다.

SELECT OBJECT_NAME(OBJECT_ID), *
FROM SYS.PARTITIONS
WHERE OBJECT_ID = OBJECT_ID('PT_LOG')
ORDER BY PARTITION_NUMBER, INDEX_ID;

결과

PT_LOG    72057594039042048    2073058421    0    3    72057594039042048    0
실제 파티션이 비어 있음에도 FUNCTION 제약 조건이 단일 테이블에는 적용되어 있지 않기 때문에 다시 스위칭 하는것을 허용하지 않는것 같습니다.

꽁수를 부려라...

-- 꽁수 시작
CREATE PARTITION FUNCTION MYRANGEPF3_AUX (INT)
AS RANGE RIGHT FOR VALUES ( NULL, -100, 1, 100, 1000);
GO

CREATE PARTITION SCHEME PSMYSCHEME3_AUX
AS PARTITION MYRANGEPF3
ALL TO (FG3)

IF OBJECT_ID('DBO.PT_LOG_SRC') IS NOT NULL
DROP TABLE DBO.PT_LOG_AUX
GO

CREATE TABLE [dbo].[PT_LOG_SRC] (
                 PIDX INT -- 파티션 기준값
             , IDX INT IDENTITY(1,1)
             , DATA VARCHAR(6000) NULL
) ON PSMYSCHEME3_AUX(PIDX)
GO

SET IDENTITY_INSERT PT_LOG_SRC ON

INSERT INTO PT_LOG_SRC (PIDX, IDX, DATA)
             SELECT PIDX, IDX, DATA FROM PT_LOG_AUX

SET IDENTITY_INSERT PT_LOG_SRC OFF

ALTER TABLE PT_LOG_SRC SWITCH PARTITION 3 TO [PT_LOG] PARTITION 3 ;
GO

원래대로 끼워넣기 성공 파티션 스위치에 대한 감을 잡으셨기를 바랍니다.

다음 기사는 MSDN 에 올라온 글입니다.
관심있는 독자는 읽어보시길 바랍니다. SAN DISK 환경에서 일별로 어마어마한 로그가 쌓인다는 가정하에 2개의 자동화 프로시져를 이용하여 특정일 이후의 데이터를 삭제하는 멋진 로직을 설명 하고 있습니다. SQL2000 에서는 SP_RENAME 을 이용한 일별로그를 분리하곤 하지만 다양한 관리의 부담을 보이고 있으나, 오늘 배운 파티셔닝 기술과 SQL AGENT 로 두개의 프로시져만 번갈아 부르면 자동으로 현재일로부터 과거 몇일까지의 데이터를 저장하고 있는 테이블을 만들 수 있습니다.

EXAMPLE
            HOW TO IMPLEMENT AN AUTOMATIC SLIDING WINDOW IN A PARTITIONED TABLE ON SQL 2005
            http://msdn2.microsoft.com/en-us/library/aa964122.aspx

끝으로 이번에 장가 갑니다. 멀리서나마 축하해 주시면 감사하겠습니다.
또한 김연욱 선배님(SQL MVP), 김대우님(MS DPE팀), 김종균님(TECHDATA), 송혁님(SQL MVP),장홍창님 그리고 많은 우리 SQL OFFLINE STUDY 사람들이 도와줘서 이글을 쓰게 되었습니다. 감사합니다.

HTTP://SQLER.PE.KR에 오시면 석이를 만날 수 있습니다. ^^;
부족한 글 끝까지 읽어 주셔서 감사합니다.

728x90
제 2강 : 파티션드 테이블 인덱스

이름: 김민석
편지: MINSOUK@HOTMAIL.COM
NHN WWW.NHNCORP.COM DBA

전편에 이은 파티션드 인덱스에 대한 내용을 기술해 보고자 합니다. 파티션드 테이블은 로그성 데이터같이 시간순서로 계속 쌓이면서 일정 기간이 지나면 버려지는 데이터에 대해서 적합한 솔루션 입니다. 대표적인 예가 전편에 소개한 슬라이딩 윈도우입니다.

그러나 성능을 높이기 위한 OLTP 에서 파티션드 테이블을 고려하기 위해서는 많은 고민이 필요할 것 같습니다.

아래 TEST 는 다음 버전에서 수행 되었습니다.

Microsoft SQL Server 2005 - 9.00.3042.00 (Intel X86)
Feb 9 2007 22:47:07
Copyright (c) 1988-2005 Microsoft Corporation Developer Edition
on Windows NT 5.1 (Build 2600: Service Pack 2)

1. 데이터 베이스가 있으면 삭제합니다.

IF EXISTS
       (
             SELECT *
             FROM MASTER. DBO. SYSDATABASES
             WHERE NAME = 'PDB'
       )
       BEGIN
             DECLARE @KILL_LIST VARCHAR (MAX )
             SET @KILL_LIST = ''

             SELECT @KILL_LIST =
                    (
                    SELECT 'KILL ' + CAST (SPID AS VARCHAR (10))+ '; '
                    FROM MASTER. DBO. SYSPROCESSES
                    WHERE DBID = DB_ID ('PDB' )
                    FOR XML PATH ('' )
                    )
                    -- PRINT @KILL_LIST
                    EXEC (@KILL_LIST)

                    DROP DATABASE PDB
       END
GO

2. 파일그룹을 생성합니다.

FG1-8 파티션드 테이블을 테스트 할 목적의 파일그룹이고, 나머지 두개는 일반 테이블을 만들어 테스트 할 목적입니다.

ALTER DATABASE PDB
ADD FILEGROUP FG1;
ALTER DATABASE PDB
ADD FILEGROUP FG2;
ALTER DATABASE PDB
ADD FILEGROUP FG3;
ALTER DATABASE PDB
ADD FILEGROUP FG4;
ALTER DATABASE PDB
ADD FILEGROUP FG5;
ALTER DATABASE PDB
ADD FILEGROUP FG6;
ALTER DATABASE PDB
ADD FILEGROUP FG7;
ALTER DATABASE PDB
ADD FILEGROUP FG8;

ALTER DATABASE PDB
ADD FILEGROUP FG_DATA;
ALTER DATABASE PDB
ADD FILEGROUP FG_INDEX;

3. 파일그룹에 파일을 할당 합니다.

ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT1, FILENAME = 'C:\FG1\PDB_DAT1.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG1
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT2, FILENAME = 'C:\FG2\PDB_DAT2.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG2
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT3, FILENAME = 'C:\FG3\PDB_DAT3.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG3
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT4, FILENAME = 'C:\FG4\PDB_DAT4.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG4
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT5, FILENAME = 'C:\FG5\PDB_DAT5.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG5
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT6, FILENAME = 'C:\FG6\PDB_DAT6.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG6
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT7, FILENAME = 'C:\FG7\PDB_DAT7.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG7
ALTER DATABASE PDB ADD FILE (NAME = PDB_DAT8, FILENAME = 'C:\FG8\PDB_DAT8.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG8

ALTER DATABASE PDB ADD FILE (NAME = PDB_DATA, FILENAME = 'C:\FG8\PDB_DATA.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG_DATA
ALTER DATABASE PDB ADD FILE (NAME = PDB_INDEX, FILENAME = 'C:\FG8\PDB_INDEX.NDF' , SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP FG_INDEX

-- PDB 실제파일이어떻게구성되어있는지확인합니다 .
SELECT * FROM MASTER. DBO. SYSALTFILES WHERE DBID = DB_ID ('PDB' )

4. 파티션 하지 않을 테이블과 파티션 할 테이블을 만들고, 파티션 스위칭을 위해 같은 스키마 구조의 테이블을 추가로 생성 합니다.

-- 파티션하지않고성능테스트할테이블을 FG_DATA 에할당합니다 .
CREATE TABLE [dbo]. [tblx](
       [productid] [int] NULL,
       [ReferenceOrderID] [int] NOT NULL,
       [ReferenceOrderLineID] [int] NOT NULL,
       [TransactionDate] [datetime] NOT NULL,
       [TransactionType] [nchar](1) NOT NULL,
       [Quantity] [int] NOT NULL,
       [ActualCost] [money] NOT NULL,
       [ModifiedDate] [datetime] NOT NULL
) ON FG_DATA

-- 파티션할테이블생성합니다 .
CREATE TABLE [dbo]. [tbly](
       [productid] [int] NULL,
       [ReferenceOrderID] [int] NOT NULL,
       [ReferenceOrderLineID] [int] NOT NULL,
       [TransactionDate] [datetime] NOT NULL,
       [TransactionType] [nchar](1) NOT NULL,
       [Quantity] [int] NOT NULL,
       [ActualCost] [money] NOT NULL,
       [ModifiedDate] [datetime] NOT NULL
) ON PSMYSCHEME3([productid])

-- 파티션스위칭을위해같은구조의스키마를가지고있는테이블을만들어둡니다 .
CREATE TABLE [dbo]. [tblZ](
       [productid] [int] NULL,
       [ReferenceOrderID] [int] NOT NULL,
       [ReferenceOrderLineID] [int] NOT NULL,
       [TransactionDate] [datetime] NOT NULL,
       [TransactionType] [nchar](1) NOT NULL,
       [Quantity] [int] NOT NULL,
       [ActualCost] [money] NOT NULL,
       [ModifiedDate] [datetime] NOT NULL
) ON PSMYSCHEME3([productid])

5. 파티션 펑션과 스키마를 만들어 줍니다.

-- 파티션펑션만들기
CREATE PARTITION FUNCTION MYRANGEPF3 (INT)
AS RANGE RIGHT FOR VALUES (0, 100, 200, 300, 400, 500, 600);
GO

CREATE PARTITION FUNCTION MYRANGEPF3_COPY (INT)
AS RANGE RIGHT FOR VALUES (0, 100, 200, 300, 400, 500, 600);
GO

-- 파티션스키마 만들기

CREATE PARTITION SCHEME PSMYSCHEME3
AS PARTITION MYRANGEPF3
TO (FG1, FG2, FG3, FG4, FG5, FG6, FG7, FG8)
GO

IF OBJECT_ID ('DBO.TBL_TEMP') IS NOT NULL
DROP TABLE TBL_TEMP
IF OBJECT_ID ('DBO.TBLX') IS NOT NULL
DROP TABLE TBLX
IF OBJECT_ID ('DBO.TBLY') IS NOT NULL
DROP TABLE TBLY
IF OBJECT_ID ('DBO.TBLZ') IS NOT NULL
DROP TABLE TBLZ
GO

6. 파티션 테스트를 위한 데이터를 카피해 옵니다.

SELECT A.* INTO PDB. DBO. TBL_TEMP
FROM
       (
       SELECT
             CASE
               WHEN PRODUCTID > 600 THEN PRODUCTID - 600
               ELSE PRODUCTID END PRODUCTID
             , REFERENCEORDERID
             , REFERENCEORDERLINEID, TRANSACTIONDATE, TRANSACTIONTYPE
             , QUANTITY, ACTUALCOST, MODIFIEDDATE
       FROM
             ADVENTUREWORKS . PRODUCTION. TRANSACTIONHISTORYARCHIVE
       ) A
GO

7. tblx tbly 라는 테이블에 데이터를 삽입 합니다.

INSERT INTO TBLX
       SELECT * FROM TBL_TEMP
INSERT INTO TBLY
       SELECT * FROM TBL_TEMP

8. 위 절차를 수행한 후 데이터가 어떠한 파일 그룹에 위치해 있는지 확인 합니다.

DBCC SHOWFILESTATS

/*-----------------------------------------------------------------------
TBL_TEMP 파일그룹을 지정하지 않았으므로 PDB_PRIM 생성
TBLX PDB_DATA
TBLY 지정한 파일 그룹에 나누어져서

1      1      160    93     PDB_PRIM     C:\FG0\FG0_PRI.MDF        
-- TBL_TEMP
3      2      80     1      PDB_DAT1     C:\FG1\PDB_DAT1.NDF        --
4      3      80     2      PDB_DAT2     C:\FG2\PDB_DAT2.NDF          |
5      4      80     33     PDB_DAT3     C:\FG3\PDB_DAT3.NDF          |
6      5      80     22     PDB_DAT4     C:\FG4\PDB_DAT4.NDF          |
7      6      80     15     PDB_DAT5     C:\FG5\PDB_DAT5.NDF          |
8      7      80     4      PDB_DAT6     C:\FG6\PDB_DAT6.NDF          |
9      8      80     8      PDB_DAT7     C:\FG7\PDB_DAT7.NDF          |
10     9      80     1      PDB_DAT8     C:\FG8\PDB_DAT8.NDF        --
-- TBLY, (TBLZ 영역정의 )
11     10     80     73     PDB_DATA     C:\FG8\PDB_DATA.NDF       
-- TBLX
12     11     80     1      PDB_INDEX    C:\FG8\PDB_INDEX.NDF
------------------------------------------------------------------------*/

9. SYSPARTITION META 정보확인

SELECT OBJECT_NAME (OBJECT_ID ), *
FROM SYS.PARTITIONS
WHERE OBJECT_ID = OBJECT_ID ('tbly' )
ORDER BY PARTITION_NUMBER, INDEX_ID;

SELECT OBJECT_NAME (OBJECT_ID ), *
FROM SYS.PARTITIONS
WHERE OBJECT_ID = OBJECT_ID ('tblZ' )
ORDER BY PARTITION_NUMBER, INDEX_ID;

10. 인덱스를 만들어 봅니다.

그러나 실수 할 수 있으니 주의 하시길 바랍니다. SQL Server 에서 Clustered index 라는 것은 데이터 구조 입니다. 이러한 Clustered index 가 없는 데이터 구조는 HEAP 이라는 데이터 구조를 가지게 되고 [Fileid, Pageid, slotid] 로 데이터에 접근하게 됩니다. 그러므로 인덱스와 파일 구조를 분리하기 위해서 Clustered index 를 index 파일 그룹에 위치시킨다면 모든 데이터가 인덱스 파일 그룹으로 옮겨가게 되는 현상이 생기게 됩니다. 반드시 클러스터 인덱스는 데이터 파일 그룹에 형성 하시길 바랍니다.

이제 파티션 되지 않은 tblx Clustered index 를 만들어 보도록 하겠습니다.
만들고 난 후 DBCC SHOWFILESTATS 명령어로 확인해 있습니다 .

CREATE CLUSTERED INDEX CL_TBLX
       ON TBLX(PRODUCTID, TRANSACTIONDATE)
       ON FG_DATA

11. 파티션드 테이블에도 Clustered index 를 만들어 줍니다.

IF EXISTS (SELECT * FROM SYSINDEXES WHERE ID = OBJECT_ID ('DBO.TBLY') AND NAME = 'CL_TBLY' )
DROP INDEX TBLY. CL_TBLY
IF EXISTS (SELECT * FROM SYSINDEXES WHERE ID = OBJECT_ID ('DBO.TBLZ' ) AND NAME = 'CL_TBLZ' )
DROP INDEX TBLZ. CL_TBLZ
GO

-- 파티션 키로 분배되어 들어간 데이터 그대로 CLUSTERED INDEX 생성
CREATE CLUSTERED INDEX CL_TBLY
       ON TBLY(PRODUCTID, TRANSACTIONDATE)
       ON PSMYSCHEME3(PRODUCTID);

-- 동일인덱스 생성
CREATE CLUSTERED INDEX CL_TBLZ
       ON TBLZ(PRODUCTID, TRANSACTIONDATE)
       ON PSMYSCHEME3 (PRODUCTID);
GO

IF EXISTS (SELECT * FROM SYSINDEXES WHERE ID = OBJECT_ID ('DBO.TBLY') AND NAME = 'NC_TBLY_01')
DROP INDEX TBLY. NC_TBLY_01
GO
-- LOCAL NONCLUSTERED INDEX 생성
CREATE NONCLUSTERED INDEX NC_TBLY_01
       ON TBLY(TRANSACTIONDATE)
GO

12. 잠깐 용어 정리를 하도록 하겠습니다. 아직까지 SQL Server 2005 에서 관련 용어를 정의 하지 않았으므로, 먼저 이러한 기술을 도입한 다른 DBMS의 용어를 그대로 차용해 이 강좌에서 설명하도록 하겠습니다.

* LOCAL INDEX   : CØ 당 파티션에 해당 영역의 인덱스만 존재 하는 경우

* GLOBAL INDEX : 하나의 인데스 파일그룹이 모든 파티션을 통합한 인덱스를 가지는 경우

두 형태의 인덱스를 어떠한 상황에서 사용해야 하는가에 대해서 간단하게 설명하자면 LOCAL INDEX 의 경우 CLUSTERED INDEX 이든 NONCLUSTERED INDEX 이든 해당 파티션만의 억세스로 모든 억세스가 끝나는 경우이고 GLOBAL INDEX 의 경우는 찾고자 하는 데이터가 어떤 파티션에 있을지 모르는 상황에서의 인덱스를 정의 하시면 됩니다.그러나 LOCAL INDEX GLOBAL INDEX 의 여부에 따라서 파티션 스위칭의 제한 사항이 있으므로 각자의 상황에 따라 적절한 인덱스를 사용하시면 됩니다.

13. 다음은 CLUSTERED INDEX NONCLUSTERED INDEX 가 있는 상황에서의 물리적 상황을 조회하는 DMV 입니다.

SELECT
OBJECT_NAME (OBJECT_ID)
, INDEX_TYPE_DESC
, ALLOC_UNIT_TYPE_DESC -- , *
FROM
sys.dm_db_index_physical_stats
(db_id ( ), object_id ('tblY'), NULL, NULL, NULL);
GO

/* ---------------------------------------------------------------------------
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   CLUSTERED INDEX     IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA
tbly   NONCLUSTERED INDEX  IN_ROW_DATA

LOCAL NONCLUSTERED INDEX 생성 되었음
--------------------------------------------------------------------------- */

위의 결과를 출력해보면 CLUSTERED INDEX 의 파티션 된 개수와 NONCLUSTERED INDEX 의 상황을 볼 수 있습니다. 실제로 (결과값에서) 파티션드 키를 조건절에 넣지 못하는 상황에서 NONCLUSTERED INDEX 로 조회하는 상황이 발생한다면, NONCLUSTERED INDEX 모든 파티션에 접근해야 한다는 겁니다. 다음 테스트를 통해서 확인해 보도록 하겠습니다.

14. TEST

1. 원본테이블에서 2 개의 파티션에만 데이터가 있도록 가공

DELETE Y
--SELECT *
FROM TBLY Y
WHERE
       TRANSACTIONDATE = '20030708'
       AND PRODUCTID <> 106
       AND PRODUCTID <> 533
GO

SET SHOWPLAN_TEXT ON
GO
SELECT * FROM TBLY WHERE   transactionDate = '20030708'
GO
SET SHOWPLAN_TEXT OFF
GO
|--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1006], [Uniq1002], [PDB].[dbo].[tbly].[productid], [PDB].[dbo].[tbly].[TransactionDate], [Expr1008]) PARTITION ID:([PtnIds1006]) WITH UNORDERED PREFETCH)
       |--Compute Scalar(DEFINE:([PtnIds1006]=RangePartitionNew([PDB].[dbo].[tbly].[productid],(1),(0),(100),(200),(300),(400),(500),(600))))
       |    |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1007]) PARTITION ID:([PtnIds1007]))
        |         |--Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5)),((6)),((7)),((8))))
       |         |--Index Seek(OBJECT:([PDB].[dbo].[tbly].[NC_TBLY_01]), SEEK:([PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000') ORDERED FORWARD PARTITION ID:([PtnIds1007]))
       |--Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=[PDB].[dbo].[tbly].[productid] AND [PDB].[dbo].[tbly].[TransactionDate]=[PDB].[dbo].[tbly].[TransactionDate] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD PARTITION ID:([PtnIds1006]))

** Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5)),((6)),((7)),((8)))) 부분을주의깊게보길바랍니다 . 모든 파티션에 접근하게 됩니다 .

IF EXISTS (SELECT * FROM SYSINDEXES WHERE ID = OBJECT_ID ('DBO.TBLY') AND NAME = 'NC_TBLY_01' )
DROP INDEX TBLY. NC_TBLY_01
GO

CREATE NONCLUSTERED INDEX NC_TBLY_01
       ON TBLY(TRANSACTIONDATE)
       ON FG_INDEX
GO

SET SHOWPLAN_TEXT ON
GO
SELECT * FROM TBLY WHERE   transactionDate = '20030708'
GO
SET SHOWPLAN_TEXT OFF
GO

/*-------------------------------------------------------------------------
  |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1006], [Uniq1002], [PDB].[dbo].[tbly].[productid], [PDB].[dbo].[tbly].[TransactionDate], [Expr1007]) PARTITION ID:([PtnIds1006]) WITH UNORDERED PREFETCH)
       |--Compute Scalar(DEFINE:([PtnIds1006]=RangePartitionNew([PDB].[dbo].[tbly].[productid],(1),(0),(100),(200),(300),(400),(500),(600))))
       |    |--Index Seek(OBJECT :([PDB].[dbo].[tbly].[NC_TBLY_01]), SEEK:([PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000') ORDERED FORWARD)
       |--Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=[PDB].[dbo].[tbly].[productid] AND [PDB].[dbo].[tbly].[TransactionDate]=[PDB].[dbo].[tbly].[TransactionDate] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD PARTITION ID:([PtnIds1006]))

** 이번에는 모든 파일그룹과 조인 하는 실행계획이 빠진 것을 확인 할 수 있습니다.

위 테스트는 파티션드 인덱스에서 매우 중요한 사항이므로 반드시 알고 있어야 할 사항입니다.

그렇다고 하나의 인덱스 파티션(GLOBAL INDEX)으로 만들 경우 파티션 스위칭시 하나의 인덱스 파일그룹으로 되어 있으면 해당하는 인덱스를 DISABLE 해두고 스위칭 후 다시 REBUILD 해야 합니다. REBUILD가 완료될 때 까지 해당하는 인덱스를 사용할 수 없다는 점을 알고 있어야 합니다.

15. TEST

ALTER TABLE TBLY SWITCH PARTITION 3 TO TBLZ PARTITION 3 ;
GO
--Msg 7733, Level 16, State 4, Line 1
--'ALTER TABLE SWITCH' statement failed. The table 'PDB.dbo.TBLY' is partitioned while index 'NC_TBLY_01' is not partitioned.
-- NC_TBLY_01 이파티셔닝되지않았으므로스위칭할수없다는에러메시지입니다 .

ALTER INDEX [NC_TBLY_01] ON [dbo]. [tbly] DISABLE
GO

-- 성공적으로실행완료
ALTER TABLE TBLY SWITCH PARTITION 3 TO TBLZ PARTITION 3 ;
GO

-- DISABLE [NC_TBLY_01] 를다시리빌드함
ALTER INDEX [NC_TBLY_01] ON [DBO]. [TBLY] REBUILD
GO

-- NONCLUSTERED INDEX LOCAL INDEX 로구성
IF EXISTS (SELECT * FROM SYSINDEXES WHERE ID = OBJECT_ID ('DBO.TBLY') AND NAME = 'NC_TBLY_01')
DROP INDEX TBLY. NC_TBLY_01
GO

CREATE NONCLUSTERED INDEX NC_TBLY_01
       ON TBLY(TRANSACTIONDATE)
GO

ALTER TABLE TBLZ SWITCH PARTITION 3 TO TBLY PARTITION 3 ;
GO
--Msg 4947, Level 16, State 1, Line 1
--ALTER TABLE SWITCH statement failed. There is no identical index in source table 'PDB.dbo.TBLZ' for the index 'NC_TBLY_01' in target table 'PDB.dbo.TBLY' .
-- 소스파티션에없는인덱스구조가타겟에있으므로옮길수없다
CREATE NONCLUSTERED INDEX NC_TBLZ_01
       ON TBLZ(TRANSACTIONDATE)
GO

-- 스위칭성공
ALTER TABLE TBLZ SWITCH PARTITION 3 TO TBLY PARTITION 3 ;
GO

-- 이제는모두 LOCAL INDEX 이므로자유롭게타겟이비어있으면스위칭됩니다 .
ALTER TABLE TBLY SWITCH PARTITION 3 TO TBLZ PARTITION 3 ;
GO

ALTER TABLE TBLZ SWITCH PARTITION 3 TO TBLY PARTITION 3 ;
GO

  ** 인덱스재구성
특정 파티션 테이블을 온라인으로 리빌드하는 것은 문법상 불가능 합니다 .
이게 되면 좋을 같은데 , 아쉽습니다 .

-- 단일파일그룹테이블온라인리인덱스
ALTER INDEX ALL ON DBO. TBLX
REBUILD WITH (ONLINE = ON);

-- 특정파티션인덱스리빌드
ALTER INDEX CL_TBLY ON DBO. TBLY
REBUILD Partition = 3;

-- 파티션테이블을가지는테이블의온라인리인덱스
-- Online index operations are available only in SQL Server 2005 Enterprise Edition.

-- 파티션된테이블의모든인덱스온라인리빌드
ALTER INDEX ALL ON DBO. TBLY
REBUILD WITH (ONLINE = ON);

-- 클러스터인덱스만리빌드 (UNIQUE 일때는클러스터인덱스만리빌딩됩니다 .)
-- UNIQUE 제약조건이빠지면 NONCLUSTERED INDEX 도클러스터인덱스가리빌드될때같이됩니다 .
ALTER INDEX CL_TBLY ON DBO. TBLY
REBUILD WITH (ONLINE = ON);

-- 특정 NONCLUSTERED INDEX REBUILD
ALTER INDEX NC_TBLY_01 ON DBO. TBLY
REBUILD WITH (ONLINE = ON);

-- 파티션테이블의특정파티션만리빌드하면서 CPU 동시성을 1 로제한
ALTER INDEX NC_TBLY_01 ON DBO. TBLY
REBUILD Partition = 3 WITH (MAXDOP = 1)

마지막으로 파티션드 테이블을 온라인 리빌딩 할 수 있었고, 데이터가 많아지면 많이 느려지지만 무중단 시스템에서 점검 시간없이 인덱스 리빌딩이 가능하다는 결론을 얻을 수 있습니다. 실제 서비스 시스템에서 이것을 적용 할 지 못할지는 해당 DBA 가 업무특성에 따라 잘 결정해야 합니다.

다음은 SQL MVP 이신 김연욱 선배님이 테스트 해주신 자료 입니다. 참고 하시길 바랍니다.

et statistics profile on
set statistics io on
GO
SELECT * FROM TBLX WHERE   transactionDate = '20030708'
테이블 'tblx' . 검색수 1, 논리적읽기수 369
117    1      SELECT * FROM [TBLX] WHERE [transactionDate]= @1
117    1        | --Nested Loops(Inner Join, OUTER REFERENCES:([Uniq1002], [PDB].[dbo].[tblx].[productid], [PDB].[dbo].[tblx].[TransactionDate], [Expr1005]) WITH UNORDERED PREFETCH)
117    1             | --Index Seek(OBJECT:([PDB].[dbo].[tblx].[NC_TBLX_01]), SEEK:([PDB].[dbo].[tblx].[TransactionDate]='2003-07-08 00:00:00.000') ORDERED FORWARD)
117    117           | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tblx].[CL_TBLX]), SEEK:([PDB].[dbo].[tblx].[productid]=[PDB].[dbo].[tblx].[productid] AND [PDB].[dbo].[tblx].[TransactionDate]=[PDB].[dbo].[tblx].[TransactionDate] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD)

SELECT * FROM TBLY WHERE   transactionDate = '20030708'
테이블 'tbly' . 검색수 8, 논리적읽기수 289
117    1      SELECT * FROM [TBLY] WHERE [transactionDate]= @1
117    1        | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1006], [Uniq1002], [PDB].[dbo].[tbly].[productid], [PDB].[dbo].[tbly].[TransactionDate], [Expr1008]) PARTITION ID:([PtnIds1006]) WITH UNORDERED PREFETCH)
0      0             | --Compute Scalar(DEFINE:([PtnIds1006]=RangePartitionNew([PDB].[dbo].[tbly].[productid],(1),(0),(100),(200),(300),(400),(500),(600))))
117    1             |     | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1007]) PARTITION ID:([PtnIds1007]))
8      1             |          | --Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5)),((6)),((7)),((8))))
117    8             |          | --Index Seek(OBJECT:([PDB].[dbo].[tbly].[NC_TBLY_01]), SEEK:([PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000') ORDERED FORWARD PARTITION ID:([PtnIds1007]))
117    117           | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=[PDB].[dbo].[tbly].[productid] AND [PDB].[dbo].[tbly].[TransactionDate]=[PDB].[dbo].[tbly].[TransactionDate] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD PARTITION ID:([PtnIds1006]))


SELECT count (*) FROM TBLX WHERE   transactionDate = '20030708'
1      1      SELECT COUNT (*) FROM [TBLX] WHERE [transactionDate]= @1
0      0        | --Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1006],0)))
1      1             | --Stream Aggregate(DEFINE:([Expr1006]=Count(*)))
117    1                  | --Index Seek(OBJECT:([PDB].[dbo].[tblx].[NC_TBLX_01]), SEEK:([PDB].[dbo].[tblx].[TransactionDate]=CONVERT_IMPLICIT(datetime,[@1],0)) ORDERED FORWARD)
테이블 'tblx' . 검색수 1, 논리적읽기수 2

SELECT count (*) FROM TBLY WHERE   transactionDate = '20030708'
1      1      SELECT COUNT (*) FROM [TBLY] WHERE [transactionDate]= @1
0      0        | --Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1010],0)))
1      1             | --Stream Aggregate(DEFINE:([Expr1010]=Count(*)))
117    1                   | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1009]) PARTITION ID:([PtnIds1009]))
8      1                       | --Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5)),((6)),((7)),((8))))
117    8                       | --Index Seek(OBJECT:([PDB].[dbo].[tbly].[NC_TBLY_01]), SEEK:([PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000') ORDERED FORWARD PARTITION ID:([PtnIds1009]))
테이블 'tbly' . 검색수 8, 논리적읽기수 12

SELECT * FROM TBLX WHERE   productid = '111'
테이블 'tblx' . 검색수 1, 논리적읽기수 11
870    1      SELECT * FROM [TBLX] WHERE [productid]= @1
870    1        | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tblx].[CL_TBLX]), SEEK:([PDB].[dbo].[tblx].[productid]=CONVERT_IMPLICIT(int,[@1],0)) ORDERED FORWARD)

SELECT * FROM TBLY WHERE   productid = '111'
테이블 'tbly' . 검색수 1, 논리적읽기수 10
870    1      SELECT * FROM [TBLY] WHERE [productid]= @1
870    1        | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=(111)) ORDERED FORWARD PARTITION ID:((3)))

SELECT * FROM TBLX WHERE   productid = '111' and transactionDate = '20030708'
4      1      SELECT * FROM [TBLX] WHERE [productid]= @1 AND [transactionDate]= @2
4      1        | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tblx].[CL_TBLX]), SEEK:([PDB].[dbo].[tblx].[productid]=CONVERT_IMPLICIT(int,[@1],0) AND [PDB].[dbo].[tblx].[TransactionDate]=CONVERT_IMPLICIT(datetime,[@2],0)) ORDERED FORWARD)
테이블 'tblx' . 검색수 1, 논리적읽기수 3

SELECT * FROM TBLY WHERE   productid = '111' and transactionDate = '20030708'
4      1      SELECT * FROM [TBLY] WHERE [productid]= @1 AND [transactionDate]= @2
4      1        | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=(111) AND [PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000') ORDERED FORWARD PARTITION ID:((3)))
테이블 'tbly' . 검색수 1, 논리적읽기수 3

SELECT count (*) FROM TBLX WHERE   productid in ('2' , '111' ) and transactionDate = '20030708'
1      1      SELECT count (*) FROM TBLX WHERE   productid in ('2' , '111' ) and transactionDate = '20030708'
0      0        | --Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1007],0)))
1      1             | --Stream Aggregate(DEFINE:([Expr1007]=Count(*)))
4      1                  | --Index Seek(OBJECT:([PDB].[dbo].[tblx].[NC_TBLX_01]), SEEK:([PDB].[dbo].[tblx].[TransactionDate]='2003-07-08 00:00:00.000' AND [PDB].[dbo].[tblx].[productid]=(2) OR [PDB].[dbo].[tblx].[TransactionDate]='2003-07-08 00:00:00.000' AND [PDB].[dbo].[tblx].[productid]=(111)) ORDERED FORWARD)
테이블 'tblx' . 검색수 2, 논리적읽기수 4

SELECT count (*) FROM TBLY WHERE   productid in ('2' , '111' ) and transactionDate = '20030708'
1      1      SELECT count (*) FROM TBLY WHERE   productid in ('2' , '111' ) and transactionDate = '20030708'
0      0        | --Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1012],0)))
1      1             | --Stream Aggregate(DEFINE:([Expr1012]=Count(*)))
4      1                  | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1005]) PARTITION ID:([PtnIds1005]))
2      1                       | --Constant Scan(VALUES:(((2)),((3))))
4      2                       | --Index Seek(OBJECT:([PDB].[dbo].[tbly].[NC_TBLY_01]), SEEK:([PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000' AND [PDB].[dbo].[tbly].[productid]=(2) OR [PDB].[dbo].[tbly].[TransactionDate]='2003-07-08 00:00:00.000' AND [PDB].[dbo].[tbly].[productid]=(111)) ORDERED FORWARD PARTITION ID:([PtnIds1005]))
테이블 'tbly' . 검색수 4, 논리적읽기수 8


SELECT * FROM TBLX WHERE   productid IN ('2' , '111' )
테이블 'tblx' . 검색수 2, 논리적읽기수 15
875    1      SELECT * FROM TBLX WHERE   productid IN ('2' , '111' )
875    1        | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tblx].[CL_TBLX]), SEEK:([PDB].[dbo].[tblx].[productid]=(2) OR [PDB].[dbo].[tblx].[productid]=(111)) ORDERED FORWARD)

SELECT * FROM TBLY WHERE   productid IN ('2' , '111' )
테이블 'tbly' . 검색수 4, 논리적읽기수 20
875    1      SELECT * FROM TBLY WHERE   productid IN ('2' , '111' )
875    1        | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))
2      1             | --Constant Scan(VALUES:(((2)),((3))))
875    2             | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=(2) OR [PDB].[dbo].[tbly].[productid]=(111)) ORDERED FORWARD PARTITION ID:([PtnIds1004]))


SELECT productid, count (*) FROM TBLX WHERE   productid IN ('2' , '111' ) group by productid
2      1      SELECT productid, count (*) FROM TBLX WHERE   productid IN ('2' , '111' ) group by productid
0      0        | --Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1007],0)))
2      1             | --Stream Aggregate(GROUP BY:([PDB].[dbo].[tblx].[productid]) DEFINE:([Expr1007]=Count(*)))
875    1                  | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tblx].[CL_TBLX]), SEEK:([PDB].[dbo].[tblx].[productid]=(2) OR [PDB].[dbo].[tblx].[productid]=(111)) ORDERED FORWARD)
테이블 'tblx' . 검색수 2, 논리적읽기수 15


SELECT productid, count (*) FROM TBLY WHERE   productid IN ('2' , '111' ) group by productid
2      1      SELECT productid, count (*) FROM TBLY WHERE   productid IN ('2' , '111' ) group by productid
0      0        | --Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1013],0)))
2      1             | --Stream Aggregate(GROUP BY:([PDB].[dbo].[tbly].[productid]) DEFINE:([Expr1013]=Count(*)))
875    1                  | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1005]) PARTITION ID:([PtnIds1005]))
2      1                       | --Constant Scan(VALUES:(((2)),((3))))
875    2                       | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid]=(2) OR [PDB].[dbo].[tbly].[productid]=(111)) ORDERED FORWARD PARTITION ID:([PtnIds1005]))
테이블 'tbly' . 검색수 4, 논리적읽기수 20


SELECT * FROM TBLX WHERE   productid BETWEEN '90' AND '111'
2875   1      SELECT * FROM [TBLX] WHERE [productid]>= @1 AND [productid]<= @2
2875   1        | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tblx].[CL_TBLX]), SEEK:([PDB].[dbo].[tblx].[productid] >= CONVERT_IMPLICIT(int,[@1],0) AND [PDB].[dbo].[tblx].[productid] <= CONVERT_IMPLICIT(int,[@2],0)) ORDERED FORWARD)
테이블 'tblx' . 검색수 1, 논리적읽기수 25


SELECT * FROM TBLY WHERE   productid BETWEEN '90' AND '111'
2875   1      SELECT * FROM [TBLY] WHERE [productid]>= @1 AND [productid]<= @2
2875   1        | --Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1006]) PARTITION ID:([PtnIds1006]))
2      1             | --Constant Scan(VALUES:(((2)),((3))))
2875   2             | --Clustered Index Seek(OBJECT:([PDB].[dbo].[tbly].[CL_TBLY]), SEEK:([PDB].[dbo].[tbly].[productid] >= (90) AND [PDB].[dbo].[tbly].[productid] <= (111)) ORDERED FORWARD PARTITION ID:([PtnIds1006]))
테이블 'tbly' . 검색수 2, 논리적읽기수 27

감사합니다. 이상으로 SQL 2005의 파티션드 테이블 인덱스 특성에 대해서 조금이나마 이해 할 수 있는 자료가 되었으면 합니다. 언제나 글을 적고 나면 저 당시 왜그리 지식의 수준이 그것밖에 되지 않았을까 부끄러울 때가 많습니다. 서로 돕고 마음의 문을 열고 지식을 공유할 때 그 지식이 아름다운 꽃이 되지 않을까 생각합니다. 윗 글에 대한 의문사항이나 질문 있으시면 언제나 연락 주시길 바랍니다. SQLER.PE.KR 에서 또 뵙겠습니다.

* 도움주신분들
- NHN ORACLE 엔지니어
- 김연욱 (SQL MVP)
- 송혁 (SQL MVP)
- 김경연 (http://www.enuri.com/) DBA

* 참고도서
- EFFECTIVE ORACLE
- SQL2005 BOOKS ONLINE
728x90
제 1강 : Update 되는 Top ~ Order by 뷰 및 CTE를 이용한 접속로그 테이블 관리

이름 : 최석준
Email : beatchoi@gmail.com
CJIG ANIMA 개발실 DBA

로그인 로그아웃 시간을 저장하는 방법은 많이 있다. 기능을 소개하기 위해서 간단한 접속로그 테이블의 예를 통해서 MSSQL2000에서는 실행될 수 없었지만 MSSQL2005에서 가능해진 기능을 소개하고자 한다.

유저의 로그인 로그아웃 시간을 저장하는 다음과 같은 구조의 접속로그 테이블이 있다.

if object_id('ConnectLog','U') is not null
drop table ConnectLog
create table ConnectLog (
id varchar(10),
login datetime,
logout datetime
)

리스트 1 접속로그 테이블 만들기

이때 조건은 유저는 중간에 어플리케이션등의 오류로 인해 로그아웃이 기록되지 않은 채 로그아웃이 될 수도 있다. 가장 최근에 접속한 행을 업데이트 하는 문제이다.

declare @i int
set @i = 0
while @i < 365
begin
insert into ConnectLog
values('beatchoi',
        dateadd(ss,rand()* 1440,dateadd(d,@i,'2006-01-01')),
        case when @i = 364 then null
                when convert(int,rand() * 100) % 100 = 1 then null
            else dateadd(ss,(rand()+10) * 1440,
                    dateadd(d,@i,'2006-01-01'))
        end)
set @i = @i + 1
end

리스트 2 접속로그 테이블에 더미 데이터 입력

위와 같이 테이블에 1년간의 더미 데이터를 입력한다.

다음과 같이 많은 개발자들이 MSSQL2000 에서 정렬을 갖거나 행수 제한을 하기위한 뷰를 구현하기 위해서 Top ~ Order By 를 가진 정렬된 뷰를 생성을 할 수 있었다.

create view vw_ConnectLog
as
select top 1 Id,Logout
    from ConnectLog
  order by Login desc

리스트 3 Top ~ Order By 를 가진 정렬된 뷰만들기

MSSQL2000에서는 Top ~ Order By 포함하는 가진 뷰를 생성할 수는 있지만 업데이트시 다음과 같은 4427 에러를 발생시키고 업데이트는 할 수 없다.

update vw_ConnectLog
   set Logout = getdate()
  where Id = 'beatchoi'

서버: 메시지4427, 수준16, 상태1, 줄1
뷰또는함수'vw_ConnectLog' 정의에TOP 절이있어서업데이트할수없습니다.

리스트 4 Top ~ Order By를 가진 정렬된 뷰 업데이트 시 오류


UPDATE 가능한 MSSQL 2000 의 TOP ~ ORDER BY 를 포함하는 VIEW

Microsoft Certified Trainer (MCT)인 Zoltan Kovacs가 발견한 업데이트 가능한 정렬된 뷰를 생성할 수 있도록 해 주는 백도어가 있다. 이 기법은 Openquery() 함수내의 Select 쿼리에 Order By 절을 지정하는 것이다.

백도어를 이용하는 방법은 다음과 같다. Openquery () 를 호출하여 자기 자신 서버에 대하여 쿼리를 실행하기 위해서는 우선 Data Access 서버 옵션을 설정하는 것이 필요하다.

exec sp_serveroption '[원격서버명]','data access', true

create view vw_ConnectLog_Backdoor as
SELECT a
FROM OPENROWSET('SQLOLEDB','[원격서버명]';'[로그인]';'[암호]'
    'SELECT top 1 * FROM tempdb.dbo.ConnectLog ORDER BY login') AS a

update vw_ConnectLog_Backdoor
    set Logout = getdate()
  where Id = 'beatchoi'

서버: 메시지7320, 수준16, 상태2, 줄1
OLE DB 공급자'SQLOLEDB'에대해쿼리를실행할수없습니다. 공급자가필요한행조회인터페이스를지원할수없습니다. 공급자가다른속성또는요구사항과의충돌이발생했음을나타냅니다.
[OLE/DB provider returned message: 여러단계OLE DB 작업을하는동안오류가발생했습니다. 각OLE DB 상태값이있으면확인해보십시오. 완료된작업이없습니다.]
OLE DB 오류추적[OLE/DB Provider 'SQLOLEDB' ICommandText::Execute returned 0x80040e21: SELECT top 1 * FROM tempdb.dbo.ConnectLog ORDER BY login[PROPID=DBPROP_IRowsetLocate VALUE=True STATUS=DBPROPSTATUS_CONFLICTING], [PROPID=DBPROP_BOOKMARKS VALUE=True STATUS=DBPROPSTATUS_CONFLICTING]].

리스트 5 MSSQL2000 백도어를 이용한 업데이트 되는 Top ~ Order By를 가진 뷰

업데이트를 실행하면 위와 같은 오류가 발생하는데, 이는 SQL Server OLE DB 공급자는 UPDATE 또는 DELETE 작업을 위한 기본 테이블에 고유한 색인을 필요로 하기 때문이다. 고유한 색인이 원격 테이블에 없을 경우 Update나 Delete를 시도하면 다음 오류가 발생하며, OpenQuery와 이름이 4부분으로 된 Update와 Delete작업 모두에 적용된다. 원격 테이블에 다음과 같이 고유한 색인을 추가하면 문제가 해결된다.

--고유한 색인 추가
alter table ConnectLog add idx int identity primary key

update vw_ConnectLog_Backdoor
     set Logout = getdate()
  where Id = 'beatchoi'

(1개행적용됨)

리스트 6 MSSQL2000 백도어를 이용한 업데이트 되는 뷰를 위해 색인 추가

고유한 색인을 추가면 정렬도 되고 업데이트도 가능하지만 다음과 같은 단점이 있다. ANSI 호환이 아니라는 점 외에 로컬 쿼리에 비해 더 많은 비용이 드는 분산 쿼리를 사용한다는 점이다.

따라서 MSSQL2000에서 가장 최근의 행을 업데이트 하기 위해서는 다음과 같은 쿼리를 해야 했다.

update a
    set Logout = getdate()
  from ConnectLog a
    join ( select id, max( login ) as login
              from ConnectLog
          group by id) b
     on a.id = b.id and a.login = b.login

(1개행적용됨)

리스트 7 MSSQL2000에서 가장 최근의 행을 업데이트 하는 쿼리

Openquery를 이용한 View와 위의 쿼리의 예상실행계획을 보면, Openquery를 이용한 View가 99.15% 일반 쿼리가 0.85% 로 성능과 제약조건으로 인해 Openquery를 이용한 View 는 실제 서비스에서 쓰기는 어려워 보인다.

리스트 8 백도어를 이용한 뷰와 일반쿼리 실행계획


UPDATE 가능한 MSSQL 2005의 TOP ~ ORDER BY 를 포함하는 VIEW

하지만 MSSQL2005 에서는 Top ~ Order By 를 가진 뷰를 만들고 업데이트 할수 있다.

create view vw_ConnectLog
as
select top 1 Id,Logout
   from ConnectLog
   order by Login desc

update vw_ConnectLog
    set Logout = getdate()
  where Id = 'beatchoi'

(1개행적용됨)

리스트 9 MSSQL2005에서 업데이트 되는 Top ~ Order By를 가진 뷰

다음은 MSSQL2000 방식과 MSSQL2005 두 쿼리의 예상 실행계획 이다. TOP ~ ORDER BY 포함하는 가진 뷰가 실행계획이 좀 더 간결하다.

리스트 10 MSSQL2005 업데이트 되는 Top ~ Order by를 가진 뷰와 일반쿼리 실행계획


MSSQL 2005의 공통테이블식 CTE (COMMON_TABLE_EXPRESSION)

MSSQL2005에서 구현할수 있는 또 다른 방법은 공통테이블식 CTE (Common_Table_Expression) 를 이용하는 방법이다.

CTE 구문

[ WITH <common_table_expression> [ ,...n ] ]

<common_table_expression>::=
          expression_name [ ( column_name [ ,...n ] ) ]
       AS
          ( CTE_query_definition )

리스트 11 CTE 구문

일반적으로 Select 문과 재귀적인 쿼리문에만 CTE를 사용하는 경우가 있지만 CTE는 Update,Insert,Delete 모두 지원한다. 코드도 간결하며, 뷰 같은 물리적인 오브젝트를 관리하지 않아도 된다는 장점이 있으며 다음 실행에서 보면, 실행시간 및 I/O등에서도 별반 차이가 없다.

뷰를 사용한 업데이트 문을 CTE구문에 맞게 작성을 해보면 다음과 같다.

;with cte_ConnectLog (id,logout) as (
select top 1 id,logout
    from ConnectLog
  order by login desc
)
update cte_ConnectLog
    set Logout = getdate()
  where Id = 'beatchoi'

(1개행적용됨)

리스트 12 CTE를 이용한 가장 최근의 행을 업데이트 하는 쿼리

리스트 13 MSSQL2005 업데이트 되는 Top ~ Order by를 가진 뷰와 CTE실행계획

뷰를 사용한 업데이트 문을 CTE를 실행하여보면 둘의 실행계획은 정확하게 일치한다. 다음은 위의 세가지 식을 테스트한 결과이다. 테스트 결과를 보면 TOP ~ ORDER BY 뷰와 CTE는 차이가 없으며, 기존 MSSQL2000방식보다 미세하게 성능이 나은 것을 볼수 있다.

SET STATISTICS IO ON
SET STATISTICS TIME ON
GO
update a
     set Logout = getdate()
   from ConnectLog a
     join ( select id,max(login) as login
               from ConnectLog
            group by id) b
      on a.id = b.id and a.login = b.login

SQL Server 구문분석및컴파일시간:
      CPU 시간 = 16ms, 경과시간 = 16ms.
테이블'ConnectLog'. 검색수1, 논리적읽기수3, 물리적읽기수0, 미리읽기수0, LOB
논리적읽기수0, LOB 물리적읽기수0, LOB 미리읽기수0.

SQL Server 실행시간:
      CPU 시간 = 0ms, 경과시간 = 4ms.

(1개행적용됨)

update vw_ConnectLog
      set logout = getdate()
   where id = 'beatchoi'

SQL Server 구문분석및컴파일시간:
     CPU 시간 = 5ms, 경과시간 = 5ms.
SQL Server 구문분석및컴파일시간:
     CPU 시간 = 0ms, 경과시간 = 1ms.
테이블'ConnectLog'. 검색수1, 논리적읽기수3, 물리적읽기수0, 미리읽기수0, LOB
논리적읽기수0, LOB 물리적읽기수0, LOB 미리읽기수0.

SQL Server 실행시간:
     CPU 시간 = 0ms, 경과시간 = 2ms.

(1개행적용됨)

;with cte_ConnectLog (id,Logout) as (
select top 1 id,Logout
      from ConnectLog
   order by login desc
)
update cte_ConnectLog
      set logout = getdate()
   where id = 'beatchoi'

SQL Server 구문분석및컴파일시간:
     CPU 시간 = 0ms, 경과시간 = 4ms.
테이블'ConnectLog'. 검색수1, 논리적읽기수3, 물리적읽기수0, 미리읽기수0, LOB
논리적읽기수0, LOB 물리적읽기수0, LOB 미리읽기수0.

SQL Server 실행시간:
     CPU 시간 = 15ms, 경과시간 = 2ms.

(1개행적용됨)

리스트 14 일반쿼리와 되는 Top ~ Order by를 가진 뷰와 CTE 실행통계

+ Recent posts