변수와 흐름 제어
DECLARE 로 값을 담고 IF · WHILE 로 조건에 따라 다른 문장을 실행합니다. 배치와 GO, 그리고 조용히 틀리는 자리 셋을 봅니다.
SQL 에 절차를 적습니다
3부까지는 무엇을 원하는지만 적었습니다. 어떻게 가져올지는 엔진이 정했습니다. 그런데 실제 작업에는 순서가 있습니다 — 3.9 의 답글 넣기가 그랬습니다. 부모를 찾고, 뒤를 밀고, 넣습니다.
T-SQL 은 SQL 에 절차를 적을 수 있게 더한 것입니다. 값을 담는
변수, 조건에 따라 갈라지는 IF, 되풀이하는
WHILE 이 있습니다. 4부는 이 문법으로 시작해 저장
프로시저까지 갑니다.
다만 이 단원의 결론을 먼저 적어 둡니다. 절차를 적을 수 있다는 것과 적어야 한다는 것은 다릅니다. 마지막 절에서 507번 도는 반복문과 한 문장을 견줍니다.
값을 담습니다
DECLARE 로 선언하고 이름은
@ 로 시작합니다. 선언과 함께 값을 줄 수 있습니다.
DECLARE @board int = 2; DECLARE @name nvarchar(50), @cnt int; -- 값을 주지 않으면 NULL 입니다 -- 대입하는 방법이 둘입니다. SET @name = (SELECT name FROM Board.BOARDS WHERE num = @board); SELECT @cnt = COUNT(*) FROM Board.POSTS WHERE board_num = @board; SELECT @name AS 게시판, @cnt AS 글수;
| 게시판 | 글수 |
|---|---|
| 자유게시판 | 315 |
SET 과 SELECT 은 다르게 실패합니다
둘 다 값을 담지만 맞는 행이 없을 때와 여러 개일 때 다르게 움직입니다. 이것이 조용히 틀리는 자리입니다.
-- 맞는 행이 하나도 없는 조건입니다. DECLARE @a int = 999, @b int = 999; SET @a = (SELECT hit_count FROM Board.POSTS WHERE num = -1); SELECT @b = hit_count FROM Board.POSTS WHERE num = -1; SELECT @a AS SET으로, @b AS SELECT으로;
| SET 으로 | SELECT 으로 |
|---|---|
| NULL | 999 |
이것이 위험한 까닭은 오류가 나지 않기 때문입니다. 반복문
안에서 SELECT 로 대입하면 조건에 맞는 행이 없는
회차에서 지난 회차의 값이 그대로 남아 그것을 다시 사용하게
됩니다.
여러 행이 나올 때도 갈립니다.
-- 공지사항 35건이 나오는 조건입니다. DECLARE @c int; SELECT @c = hit_count FROM Board.POSTS WHERE board_num = 1; SELECT @c AS 남은값; GO DECLARE @d int; SET @d = (SELECT hit_count FROM Board.POSTS WHERE board_num = 1);
값 하나를 담을 때는 SET 을
사용하십시오. 조건이 잘못되었으면 그 자리에서 알려 줍니다.
SELECT 대입은 여러 변수에 한 번에
담을 때가 제 자리입니다 —
SELECT @grp = group_num, @dep = depth + 1 FROM …
처럼 3.9 에서 사용한 방식입니다.
@@ROWCOUNT 는 바로 다음 문장에서만 유효합니다
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = hit_count WHERE board_num = 1; SELECT @@ROWCOUNT AS 고친행수; SELECT @@ROWCOUNT AS 한문장뒤; ROLLBACK;
| 언제 읽었나 | 값 |
|---|---|
| UPDATE 바로 뒤 | 35 |
| 한 문장 뒤 | 1 |
모든 문장이 이 값을 다시 씁니다.
SELECT 도, IF 안의
조회도 그렇습니다. 필요하면 바로 변수에 담으십시오 —
DECLARE @n int = @@ROWCOUNT; 입니다.
GO 는 SQL 문장이 아닙니다
SQL Server 로 보내는 단위를 배치라고 합니다.
GO 는 그 경계를 나타내는 표시이고,
서버가 아니라 도구가 읽습니다. SSMS 와
sqlcmd 는 GO 를 만나면
그때까지의 문장을 한 덩어리로 보냅니다.
그래서 변수는 배치를 넘지 못합니다.
DECLARE @e int = 1; GO PRINT @e;
같은 배치에서 이름을 두 번 선언할 수도 없습니다.
DECLARE @x int; DECLARE @x int;
GO 뒤에 숫자를 적으면
그 배치를 그만큼 되풀이합니다. 시험용 자료를 만들 때
편리합니다.
PRINT N'세 번 나옵니다'; GO 3
IF 와 ELSE
조건에 따라 다른 문장을 실행합니다. 2.8 의
CASE 는 값 하나를 고르는 식이었고,
IF 는 문장을 고르는 것입니다.
IF EXISTS (SELECT 1 FROM Board.POSTS WHERE board_num = 1) PRINT N'공지사항에 글이 있습니다'; ELSE PRINT N'없습니다';
BEGIN 과 END 를 빠뜨리면
IF 는 바로 다음 한 문장만
거느립니다. 두 문장을 넣으려면 묶어야 합니다.
DECLARE @cnt int = 0; IF @cnt > 0 PRINT N'글이 있습니다'; PRINT N'이 줄은 조건과 상관없이 나옵니다';
들여쓰기는 아무 뜻이 없습니다. 조건이 거짓인데도 둘째 줄이
실행되었습니다. 이 자리에 UPDATE 나
DELETE 가 있었다면 조건과 상관없이 자료가
바뀝니다.
IF @cnt > 0 BEGIN PRINT N'글이 있습니다'; PRINT N'두 줄 모두 조건 안입니다'; END PRINT N'--- 블록 밖 ---';
한 문장이라도 BEGIN ·
END 로 묶는 습관을 들이십시오. 나중에 줄을
하나 더할 때 이 함정을 밟게 됩니다.
블록 안에서 선언해도 배치 전체입니다
다른 언어의 블록 범위를 떠올리면 어긋납니다.
IF 1 = 0 BEGIN DECLARE @never int = 1; END SELECT @never AS 값;
| 값 |
|---|
| NULL |
선언은 배치를 컴파일할 때 처리되고 대입은 실행할 때 일어납니다.
그래서 조건이 거짓이어도 변수는 있고, 값은 NULL
입니다. DECLARE 는 블록 안이 아니라
배치 앞머리에 모아 두는 편이 읽기 좋습니다.
WHILE
조건이 참인 동안 되풀이합니다. BREAK 로 빠져나오고
CONTINUE 로 다음 회차로 건너뜁니다.
DECLARE @i int = 1, @sum int = 0; WHILE @i <= 10 BEGIN IF @i = 6 BREAK; SET @sum += @i; SET @i += 1; END SELECT @i AS 멈춘자리, @sum AS 합;
| 멈춘자리 | 합 |
|---|---|
| 6 | 15 |
증가시키는 줄을 빠뜨리면 끝나지 않습니다. 위에서
SET @i += 1 이 CONTINUE
뒤에 있으면 그 회차에서 건너뛰어 무한히 돕니다.
CONTINUE 를 사용할 때는 증가 문장이 그보다 앞에
있는지 확인하십시오.
그런데 되도록 반복하지 마십시오
게시판별 글 수를 세는 일을 두 가지로 적어 봅니다. 하나는 배운 문법을 모두 사용하고, 다른 하나는 3부까지의 방식입니다.
-- (가) 한 건씩 읽으면서 셉니다. DECLARE @i int = 1, @max int = (SELECT MAX(num) FROM Board.POSTS), @c1 int = 0, @c2 int = 0, @c3 int = 0; WHILE @i <= @max BEGIN DECLARE @b int = (SELECT board_num FROM Board.POSTS WHERE num = @i); IF @b = 1 SET @c1 += 1; ELSE IF @b = 2 SET @c2 += 1; ELSE IF @b = 3 SET @c3 += 1; SET @i += 1; END SELECT @c1 AS 공지, @c2 AS 자유, @c3 AS 질문; -- (나) 한 문장으로 셉니다. SELECT board_num, COUNT(*) AS 글수 FROM Board.POSTS GROUP BY board_num;
| 공지 | 자유 | 질문 |
|---|---|---|
| 35 | 315 | 157 |
| 방식 | 엔진에 보낸 조회 | 논리적 읽기 |
|---|---|---|
| WHILE 로 한 건씩 | 507 | 1,014 |
| 한 문장으로 | 1 | 16 |
507번 물어본 것과 한 번 물어본 것의 차이입니다. 반복문은 한 번에 한 행씩 다루므로, 행이 늘면 오가는 횟수도 그대로 늡니다. 한 문장은 무엇을 원하는지만 적고 어떻게 모을지는 엔진이 정합니다 — 3.5 에서 본 인덱스도 그때 고려됩니다.
507행이라 차이가 시간으로는 잘 드러나지 않습니다. 그러나 이 구조는 행 수에 비례해 벌어집니다. 50만 행이면 50만 번 물어보게 됩니다.
반복문이 필요한 자리가 없지는 않습니다. 큰 삭제를 끊어서 하는 경우(5.7), 배치 작업을 나누는 경우가 그렇습니다. 그러나 한 문장으로 적을 수 있는 일을 반복문으로 적지는 마십시오. 커서까지 포함해 4.8 에서 다시 다룹니다.
직접 해보기
아래 코드는 없는 글 번호를 넣었는데도 조회수 -1 을 냅니다. 까닭을 말하고, 없으면 없다고 알리도록 고쳐 보세요.
DECLARE @hit int = -1; SELECT @hit = hit_count FROM Board.POSTS WHERE num = 99999; PRINT CONCAT(N'조회수 ', @hit);
3.9 에서 답글 넣기를 세 문장으로 적었습니다. 그런데
없는 글에 답글을 달려고 하면 부모를 찾지 못해
group_num 이 NULL 인
채로 넣게 됩니다. 먼저 확인하고 없으면 알리도록 고쳐 보세요. 몇 행을
밀었는지도 함께 냅니다.
- 변수는
DECLARE로 선언하고 값을 주지 않으면 NULL 입니다. - 맞는 행이 없을 때
SET은 NULL 을 넣고SELECT대입은 이전 값을 그대로 둡니다. 값 하나를 담을 때는SET을 사용하십시오. - 여러 행이 나오면
SET은 512 로 막고SELECT은 조용히 하나만 남깁니다. @@ROWCOUNT는 바로 다음 문장에서만 유효합니다(35 → 1). 필요하면 즉시 변수에 담으십시오.GO는 SQL 문장이 아니라 도구가 읽는 배치 구분자입니다. 변수는 배치를 넘지 못합니다(137).IF는 바로 다음 한 문장만 거느립니다. 들여쓰기는 뜻이 없으므로BEGIN·END로 묶으십시오.- 블록 안에서
DECLARE해도 배치 전체에 유효합니다. 조건이 거짓이면 선언만 남고 값은 NULL 입니다. - 한 문장으로 적을 수 있는 일을 반복문으로 적지 마십시오. 507행을 세는 데 조회 507번(1,014장)과 조회 한 번(16장)의 차이입니다.