뷰
언제 사용하고 언제 사용하지 말아야 하는지, 인덱싱된 뷰는 무엇인지 봅니다. 뷰가 자료를 담지 않는다는 사실에서 나머지가 따라옵니다.
저장해 두는 SELECT 문
목록 화면 하나를 내려면 표 넷을 이어야 합니다. 글에 게시판 이름, 말머리,
작성자 닉네임을 붙이는 조인입니다. 같은 조인을 화면마다 다시 적으면 어딘가에서
LEFT JOIN 을
JOIN 으로 적는 사람이 나옵니다.
뷰는 그 문장에 이름을 붙여 두는 것입니다.
CREATE VIEW Board.V_POST_LIST AS SELECT p.num, b.name AS board_name, c.name AS category_name, u.nickname, p.title, p.hit_count, p.reg_date, p.depth FROM Board.POSTS p JOIN Board.BOARDS b ON b.num = p.board_num LEFT JOIN Board.CATEGORIES c ON c.num = p.category_num JOIN Member.USERS u ON u.num = p.user_num; GO -- 이제 표처럼 조회합니다. SELECT TOP (3) num, board_name, category_name, nickname, title FROM Board.V_POST_LIST ORDER BY num;
| num | board_name | category_name | nickname | title |
|---|---|---|---|---|
| 1 | 자유게시판 | 후기 | 김철수 | 조인 질문드립니다 |
| 2 | 자유게시판 | 정보 | 이영희 | 트랜잭션 질문드립니다 |
| 3 | 자유게시판 | 질문 | 박민수 | 페이징 질문드립니다 |
뷰는 자료를 담지 않습니다. 저장되는 것은 문장뿐이고, 조회할 때마다 안쪽 문장이 실행됩니다. 그래서 원본이 바뀌면 뷰도 즉시 바뀝니다.
가져오지 않는 조인은 실행되지 않습니다
표 넷을 잇는 뷰이므로 무엇을 가져오든 넷을 다 읽을 것 같습니다. 그렇지 않습니다. 읽은 페이지를 세어 봅니다.
SET STATISTICS IO ON; -- 글 자체의 열만 가져옵니다. SELECT num, title FROM Board.V_POST_LIST WHERE num = 250; -- 게시판 이름과 작성자까지 가져옵니다. SELECT num, title, board_name, nickname FROM Board.V_POST_LIST WHERE num = 250; SET STATISTICS IO OFF;
| 가져오는 열 | 읽은 표 | 논리적 읽기 |
|---|---|---|
| num, title | POSTS | 2 |
| + board_name, nickname | POSTS · BOARDS · USERS | 2 + 2 + 2 |
계획을 보면 분명합니다.
SET SHOWPLAN_TEXT ON; GO SELECT num, title FROM Board.V_POST_LIST WHERE num = 250; GO SET SHOWPLAN_TEXT OFF;
이것을 조인 제거라고 합니다. 엔진은 두 가지를 알고 있어 조인을
지웁니다. LEFT JOIN 은 결과 행 수를 늘리지 않고
그 표의 열을 가져오지 않으므로 지워도 됩니다.
JOIN 쪽은 3.3 에서 건 외래 키가
가리키는 상대가 반드시 있음을 보증하므로 역시 지워도 결과가
같습니다.
외래 키를 신뢰할 수 없는 상태로 두면 이 최적화가 사라집니다.
3.3 에서 WITH NOCHECK 로 붙인 제약이
is_not_trusted = 1 이 된다고 했습니다. 그 상태에서는
엔진이 보증을 믿지 못해 조인을 그대로 실행합니다. 제약을 걸어 두는 것이 조회
속도로도 돌아오는 자리입니다.
그러므로 뷰를 사용한다고 해서 느려지지 않습니다. 뷰는 조회할 때 안쪽 문장으로 펼쳐지고, 그다음부터는 직접 적은 문장과 똑같이 최적화됩니다.
뷰를 통해 고칠 수 있습니다
뷰는 조회 전용이 아닙니다. 어느 행의 어느 열을 고치라는 것인지 뚜렷하면 엔진이 원본에 그대로 적용합니다. 공지사항만 보는 뷰로 확인합니다.
CREATE VIEW Board.V_NOTICE AS SELECT num, board_num, title, hit_count FROM Board.POSTS WHERE board_num = 1; GO UPDATE Board.V_NOTICE SET hit_count = hit_count + 1 WHERE num = 250; SELECT num, hit_count FROM Board.POSTS WHERE num = 250;
| num | hit_count |
|---|---|
| 250 | 251 |
조건 밖으로 밀어낼 수 있습니다
이 뷰는 board_num = 1 인 글만 봅니다. 그런데
그 조건 자체를 고치면 어떻게 될까요.
UPDATE Board.V_NOTICE SET board_num = 2 WHERE num = 250; SELECT COUNT(*) AS 뷰에남았나 FROM Board.V_NOTICE WHERE num = 250; -- 되돌립니다. 앞의 조회수도 함께 되돌립니다. UPDATE Board.POSTS SET board_num = 1, hit_count = hit_count - 1 WHERE num = 250;
| 뷰에남았나 |
|---|
| 0 |
방금 고친 행을 다시 조회하면 없습니다. 화면에서 저장 단추를
눌렀는데 목록에서 사라지는 상황이 이렇게 만들어집니다.
WITH CHECK OPTION 이 이것을 막습니다.
ALTER VIEW Board.V_NOTICE AS SELECT num, board_num, title, hit_count FROM Board.POSTS WHERE board_num = 1 WITH CHECK OPTION; GO UPDATE Board.V_NOTICE SET board_num = 2 WHERE num = 250;
조인 뷰는 한 표씩만
표 넷을 잇는 V_POST_LIST 도 고칠 수 있습니다.
다만 한 번에 한 표의 열만입니다.
-- POSTS 의 열만 고칩니다. 됩니다. UPDATE Board.V_POST_LIST SET title = N'바꿔봅니다' WHERE num = 250; -- POSTS 의 title 과 USERS 의 nickname 을 함께 고칩니다. UPDATE Board.V_POST_LIST SET title = N'x', nickname = N'y' WHERE num = 250; -- 지우는 것도 마찬가지입니다. 어느 표에서 지울지 정할 수 없습니다. DELETE FROM Board.V_POST_LIST WHERE num = 250; -- 첫 문장이 성공했으므로 제목을 되돌립니다. UPDATE Board.POSTS SET title = N'제약 조건 이렇게 쓰는 게 맞나요' WHERE num = 250;
뷰를 통한 수정은 권하지 않습니다. 되는 경우와 안 되는 경우의 경계가 뚜렷하지 않고, 되는 쪽은 어느 표가 바뀌는지 문장만 보고 알 수 없습니다. 수정은 4부에서 다룰 저장 프로시저로 하고, 뷰는 조회에 사용하십시오.
뷰에 SELECT * 를 적지 마십시오
뷰를 만들 때 SELECT * 를 적으면
그 시점의 열 목록이 뷰에 새겨집니다. 이후 원본에 열을 더해도
뷰는 모릅니다.
CREATE VIEW Board.V_STAR AS SELECT * FROM Board.BOARDS; GO -- 원본에 열을 하나 더합니다. ALTER TABLE Board.BOARDS ADD memo nvarchar(100) NULL; SELECT name AS 열 FROM sys.columns WHERE object_id = OBJECT_ID('Board.V_STAR') ORDER BY column_id;
| 열 |
|---|
| num |
| code |
| name |
| use_category |
| use_reply |
| reg_date |
오류가 나지 않고 조용히 옛 목록을 냅니다. 되살리려면 뷰를 다시 해석하게 해야 합니다.
EXEC sp_refreshview 'Board.V_STAR'; -- 이제 memo 가 나옵니다
더 나쁜 경우가 있습니다. 열을 지우고 다른 열을 더하면 이름과 값이 어긋난 채로 나옵니다.
CREATE TABLE Board.T_V (a int, b nvarchar(20), c int); INSERT INTO Board.T_V VALUES (1, N'비', 100), (2, N'비2', 200); GO CREATE VIEW Board.V_T AS SELECT * FROM Board.T_V; GO -- 가운데 열을 지우고 뒤에 다른 열을 더합니다. ALTER TABLE Board.T_V DROP COLUMN b; GO ALTER TABLE Board.T_V ADD d nvarchar(20) NULL; GO UPDATE Board.T_V SET d = N'디' WHERE a = 1; GO SELECT * FROM Board.T_V; SELECT * FROM Board.V_T;
| a | c | d |
|---|---|---|
| 1 | 100 | 디 |
| 2 | 200 | NULL |
| a | b | c |
|---|---|---|
| 1 | 100 | 디 |
| 2 | 200 | NULL |
오류가 아니라 잘못된 값입니다. 뷰는 만들 때의 자리 번호를
들고 있고 원본은 자리가 밀렸는데, 아무도 그것을 확인해 주지 않습니다.
c 는 int 로 선언된
열인데 글자가 나오는 것도 보십시오.
뷰에는 필요한 열을 적어 두십시오. 열을 적어 두면 원본이 바뀌어도 뷰가 내는 것은 그대로이고, 없어진 열을 적고 있으면 조회할 때 오류로 알려 줍니다.
원본을 함부로 못 고치게 묶습니다
WITH SCHEMABINDING 을 붙이면 뷰가 참조하는 열을
고치거나 지울 수 없게 됩니다. 뷰가 깨지는 것을 나중이 아니라
그 자리에서 막습니다.
CREATE VIEW Board.V_SB WITH SCHEMABINDING AS SELECT num, title, hit_count FROM Board.POSTS; GO -- 뷰가 보고 있는 열의 형식을 바꿔 봅니다. ALTER TABLE Board.POSTS ALTER COLUMN hit_count bigint NOT NULL; -- 지우는 것도 막힙니다. ALTER TABLE Board.POSTS DROP COLUMN hit_count;
두 가지 얼굴이 있습니다. 지켜 주는 것이면서 발목을 잡는 것이기도 합니다. 열 하나를 고치려면 뷰를 먼저 지우고, 고치고, 다시 만들어야 합니다. 그래서 모든 뷰에 붙이지는 않고, 깨지면 곤란한 뷰와 인덱싱된 뷰에 붙입니다. 뒤쪽은 선택이 아니라 필수입니다.
SCHEMABINDING 뷰에서는
SELECT * 를 적을 수 없고, 참조하는 표를
Board.POSTS 처럼 두 부분 이름으로
적어야 합니다. 앞 절의 함정을 문법으로 막아 둔 셈입니다.
자료를 실제로 담게 만듭니다
여기까지 뷰는 문장일 뿐이었습니다. 클러스터형 인덱스를 걸면 뷰가 낸 결과를 실제로 저장합니다. 게시판별 집계처럼 자주 보면서 매번 세기에는 아까운 것에 사용합니다.
먼저 집계를 그냥 내 봅니다.
SET STATISTICS IO ON; SELECT board_num, COUNT_BIG(*) AS post_count, SUM(hit_count) AS hit_sum FROM Board.POSTS GROUP BY board_num; SET STATISTICS IO OFF;
| board_num | post_count | hit_sum |
|---|---|---|
| 1 | 35 | 9,100 |
| 2 | 315 | 59,462 |
| 3 | 157 | 29,959 |
이것을 뷰로 만들고 인덱스를 걸어 봅니다. 조건이 까다롭습니다.
-- SCHEMABINDING 을 빠뜨리면 CREATE VIEW Board.V_BOARD_STAT_NS AS SELECT board_num, COUNT_BIG(*) AS post_count, SUM(hit_count) AS hit_sum FROM Board.POSTS GROUP BY board_num; GO CREATE UNIQUE CLUSTERED INDEX CX_NS ON Board.V_BOARD_STAT_NS (board_num); GO -- COUNT_BIG(*) 을 빠뜨리면 CREATE VIEW Board.V_BOARD_STAT_NC WITH SCHEMABINDING AS SELECT board_num, SUM(hit_count) AS hit_sum FROM Board.POSTS GROUP BY board_num; GO CREATE UNIQUE CLUSTERED INDEX CX_NC ON Board.V_BOARD_STAT_NC (board_num);
COUNT_BIG(*) 를 요구하는 것은
원본이 바뀔 때 저장된 집계를 고쳐 넣어야 하기 때문입니다. 글이
하나 늘면 그 게시판의 줄에서 개수를 1 늘리면 되고, 마지막 글이 지워지면 줄
자체를 없애야 합니다. 그 판단에 개수가 필요합니다.
조건을 맞춰 다시 만듭니다.
CREATE VIEW Board.V_BOARD_STAT WITH SCHEMABINDING AS SELECT board_num, COUNT_BIG(*) AS post_count, SUM(hit_count) AS hit_sum FROM Board.POSTS GROUP BY board_num; GO CREATE UNIQUE CLUSTERED INDEX CX_V_BOARD_STAT ON Board.V_BOARD_STAT (board_num); -- 뷰가 자리를 차지하고 있는지 봅니다. SELECT i.name AS 인덱스, ps.page_count AS 페이지, ps.record_count AS 행 FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('Board.V_BOARD_STAT'), NULL, NULL, 'DETAILED') ps JOIN sys.indexes i ON i.object_id = ps.object_id AND i.index_id = ps.index_id;
| 인덱스 | 페이지 | 행 |
|---|---|---|
| CX_V_BOARD_STAT | 1 | 3 |
그런데 그냥 조회하면 사용되지 않습니다
SET STATISTICS IO ON; SELECT board_num, post_count, hit_sum FROM Board.V_BOARD_STAT; SELECT board_num, post_count, hit_sum FROM Board.V_BOARD_STAT WITH (NOEXPAND); SET STATISTICS IO OFF;
| 적은 문장 | 읽은 것 | 논리적 읽기 |
|---|---|---|
| 그냥 조회 | POSTS | 16 |
| WITH (NOEXPAND) | V_BOARD_STAT | 2 |
인덱싱된 뷰를 자동으로 알아보는 것은 Enterprise 계열뿐입니다.
Standard 나 Express 에서는 WITH (NOEXPAND) 를 적어야
저장된 값을 읽습니다. 위 결과는 LocalDB(Express)에서 잰 것이라 차이가 그대로
드러났습니다.
그래서 인덱싱된 뷰는 만들 때 어디에서 돌 것인지 함께 정해야 합니다. 개발 장비가 Developer 에디션이면 힌트 없이 잘 돌다가, 운영이 Standard 면 아무 이득 없이 유지 비용만 물게 됩니다. 힌트를 적으면 두 곳에서 같게 동작합니다.
원본을 고칠 때 함께 갱신됩니다
저장해 두었으니 원본이 바뀌면 맞춰 두어야 합니다. 그 일이 수정 문장 안에서 함께 일어납니다. 3.5 에서 인덱스를 재던 방식으로 로그를 세어 봅니다.
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = hit_count + 1 WHERE num BETWEEN 1 AND 100; SELECT database_transaction_log_record_count AS 로그레코드, database_transaction_log_bytes_used AS 로그바이트 FROM sys.dm_tran_database_transactions WHERE database_id = DB_ID(); ROLLBACK;
| 상태 | 로그 레코드 | 로그 바이트 |
|---|---|---|
| 인덱싱된 뷰 있음 | 207 | 39,732 |
| 인덱싱된 뷰 없음 | 201 | 39,000 |
읽기는 16 이 2 가 되고 쓰기는 201 이 207 이 되었습니다. 조회가 잦고 수정이 드문 집계에 어울리는 까닭입니다. 반대로 글이 초당 여러 건 올라오는 표라면 집계 줄 하나를 두고 모두가 부딪히게 됩니다.
언제 사용하고 언제 사용하지 않습니까
사용하는 자리
- 같은 조인이 여러 곳에서 반복될 때. 한곳에 적어 두면 조인 조건을 잘못 적는 자리가 하나로 줄어듭니다.
- 열을 가려서 내보낼 때.
Member.USERS에서 전자 메일을 뺀 뷰를 만들고 그 뷰에만 권한을 주면, 표에는 권한을 주지 않고도 회원 목록을 보여 줄 수 있습니다. 권한은 6.1 에서 다룹니다. - 표 구조를 바꾸면서 옛 이름을 지킬 때. 표를 둘로 나눈 뒤 옛 이름의 뷰를 두면, 그 이름을 사용하던 코드가 계속 돕니다.
사용하지 않는 자리
- 뷰 위에 뷰를 얹는 것. 뷰가 뷰를 부르고 그것이 또 뷰를 부르면, 조회 하나에 표 열 몇 개가 딸려 옵니다. 정작 필요한 것은 두 개인데도 그렇습니다. 이때는 조인 제거도 잘 듣지 않습니다.
- 수정 통로로 사용하는 것. 앞에서 본 대로 되는 경우와 안 되는 경우의 경계가 흐립니다.
- 느린 문장을 감추는 것. 뷰는 이름을 줄 뿐 문장을 빠르게 하지 않습니다. 느린 조인을 뷰에 넣으면 느린 뷰가 됩니다.
뷰의 정의는 sys.sql_modules 에서 볼 수
있습니다.
SELECT definition FROM sys.sql_modules WHERE object_id = OBJECT_ID('Board.V_POST_LIST')
입니다. 남이 만든 뷰를 조회하기 전에 무엇을 이어 놓았는지 먼저 보십시오.
직접 해보기
Member.USERS 에서 전자 메일을 빼고 나머지를
내는 뷰를 만들어 보세요. 뒤에 열이 늘어도 전자 메일이 새어 나가지 않아야
합니다.
V_POST_LIST 를 인덱싱된 뷰로 만들면 목록 화면이
빨라질 것 같습니다. WITH SCHEMABINDING 을 붙이고
인덱스를 걸어 보세요. 되지 않는다면 무엇을 고쳐야 합니까.
이 단원에서 만든 것을 지웁니다.
DROP VIEW Board.V_POST_LIST, Board.V_NOTICE, Board.V_SB, Board.V_BOARD_STAT, Board.V_STAR, Board.V_T;
ALTER TABLE Board.BOARDS DROP COLUMN memo;
DROP TABLE Board.T_V;
- 뷰는 저장해 둔 SELECT 문입니다. 자료를 담지 않고 조회할 때 펼쳐집니다.
- 펼쳐진 뒤에는 직접 적은 문장과 똑같이 최적화됩니다. 가져오지 않는 열의 조인은 아예 실행되지 않습니다(표 넷을 잇는 뷰에서 2장). 외래 키가 신뢰 상태여야 되는 최적화입니다.
- 뷰로 고칠 수 있지만 한 번에 한 표의 열만입니다(4405). 조건 밖으로 밀어내는 수정은 막히지 않으므로
WITH CHECK OPTION을 붙입니다(550). - 뷰에
SELECT *를 적지 마십시오. 열 목록이 새겨져 원본과 어긋나고, 오류가 아니라 이름과 값이 뒤바뀐 결과가 나옵니다. WITH SCHEMABINDING은 원본 열을 잠급니다(5074). 인덱싱된 뷰에는 필수입니다.- 인덱싱된 뷰는 결과를 실제로 저장합니다.
SCHEMABINDING(1939) ·COUNT_BIG(*)(10138) · 안쪽 조인만(10113) 이라는 조건이 붙습니다. - Enterprise 계열이 아니면
WITH (NOEXPAND)를 적어야 저장된 값을 읽습니다(16장 대 2장). - 인덱싱된 뷰는 조회가 잦고 수정이 드문 집계에 사용합니다. 원본을 고칠 때마다 함께 갱신되기 때문입니다.