MSSQL LAB
MSSQL 3.10 · 3부. 설계

실습 — 게시판 스키마 설계

요구사항에서 테이블까지, 3부에서 배운 것을 한 번에 적용합니다. 태그·좋아요·지우기 셋을 설계하며 무엇을 내줄지 고릅니다.

예상 학습 시간 25분 난이도 실전
이 단원에서 하는 것

요구사항 셋을 표로 옮깁니다

운영 중인 게시판에 기능을 더한다고 합시다. 기획서에 적힌 것은 이 셋입니다.

  • 태그 — 글에 태그를 여러 개 붙입니다. 태그를 눌러 그 태그가 붙은 글을 봅니다.
  • 좋아요 — 회원이 글에 좋아요를 누릅니다. 한 사람이 같은 글에 두 번 누를 수는 없습니다.
  • 지우기 — 글을 지워도 바로 사라지지 않습니다. 언제 지웠는지 남고, 관리자는 볼 수 있습니다.

3부에서 배운 것을 순서대로 적용합니다. 정규화로 표를 나누고, 키를 정하고, 제약으로 규칙을 적고, 인덱스를 걸고, 뷰로 감쌉니다. 하나씩 해나가되 왜 그렇게 정했는지를 매번 적습니다.

요구사항 1

태그 — 한 칸에 여러 개를 넣지 않습니다

가장 빨리 끝내는 방법은 열 하나를 더하고 쉼표로 이어 넣는 것입니다. 실제로 어떻게 되는지 봅니다.

SQL
CREATE TABLE Board.T_TAGCSV (post_num int PRIMARY KEY, tags nvarchar(200) NULL);
INSERT INTO Board.T_TAGCSV VALUES
 (1, N'SQL,조인,입문'), (2, N'SQL,트랜잭션'), (3, N'입문'),
 (4, N'조인,성능'), (5, N'SQL서버,백업'), (6, N'성능');
GO
-- (가) SQL 태그가 붙은 글을 찾습니다.
SELECT post_num, tags FROM Board.T_TAGCSV WHERE tags LIKE N'%SQL%';

-- (나) 태그별 글 수를 셉니다.
SELECT tags, COUNT(*) AS 글수 FROM Board.T_TAGCSV GROUP BY tags;

-- (다) 태그 이름을 SQL 에서 T-SQL 로 고칩니다.
SELECT post_num, REPLACE(tags, N'SQL', N'T-SQL') AS 고친결과 FROM Board.T_TAGCSV;
결과 (가) — SQL 태그를 찾으면
post_numtags
1SQL,조인,입문
2SQL,트랜잭션
5SQL서버,백업
결과 (나) — 태그별 글 수를 세면
tags글수
SQL,조인,입문1
SQL,트랜잭션1
SQL서버,백업1
조인,성능1
결과 (다) — 이름을 고치면
post_num고친결과
1T-SQL,조인,입문
5T-SQL서버,백업
셋 다 틀렸습니다. SQL서버가 딸려 오고, 조합별로 세어지고, 이름이 망가집니다.

한 칸에 값을 여러 개 넣으면 그 값을 개별로 다룰 수 없습니다. 3.1 의 1정규형이 말하는 것이 이것입니다. 게다가 태그 이름이 글마다 되풀이되어 있어 하나를 고치려면 모든 글을 훑어야 합니다.

나눈 설계

태그는 태그대로 한 표에 두고, 글과 태그를 잇는 표를 따로 둡니다. 글 하나에 태그가 여럿, 태그 하나에 글이 여럿이므로 다대다이고, 다대다는 연결 표로 풉니다.

SQL
CREATE TABLE Board.TAGS (
    num  int          IDENTITY(1,1) NOT NULL,
    name nvarchar(30)               NOT NULL,
    CONSTRAINT PK_TAGS PRIMARY KEY CLUSTERED (num),
    CONSTRAINT UX_TAGS_name UNIQUE (name)      -- 같은 이름이 둘 있으면 안 됩니다
);
GO
CREATE TABLE Board.POST_TAGS (
    post_num int NOT NULL,
    tag_num  int NOT NULL,
    -- 두 열이 함께 기본 키입니다. 대리 키를 따로 두지 않습니다.
    CONSTRAINT PK_POST_TAGS PRIMARY KEY CLUSTERED (post_num, tag_num),
    CONSTRAINT FK_POST_TAGS_POSTS FOREIGN KEY (post_num)
        REFERENCES Board.POSTS (num) ON DELETE CASCADE,
    CONSTRAINT FK_POST_TAGS_TAGS FOREIGN KEY (tag_num)
        REFERENCES Board.TAGS (num)          -- 태그는 연쇄 삭제하지 않습니다
);
GO
-- 태그로 글을 찾는 반대 방향입니다. 두 열뿐이라 그대로 커버링이 됩니다.
CREATE NONCLUSTERED INDEX IX_POST_TAGS_tag ON Board.POST_TAGS (tag_num, post_num);

여기서 정한 것 넷

  • 연결 표에 대리 키를 두지 않았습니다. (post_num, tag_num) 자체가 겹치면 안 되는 값이므로, 복합 기본 키가 곧 중복 방지입니다. 번호 열을 따로 두면 UNIQUE 제약을 또 걸어야 합니다(3.2 · 3.4).
  • 클러스터형 키 순서를 (post_num, tag_num) 으로 두었습니다. 글 하나의 태그를 모아 읽는 것이 더 잦기 때문입니다. 3.5 의 선행 열 규칙대로, 이 순서라야 WHERE post_num = ? 가 탐색이 됩니다.
  • 반대 방향 인덱스를 하나 더 두었습니다. (tag_num, post_num) 입니다. 태그를 눌렀을 때 필요하고, 열이 둘뿐이라 표에 갈 일이 없습니다.
  • 글에는 CASCADE, 태그에는 걸지 않았습니다. 글이 사라지면 그 글의 태그 연결도 뜻이 없지만, 태그를 지웠다고 글의 연결이 조용히 사라지면 곤란합니다(3.3).

실습 데이터의 300건에 태그를 붙이고 다시 물어봅니다.

SQL
-- (가) SQL 태그가 붙은 글
SELECT COUNT(*) AS 글수 FROM Board.POST_TAGS pt
    JOIN Board.TAGS t ON t.num = pt.tag_num
WHERE t.name = N'SQL';

-- (나) 태그별 글 수
SELECT t.name AS 태그, COUNT(*) AS 글수 FROM Board.POST_TAGS pt
    JOIN Board.TAGS t ON t.num = pt.tag_num
GROUP BY t.name ORDER BY 글수 DESC, 태그;

-- (다) 이름 고치기
UPDATE Board.TAGS SET name = N'T-SQL' WHERE name = N'SQL';
결과
물음쉼표 한 열나눈 설계
SQL 태그가 붙은 글SQL서버까지 딸려 옴128건 정확히
태그별 글 수조합별로 세어짐태그마다 정확히
이름 고치기5행을 고치며 SQL서버까지 망가짐1행
태그별 글 수
태그글수
SQL128
입문128
조인128
SQL서버43
백업43
연결이 556개입니다. 양쪽 조회 모두 2장에 나옵니다(PK 2페이지 · 반대 인덱스 1페이지).
요구사항 2

좋아요 — 키가 곧 규칙입니다

"한 사람이 같은 글에 두 번 누를 수 없다" 는 요구는 표를 어떻게 잡느냐로 이미 지켜집니다. 태그 연결 표와 같은 모양입니다.

SQL
CREATE TABLE Board.POST_LIKES (
    post_num int          NOT NULL,
    user_num int          NOT NULL,
    reg_date datetime2(0) NOT NULL
        CONSTRAINT DF_POST_LIKES_reg_date DEFAULT SYSDATETIME(),
    CONSTRAINT PK_POST_LIKES PRIMARY KEY CLUSTERED (post_num, user_num),
    CONSTRAINT FK_POST_LIKES_POSTS FOREIGN KEY (post_num)
        REFERENCES Board.POSTS (num) ON DELETE CASCADE,
    CONSTRAINT FK_POST_LIKES_USERS FOREIGN KEY (user_num)
        REFERENCES Member.USERS (num)
);
GO
-- 내가 누른 글 목록을 내는 방향입니다.
CREATE NONCLUSTERED INDEX IX_POST_LIKES_user ON Board.POST_LIKES (user_num, post_num);
GO
-- 같은 사람이 같은 글에 또 누르면
INSERT INTO Board.POST_LIKES (post_num, user_num) VALUES (1, 2);
오류
메시지 2627 Violation of PRIMARY KEY constraint 'PK_POST_LIKES'. Cannot insert duplicate key in object 'Board.POST_LIKES'. The duplicate key value is (1, 2).
응용 프로그램에서 확인하지 않아도 막힙니다. 3.4 에서 본 그 번호입니다.

번호 열을 따로 두었다면 이 규칙을 잃습니다. num int IDENTITY 를 기본 키로 삼으면 같은 사람이 같은 글에 몇 번이든 누를 수 있고, 막으려면 UNIQUE (post_num, user_num) 을 따로 걸어야 합니다. 그러면 인덱스가 둘이 되고 쓰기가 그만큼 비싸집니다(3.5).

좋아요 수를 미리 담아 둘까요

목록 화면에 좋아요 수를 보이려면 매번 세야 합니다. 미리 담아 두고 싶어집니다. 3부에서 배운 도구로 두 가지를 시도해 봅니다.

SQL
-- (가) 계산 열로 두려 하면
ALTER TABLE Board.POSTS ADD like_count AS
    (SELECT COUNT(*) FROM Board.POST_LIKES WHERE post_num = num);
GO

-- (나) 인덱싱된 뷰로 두면
CREATE VIEW Board.V_POST_LIKE_COUNT WITH SCHEMABINDING AS
SELECT post_num, COUNT_BIG(*) AS like_count
FROM Board.POST_LIKES GROUP BY post_num;
GO
CREATE UNIQUE CLUSTERED INDEX CX_V_POST_LIKE_COUNT ON Board.V_POST_LIKE_COUNT (post_num);
오류 — 계산 열
메시지 1046 Subqueries are not allowed in this context. Only scalar expressions are allowed.
결과 — 인덱싱된 뷰는 만들어집니다
방법논리적 읽기
그냥 세기2
인덱싱된 뷰(NOEXPAND)2
읽기가 같습니다. 얻은 것이 없습니다.

계산 열은 3.6 에서 본 대로 다른 표를 볼 수 없어 애초에 안 됩니다. 인덱싱된 뷰는 만들어지지만 세는 비용이 이미 2장이라 줄일 것이 없습니다. PK_POST_LIKES (post_num, user_num) 가 있어 WHERE post_num = ? 가 탐색 한 번에 끝나기 때문입니다.

그러므로 담아 두지 않습니다. 배운 도구가 있다고 해서 쓸 자리가 되는 것은 아닙니다. 3.1 에서 정규화를 어디서 멈출지 물었던 것과 같은 판단입니다 — 재 보고 이득이 없으면 넣지 않습니다. 좋아요가 글마다 수만 건이 되어 세는 것이 비싸지면, 그때 다시 재고 넣으면 됩니다.

요구사항 3

지우기 — 지울 수 없다는 데서 시작합니다

"지워도 바로 사라지지 않는다" 를 만들기 전에, 진짜로 지우면 어떻게 되는지부터 봅니다. 답글이 둘 딸린 6번 글입니다.

SQL
DELETE FROM Board.POSTS WHERE num = 6;
오류
메시지 547 The DELETE statement conflicted with the SAME TABLE REFERENCE constraint "FK_POSTS_PARENT". The conflict occurred in database "MssqlLab", table "Board.POSTS", column 'parent_num'.
3.3 에서 본 자리입니다. 자기 자신을 가리키는 외래 키에는 CASCADE 를 걸 수 없습니다.

답글이 달린 글은 애초에 지울 수 없습니다. 지우려면 답글부터 지워야 하고, 그러면 남의 글이 사라집니다. 답글만 남기면 부모 없는 답글이 됩니다. 지우지 않고 표시만 남기는 설계는 취향이 아니라 이 구조가 요구하는 것입니다.

열 하나를 더합니다

SQL
ALTER TABLE Board.POSTS ADD del_date datetime2(0) NULL;

usable bit 이 아니라 del_date datetime2 로 둔 까닭이 셋입니다.

  • 언제 지웠는지가 함께 남습니다. 보관 기간을 두고 정리하려면 그 날짜가 필요합니다(6.6).
  • NULL 이 곧 살아 있음입니다. 1.9 에서 본 대로 NULL 은 "값이 없다" 는 뜻이고, 지운 적이 없다는 것이 정확히 그 뜻입니다.
  • 필터 인덱스가 자연스럽습니다. WHERE del_date IS NULL 로 살아 있는 글만 인덱싱합니다(3.4).

목록 인덱스를 필터 인덱스로

목록 화면은 이제 살아 있는 글만 냅니다. 그렇다면 인덱스도 그것만 담으면 됩니다.

SQL
CREATE NONCLUSTERED INDEX IX_POSTS_live
    ON Board.POSTS (board_num, group_num DESC, sort_no)
    INCLUDE (title, user_num, reg_date, hit_count, depth)
    WHERE del_date IS NULL;
GO
SELECT TOP (20) num, title FROM Board.POSTS
WHERE board_num = 2 AND del_date IS NULL
ORDER BY group_num DESC, sort_no;
계획
|--Sort(TOP 20, ORDER BY:([POSTS].[group_num] DESC, [POSTS].[sort_no] ASC)) |--Clustered Index Scan(OBJECT:([POSTS].[PK_POSTS]), WHERE:([POSTS].[board_num]=(2) AND [POSTS].[del_date] IS NULL))
읽기 16장. 방금 만든 인덱스를 아예 사용하지 않았습니다.

살아 있는 글만 담은 인덱스를 만들었는데 쓰이지 않습니다. 까닭을 보려면 힌트로 강제해 봅니다.

SQL
SELECT TOP (20) num, title FROM Board.POSTS WITH (INDEX(IX_POSTS_live))
WHERE board_num = 2 AND del_date IS NULL
ORDER BY group_num DESC, sort_no;
계획
|--Top(TOP EXPRESSION:((20))) |--Nested Loops(Inner Join, OUTER REFERENCES:([POSTS].[num])) |--Index Seek(OBJECT:([POSTS].[IX_POSTS_live]), ...) |--Clustered Index Seek(OBJECT:([POSTS].[PK_POSTS]), WHERE:([POSTS].[del_date] IS NULL) LOOKUP ORDERED FORWARD)
읽기 42장. 키 조회가 붙어 표 전체를 읽는 것보다 비쌉니다.

인덱스가 이미 살아 있는 글만 담고 있는데도 표로 되돌아갑니다. del_date 를 인덱스가 들고 있지 않아 IS NULL 을 확인할 데가 없기 때문입니다. 그 키 조회 때문에 42장이 들고, 그래서 옵티마이저가 16장짜리 스캔을 고른 것입니다. 필터에 사용한 열을 포함 열에 넣으면 사라집니다.

SQL
DROP INDEX IX_POSTS_live ON Board.POSTS;
GO
CREATE NONCLUSTERED INDEX IX_POSTS_live
    ON Board.POSTS (board_num, group_num DESC, sort_no)
    INCLUDE (title, user_num, reg_date, hit_count, depth, del_date)   -- 필터 열을 더합니다
    WHERE del_date IS NULL;
결과 — 같은 문장을 다시
인덱스논리적 읽기계획
필터 열이 없을 때16Sort + Clustered Index Scan
필터 열이 없을 때(힌트로 강제)42Index Seek + LOOKUP
필터 열을 넣었을 때2Top + Index Seek
인덱스 크기
인덱스페이지담은 행
IX_POSTS_list (전부)14507
IX_POSTS_live (살아 있는 것만)5406
지운 글 101건이 인덱스에서 빠졌습니다. 지운 글이 쌓일수록 차이가 벌어집니다.

필터 인덱스를 만들 때는 필터에 사용한 열을 포함 열에 함께 넣으십시오. 3.4 와 3.5 가 만나는 자리이고, 넣지 않아도 오류가 나지 않아 알아채기 어렵습니다.

표시만 남기면 계층이 지켜집니다

SQL
UPDATE Board.POSTS SET del_date = '2026-03-01' WHERE num = 6;

SELECT num, parent_num, depth, sort_no, del_date, LEFT(title, 20) AS 제목
FROM Board.POSTS WHERE group_num = 6 ORDER BY sort_no;
결과
numparent_numdepthsort_nodel_date제목
6NULL002026-03-01저장 프로시저 질문드립니다
352611NULL[답글] 저장 프로시저…
45635222NULL[답글] [답글] 저장…
원글에 표시가 붙었고 답글은 그대로 남았습니다. 3.9 의 정렬 열도 흐트러지지 않습니다.

화면에서는 지운 원글을 "삭제된 글입니다" 로 그리고 답글은 그대로 보입니다. 진짜로 지웠다면 답글까지 사라지거나 547 에 막혔을 것입니다.

살아 있는 글만 보는 뷰

모든 화면에서 del_date IS NULL 을 적는 것은 빠뜨리기 쉽습니다. 한곳에 적어 두고 이름을 붙입니다.

SQL
CREATE VIEW Board.V_LIVE_POSTS AS
SELECT num, board_num, category_num, user_num, parent_num, group_num, depth, sort_no,
       title, content, hit_count, reg_date, mod_date
FROM Board.POSTS WHERE del_date IS NULL;
결과
보는 것행수
Board.POSTS507
Board.V_LIVE_POSTS405
관리자 화면은 표를 직접 보고, 나머지는 뷰를 봅니다.

del_date 를 뷰에서 뺀 것은 3.7 의 열 감추기와 같은 뜻입니다. 이 뷰를 보는 쪽은 지운 글이 있다는 사실 자체를 알 필요가 없습니다. SELECT * 를 적지 않은 까닭도 3.7 에 있습니다.

검증

세 요구가 모두 되는지 확인합니다

SQL
-- 태그별로 살아 있는 글이 몇 건인지
SELECT TOP (5) t.name AS 태그, COUNT(*) AS 글수
FROM Board.POST_TAGS pt
    JOIN Board.TAGS t ON t.num = pt.tag_num
    JOIN Board.V_LIVE_POSTS p ON p.num = pt.post_num
GROUP BY t.name ORDER BY 글수 DESC, 태그;

-- 좋아요가 많은 글
SELECT TOP (5) p.num, LEFT(p.title, 26) AS 제목, COUNT(l.user_num) AS 좋아요
FROM Board.V_LIVE_POSTS p JOIN Board.POST_LIKES l ON l.post_num = p.num
GROUP BY p.num, p.title ORDER BY 좋아요 DESC, p.num;

-- 글을 지운 것으로 표시해도 태그와 좋아요는 남는지
UPDATE Board.POSTS SET del_date = SYSDATETIME() WHERE num = 100;
SELECT COUNT(*) AS 태그 FROM Board.POST_TAGS WHERE post_num = 100;
SELECT COUNT(*) AS 좋아요 FROM Board.POST_LIKES WHERE post_num = 100;
결과 — 태그별 살아 있는 글
태그글수
입문103
조인102
SQL101
SQL서버35
백업34
결과 — 좋아요가 많은 글
num제목좋아요
4백업 질문드립니다5
9뷰 질문드립니다5
14윈도 함수 질문드립니다5
결과 — 지운 것으로 표시한 뒤
보는 것표시 전표시 후
100번 글의 태그22
100번 글의 좋아요11
글은 목록에서 빠졌지만 딸린 자료는 그대로입니다. 되살리면 그대로 돌아옵니다.

앞의 태그별 글 수가 128 · 128 · 128 이었는데 지금은 103 · 102 · 101 입니다. 지운 것으로 표시한 글 102건이 빠졌기 때문입니다. 뷰를 조인에 넣은 것만으로 모든 집계가 살아 있는 글 기준이 되었습니다.

진짜로 지우는 일은 남습니다. 표시만 남기면 자료가 계속 쌓이므로, 일정 기간이 지난 것을 골라 실제로 지우는 작업이 따로 필요합니다. 그때는 답글부터 지워야 하고 트랜잭션으로 묶어야 합니다 — 6.6 과 5.7 의 주제입니다.

연습

직접 해보기

1. 태그가 하나도 없는 글 난이도 하

살아 있는 글 가운데 태그가 하나도 붙지 않은 것이 몇 건인지 세어 보세요. 두 가지 방법으로 적고 읽은 페이지를 비교해 보십시오.

-- (가) NOT EXISTS SELECT COUNT(*) AS 태그없는글 FROM Board.V_LIVE_POSTS p WHERE NOT EXISTS (SELECT 1 FROM Board.POST_TAGS pt WHERE pt.post_num = p.num); -- (나) LEFT JOIN 뒤 IS NULL SELECT COUNT(*) AS 태그없는글 FROM Board.V_LIVE_POSTS p LEFT JOIN Board.POST_TAGS pt ON pt.post_num = p.num WHERE pt.post_num IS NULL; -- 둘 다 166 건이고 읽은 페이지도 같습니다(POSTS 16 · POST_TAGS 4). -- 엔진이 같은 계획으로 바꾸기 때문입니다. -- NOT IN 은 사용하지 마십시오. 안쪽에 NULL 이 하나라도 있으면 결과가 비어 버립니다(2.4). -- 태그를 붙이지 않은 글이 300번 이후에 몰려 있습니다. -- 실습 자료에서 태그를 300건에만 붙였기 때문입니다.
2. 신고 기능을 설계합니다 난이도 중

요구는 이렇습니다. 회원이 글을 신고합니다. 같은 글을 두 번 신고할 수는 없습니다. 사유를 적되 너무 짧으면 받지 않습니다. 관리자가 처리했는지 남기고, 아직 처리하지 않은 것을 빠르게 찾아야 합니다. 표를 설계해 보세요.

"두 번 신고할 수 없다"는 키로, "너무 짧으면"은 제약으로, "아직 처리하지 않은 것"은 인덱스로 풉니다. 앞의 세 요구와 같은 도구입니다.
CREATE TABLE Board.POST_REPORTS ( post_num int NOT NULL, user_num int NOT NULL, reason nvarchar(500) NOT NULL, reg_date datetime2(0) NOT NULL CONSTRAINT DF_POST_REPORTS_reg_date DEFAULT SYSDATETIME(), done_date datetime2(0) NULL, -- 처리 전이면 NULL CONSTRAINT PK_POST_REPORTS PRIMARY KEY CLUSTERED (post_num, user_num), CONSTRAINT FK_POST_REPORTS_POSTS FOREIGN KEY (post_num) REFERENCES Board.POSTS (num) ON DELETE CASCADE, CONSTRAINT FK_POST_REPORTS_USERS FOREIGN KEY (user_num) REFERENCES Member.USERS (num), CONSTRAINT CK_POST_REPORTS_reason CHECK (LEN(reason) >= 5) ); GO -- 처리하지 않은 것만 담습니다. 처리한 신고는 쌓이기만 하고 찾지 않습니다. CREATE NONCLUSTERED INDEX IX_POST_REPORTS_todo ON Board.POST_REPORTS (reg_date) INCLUDE (post_num, user_num, done_date) -- 필터 열을 넣습니다 WHERE done_date IS NULL; -- 확인 INSERT INTO Board.POST_REPORTS (post_num, user_num, reason) VALUES (1, 3, N'광고성 글입니다'); INSERT INTO Board.POST_REPORTS (post_num, user_num, reason) VALUES (1, 3, N'또 신고합니다'); -- 메시지 2627 Violation of PRIMARY KEY constraint 'PK_POST_REPORTS'. INSERT INTO Board.POST_REPORTS (post_num, user_num, reason) VALUES (1, 4, N'싫음'); -- 메시지 547 conflicted with the CHECK constraint "CK_POST_REPORTS_reason" -- 정한 것 -- 복합 기본 키 두 번 신고를 키가 막습니다(3.2 · 3.4) -- CHECK 사유 길이를 표가 지킵니다(3.4) -- done_date bit 가 아니라 시각입니다. 언제 처리했는지도 남습니다 -- 필터 인덱스 처리 안 한 것만 담아 작게 유지합니다(3.4 · 3.5) DROP TABLE Board.POST_REPORTS;

이 단원에서 만든 것을 지웁니다.
DROP VIEW Board.V_LIVE_POSTS;
DROP TABLE Board.POST_LIKES, Board.POST_TAGS, Board.TAGS;
DROP INDEX IX_POSTS_live ON Board.POSTS;
ALTER TABLE Board.POSTS DROP COLUMN del_date;
ALTER INDEX PK_POSTS ON Board.POSTS REBUILD;

3부를 맺으며

설계는 무엇을 비싸게 할지 정하는 일입니다

이 단원에서 내린 판단을 다시 늘어놓으면, 하나같이 무엇을 얻고 무엇을 내줄지 고른 것이었습니다.

정한 것얻은 것내준 것
태그를 표로 나눔정확한 검색과 집계조인 하나
연결 표에 복합 키중복 방지가 공짜키가 길어짐
반대 방향 인덱스태그로 글 찾기 2장쓰기 한 번 더
좋아요 수를 담지 않음틀릴 일이 없음매번 세기(2장)
지우지 않고 표시계층과 딸린 자료 보존자료가 쌓임
필터 인덱스인덱스가 작아짐(14 → 5)필터 열을 넣어야 함

공짜로 얻은 것은 하나도 없습니다. 3부의 아홉 과가 알려 준 것은 "이렇게 하십시오" 가 아니라 무엇을 재고 무엇과 견줄지였습니다. 정규화도 인덱스도 뷰도 마찬가지입니다.

여기까지가 무엇을 담을지입니다. 4부에서는 그 위에서 무엇을 할지를 적습니다. 이 단원에서 손으로 이어 붙인 절차 — 답글의 차례를 밀고 넣기, 신고를 받고 처리 표시하기 — 를 프로시저 하나에 담고, 중간에 끊겨도 어긋나지 않게 만드는 일입니다.

요약
  • 한 칸에 값을 여러 개 넣지 마십시오. 검색이 부정확해지고(SQL 이 SQL서버를 잡음), 집계가 되지 않으며, 이름을 고칠 수 없습니다.
  • 다대다는 연결 표로 풉니다. 연결 표에는 대리 키를 두지 말고 복합 기본 키를 쓰십시오 — 그것이 곧 중복 방지입니다(2627).
  • 연결 표는 양쪽 방향 인덱스가 필요합니다. 열이 둘뿐이라 반대 방향 인덱스도 그대로 커버링이 됩니다.
  • CASCADE한쪽에만 겁니다. 글이 사라지면 연결도 뜻이 없지만, 태그를 지웠다고 연결이 사라지면 안 됩니다.
  • 미리 담아 두기 전에 재 보십시오. 좋아요 수는 이미 2장에 나오므로 인덱싱된 뷰를 만들 이유가 없었습니다.
  • 답글이 달린 글은 애초에 지울 수 없습니다(547). 표시만 남기는 설계는 취향이 아니라 구조가 요구하는 것입니다.
  • 상태는 bit 보다 시각으로 담으십시오. NULL 이 곧 "아직" 이고, 언제였는지가 함께 남습니다.
  • 필터 인덱스에는 필터에 사용한 열을 포함 열로 넣으십시오. 넣지 않으면 키 조회가 붙어 옵티마이저가 그 인덱스를 버립니다(16장). 넣으면 2장입니다.
  • 조건을 뷰에 한 번만 적어 모든 화면이 같은 기준을 보게 합니다.