행 단위 프로세싱

커서를 사용하여 한 번에 한 행씩 처리하는 데이터베이스 많은 응용 프로그램이 존재한다. 개발자는 행 단위 방식으로 데이터를 처리하는 것에 대해 생각하는 경향이 있다. Oracle은 고속 데이터 액세스 메커니즘으로 커서라는 것을 사용하기도 한다. 하지만 SQL Server의 커서는 다르다. SQL Server에서 커서를 통한 데이터 조작은 상당한 추가 오버헤드를 발생시키므로 데이터베이스 애플리케이션에서는 왠만하면 커서 사용을 피해야 한다. T-SQL 및 SQL Server는 한 번에 한 행씩이 처리하는 것이 아닌 데이터 집합 세트에서 가장 잘 작동하도록 설계되었다. Jeff Moden은 이러한 유형의 처리를 RBAR("ree-bar"로 발음)라고 명명했는데, 이는 행 단위로 고민하며 처리한다는 의미입니다. 그러나 커서를 꼭 사용해야 한다면 비용이 가장 적은 커서를 선택해 사용하자.

이번 장에서는 다음과 같은 주제를 다룬다.

* 커서 기초
* 다른 커서 특성의 비용 분석
* 커서를 사용한 기본 결과 집합의 장단점
* 커서의 비용 오버헤드를 최소화하기 위한 권장 사항

1) 커서 기초

응용 프로그램에서 쿼리를 실행하면 SQL Server는 행으로 구성된 데이터 집합을 반환한다. 일반적으로 애플리케이션은 여러 행을 함께 처리할 수 없기 때문에 SQL Server에서 반환된 결과 집합을 받아 한 번에 한 행씩 처리한다. 한번에 한 행씩 처리하는 기능을 커서라고 부른다.

T-SQL 문으로 커서를 처리할 때는 다음 단계로 진행된다. 1. 커서를 선언하여 SELECT 문과 연결하고 커서의 특징을 정의. 2. 커서를 열어 SELECT 문에서 반환된 결과 집합에 액세스. 3. 커서의 SELECT 결과집합을 한 행씩 루핑하며 읽어 들이지만 극히 일부에서는 커서를 통해 행을 수정하기도 함(나는 커서로 데이터 수정 한번도 해본적 없음). 4. 결과 집합의 다음 행으로 루핑 이동. 5. 결과 집합의 모든 행이 다 루핑했으면 커서를 닫고 커서에 할당된 리소스를 해제.

데이터베이스에서 쿼리를 통해 나온 결과집합을 처리하기 위해 응용 프로그램에서 커서를 만들 수도 있다. 이렇게 생성된 커서를 일반적으로 클라이언트 커서라고 한다. T-SQL을 사용해 MS-SQL에서 작성된 커서는 서버 커서라고 한다. 다음은 테이블의 쿼리 결과를 처리하는 서버 커서의 예.

USE AdventureWorks2017;
GO
SET NOCOUNT ON
DECLARE MyCursor CURSOR /* 커서 특성 지정 부분 */
FOR
    SELECT adt.AddressTypeID,
        adt.Name,
        adt.ModifiedDate
    FROM Person.AddressType AS adt;

OPEN MyCursor;
DECLARE @AddressTypeId INT,
        @Name VARCHAR(50),
        @ModifiedDate DATETIME;

FETCH NEXT FROM MyCursor INTO @AddressTypeId, @Name, @ModifiedDate;
WHILE @@FETCH_STATUS = 0
BEGIN
    PRINT 'NAME = ' + @Name;    -- 한행 읽기

    UPDATE Person.AddressType
    SET Name = Name + 'z'
    WHERE CURRENT OF MyCursor;  -- 커서 위치 행 수정
    
    FETCH NEXT FROM MyCursor INTO @AddressTypeId, @Name, @ModifiedDate;
END
CLOSE MyCursor;
DEALLOCATE MyCursor;

커서의 오버헤드 중 일부는 커서 특성에 따라 달라진다. SQL Server에서 제공하는 커서와 데이터 응용프로그램 액세스 계층의 특성은 크게 세 가지 범주로 분류할 수 있습니다.

* 커서 위치: 커서 생성 위치를 정의합니다.
* 커서 동시성: 커서와 기본 콘텐츠의 격리 및 동기화 정도를 정의합니다. 
* 커서 유형: 커서의 특정 특성을 정의합니다.

커서의 비용을 살펴보기 전에 커서의 다양한 특징을 몇 페이지에 걸쳐 소개하겠다. 다음 쿼리를 사용하여 Person.AddressType 테이블에 대한 변경 사항을 실행 취소할 수 있습니다.

  • a. 커서의 위치

    커서가 위치하는 곳에 따라 두가지로 나뉜다.

      * 클라이언트 사이드 커서
      * 서버 사이드 커서
    

    서버 커서는 항상 SQL Server쪽에 존재한다. C#을 사용해 ADO.NET이나 Entityframework 같은 드라이버, ORM을 통해서 응용프로그램이 존재하는 곳에 위치한 클라이언트를 만들수 있다. 데이터베이스 어플리케이션을 개발한다면 클라이언트 커서를 99%사용하며 SSMS를 사용하여 SQL Server상에서 저장 프로시저나 함수를 만들때는 서버커서를 만든다.

    • 클라이언트 커서
      이름에서 알 수 있듯이 클라이언트 측 커서는 앱이 서비스인지, 데이터 액세스 계층인지, 사용자를 위한 프런트 엔드인지에 관계없이 애플리케이션을 실행하는 시스템에 생성된다.

        * 클라이언트 머신에서 생성
        * 클라이언트 머신에서 커서 메타데이터 유지됨.
        * 대부분의 데이터 액세스 레이어(OLEDB 프로바이더, ADO.net 등등)를 사용하여 생성.
        * 포워드 전용 또는 정적 커서              
      
    • 서버 커서
      SQL Server 머신에서 존재.

        * 서버 머신에서 생성
        * 서버 머신에서 커서 메타데이터 유지됨.
        * 데이터 액세스 레이어 또는 T-SQL 사용하여 생성
        * 모든 커서 타입이 가능
      
  • b. 커서 동시성

    기본 콘텐츠와의 필요한 격리 및 동기화 정도에 따라 커서는 다음과 같은 동시성 모델로 분류될 수 있습니다.

      * 읽기 전용 : 업데이트 불가능 커서
      * 낙관적: 낙관적 동시성 모델을 사용하는 업데이터 가능 커서
      * 스크롤 락 : 업데이트되는 모든 데이터 로우에 잠금을 보유하는 업데이트 가능 커서
    
    • 읽기 전용
      읽기 전용 커서는 업데이트할 수 없다. 베이스 테이블에는 어떤 잠금도 유지되지 않는다. 커서 행을 가져오는 동안 베이스 테이블의 행에서 (S) 잠금이 획득되는지 여부는 세션의 격리 수준과 커서에 대한 SELECT 문에 사용된 잠금 힌트에 따라 달라진다. 그러나 기본적으로는 행을 가져오면 기본적으로 잠금이 해제된다. 다음 T-SQL 문은 읽기 전용 T-SQL 커서를 만듭니다.

        DECLARE MyCursor CURSOR READ_ONLY FOR
            SELECT adt.Name
            FROM Person.AddressType AS adt
            WHERE adt.AddressTypeID = 1;
      

      가능한 최소한의 잠금 오버헤드를 사용하는 것은 읽기 전용 커서를 더 빠르고 안전하게 해준다. 읽기 전용 커서이기 때문에 데이터를 수정할수 없다는 것을 기억하자. (하지만 커서를 이용해 데이터를 수정하는 짓은 30년동안 한번도 안해봤다. 그 정도로 읽기만 사용하는 것이 대부분). 하지만 커서중에서 가장 빠른 성능을 보여준다.

    • 낙관성
      값 동시성 모델을 사용하는 낙관적 모델은 커서를 업데이트할 수 있게 만듭니다. 기본 데이터에는 잠금이 유지되지 않습니다. 기본 행에서 (S) 잠금을 획득할지 여부를 결정하는 요소는 읽기 전용 커서의 경우와 동일합니다.

      낙관적 동시성 모델은 커서로 행을 읽는 동안 행을 잠그는 대신 행 버전 관리를 사용하여 행을 커서로 읽은 후 행이 수정되었는지 여부를 확인합니다. 버전 기반 낙관적 동시성을 사용하려면 커서가 생성된 기본 사용자 테이블에 ROWVERSION 열이 필요합니다. ROWVERSION 데이터 유형은 행 수정의 상대적 순서를 나타내는 이진수입니다. ROWVERSION 열이 있는 행이 수정될 때마다 SQL Server는 전역 ROWVERSION 값의 현재 값인 @@DBTS를 ROWVERSION 열에 저장합니다. 그런 다음 @@DBTS 값을 증가시킵니다.

      낙관적 커서를 통해 수정 사항을 적용하기 전에 SQL Server는 해당 행의 현재 ROWVERSION 열 값이 커서로 읽혀졌을 때 해당 행의 ROWVERSION 열 값과 일치하는지 여부를 확인합니다. 기본 행은 ROWVERSION 값이 일치하는 경우에만 수정됩니다. 이는 그 동안 다른 사용자가 행을 수정하지 않았음을 나타냅니다. 그렇지 않으면 오류가 발생합니다. 오류가 발생한 경우 업데이트된 데이터로 커서를 새로 고칩니다.

      기본 테이블에 ROWVERSION 열이 없으면 커서는 기본적으로 값 기반 낙관적 동시성으로 설정됩니다. 이를 위해서는 행의 현재 값을 커서로 행을 읽을 때의 값과 일치시켜야 합니다. 버전 기반 동시성 제어는 기본 행의 수정을 결정하는 데 필요한 처리량이 적기 때문에 값 기반 동시성 제어보다 더 효율적입니다. 따라서 낙관적 동시성 모델을 사용하는 커서의 성능을 최대화하려면 기본 테이블에 ROWVERSION 열이 있는지 확인하세요. 다음 T-SQL 문은 낙관적 T-SQL 커서를 만듭니다.

        DECLARE MyCursor CURSOR OPTIMISTIC
        FOR
            SELECT adt.Name
            FROM Person.AddressType AS adt
            WHERE adt.AddressTypeID = 1;
      
    • 스크롤 잠금
      스크롤 잠금 동시성이 있는 커서는 다른 커서 행을 가져오거나 커서가 닫힐 때까지 기본 행에 (U) 잠금을 유지합니다. 이렇게 하면 커서가 기본 행을 가져올 때 다른 사용자가 기본 행을 수정하는 것을 방지할 수 있습니다. 스크롤 잠금 동시성 모델은 커서를 업데이트할 수 있게 만듭니다. 다음 T-SQL 문은 스크롤 잠금 동시성 모델을 사용하여 T-SQL 커서를 만듭니다.

        DECLARE MyCursor CURSOR SCROLL_LOCKS
        FOR
            SELECT adt.Name
            FROM Person.AddressType AS adt
            WHERE adt.AddressTypeID = 1;
      

      참조되는 행에 잠금이 유지되므로(다른 커서 행을 가져오거나 커서가 닫힐 때까지) 해당 기간 동안 행을 수정하려는 다른 모든 사용자를 차단합니다. 이로 인해 데이터베이스 동시성이 손상되지만 커서를 통해 데이터를 수정하는 경우 오류가 발생하지 않습니다.

  • c. 커서 타입

    커서는 다음 4가지 종류가 있다.

      * 전진 전용 커서(Forward-only cursors)
      * 정적 커서(Static cursor)
      * 키셋 위주 커서(Keyset-driven cursor)
      * 동적 커서(Dynamic cursor)
    
    • 전진 전용 커서(Forward-only cursors)

      • 베이스 테이블에 직접 접근
      • 기본 테이블의 행은 일반적으로 커서 FETCH 작업을 사용하여 커서 행을 가져올 때까지 검색되지 않습니다. 그러나 데이터베이스 API 정방향 전용 커서 유형은 다음과 같은 추가 특성을 가지며 먼저 기본 테이블에서 모든 행을 검색합니다.
        • 클라이언트 커서 위치
        • 서버 커서 위치와 읽기 전용 커서 동시성
      • 전진 스크롤 전용(FETCH NEXT) 지원
      • 커서를 통해 모든 변경(INSERT UPDATE, DELETE) 허용. 또한 베이스 테이블에서 만들어진 모든 변경사항을 반영한다

      데이터베이스 API 커서(클라이언트 커서)와 T-SQL 커서(서버 커서)에 따라 전진 전용 커서는 다르게 구현된다. 데이터 액세스 계층은 이전에 나열된 네 가지 커서 유형 중 하나로 Forward-only 커서 특성을 구현한다. 그러나 T-SQL 커서는 커서 유형으로 정방향 전용 커서 특성을 구현하지 않습니다. 오히려 커서의 스크롤 가능(scrollable) 동작을 정의하는 속성으로 구현합니다. 따라서 T-SQL 커서의 경우 정방향 전용 특성을 사용하여 나머지 세 가지 커서 유형 중 하나의 스크롤 가능 동작을 정의할 수 있습니다.

      T-SQL 구문은 빠른 전진 전용 커서를 생성하기 위해 특정 커서 유형 옵션인 FAST_FORWARD를 제공합니다. FAST_FORWARD 커서의 별명은 소방 호스입니다. 커서를 통해 데이터를 이동하는 가장 빠른 방법이고 모든 정보가 한 방향으로 흐르기 때문입니다. 그러나 "firehose"가 여전히 전통적인 세트 기반 작업만큼 빠르지 않더라도 놀라지 마십시오. 다음 T-SQL 문은 빨리 감기 전용 T-SQL 커서를 만듭니다.

        DECLARE MyCursor CURSOR FAST_FORWARD
        FOR
            SELECT adt.Name
            FROM Person.AddressType AS adt
            WHERE adt.AddressTypeID = 1;
      

      FAST_FORWARD 속성은 성능 최적화가 활성화된 전진 전용, 읽기 전용 커서를 지정합니다.