728x90
제 1강 : SQL Server 2005 .NET CLR 통합기능

이름 : 김종균
전) 벅스(주) 프로그래머, DBA
현) (주) 테크데이타 SQL Server Technical Support Engineer

개요

SQL 서버 2005에서는 .NET Framework2.0의 Common Language Runtime 기술을 이용하여 매우 향상된 데이터베이스 프로그램을 구현 할 수 있습니다. 이는 Microsoft Visual C#, Microsoft Visual Basic.NET, Microsoft Visual C++ 등의 CLR 언어를 통해서 저장프로시저, 사용자정의 함수, 트리거를 생성할 수 있을 뿐 아니라, 사용자 정의 데이터타입 및 집계의 생성을 가능하게 합니다.

TSQL의 그 능력의 한계

TSQL은 RDBMS만을 위한 언어이기 때문에 DML, DDL외의 프로그래밍 언어로서의 기능이 아주 미약합니다. TSQL에서 제공하는 함수는 제한적이며 (SQL2005에서 기능이 많이 추가되긴 하였지만), TSQL 로는 SQL 서버 외부 개체에 접근할 수 없습니다. 예를 들면 파일시스템 오브젝트를 통해서 파일의 내용을 읽고, 쓰는 등의 작업은 TSQL 로는 상상도 할 수 없습니다. 물론 SQL2000에서도 Extended Stored Procedure를 통해서 SQL외부 개체에 대한 핸들링을 할 수 있었으나, 상대적으로 어려운 C,C++프로그래밍에 대한 접근성과 in-process형태로 Sqlserver프로세스에서 실행되었기 때문에 자칫 확장 저장프로시져에 치명적인 코드결함이 있으면, SQL서버가 다운되는 경우도 있었습니다
하지만 CLR을 사용함으로써 SQL서버에서 기존에는 할 수 없었던 다양한 프로그래밍을 가능하게 합니다.

TSQL과 CLR의 몇 가지 장단점

  • TSQL은 인터프리터 방식이므로 느리다.
  • TSQL의 에러핸들링은 SQL2005에서 많이 향상되긴 했지만 비교적 좋지 않다. 
  • CLR은 컴파일 된 형태이므로 실행이 빠르다. 
  • CLR은 .NET프로그래밍 언어의 방대한 Class라이브러리를 사용하여 막강한 프로그래밍을 할 수 있다. 
  • TSQL은 대량의 데이터 액세스 혹은 데이터 조작 작업등에 유리하다. 
  • TSQL은 절차적 언어가 아니라 선언적 언어라서 문자열 조작, 데이터 포맷팅, 절차적 혹은 반복적인 로직에는 성능이 좋지 않다. 
  • CLR은 계산 집중적이거나 절차적,반복적 로직, 문자열 처리에서 좋은 성능을 나타낸다.

CLR 통합기능을 활용한 개발 절차

   1. SQL2005에서 CLR통합기능 옵션 활성화

   2. .NET Framework에서 지원하는 언어를 이용하여 클래스 작성

   3. 언어컴파일러를 통한 클래스 컴파일

   4. SQL서버에서 ASSEMBLY등록

   5. 등록한 ASSEMBLY 를 이용하여 사용자 개체 생성

.NET CLR통합기능 활성화

SQL2005는 보안상의 이유로 .NET CLR통합기능을 비활성화 해두고 있습니다. 따라서 CLR통합기능을 사용하기 위해서는 ‘Clr Enabled’ 라는 구성옵션을 활성화 해야 합니다. .NET CLR통합 기능을 사용할 수 있게 해주는 옵션을 활성화하는 방법은 두 가지가 입니다.

첫 번째 방법은, sp_configure 구성옵션 명령을 통해서 ‘clr enabled’ 라는 구성옵션을 ‘1’로 세팅해주는 것입니다. 이 옵션은 수정 즉시 적용되므로, SQL서버를 재 시작 할 필요가 없습니다.

SP_CONFIGURE 'clr enabled',1
RECONFIGURE WITH OVERRIDE
GO
SP_CONFIGURE
GO
name minimum maximum config_value run_value
----------------------- ------------ ------------ ------------ ------------
clr enabled 0 1 1 1
lightweight pooling 0 1 0 0

유의해야 할 사항은 clr enabled옵션이 해제되게 되면 등록된 모든 ASSEMBLY가 unload되게 됩니다.
그리고 clr enabled옵션과 lightweight pooling옵션은 병행해서 사용이 불가능합니다.

두 번째 방법은, SQL서버 노출영역 구성에서 기능에 대한 노출영역을 구성하는 것 입니다. 아래 그림과 같이 clr enabled 옵션을 체크 해주면 됩니다.

SQL서버 노출영역 구성

예제1 – 사용자 정의 스칼라 함수 구현

그럼, CLR을 통한 간단한 사용자 정의 함수를 생성하고 SQL서버에 이식시켜 사용해보는 예제를 구현해보면서, CRL을 이용한 SQL프로그래밍에 대한 감을 잡아보겠습니다.

예제로 email주소를 정합성을 검사해주는 사용자함수 클래스를 C#으로 구현해 보겠습니다. 소스코드 작성을 위해서 메모장을 이용하셔도 무방합니다만.. 가능하시면 Visual Studio를 이용하시면 보다 더 훌륭한 개발 인터페이스를 통해서 더 빠르고 쉽게 코딩이 가능합니다. 여기서, c#코드에 대한 설명은 하지 않습니다.

개발 도구를 사용하여 아래 c#코드를 작성합니다.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text.RegularExpressions;


public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction]
public static bool emailCheck(string email)
{
string pattern=@"^[a-z][a-z|0-9|]*([_][a-z|0-9]+)*([.][a-z|" + @"0-9]+([_][a-z|0-9]+)*)?@[a-z][a-z|0-9|]*\.([a-z]" + @"[a-z|0-9]*(\.[a-z][a-z|0-9]*)?)$";
Match match = Regex.Match(email, pattern, RegexOptions.IgnoreCase);

if(match.Success) return true;
else return false;
}
};

[소스1] emailCheck.cs

소스코드 작성이 완료되면 c#컴파일러를 통해 cs파일을 dll형태로 컴파일 합니다.

csc.exe /target:library /out:emailcheck.dll emailcheck.cs

참고. csc컴파일러의 위치는 .NetFramework이 설치된 폴더에 있습니다.
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727

자 이렇게 해서 emailcheck.dll 이라는 어셈블리 파일을 생성했습니다. 이 파일을 관리하시는 적절한 폴더에 복사 하십시요. 이 파일은 추후에 SQL2005의 ASSEMBLY로 등록하게 되면, 삭제해도 무관합니다.

다음 단계로 CLR사용자 함수 ASSEMBLY를 SQL서버에 ASSEMBLY로 등록합니다.

CREATE ASSEMBLY SQLER_UDF
FROM 'C:\TEMP\emailcheck.dll'
WITH PERMISSION_SET = SAFE
GO

CREATE ASSEMBLY는 이미 .dll파일로 미리 컴파일된 ASSEMBLY를 SQL2005 내부에서 사용할 수 있도록 업로드 혹은 바인딩 하는 작업입니다.


다음 단계로 ASSEMBLY를 매핑하여 사용자 정의 함수를 생성합니다.

CREATE FUNCTION UDF_VALIDATEEMAIL (@email as nvarchar (100))
RETURNS bit
AS EXTERNAL NAME SQLER_UDF.MyFunctions.emailCheck
GO

사용자 개체 생성시EXTERNAL NAME 지정규칙은 다음과 같습니다.
[SQL Server Assembly Name].[Fully Qualified Path to Class].[Static Method Name]

자. 그럼 이제 생성한 스칼라 사용자 정의 함수를 사용해보겠습니다.

SELECT DBO.UDF_VALIDATEEMAIL ('bellvirus1@naver.com')     --1
SELECT DBO.UDF_VALIDATEEMAIL ('bellvirus1@naver')           --0
SELECT DBO.UDF_VALIDATEEMAIL ('bellvirus1.com')               --0
GO

상기 절차로 .NET 프로그램 코드를 통한 UDF생성을 간단하게 살펴보았습니다.

예제2 – 스칼라 함수, 저장프로시져 및 트리거 구현

다음 예제로 테이블의 전체 행 수를 반환하는 스칼라 함수와, 레코드 셋을 가져오는 저장프로시져, 데이터 삭제에 대해 로그를 기록하는 트리거를 Visual Basic .NET 으로 구현 해보겠습니다.

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes

Public Class MySQLClass
<SqlFunction(DataAccess:=DataAccessKind.Read)>_
Public Shared Function ReturnOrderCount() As Integer
Using conn As New SqlConnection("context connection=true")
conn.Open()
Dim cmd As New SqlCommand("SELECT COUNT(*) AS 'ORDERCNT' FROM SALES.SALESORDERDETAIL", conn)
Return CType(cmd.ExecuteScalar(), Integer)
End Using
End Function


Public Shared Sub GetSalesOrderDetail()
Using conn As New SqlConnection("context connection=true")
conn.Open()
Dim cmd As New SqlCommand("SELECT COUNT(*) AS 'ORDERCNT' FROM SALES.SALESORDERDETAIL", conn)
SqlContext.Pipe.ExecuteAndSend(command)
End Using
End Sub


<Microsoft.SqlServer.Server.SqlTrigger()>_
Public Shared Sub writeDeleteLog()
Dim oTc As SqlTriggerContext
oTc = SqlContext.TriggerContext
If (oTc.TriggerAction = TriggerAction.Delete) Then
Dim oFi As New System.IO.StreamWriter("c:\temp\log.txt", True)
oFi.Write("something deleted !")
oFi.Close()
End If
End Sub

End Class

[소스2] MySQLClass.vb

코드 작성이 완료되면 컴파일러를 통해 DLL형태의 파일을 생성합니다.

vbc /target:library /out:c:\temp\MySQLClass.dll c:\temp\MySQLClass.vb

그런 다음에 SQL서버에서 ASSEMBLY로 등록합니다.
이번 예제에서는 ASSEMBLY생성시에 PERMISSION_SET옵션을 EXTERNAL_ACCESS를 지정하였는데, 그 이유는 writeDeleteLog 메서드가 SQL 외부 개체에 액세스 할 필요가 있기 때문입니다.

여기서 잠깐 PERMISSION_SET옵션에 대해 알아보고 넘어 가겠습니다.

  • SAFE (기본옵션) : 등록하게 되는 어셈블리에서 실행한 코드는 파일, 네트워크, 환경변수 또는 레지스트리와 같은 외부 시스템 리소스에 액세스할 수 없습니다.
  • EXTERNAL_ACCESS : 파일, 네트워크, 환경 변수 또는 레지스트리와 같은 외부 시스템 리소스에 액세스할 수 있습니다.
  • UNSAFE : SQL Server 인스턴스의 내부 리소스와 외부 리소스 모두에 제한 없이 액세스할 수 있습니다.
CREATE ASSEMBLY SQLER_CLASS
FROM 'C:\TEMP\MYSQLClass.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS
GO

생성 명령을 실행하였으나 아래와 같은 오류가 발생합니다.


메시지10327, 수준14, 상태1, 줄1 어셈블리'MySQLClass'에PERMISSION_SET = EXTERNAL_ACCESS에대한권한이없으므로어셈블리'MySQLClass'에대한CREATE ASSEMBLY가실패했습니다. 어셈블리는DBO(데이터베이스소유자)에게EXTERNAL ACCESS ASSEMBLY 권한이있고데이터베이스에TRUSTWORTHY 데이터베이스속성이있는경우또는어셈블리가현재인증서로서명되어있거나EXTERNAL ACCESS ASSEMBLY 권한이있는관련로그인을소유한비대칭키로서명되어있는경우에권한이부여됩니다.

사용자 정의함수나 저장프로시져에서 외부 개체에 접근하려면 Database의 TRUSTWORTHY 옵션을 활성화 해주어야 합니다. 아래 명령을 실행하여 외부 개체 접근을 허용해줍니다. 그런 다음 다시 ASSEMBLY를 생성해줍니다.

ALTER DATABASE AdventureWorks SET TRUSTWORTHY ON
GO

CREATE ASSEMBLY SQLER_CLASS
FROM 'C:\TEMP\MYSQLClass.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS
GO

SQLER_CLASS 어셈블리를 활용하여 사용자 정의 스칼라 함수를 생성합니다.

CREATE FUNCTION UDF_getOrderCount()
RETURNS INT
AS EXTERNAL NAME SQLER_CLASS.MySQLClass.ReturnOrderCount
GO

생성한 함수를 실행하여 보겠습니다.

SELECT DBO.UDF_getOrderCount() AS orderCount
GO
orderCount
-----------
121317

SQLER_CLASS 어셈블리를 활용하여 저장프로시져를 생성합니다.

CREATE PROC UP_getSalesOrderDetail
AS EXTERNAL NAME SQLER_CLASS.MySQLClass.GetSalesOrderDetail
GO

프로시져를 실행하여 보겠습니다.

EXEC UP_getSalesOrderDetail
GO

43659  1      4911-403C-98 1      776    1      2024.994
43659  2      4911-403C-98 3      777    1      2024.994
43659  3      4911-403C-98 1      778    1      2024.994
43659  4      4911-403C-98 1      771    1      2039.994
43659  5      4911-403C-98 1      772    1      2039.994
43659  6      4911-403C-98 2      773    1      2039.994
43659  7      4911-403C-98 1      774    1      2039.994
43659  8      4911-403C-98 3      714    1      28.8404
43659  9      4911-403C-98 1      716    1      28.8404

결과를 잘 반환해 줍니다.

다음으로 트리거를 생성하여 보겠습니다.
이 예제를 위해서 간단한 샘플 테이블을 생성하겠습니다.

CREATE TABLE TBL_TEST (idx int, name varchar(20))
GO
INSERT INTO TBL_TEST VALUES (1,'KIM')
INSERT INTO TBL_TEST VALUES (1,'LEE')
INSERT INTO TBL_TEST VALUES (1,'PARK')
GO

TBL_TEST테이블에 대해서 DELETE명령에 대한 트리거 생성합니다.

CREATE TRIGGER TRG_DELETE
ON TBL_TEST
FOR DELETE
AS EXTERNAL NAME SQLER_CLASS.MySQLClass.writeDeleteLog
GO

트리거가 잘 생성되었고, 실제로 데이터를 삭제하여 트리거가 정상적으로 동작하는지 살펴보겠습니다. 앞서 작성한 writeDeleteLog 메서드는 삭제 트리거가 발생하면 메모장에서 log.txt파일에 Something Deleted! 를 기록하게 됩니다.

DELETE FROM TBL_TEST WHERE idx = 1
GO

메모장에서 log.txt파일 확인

예제3. 사용자 정의 집계함수(Aggegation)의 구현

이번 예제에서는 각 행들의 특정 문자열 컬럼을 합해주는 함수를 만들어 보겠습니다.
이 함수는 숫자형 집계함수인 SUM()과 유사합니다 ^^
이번 예제는 온라인 도움말에서 제공하는 샘플을 사용합니다.

using System;
using System.IO;
using System.Data.Sql;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1020:AvoidNamespacesWithFewTypes"
, Scope = "namespace", Target =
"Microsoft.Samples.SqlServer")]

namespace Microsoft.Samples.SqlServer
{
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
Microsoft.SqlServer.Server.Format.UserDefined,       //use clr
serialization to serialize the intermediate result

IsInvariantToNulls = true,       //optimizer property
IsInvariantToDuplicates = false,       //optimizer property
IsInvariantToOrder = false,       //optimizer property
MaxByteSize = 8000)       //maximum size in bytes of persisted value
]
public classConcatenate : Microsoft.SqlServer.Server.IBinarySerialize
{
/// <summary>
///The variable that holds the intermediate result of the concatenation
/// </summary>
private StringBuilder intermediateResult;

/// <summary>
/// Initialize the internal data structures
/// </summary>
public void Init()
{
intermediateResult = new StringBuilder();
}

       
/// <summary>

///Accumulate the next value, nop if the value is null
/// </summary>
/// <param name="value"></param>
public void Accumulate(SqlString value)
{

if (value.IsNull)
{
return;
}
intermediateResult.Append(value.Value).Append(',');
}

/// <summary>
///Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(Concatenate other)
{
intermediateResult.Append(other.intermediateResult);
}

/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation
/// </summary>
/// <returns></returns>
public SqlString Terminate()
{
string output = string.Empty;
//delete the trailing comma, if any
if (intermediateResult != null && intermediateResult.Length > 0)
output = intermediateResult.ToString(0, intermediateResult.Length - 1);
return new SqlString(output);
}
public void Read(BinaryReader r)
{
intermediateResult = new StringBuilder(r.ReadString());
}

public void Write(BinaryWriter w)
{
w.Write(intermediateResult.ToString());
}
}
}
[소스3] concat.cs

소스코드 작성이 완료되면 c#컴파일러를 통해 cs파일을 dll형태로 컴파일 합니다.

csc.exe /target:library /out:concat.dll concat.cs

concate.dll파일을 ASSEMBLY로 등록하고, 집계함수를 생성합니다.

CREATE ASSEMBLY STRINGHANDLES3
FROM 'C:\TEMP\concat.dll'

CREATE AGGREGATE [dbo].[Concatenate](@input nvarchar(4000))
RETURNS nvarchar(4000)
EXTERNAL NAME
STRINGHANDLES3.[Microsoft.Samples.SqlServer.Concatenate];
GO

테스트를 위한 샘플 테이블을 생성하고, 데이터를 몇 건 삽입합니다.

CREATE TABLETEST (IDX INT, NAME VARCHAR(10))
GO

INSERT INTO TEST VALUES (1,'AAA')
INSERT INTO TEST VALUES (2,'BBB')
INSERT INTO TEST VALUES (3,'CCC')
INSERT INTO TEST VALUES (4,'DDD')
INSERT INTO TEST VALUES (5,'EEE')
INSERT INTO TEST VALUES (6,'KKK')
GO

집계함수를 활용하여 쿼리를 실행합니다.

SELECT SUM(IDX)AS SUMIDX, dbo.concatenate(name) AS CONCATNAMES
FROM TEST
GO

SUMIDX      CONCATNAMES
---------  ----------------------------------
21          aaa,bbb,ccc,ddd,eee,kkk

(1개행적용됨)

예제4. 사용자 정의 테이블 값 함수의 구현

이번에는 특정 구분자를 이용하여 요소를 분리해주는 SPLIT함수를 구현해 보겠습니다.

using System;
using System.Collections;
using System.Data;
using System.Data.Sql;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;


public sealed class StringSplitter
{
[SqlFunction(FillRowMethodName = "FillRow")]
public static IEnumerable Split(SqlString argument1, Char []
argument2)
{
string value;

if (argument1.IsNull)
value = "";
else
value = argument1.Value;

return value.Split(argument2);
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1021:AvoidOutParameters")]
public static void FillRow(Object obj, out string stringElement)
{
stringElement = (string)obj;
}
}
[소스4] Split.cs

소스를 컴파일 합니다.

csc.exe /target:library /out:split.dll split.cs

어셈블리를 등록합니다.
CREATE ASSEMBLY STRINGHANDLES
FROM 'C:\TEMP\split.dll'
GO

사용자 정의 함수를 생성합니다.

CREATE FUNCTION DBO.UDF_SPILT (@INPUT NVARCHAR (4000), @DELIMETER
NVARCHAR(10))
RETURNS TABLE (ELEMENT NVARCHAR (MAX))
AS EXTERNAL NAME STINGHANDLE.[ StringSplitter]. Split;
GO

실행테스트

SELECT * FROM DBO.UDF_SPILT 'MSSQL/EXCHANGE/OFFICE/VISTA' , >'/');
GO

element
-----------------
MSSQL
EXCHANGE
OFFICE
VISTA
(4개행적용됨)

마치며..

간단한 예제 중심으로 SQL서버2005 .NET CLR통합 기능에 대해서 살펴보았습니다. 이처럼, TSQL만으로는 불가능 했던 프로그래밍을 가능하게 TSQL에 날개를 달아주는 것이 .NET CLR입니다. 언제나 그렇듯이, 좋은 기능을 제대로 사용하지 않거나, 남용하면 역효과를 가져 올 수 있습니다. TSQL만으로 구현 가능한 것들은 TSQL로 구현하는게 바람직 할 것입니다. 주어진 상황에 대해서 TSQL과 CLR 중 어떠한 것을 적용해야 효율적인가 에 대한 많은 고민과 테스트 과정을 통해서 올바른 판단을 해야 할 것입니다.

감사합니다.

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
제 1강 : SQL Server 재 컴파일

이름: 송혁
Email : hyok81@nate.com
넥슨 DSM팀 DBA로 근무


1. SQL Server의 재 컴파일이란?

SQL Server 는 비용 기반의 최적화(cost based optimizer)를 기반으로 쿼리의 실행 계획을 생성하며, 생성시 통계 정보, 테이블 스키마 정보 및 쿼리에 대한 내용을 바탕으로 실행 계획을 생성합니다. 이 작업은 대부분 CPU 리소스를 사용하고 있습니다.

매번 쿼리 요청이 될 때 마다 쿼리의 실행 계획을 계속 생성한다는 작업한다면 많은 성능 적 문제를 야기 할 수 있어 SQL Server에는 프로시져 캐시라는 메모리 공간에 쿼리가 수행된 실행 계획을 메모리에 적재 후 추후 동일한 쿼리요청이 들어온다면 캐시에 있는 내용을 재사용을 하게 됩니다. 이러한 것을 실행 계획 재사용이라고 부르며, 기존의 실행계획을 사용하면 안될 경우 및 사용할 수 없는 경우에 새로운 실행 계획을 만들기 위해 재 컴파일이라는 것을 하게 됩니다.

2. OLTP 서비스 환경에서의 재 컴파일이 주는 장점 및 단점

OLTP환경 즉 많은 트랙잭션이 빠른 시간 안에 처리되어야 하는 환경에서는 OLAP환경 보다 재 컴파일에 대해서 보다 민감하고, 성능에 많은 영향을 줄 수 있습니다.

위에서 설명 하고 있듯이 SQL Server는 프로시져 캐시라는 곳에 실행계획을 저장하고 있어 재사용을 할 수 있습니다. 재사용으로 인해 매번 실행계획을 다시 생성하지 않아도 되어 보다 적은 리소스로 쿼리에 대한 응답을 할 수 있습니다.

그러나 예전에 생성된 실행 계획을 재 사용할 경우 문제가 발생 할 수도 있습니다.

기존에 실행계획은 최초 실행계획의 생성시 받는 매개변수에 대해서 실행 계획을 만드는 것이 일반적이며, 기존에 실행계획을 생성한 매개변수와 나중에 수행되는 매개변수에 대한 카디널리티 편차가 크다면 실행 계획 재사용으로 인해 적절한 실행 계획을 찾지 못하고 기존의 실행 계획을 이용하여 성능상 문제를 야기 할 수 있습니다.

실행계획의 재사용이란 항상 좋은 것만이 아닌 필요에 따라서는 재 컴파일을 통해 새로운 실행 계획을 생성 하는 것이 성능상 이점을 가질 수 있습니다.

아래의 링크에서 이러한 실행계획의 재사용으로 인해서 발생 될 수 있는 문제에 대해서 간략히 설명 하고 있습니다.

프로시져 parameter sniffing에 의한 실행계획의 차이점

3. 강제 매개변수화, 자동 매개변수화란.

SQL Server 2005에서는 자동 매개 변수화 만이 아닌 강제매개변수화 라는 기능이 추가 되었습니다. 강제 매개변수화의 경우 실행 계획을 생성할 때 사용자가 설정한 매개변수로 해당 실행계획을 생성할 수 있습니다.

이러한 강제 매개변수화를 잘 사용한다면 특정 환경에서 카디널리티 편차가 심한 데이터에 대해서 재 컴파일로 최적의 실행 계획을 만드는 것이 아닌 변수의 분포도에 따라 몇 가지 실행 계획을 생성 하여 보다 효율적으로 작성 할 수 도 있습니다.

SQL Server 2005 예전 버전에도 존재하던 기능 중에 하나인 자동매개변수화(단순매개변수화)는 실행 계획을 재사용하기 위해서 단순한 구문 및 아래의 조건에 매칭되지 않는 경우에 대해서 상수를 변수로 변환하여 실행계획을 생성 할 수 도 있습니다.

이러한 내용들은 보면 최대한 재 컴파일을 막기 위한 SQL Server 개발진들의 노력이 보입니다. 이러한 노력을 보면 재 컴파일이 성능에 얼만큼 영향을 주는 부분인지 단편적으로 생각 해 볼 수 있을 것 같습니다.

자동 매개변수화가 될 수 없는 조건

   1. BULK INSERT

   2. IN 구문 또는 OR

   3. UPDATE 절에 SET @변수 = col

   4. SELECT 절에 UNION구문

   5. SELECT 절에 INTO구문

   6. SELECT/UPDATE 절에 FOR BROWSE

   7. SELECT/UPDATE/DELETE 에 OPTION 쿼리 힌트

   8. SELECT 절에 DISTINCT 구문

   9. SELECT/UPDATE/DELETE/INSERT절에 TOP구문

   10. WAITFOR 구문

   11. SELECT절에 GROUP BY, HAVING, COMPUTE 구문

   12. DELETE/UPDATE 절에 FROM CLAUSE 구문

   13. 전체 검색, 연결된 서버, 테이블 변수 참조

   14. 서브쿼리

   15. 조건 절에 <> 상수

   16. 조인이 포함된 구문

   17. 두 개 이상의 테이블을 참조

   18. DELETE, UPDATE 에 FROM절

4. SQL Server 2005에서 변경된 구문단위 재 컴파일

보통 SQL Server을 사용할 때 주로 프로시져 단위로 SQL구문을 작성 하여 개발 하는 경우가 많습니다. SQL Server 2000에서는 프로시져 내부에 재 컴파일이 해야 하는 경우 프로시져 내부에 존재 하는 모든 구문에 대해서 재 컴파일을 하였습니다.

만약 수만 줄의 쿼리 가 있었다면 이것을 재 컴파일 한다는 것은 그 작업만으로도 상당한 리소스를 소비 하게 됩니다. 그래서 이러한 경우를 대처 하기 위해서 자주 재 컴파일 되는 부분을 다른 프로시져로 만들고 원본 프로시져에서 호출 하는 방식을 사용하였습니다.

이러한 방법으로 전체 재 컴파일이 되는 것을 막을 수 는 있었지만 많이 번거로운 작업 이었습니다.

그러나 SQL Server 2005에서는 구문 단위의 재 컴파일이 도입 되어 SQL Serve 2000에서 사용하던 것처럼 따로 프로시져를 만들어 호출 하지 않아도 구문단위로 재 컴파일을 할 수 있습니다.

테스트!!

5. 프로시져 캐시 에서의 지연기록기의 역할

SQL Server에서는 프로시져 캐시라는 곳에 예전에 실행된 실행 계획을 가지고 있어 재사용한다고 하였습니다. 그렇다면 이 프로시져 캐시에 존재 하는 것들에 대해서 사용되지 않거나 사용빈도가 낮은 것에 대해서는 메모리에서 제거를 해야 하는 작업이 필요합니다.

만약 이러한 작업이 없다면 계속 프로시져 캐시의 증가로 인해 서버 메모리에 문제가 발생 할 수 있습니다. 그러나 SQL Server에서는 이러한 문제가 거의 발생 되지 않습니다.

지연기록기(LazyWriter)라는 백그라운드 프로세스가 존재 하기 때문입니다. 지연기록기 는 free page를 확보하기 위해 주기적으로 버퍼캐시를 검색 합니다.

이러한 작업을 수행 하면서 해당 조건에 보다 낮거나 같은 수치를 가지는 것을 캐시영역에서 제거 하여 free page를 확보 하게 됩니다.

그리고 지연기록기가 하는 일중 다른 하나는 운영체제의 메모리가 부족 하다면 지연기록기에서 SQL Server에서 사용되는 버퍼 풀 메모리의 일부분을 free하여 운영체제에게 반환하는 역할을 합니다.

SQL Server 2005에서 지원이 보다 강화된 NUMA(Non-Uniform Memory Access )에 대해서는 각 노드에 대해 지연기록기 프로세스가 존재 하게 됩니다.

NUMA(Non-Uniform Memory Access) 이해

(ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.ko/udb9/html/2a77699c-e06b-4377-8acf-4d565d225f3c.htm)

아래의 DMV를 이용하여 현재 재사용 되는 실행계획의 개수 및 재사용되는 상위 50개의 구문이 확인이 가능 합니다.

--Compare Single-Use and Re-Used Plans
declare @single int, @reused int, @total int
select @single=sum(case(usecounts) when 1 then 1 else 0 end), @reused= sum(case(usecounts) when 1 then 0 else 1 end),
     @total=count(usecounts)
from sys.dm_exec_cached_plans

select
'Single use plans (usecounts=1)'= @single,
'Re-used plans (usecounts>1),'= @reused,
're-use %'=cast(100.0*@reused / @total as dec(5,2)),
'total usecounts'=@total

select 'single use plan size'=sum(cast(size_in_bytes as bigint))
from sys.dm_exec_cached_plans
where usecounts = 1

--List Statements By Plan Re-Use Count
SELECT TOP 50
         qs.sql_handle
                 ,qs.plan_handle
                 ,cp.cacheobjtype
                 ,cp.usecounts
                 ,cp.size_in_bytes
                 ,qs.statement_start_offset
                 ,qs.statement_end_offset
                 ,qt.dbid
                 ,qt.objectid
                 ,qt.text
                 ,SUBSTRING(qt.text,qs.statement_start_offset/2,
                         (case when qs.statement_end_offset = -1
                         then len(convert(nvarchar(max), qt.text)) * 2
                         else qs.statement_end_offset end -qs.statement_start_offset)/2)
                 as statement
FROM sys.dm_exec_query_stats qs
cross apply sys.dm_exec_sql_text(qs.sql_handle)as qt
inner join sys.dm_exec_cached_plans as cp on qs.plan_handle=cp.plan_handle
where cp.plan_handle=qs.plan_handle
--and qt.dbid = db_id()
ORDER BY [Usecounts] DESC


6. 재 컴파일의 원인 및 프로시져 캐시 사라지는 경우

위에서 보듯이 쿼리의 자주 실행 되는 것이 아니라면 지연기록기에 의해 프로시져 캐시에서 실행계획이 제거 될 수 있습니다. 이렇게 되면 다시 쿼리가 호출 되면 다시 컴파일을 수행 하여야 합니다.

여기서는 지연기록기에 프로시져 캐시가 사라지는 경우가 아닌 다른 작업으로 사라지는 경우와, 기존의 실행계획이 있지만 다시 재 컴파일을 해야 하는 경우 대해서 알아보도록 하겠습니다.

재 컴파일의 원인은 아래와 같이 스키마 변경, 통계정보 업데이트 등으로 발생 될 수 있으며, ALTER DATABASE구문이나 DBCC CHECKDB구문 등으로 프로시져 캐시의 내용이 모두 사라져 모든 쿼리에 대해서 다시 컴파일을 하여야 합니다.

하나의 테이블에 통계정보가 변경되어 해당 테이블과 연관된 프로시져를 재 컴파일을 하는 것은 오히려 좋을 수 있습니다. 보다 최신의 통계정보를 바탕으로 오히려 예전 보다 더욱 효율적인 실행계획을 가지고 처리 할 수 있기 때문입니다.

그러나 ALTER DATABASE, RESTORE DATEBASE같은 경우는 서비스 운영 중에 많이 사용하고 있지 않는 구문이지만 만약 이 구문들이 사용된다면 전체 프로시져 캐시에 존재하는 실행계획이 사라져서 다시 모든 쿼리에 대해서 컴파일을 하여야 합니다.

많은 트랜잭션을 가지고 있는 사이트라면 순간적으로 CPU 리소스가 높이 사용 될 것입니다. 그러기에 이러한 동작을 유발 하는 구문에 대해서 서비스 중 작업이 필요하다면 충분한 주의가 필요 합니다.

재 컴파일 원인

  • 스키마 변경(ALTER TABLE, ALTER VIEW, Alter Index)

  • 통계정보 변경

  • 지연된 컴파일

  • SET 옵션 변경

  • 임시 테이블 변경

  • OPTION(RECOMPILE) 쿼리 힌트

  • sp_recompile 호출

프로시져 캐쉬가 사라지는 작업

  • ALTER DATABASE [dbName] SET ONLINE

  • ALTER DATABASE [dbName] SET OFFLINE

  • ALTER DATABASE [dbName] SET READ_ONLY

  • ALTER DATABASE [dbName] SET READ_WRITE

  • ALTER DATABASE [dbName] MODIFY NAME = [SomeDB_Name]

  • ALTER DATABASE [dbName] MODIFY FILEGROUP Test1FG1 DEFAULT

  • ALTER DATABASE [dbName] MODIFY FILEGROUP Test1FG1 READ_WRITE

  • ALTER DATABASE [dbName] MODIFY FILEGROUP Test1FG1 READ_ONLY

  • ALTER DATABASE [dbName] COLLATE Collation_Name

  • DROP DATABASE [db_Snapshot_Name]

  • Restore database

  • Detach database

  • DBCC FREEPROCCACHE

  • DBCC FREESYSTEMCACHE

  • DBCC CHECKDB (참고 : SQL Server 2005 SP2 이전에는 DBCC CHECKDB를 수행하면 프로시져의 캐시가 사라지게 되나 SP2에서는 제거되지 않음.)

SQL Server 2005 SP2에서는 프로시져 캐시가 사라지는 작업 시 SQL 에러로그에 아래와 같은 메시지가 추가 되었습니다.

2007-01-19 14:29:07.39 spid54       Starting up database 'R'.
2007-01-19 14:29:23.84 spid54       Setting database option OFFLINE to ON for database R.
2007-01-19 14:29:24.06 spid54       SQL Server has encountered 13 occurrence(s) of cachestore flush for the 'Object Plans' cachestore (part of plan cache) due to 'DBCC FREEPROCCACHE' or 'DBCC FREESYSTEMCACHE' operations.
2007-01-19 14:29:24.06 spid54       SQL Server has encountered 1 occurrence(s) of cachestore flush for the 'Object Plans' cachestore (part of plan cache) due to some database maintenance or reconfigure operations.
2007-01-19 14:29:24.08 spid54       SQL Server has encountered 13 occurrence(s) of cachestore flush for the 'SQL Plans' cachestore (part of plan cache) due to 'DBCC FREEPROCCACHE' or 'DBCC FREESYSTEMCACHE' operations.
2007-01-19 14:29:24.08 spid54       SQL Server has encountered 1 occurrence(s) of cachestore flush for the 'SQL Plans' cachestore (part of plan cache) due to some database maintenance or reconfigure operations.
2007-01-19 14:29:24.08 spid54       SQL Server has encountered 13 occurrence(s) of cachestore flush for the 'Bound Trees' cachestore (part of plan cache) due to 'DBCC FREEPROCCACHE' or 'DBCC FREESYSTEMCACHE' operations.
2007-01-19 14:29:24.08 spid54       SQL Server has encountered 1 occurrence(s) of cachestore flush for the 'Bound Trees' cachestore (part of plan cache) due to some database maintenance or reconfigure operations.


7. 재 컴파일 발생 원인 및 빈도 찾기

이러한 재 컴파일이 문제가 될 수 있다는 것을 위에서 알아 보았습니다. 그러면 이제는 현재 운영하고 있는 사이트에서 얼마나 많은 재 컴파일이 발생하는지 그리고 이유는 무엇인지 확인할 필요가 있습니다.

많은 트랜잭션이 발생하지 않는다면, 요즘은 워낙 CPU사양도 좋고 해서 재 컴파일에 의한 문제는 그리 크게 사람에게 다가오지 않을 수도 있습니다. 그러나 많은 트랜잭션이 발생 하는 환경이라면 분명 확인 할 가치는 있을 겁니다.

보통 재 컴파일에 대한 문제로 원인 및 빈도를 확인 하기 위해서는 프로파일러와 성능 모니터를 통해 확인 합니다.

프로파일러의 경우 모든 SQL Server로 들어오는 쿼리에 대해서 재 컴파일이 발생 되었다면 SP: Recompile, SQL: StmtRecompile 이벤트 및 EventSubClass컬럼을 추가 하여 원인 및 빈도를 확인 할 수 있습니다.

그렇지만 프로파일러를 서비스중인 서버에 사용하는 것도 힘든 곳도 많을 것입니다. 그리고 구문단위까지 이벤트를 걸게 되면 추적 파일 크기는 금방 커질 것 입니다.

비쥬얼하게 보여지는 프로파일러 대신 SQLDIAG나 저장 프로시져를 사용하는 것이 서버의 리소스 소비에 대해서 보다 효과 적입니다.

SQLDIAG는 기존 SQL Server 2000에도 존재 하였지만 일반적인 에러로그 및 수행 당시의 시스템 테이블 정보 등 이었지만 SQL Server 2005의 SQLDIAG는 기존에 Microsoft PSS팀에서 사용하던 PSSDIAG의 기능을 포함 하고 있어 운영체제 이벤트 로그, 추적, 성능 카운터 등을 한번에 수집이 가능 하여 보다 쉽게 관련 데이터를 수집 할 수 있습니다. 그러나 이러한 이벤트나 카운터를 수정 해야 한다면 XML파일을 직접 수정 해야 하는 번거로움은 있습니다.

단순히 재 컴파일에 대해서 어떠한 원인으로 발생 되었는지 확인 하려면 아래와 같이 열 필터의 EventSubClass의 “값이 없는 행 제외”를 추가하여 추적하여 성능 모니터의 수치와 비교해 본다면 보다 쉽게 재 컴파일의 원인을 확인 할 수 있습니다.

성능 모니터를 통해 재 컴파일의 빈도를 찾기 위해서는 아래에 있는 카운터 중 SQL Re-Compilations/sec, Batch Requests/sec 을 추가하여 빈도를 확인 할 수 있습니다.

SQLDIAG, 저장 프로시져를 이용하여 추적 하는 방법은 온라인 설명서를 참조하시면 됩니다.

SQLdiag 유틸리티(ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.ko/sqlcmpt9/html/45ba1307-33d1-431e-872c-a6e4556f5ff2.htm)

SQL Server 프로파일러 저장 프로시저(Transact-SQL)(ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.ko/tsqlref9/html/8c99c3db-0b04-46c3-aa59-d6f680522fdd.htm)

<그림 : 열 필터 설정>

<그림: 열 필터 설정 후 프로파일러 모습>

EventSubClass 값

설명

1

스키마가 변경되었습니다.

2

통계가 변경되었습니다.

3

지연된 컴파일입니다.

4

SET 옵션이 변경되었습니다.

5

임시 테이블이 변경되었습니다.

6

원격 행 집합이 변경되었습니다.

7

FOR BROWSE 권한이 변경되었습니다.

8

쿼리 알림 환경이 변경되었습니다.

9

분할된 뷰가 변경되었습니다.

10

커서 옵션이 변경되었습니다.

11

OPTION (RECOMPILE)이 요청되었습니다.

<EventSubClass 컬럼 설명>

성능 모니터 (perfmon)의 SQL Statistics 카운터를 통해 분석 및 빈도를 확인 할 수 있습니다.

SQL Server SQL Statistics 카운터

설명

Auto-Param Attempts/sec

초당 자동 매개 변수화 시도 수입니다. 합계는 자동 매개 변수화의 실패한 횟수, 안전한 횟수 그리고 안전하지 않은 횟수의 합과 같습니다. 자동 매개 변수화는 유사한 여러 요청을 처리할 때 캐시된 결과 실행 계획을 다시 사용하기 위해 SQL Server 인스턴스가 일부 리터럴을 매개 변수로 바꾸어 Transact-SQL 요청을 매개 변수화할 때 일어납니다. Microsoft SQL Server 2000 에서는 자동 매개 변수화를 단순 매개 변수화라고도 합니다. 이 카운터에는 강제 매개 변수화는 포함되지 않습니다.

Batch Requests/sec

초당 받는 Transact-SQL 명령 일괄 처리 수입니다. 이 통계는 모든 제약 조건(I/O, 사용자 수, 캐시 크기, 요청의 복잡도 등)의 영향을 받습니다. 높은 일괄 처리 수치는 높은 처리 효율을 의미합니다.

Failed Auto-Params/sec

실패한 자동 매개 변수화의 초당 시도 횟수입니다. 이 값은 작을수록 좋습니다.

Forced Parameterizations/sec

초당 성공한 강제 매개 변수화 수입니다.

Safe Auto-Params/sec

안전한 자동 매개 변수화의 초당 시도 횟수입니다. 안전한 자동 매개 변수화는 여러 유사한 Transact-SQL 문 간에 캐시된 실행 계획을 공유할 수 있도록 하는 결정을 참조하며 SQL Server 가 시도하는 많은 자동 매개 변수화 중 일부는 성공하고 나머지는 실패합니다. SQL Server 2005 에서는 자동 매개 변수화를 단순 매개 변수화라고도 합니다. 강제 매개 변수화는 포함되지 않습니다.

SQL Attention rate

초당 주의 수입니다. 주의는 현재 실행 중인 요청을 종료하기 위한 클라이언트의 요청입니다.

SQL Compilations/sec

초당 SQL 컴파일 수입니다. 컴파일 코드 경로를 입력한 횟수를 나타내며 SQL Server 2005 에서 문 수준의 다시 컴파일에 의한 컴파일을 포함합니다. SQL Server 사용자 작업이 안정되면 이 값은 일정한 수준에 이릅니다.

SQL Re-Compilations/sec

초당 문 다시 컴파일 수입니다. 문 다시 컴파일이 트리거된 횟수를 나타냅니다. 일반적으로 다시 컴파일하는 횟수는 적을수록 좋습니다. SQL Server 2000 에서 다시 컴파일은 Microsoft SQL Server 2005 의 일괄 처리 범위가 아니라 문 범위 다시 컴파일입니다. 따라서 SQL Server 2005 와 이전 버전 간에 이 카운터 값을 직접 비교할 수 없습니다.

Unsafe Auto-Params/sec

안전하지 않은 자동 매개 변수화의 초당 시도 횟수입니다. 예를 들어 쿼리는 캐시된 계획을 공유하지 않고 자동 매개변수화하는 특성이 있습니다. 이러한 자동 매개변수화는 안전하지 않은 것으로 지정됩니다. 강제 매개 변수화 수는 여기에 포함되지 않습니다.


드디어 “SQL Server의 재 컴파일이란?” 아티클에 마지막입니다.

여기서 제가 가장 하고 싶은 말은 지연기록기가 어떠한 연산을 하여 실행계획을 제거되고 서버의 메모리 중 얼마만큼 프로시져 캐시로 사용할 수 있는 그러한 내용이 아닌 “환경에 따라 독이 될 수도 약이 될 수도 있는 것이 재 컴파일이다.” 입니다.

그러나 여기서는 재 컴파일이 발생 하지 않게 모니터링 방법 등만을 기술 하였습니다. 이것은 대부분의 환경에서는 재 컴파일이라는 것이 약 이 되는 경우가 많다는 것이며 항상 재 컴파일이 나쁘다는 것은 아닙니다.

그렇지만 재 컴파일이라는 것을 약 또는 독을 만드는 것은 사람이라고 생각합니다. 만약 독이 라면 어떻게 찾고 해결할까? 그렇지만 찾는 방법만 있지 이것을 여기서 해결 하는 방법은 기술 되지 않았습니다.

이 부분은 직접 테스트를 해보면서 배우고 느끼는 것이 가장 좋다고 생각하는 개인적인 의견입니다. ^^

저는 SQL Server라는 DBMS를 접하게 된지 그리 오래 되지 않았습니다. 그래서 아직은 배워야 할 것 도 많다고 생각합니다. 그래서 더욱 재미있다고 생각합니다. DBMS라는 것에 대해서는 무엇보다 사람이 중요성이 다른 무엇보다 크다고 생각하기 때문입니다.

나중에 SQL Server 3000이 나와서 아무리 옵티마이져가 똑똑해졌다고 한다고 하더라도 사람이 해야 할 부분까지 SQL Server단에서 처리 하기는 힘들 거라고 생각합니다. 환경 및 비즈니스 로직에 따라서 사람의 단 하나의 판단으로 많은 차이가 날 수 있을 수 있기 때문입니다.

길지 않은 아티클을 작성하는데 생각보다 오랜 시간이 걸린 듯합니다. 지금 다시 보면 별 내용이 없는 것 같아 괜히 부끄러워지지만, 그래도 짧은 시간 동안 많은 공부를 할 수 있었고 정리를 하는 시간을 가져서 참 좋았습니다.

솔직히 이 재 컴파일이 주제가 정하기 전 여러 주제에 대해서 생각을 해보고 작성도 해보았지만 마음에 들지 않아 주제를 변경하기도 여러 번..아마 그 주제 들은 어딘가의 폴더 안에서 곤히 잠들고 있을 겁니다.

만약 다음에도 아티클로 다시 뵐 수 있는 기회가 온다면 더욱 열심히 준비하고 공부해서 좋은 아니 괜찮은 아티클로 다시 찾아 뵙겠습니다.

+ Recent posts