#title 커서를 언제 사용해야 할까 [[TableOfContents]] ==== 개요 ==== 데이터베이스 관련 커뮤니티에 보면 가끔 커서에 관련된 질문이나 글들이 올라오고는 한다. 대부분은 커서를 사용하지 말라고 못박아 논 경우가 대부분이다. 그 이유는 커서를 사용함으로 인해 안 좋은 일을 겪었기 때문이라고 생각한다. 그러나 필자는 커서를 무조건 사용하지 말라고 못박지는 않는다. 다만 커서를 선언해 줄 때 선언되는 SQL문의 결과집합의 양을 최대한 줄인 후 커서를 사용하라는 것이다. 물론 필자도 최대한 커서를 사용하지 말라는데 한 표를 던진다. 커서가 성능상 안 좋다고 하는 이유는 선언된 집합의 양과 행 단위 처리에 문제가 있다. 일단 데이터베이스에서 행 단위 처리를 하게 된다면 성능은 현격히 떨어진다. 그러나 어쩔 수 없이 커서를 사용해야 하는 경우도 있다. 그런 경우는 동적으로 결과가 변하는 것일 때이다. 다음과 같은 예이다. ||* 짧은 생각 - SQL작성 능력에 따라서 커서 사용여부의 판단이 달라질 수 있다. 대부분은 커서를 사용하지 않아도 SQL로 작성할 수도 있으나 요즘(2009.09)의 생각에는 이해하기 쉬운 적절한 부하를 가진 스크립트도 유지보수를 생각하면 결코 악성이라고 단정지을 수 없다. 1%의 능력자가 짜놓은 어지러운 SQL을 유지보수해야 하는 99%의 일반 개발자를 생각하면 소위 말하는 "한방쿼리"가 과연 좋은 것인가를 다시 한 번 생각 해 볼 필요가 있다. DB쟁이들은 DB만 하면 되지만 DB이외의 개발자들은 다른 언어도 해야 하고, SQL도 만질 줄 알아야 한다. 그들을 생각해주자.|| ==== 예제 ==== {{{ use tempdb go if exists (select * from sysobjects where type = 'U' and name = 'tableA') drop table tableA create table tableA(group_id int , [rand] int) go insert into tableA values(11, 2) insert into tableA values(12, 4) insert into tableA values(13, 1) go if exists (select * from sysobjects where type = 'U' and name = 'tableB') drop table tableB create table tableB(group_id int , id varchar(10)) go insert into tableB values(11, '개똥이') insert into tableB values(11, '말똥이') insert into tableB values(11, '소똥이') insert into tableB values(11, '땡칠이') insert into tableB values(12, '옥동자') insert into tableB values(12, '아무개') insert into tableB values(12, '홍길동') insert into tableB values(12, '고길동') insert into tableB values(12, '김길동') insert into tableB values(12, '박길동') insert into tableB values(13, '개뿔') insert into tableB values(13, '소뿔') go select * from tableA /* group_id rand ----------- ----------- 11 2 12 4 13 1 */ select * from tableB /* group_id id ----------- ---------- 11 개똥이 11 말똥이 11 소똥이 11 땡칠이 12 옥동자 12 아무개 12 홍길동 12 고길동 12 김길동 12 박길동 13 개뿔 13 소뿔 */ }}} Group_id 가 11인 것은 랜덤으로 2개를 가져오고, Group_id가 12인 것은 랜덤으로 4개를 가져오는 형태이다. 랜덤한 것을 보장하기 위해서 아래와 같은 뷰를 생성하였다. {{{ create view v_tableB as select top 100 percent group_id, id from tableB order by NewID() --다소 부하가 있다. --SQL Server 2005 버전에서는 TableSample 키워드를 사용하면 된다. }}} 그리고 한번에 처리하기 위해서 동적 쿼리를 작성하였다. 실제로 커서를 돌면서 SQL문이 만들어지는 과정이다. {{{ DECLARE @group_id int , @rand int , @count int , @cursor_count int , @sql varchar(1000) SET @cursor_count = 1 BEGIN DECLARE table_cursor CURSOR FOR SELECT group_id, rand FROM tableA SELECT @count = COUNT(*) FROM tableA OPEN table_cursor FETCH NEXT FROM table_cursor INTO @group_id, @rand SET @sql = 'SELECT TOP ' + CAST(@rand AS VARCHAR) + ' group_id, id FROM v_tableB ' + 'WHERE group_id = ' + CAST(@group_id AS VARCHAR) + ' UNION ALL ' WHILE @@FETCH_STATUS = 0 BEGIN FETCH NEXT FROM table_cursor INTO @group_id, @rand IF(@count <> @cursor_count) BEGIN SET @sql = @sql + 'SELECT TOP ' + CAST(@rand AS VARCHAR) + ' group_id, id FROM v_tableB ' + 'WHERE group_id = ' + CAST(@group_id AS VARCHAR) + ' ' Print @count Print @cursor_count END SET @cursor_count = @cursor_count + 1 IF(@count > @cursor_count) SET @sql = @sql + 'UNION ALL ' END CLOSE table_cursor DEALLOCATE table_cursor PRINT @sql EXEC(@sql) END /* group_id id ----------- ---------- 11 개똥이 11 말똥이 12 옥동자 12 아무개 12 홍길동 12 고길동 13 개뿔 */ }}} 실제로 @sql을 프린트 해보면 다음과 같이 쿼리문이 동적으로 만들어진 것을 볼 수 있다. {{{ SELECT TOP 2 group_id, id FROM v_tableB WHERE group_id = 11 UNION ALL SELECT TOP 4 group_id, id FROM v_tableB WHERE group_id = 12 UNION ALL SELECT TOP 1 group_id, id FROM v_tableB WHERE group_id = 13 }}} 실제로 v_tableB라는 뷰를 3번이나 접근한다. 즉, 뷰의 내용대로 tableB는 NewID()로 정렬이 되어 랜덤한 값을 얻기는 하지만 group_id가 많은 값이라면 성능은 점차 나빠질 것이다. 또한 WHERE 조건이 group_id에 의해 결정되므로 실제로는 group_id에 반드시 인덱스가 잡혀 있어야 하겠다. ==== 많은 성능 차이 ==== '조엘 온 소프트웨어'라는 책을 보면 '러시아 페인공 알고리즘'이라는 내용이 있다. 이런 것은 COUNT(*)를 이용한 순번을 만들거나 SUM()을 이용한 누적을 구할 때에 발생한다. 서브쿼리나 조인으로 풀리는데 '>=' 또는 '<=' 정도의 부등호 연산자가 사용된다. 즉, 시간에 따라서 계속적으로 X제곱의 그래프와 같이 데이터의 액세스 양이 많아지는 경우이다. 이런 경우는 커서를 쓰는 것이 낫다. ==== 상황에 따라서 ==== 상황에 따라서 커서를 이용한 솔루션이 훨씬 비용이 적게 드는 경우가 있다. 예를 들어, 누적을 구하는 것과 같은 경우이다.