T-SQL: Cursor 의 이해

September 11, 2007 17:20 by WebStoryMaker

이론적인 것을 떠나서 실습을 통해 T-SQL 에서의 커서가 무엇인가 알아보도록 하겠다. 커서의 정의나 그 외 구체적인 이론들은 msdn2 를 참조하기를 바란다.
개괄적으로 커서란?

 
Data Set 을 Row 단위로 읽어들여 그 Row 에 따른 데이터로 특정한 작업을 수행하는 것을 말한다.

이렇게 설명을 들어도 잘 모를테다. 그래서 실습을 통해 Cursor 가 무엇인지 차근 차근 알아보도록 하겠다. [단, 커서에 대한 부분은 굳이 이해하지 않아도 된다. 왜냐하면 커서는 매우 위험한 툴이다! RDBMS 개념을 파괴시키는 것이 커서이다 - 때로는 유용하게 쓰일 수도 있으나 가급적 사용은 삼가하는 것이 좋겠다]
여기서도 마찬가지로 MS AdventureWorks DB 를 통해 알아보도록 하겠다.
(샘플 데이터베이스 다운로드: http://www.microsoft.com/downloads/details.aspx?familyid=e719ecf7-9f46-4312-af89-6ad8702e4e6e&displaylang=en )
SalesOrderHeader 라는 테이블 (Sales스키마)은 상품에 대한 판매 자료를 담고 있다. 일단, 다음의 쿼리를 사용하여 하나의 Row 를 추가하자.


쿼리1 (Language : sql)

   1:  ALTER TABLE Sales.SalesOrderHeader
   2:  ADD historyCaptureDate datetime
   3:  GO

 SalesOrderHeader 에는 historyCaptureDate 라는 Column 이 없으므로 위의 쿼리는 안전하게 수행된다. 설명하기에 앞서 아래의 쿼리를 수행하여 세 개의 테이블을 더 생성해 본다.
쿼리2 (Language : sql)
   1:  CREATE schema YOUNGSBLOG
   2:  go
   3:  CREATE TABLE YOUNGSBLOG.CardTypeUsage
   4:  (
   5:      CardType nvarchar(50) NOT NULL,
   6:      Year numeric(4) NOT NULL,
   7:      OrderCount numeric(10) NOT NULL,
   8:      ChargeSum money
   9:  )
  10:  GO
  11:  CREATE TABLE YOUNGSBLOG.CustomerOrderSummary
  12:  (
  13:      CustomerID int NOT NULL,
  14:      Year numeric(4) NOT NULL,
  15:      OrderCount numeric(10) NOT NULL,
  16:      OrderValue money
  17:  )
  18:  GO
  19:  CREATE TABLE YOUNGSBLOG.SalesPersonSummary
  20:  (
  21:      SalesPersonID int NOT NULL,
  22:      Year numeric(4) NOT NULL,
  23:      OrderCount numeric(10) NOT NULL,
  24:      OrderValue money
  25:  )
  26:  GO


YOUNGSBLOG 라는 스키마를 생성한 뒤 CardTypeUsage 라는 테이블을 추가하였다. 이 테이블에는 Sales.SalesOrderHeader 에서 판매된 상품을 결재한 카드의 타입을 연도별로 묶어서 횟수를 측정하고 합계금액을 측정하기 위해 생성하였다.
마찬가지로 CustomerOrderSummary 테이블에는 Sales.SalesOrderHeader 에서 상품을 구입한 고객의 아이디를 연도별로 묶고 횟수와 합계 금액을 측정한다.
SalesPersonSummary 는 특별한 설명없이도 이해하리라 생각된다. 판매한 사람에 대한 정보를 연도별로 묶는 것이다.
이렇게 세 개의 테이블을 생성하였다. 그러면 SalesOrderHeader 에서는 카드의 타입을 볼 수 없으므로 CreditCard 테이블과 조인하여 테이블을 살펴보자.

카드 타입, 연도, 금액, 고객아이디, 판매한 사원의 아이디 .. 이렇게 추출했다. 이제 이 데이터를 가지고 각 Row 의 데이터에 따라서 새로 생성한 CardTypeUsage 에 삽입하거나 Update 하는 방법을 생각해 보자.
당연히 Stored Procedure 를 사용해야 편하게 이 방법을 수행할 수 있을 테지만 막상 쉽게 떠오르지는 않을 것이다. 자, 그러면 완성된 커서문을 가지고 차근 차근 설명해 보도록 하겠다.
커서를 사용한 Stored Procedure (Language : sql)

   1:  SET ANSI_NULLS ON
   2:  GO
   3:  SET QUOTED_IDENTIFIER ON
   4:  GO
   5:  -- =============================================
   6:  -- Author:    Young Park
   7:  -- Create date: July 23, 2007
   8:  -- =============================================
   9:  IF EXISTS (SELECT * FROM sys.objects
  10:             WHERE object_id = object_id(N'[dbo].[pParkAssignment6]') AND type IN (N'P', N'PC'))
  11:  DROP PROCEDURE [dbo].[pParkAssignment6]
  12:  GO
  13:   
  14:  CREATE PROCEDURE [dbo].[pParkAssignment6]
  15:      (
  16:          @pOrderDateYear NUMERIC(4)
  17:      )
  18:  AS
  19:  BEGIN
  20:      DECLARE @CrsrOrderDate CURSOR,
  21:              @vCardType NVARCHAR(50),
  22:              @vOrderYear NUMERIC(4),
  23:              @vOrderValue MONEY, 
  24:              @vCustomerID INT,
  25:              @vSalesPersonID INT
  26:   
  27:      SET @CrsrOrderDate = CURSOR FOR
  28:          SELECT C.CardType,
  29:                  YEAR(S.OrderDate), 
  30:                  S.TotalDue, 
  31:                  S.CustomerID, 
  32:                  S.SalesPersonID
  33:          FROM Sales.SalesOrderHeader AS S
  34:          INNER JOIN Sales.Creditcard AS C
  35:          ON S.CreditCardID = C.CreditCardID
  36:          WHERE YEAR(S.OrderDate) = @pOrderDateYear
  37:          AND S.historyCaptureDate IS NULL
  38:   
  39:      OPEN @CrsrOrderDate
  40:      
  41:      FETCH NEXT FROM @CrsrOrderDate INTO @vCardType,
  42:                                          @vOrderYear,
  43:                                          @vOrderValue,
  44:                                          @vCustomerID,
  45:                                          @vSalesPersonID         
  46:      
  47:      WHILE (@@FETCH_STATUS = 0)
  48:          BEGIN
  49:              
  50:              --*** First
  51:              IF EXISTS(SELECT * FROM YOUNGSBLOG.CardTypeUsage
  52:                        WHERE CardType = @vCardType 
  53:                              AND Year = @vOrderYear)
  54:                  BEGIN
  55:                      -- update, if cardtype and year exists in CardTypeUsage table
  56:                      UPDATE YOUNGSBLOG.CardTypeUsage
  57:                      SET OrderCount = OrderCount + 1,
  58:                          ChargeSum = ChargeSum + @vOrderValue
  59:                      WHERE CardType = @vCardType 
  60:                              AND Year = @vOrderYear
  61:                  END
  62:              ELSE
  63:                  BEGIN
  64:                      -- if not exists, insert
  65:                      INSERT INTO YOUNGSBLOG.CardTypeUsage
  66:                      VALUES (@vCardType,
  67:                              @vOrderYear,
  68:                              1,
  69:                              @vOrderValue)
  70:                  END
  71:              
  72:              --** Second
  73:              IF EXISTS (SELECT * FROM YOUNGSBLOG.CustomerOrderSummary
  74:                          WHERE CustomerID = @vCustomerID 
  75:                          AND Year = @vOrderYear)
  76:                  BEGIN
  77:                      -- update, if customerID and year exists in CutomerOrderSummary table
  78:                      UPDATE YOUNGSBLOG.CustomerOrderSummary
  79:                      SET OrderCount = OrderCount + 1,
  80:                          OrderValue = OrderValue + @vOrderValue
  81:                      WHERE CustomerID = @vCustomerID 
  82:                              AND Year = @vOrderYear
  83:                  END
  84:              ELSE
  85:                  BEGIN
  86:                      -- if not exists, insert
  87:                      INSERT INTO YOUNGSBLOG.CustomerOrderSummary
  88:                      VALUES (@vCustomerID,
  89:                              @vOrderYear,
  90:                              1,
  91:                              @vOrderValue)
  92:                  END
  93:              --** Third
  94:              IF @vSalesPersonID IS NOT NULL
  95:                  BEGIN
  96:                      IF EXISTS (SELECT * FROM YOUNGSBLOG.SalesPersonSummary
  97:                                  WHERE SalesPersonID = @vSalesPersonID
  98:                                  AND Year = @vOrderYear)
  99:                          BEGIN
 100:                              -- update
 101:                              UPDATE YOUNGSBLOG.SalesPersonSummary
 102:                              SET OrderCount = OrderCount + 1,
 103:                                  OrderValue = OrderValue + @vOrderValue
 104:                              WHERE SalesPersonID = @vSalesPersonID
 105:                                      AND Year = @vOrderYear
 106:                          END
 107:                      ELSE
 108:                          BEGIN
 109:                              -- insert
 110:                              INSERT INTO YOUNGSBLOG.SalesPersonSummary
 111:                              VALUES (@vSalesPersonID,
 112:                                      @vOrderYear,
 113:                                      1,
 114:                                      @vOrderValue)
 115:                          END
 116:                  END
 117:              
 118:          UPDATE Sales.SalesOrderHeader
 119:                  SET historyCaptureDate = GETDATE()
 120:                  WHERE CURRENT OF @CrsrOrderDate 
 121:   
 122:          FETCH NEXT FROM @CrsrOrderDate INTO @vCardType,
 123:                                              @vOrderYear,
 124:                                              @vOrderValue,
 125:                                              @vCustomerID,
 126:                                              @vSalesPersonID
 127:          END -- End of While
 128:      
 129:      -- Close Cursor
 130:      CLOSE @CrsrOrderDate
 131:      DEALLOCATE @CrsrOrderDate
 132:   
 133:  END
 134:  GO


9 - 12절은 Stored Procedure 문이 이미 존재할 경우에 삭제하고 다시 생성하라는 구문이다. 유용하므로 알아두기를 바란다.
(Language : sql)

   1:  IF EXISTS (SELECT * FROM sys.objects
   2:             WHERE object_id = object_id(N'[dbo].[pParkAssignment6]') AND type IN (N'P', N'PC'))
   3:  DROP PROCEDURE [dbo].[pParkAssignment6]
   4:  GO


그 다음의 구문을 살펴보자. Stored Procedure 를 생성하는 구문인데 @pOrderDateYear 를 파라미터로 받는 Stored Procedure 이다. SalesOrderHeader 는 2001 년도부터 2004년도까지의 데이터를 가지고 있으므로 이 경우에는 2001, 2002, 2003, 2004 이 네가지만 사용이 가능하다.
(Language : sql)

   1:  CREATE PROCEDURE [dbo].[pParkAssignment6]
   2:      (
   3:          @pOrderDateYear NUMERIC(4)
   4:      )


다음의 구문은 변수 선언이다. 유의해서 볼 것은 아래의 빨간 부분이다. 커서를 선언할 때에는 변수를 선언할 때와 마찬가지로 할 수 있다. 커서가 선언이 됐으면...
(Language : sql)

   1:      DECLARE @CrsrOrderDate CURSOR,
   2:              @vCardType NVARCHAR(50),
   3:              @vOrderYear NUMERIC(4),
   4:              @vOrderValue MONEY, 
   5:              @vCustomerID INT,
   6:              @vSalesPersonID INT


위에서 살펴본 테이블을 토대로 커서를 생성한 것이다.
(Language : sql)

   1:     SET @CrsrOrderDate = CURSOR FOR
   2:          SELECT C.CardType,
   3:                  YEAR(S.OrderDate), 
   4:                  S.TotalDue, 
   5:                  S.CustomerID, 
   6:                  S.SalesPersonID
   7:          FROM Sales.SalesOrderHeader AS S
   8:          INNER JOIN Sales.Creditcard AS C
   9:          ON S.CreditCardID = C.CreditCardID
  10:          WHERE YEAR(S.OrderDate) = @pOrderDateYear
  11:          AND S.historyCaptureDate IS NULL


2-11 절은 커서가 어떠한 테이블을 참조할 것인지를 나타낸다. 위에서 살펴본 테이블과 같으나 다른 것은 YEAR(S.OrderDate) 에 파라미터를 할당함으로서 년도를 수동으로 입력할 수 있게 해놨다.
(Language : sql)

  1.     OPEN @CrsrOrderDate


커서는 열고/닫히는 과정안에서 이루어진다. 일단, 연다...
(Language : sql)

   1:     FETCH NEXT FROM @CrsrOrderDate INTO @vCardType,
   2:                                          @vOrderYear,
   3:                                          @vOrderValue,
   4:                                          @vCustomerID,
   5:                                          @vSalesPersonID 


자, 여기서부터 중요하다. FETCH 문을 사용하여 커서로 가리키게 되는 테이블의 Row 의 각 Column 을 @파라미터로 받고 있다. 유의깊게 봐야 할 것은 위에서 커서문을 선언했을 때 사용된 테이블의 컬럼과 일치되어야 한다. SELECT 한 테이블에 사용된 컬럼수가 5개 이므로 마찬가지로 FETCH (가져오기) 를 할 때에도 5개로 일치하게 해야 한다.
FETCH 는 NEXT , PRIOR, FIRST, LAST 등의 다른 옵션도 있다. (자세한 것은 MSDN 참조)
(Language : sql)

  1.     WHILE (@@FETCH_STATUS = 0)


자, While 문으로 FETCH_STATUS 가 0 (Successful) 할때까지 순환하게 된다. 순환하는 이유에 대해서 잠깐 설명하자면, Cursor 는 각 Row 를 참조하므로 만족하는 Row 가 끝날 때 까지 순환을 해야만 전체 테이블을 읽게 되는 것이다.
(Language : sql)

   1:              IF EXISTS(SELECT * FROM YOUNGSBLOG.CardTypeUsage
   2:                        WHERE CardType = @vCardType 
   3:                              AND Year = @vOrderYear)
   4:                  BEGIN
   5:                      -- update, if cardtype and year exists in CardTypeUsage table
   6:                      UPDATE YOUNGSBLOG.CardTypeUsage
   7:                      SET OrderCount = OrderCount + 1,
   8:                          ChargeSum = ChargeSum + @vOrderValue
   9:                      WHERE CardType = @vCardType 
  10:                              AND Year = @vOrderYear
  11:                  END
  12:              ELSE
  13:                  BEGIN
  14:                      -- if not exists, insert
  15:                      INSERT INTO YOUNGSBLOG.CardTypeUsage
  16:                      VALUES (@vCardType,
  17:                              @vOrderYear,
  18:                              1,
  19:                              @vOrderValue)
  20:                  END


위의 문장을 글로 풀이를 해보자:
  만일 CardTypeUsage 테이블에서 카드의 타입과 년도가 처음 우리가 읽어들인 SELECT (조인된)문에서 가져온 카드의 타입과 년도와 일치한다면 업데이트만 하고, 그렇지 않다면 생성(Insert) 하라는 것이다. 업데이트 항목은 당연히 횟수를 나타내는 OrderCount 를 증가하고, ChargeSum 에 일치한 데이터의 금액을 합산하면 되는 것이다.
그렇지 않다면 ELSE 문으로 INSERT 하는 구문이 읽혀지게 되는 것이다.
마찬가지로 두번째, 세번째 테이블도 UPDATE/INSERT 를 하면 되는 것이다. (이 부분은 생략)
(Language : sql)

   1:          UPDATE Sales.SalesOrderHeader
   2:                  SET historyCaptureDate = GETDATE()
   3:                  WHERE CURRENT OF @CrsrOrderDate 


그리고 위의 코드에서는 SalesOrderHeader 에서 현재 참조하고 있는 (CURRENT OF) 커서 구문을 참조하여 historyCaptureDate 에 캡쳐하는 순간의 날짜를 업데이트 하는 것이다.
(Language : sql)

   1:          FETCH NEXT FROM @CrsrOrderDate INTO @vCardType,
   2:                                              @vOrderYear,
   3:                                              @vOrderValue,
   4:                                              @vCustomerID,
   5:                                              @vSalesPersonID


FETCH 문이 한번 더 나오는 이유는 그래야만 다음의 Row 로 넘어가기 때문이다. 그래서 While 문이 반복되어 테이블이 끝날 때까지 작업을 수행한다.
(Language : sql)

   1:      -- Close Cursor
   2:      CLOSE @CrsrOrderDate
   3:      DEALLOCATE @CrsrOrderDate

마지막으로 커서를 닫고 Deallocate 해주면 된다.
결과는 직접 확인하시길... 커서의 수행으로 어느정도 기다려야 결과물이 출력된다.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Tags: ,
Categories: Database
Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

Related posts

Add comment


(Will show your Gravatar icon)  

  Country flag

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]



Live preview

January 5. 2009 11:10