MSSQL LAB
MSSQL 3.8 · 3부. 설계

스키마와 명명 규약

테이블을 영역으로 나누는 기준과 이름 규칙을 정해 두는 까닭입니다. 스키마를 빼고 적은 이름이 무엇을 여는지 봅니다.

예상 학습 시간 16분 난이도 중급
개념 설명

표를 담는 영역

실습 데이터베이스의 표는 Board.POSTS 처럼 점이 든 이름을 가지고 있습니다. 앞쪽이 스키마입니다. 데이터베이스 안을 다시 나누는 영역이고, 표·뷰·프로시저가 그 안에 들어갑니다.

SQL
SELECT s.name AS 스키마, COUNT(o.object_id) AS 객체수
FROM sys.schemas s
    LEFT JOIN sys.objects o ON o.schema_id = s.schema_id AND o.is_ms_shipped = 0
WHERE s.name NOT IN ('sys', 'INFORMATION_SCHEMA')
GROUP BY s.name
HAVING COUNT(o.object_id) > 0;
결과
스키마객체수
Board27
Member3
표만이 아니라 기본 키·외래 키·기본값 제약도 객체로 셉니다.

lab-setup.sql 이 게시판 쪽 다섯 개를 Board 에, 회원 하나를 Member 에 두었습니다. 나누지 않아도 돌아갑니다. 나눠 두면 얻는 것이 셋입니다.

  • 이름이 겹쳐도 됩니다. 게시판 기록과 회원 기록을 둘 다 LOGS 라고 부를 수 있습니다.
  • 권한을 영역째로 줍니다. 표마다 주지 않아도 됩니다.
  • 이름이 짧아집니다. BOARD_POSTS 처럼 표 이름에 영역을 적지 않아도 어디 것인지 드러납니다.
SQL
-- 같은 이름을 두 스키마에 둘 수 있습니다.
CREATE TABLE Board.LOGS  (num int IDENTITY PRIMARY KEY, msg nvarchar(100));
CREATE TABLE Member.LOGS (num int IDENTITY PRIMARY KEY, msg nvarchar(100));

SELECT s.name + '.' + t.name ASFROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE t.name = 'LOGS';
결과
Member.LOGS
Board.LOGS
이름이 같은 표 둘이 한 데이터베이스에 있습니다. 스키마가 다르므로 부딪히지 않습니다.
이름 해석

스키마를 빼고 적으면 어디로 갑니까

앞의 표를 스키마 없이 불러 봅니다.

SQL
SELECT COUNT(*) FROM POSTS;
오류
메시지 208 Invalid object name 'POSTS'.
표가 있는데도 없다고 합니다.

스키마를 적지 않으면 엔진이 내 기본 스키마에서 찾고, 없으면 dbo 에서 찾습니다. 지금 접속한 계정의 기본 스키마는 dbo 이고 거기에는 POSTS 가 없으므로 208 입니다.

오류로 끝나면 다행입니다. 기본 스키마가 다른 두 사람이 같은 문장을 실행하면 서로 다른 표가 열립니다. 사용자를 둘 만들어 확인합니다.

SQL
CREATE USER board_user  WITHOUT LOGIN WITH DEFAULT_SCHEMA = Board;
CREATE USER member_user WITHOUT LOGIN WITH DEFAULT_SCHEMA = Member;
GRANT SELECT, INSERT ON SCHEMA::Board  TO board_user;
GRANT SELECT, INSERT ON SCHEMA::Member TO member_user;
GO

-- 두 사람이 글자 하나 다르지 않은 같은 문장을 실행합니다.
EXECUTE AS USER = 'board_user';
INSERT INTO LOGS (msg) VALUES (N'board_user 가 넣음');
REVERT;
GO
EXECUTE AS USER = 'member_user';
INSERT INTO LOGS (msg) VALUES (N'member_user 가 넣음');
REVERT;
GO

SELECT 'Board.LOGS' AS 표, msg FROM Board.LOGS
UNION ALL
SELECT 'Member.LOGS', msg FROM Member.LOGS;
결과
msg
Board.LOGSboard_user 가 넣음
Member.LOGSmember_user 가 넣음
같은 문장이 다른 표로 갔습니다. 오류도 경고도 없습니다.

그래서 이름은 늘 두 부분으로 적습니다. Board.POSTS 이지 POSTS 가 아닙니다. 3.7 에서 WITH SCHEMABINDING 뷰가 두 부분 이름을 요구한 것도 이 까닭이고, 4부의 저장 프로시저에서도 같습니다.

네 부분 이름도 있습니다. 서버.데이터베이스.스키마.객체 입니다. 다만 문장에 데이터베이스 이름을 박아 두면 개발·운영에서 이름이 다를 때 그대로 옮길 수 없습니다. 같은 데이터베이스 안이라면 두 부분으로 적는 것이 옮기기 좋습니다.

권한

영역째로 줄 수 있습니다

앞의 예제에서 GRANT SELECT ON SCHEMA::Board 라고 적었습니다. 표를 하나씩 적지 않았습니다. 이 권한은 나중에 만드는 표에도 적용됩니다.

SQL
CREATE USER reader WITHOUT LOGIN WITH DEFAULT_SCHEMA = Board;
GRANT SELECT ON SCHEMA::Board TO reader;
GO

-- 권한을 준 뒤에 표를 만듭니다.
CREATE TABLE Board.T_LATER (num int IDENTITY PRIMARY KEY, msg nvarchar(50));
INSERT INTO Board.T_LATER (msg) VALUES (N'나중에 만든 표');
GO

EXECUTE AS USER = 'reader';
SELECT msg AS 읽힘 FROM Board.T_LATER;   -- 권한을 따로 주지 않았습니다
SELECT COUNT(*) FROM Member.USERS;         -- 다른 스키마입니다
REVERT;
결과 — Board 안의 새 표
읽힘
나중에 만든 표
오류 — 다른 스키마
메시지 229 The SELECT permission was denied on the object 'USERS', database 'MssqlLab', schema 'Member'.

이것이 스키마로 나누는 가장 실질적인 이유입니다. 표를 더할 때마다 권한 스크립트를 고치지 않아도 됩니다. 반대로 말하면 스키마를 잘못 잡으면 권한도 함께 어긋납니다. 회원 정보를 Board 에 두었다면 게시판을 읽을 수 있는 모두가 회원 정보도 읽게 됩니다.

그래서 나누는 기준은 업무 영역이 아니라 권한 경계로 잡습니다. "누구에게 통째로 열어 줄 수 있는가" 가 같은 것끼리 묶으십시오. 권한 자체는 6.1 에서 다룹니다.

옮기기

나중에 옮길 수 있습니다

처음부터 완벽하게 나눌 필요는 없습니다. ALTER SCHEMA … TRANSFER 로 옮깁니다.

SQL
CREATE TABLE Board.T_MOVE (
    num int IDENTITY PRIMARY KEY,
    post_num int NOT NULL,
    CONSTRAINT FK_T_MOVE_POSTS FOREIGN KEY (post_num) REFERENCES Board.POSTS(num));
CREATE NONCLUSTERED INDEX IX_T_MOVE_post ON Board.T_MOVE (post_num);
GO

ALTER SCHEMA Member TRANSFER Board.T_MOVE;
GO

SELECT s.name + '.' + t.name AS 표,
       (SELECT COUNT(*) FROM sys.foreign_keys f WHERE f.parent_object_id = t.object_id) AS 외래키,
       (SELECT COUNT(*) FROM sys.indexes i WHERE i.object_id = t.object_id AND i.name IS NOT NULL) AS 인덱스
FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE t.name = 'T_MOVE';
결과
외래키인덱스
Member.T_MOVE12
외래 키도 인덱스도 따라왔습니다. 다른 스키마의 표를 가리키는 외래 키도 그대로입니다.

따라오지 않는 것이 둘 있습니다. 하나는 그 표에 직접 준 권한입니다. 옮기면 지워집니다.

SQL
GRANT SELECT ON OBJECT::Board.T_PERM TO p_user;
GO
ALTER SCHEMA Member TRANSFER Board.T_PERM;
GO
SELECT COUNT(*) AS 남은권한수 FROM sys.database_permissions
WHERE major_id = OBJECT_ID('Member.T_PERM');
결과
잰 때권한 수
옮기기 전1
옮긴 뒤0
옮긴 표를 읽으려 하면 229 로 막힙니다. 옮긴 뒤 권한을 다시 주어야 합니다.

다른 하나는 이름입니다. 제약과 인덱스의 이름은 그대로 남습니다. 위에서도 FK_T_MOVE_POSTS 라는 이름이 남았습니다. 이름에 스키마를 적어 두었다면 옮긴 뒤 어긋납니다.

스키마를 지우려면 안이 비어 있어야 합니다. 표가 남아 있으면 메시지 3729 — Cannot drop schema 'Board' because it is being referenced by object 'BOARDS' 로 막힙니다. 먼저 옮기거나 지우십시오.

SQL
-- 여기까지 만든 것을 지웁니다. 다음 절의 세기가 어긋나지 않게 합니다.
DROP TABLE Board.LOGS, Member.LOGS, Board.T_LATER;
DROP USER board_user;
DROP USER member_user;
DROP USER reader;
명명 규약

이름 규칙을 정해 두는 까닭

이름을 어떻게 지을지는 정답이 없습니다. 그런데 정해 두지 않으면 값을 치릅니다. 3.4 에서 제약에 이름을 주지 않았더니 CK__T_NM__cnt__7FB5F314 처럼 서버마다 다른 이름이 붙어 배포 스크립트가 막혔습니다. 규칙은 그런 자리를 없애려고 둡니다.

실습 데이터베이스가 무엇을 따르고 있는지 세어 봅니다.

SQL
SELECT LEFT(o.name, CHARINDEX('_', o.name + '_') - 1) AS 접두사,
       o.type_desc AS 종류, COUNT(*) AS 개수
FROM sys.objects o
WHERE o.is_ms_shipped = 0 AND o.type IN ('PK', 'UQ', 'F', 'C', 'D')
GROUP BY LEFT(o.name, CHARINDEX('_', o.name + '_') - 1), o.type_desc
ORDER BY o.type_desc;
결과
접두사종류개수보기
CKCHECK_CONSTRAINT2CK_FILES_size
DFDEFAULT_CONSTRAINT5DF_POSTS_hit_count
FKFOREIGN_KEY_CONSTRAINT8FK_POSTS_USERS
PKPRIMARY_KEY_CONSTRAINT6PK_POSTS
UXUNIQUE_CONSTRAINT3UX_USERS_user_id
인덱스 넷은 따로 세면 모두 IX 로 시작합니다. 스물여덟 개가 예외 없이 규칙을 따릅니다.

이름만 보고 무엇인지 알 수 있고, 이름을 알면 어디에 붙어 있는지도 압니다. FK_POSTS_USERSPOSTS 에서 USERS 를 가리키는 외래 키입니다. 오류 메시지에 이 이름이 나오면 어느 표를 볼지 바로 정해집니다.

실습 데이터베이스가 따르는 규칙

대상규칙보기
스키마파스칼Board · Member
대문자 · 복수POSTS · COMMENTS · FILES
소문자 · 밑줄board_num · hit_count · reg_date
기본 키PK_표PK_POSTS
외래 키FK_자식_부모FK_COMMENTS_POSTS
인덱스IX_표_용도IX_POSTS_list
고유UX_표_열UX_USERS_user_id
V_이름V_POST_LIST

여기에 정답은 없습니다. 표를 단수로 두는 곳도 많고, 열을 파스칼로 적는 곳도 많습니다. 중요한 것은 하나를 정하고 지키는 것입니다. 같은 데이터베이스 안에서 POSTSComment 가 섞이면, 이름을 적을 때마다 무엇이었는지 확인해야 합니다.

규칙에 형식은 넣지 마십시오. str_title · int_num 같은 이름은 형식을 바꾸는 순간 거짓말이 됩니다. 형식은 sys.columns 가 이미 정확히 알고 있습니다.

함정

대소문자와 예약어

이름의 대소문자는 무시됩니다

정확히 말하면 데이터베이스 대조를 따릅니다. 실습 데이터베이스는 SQL_Latin1_General_CP1_CI_AS 이고 CI 가 대소문자를 구분하지 않는다는 뜻입니다.

SQL
SELECT COUNT(*) AS 글수 FROM board.posts;      -- 전부 소문자로 적어도 됩니다

-- 값을 비교할 때도 마찬가지입니다.
SELECT user_id, nickname FROM Member.USERS WHERE user_id = 'HONG';
결과
user_idnickname
hong홍길동
저장된 값은 hong 인데 HONG 으로 찾아졌습니다.

이름을 아무렇게나 적어도 되는 것이 아니라, 틀리게 적어도 오류가 나지 않는다는 뜻입니다. 규칙을 정해도 강제되지 않으므로 사람이 지켜야 합니다. 그리고 대조가 CS 인 서버로 옮기면 그때 비로소 전부 깨집니다.

예약어를 이름으로 사용하지 마십시오

SQL
CREATE TABLE Board.USER (num int);      -- USER 는 예약어입니다
GO
CREATE TABLE Board.[USER] (num int);    -- 대괄호로 감싸면 만들어집니다
GO
DROP TABLE Board.[USER];
오류 — 대괄호 없이
메시지 156 Incorrect syntax near the keyword 'USER'.
대괄호를 씌우면 만들어집니다. 다만 이후 모든 문장에서 계속 씌워야 합니다.

실습 데이터베이스가 회원 표를 USERS 로 둔 것은 이 까닭입니다. 복수로 적는 규칙이 예약어를 피하는 데도 도움이 됩니다USER · KEY · ORDER 는 예약어지만 USERS · KEYS · ORDERS 는 아닙니다.

연습

직접 해보기

1. 쪽지 표를 규칙대로 만듭니다 난이도 하

회원끼리 주고받는 쪽지를 담을 표를 만들어 보세요. 보낸 사람과 받은 사람, 내용, 읽은 시각, 보낸 시각이 필요합니다. 어느 스키마에 둘지 정하고, 제약과 인덱스 이름을 실습 데이터베이스의 규칙에 맞추십시오.

-- 회원끼리 주고받는 것이므로 Member 입니다. 게시판을 읽는 사람에게 -- 남의 쪽지까지 열어 줄 이유가 없습니다. CREATE TABLE Member.MESSAGES ( num int IDENTITY(1,1) NOT NULL, from_num int NOT NULL, to_num int NOT NULL, content nvarchar(1000) NOT NULL, read_date datetime2(0) NULL, -- 안 읽었으면 NULL (1.9) reg_date datetime2(0) NOT NULL CONSTRAINT DF_MESSAGES_reg_date DEFAULT SYSDATETIME(), CONSTRAINT PK_MESSAGES PRIMARY KEY CLUSTERED (num), CONSTRAINT FK_MESSAGES_FROM FOREIGN KEY (from_num) REFERENCES Member.USERS (num), CONSTRAINT FK_MESSAGES_TO FOREIGN KEY (to_num) REFERENCES Member.USERS (num), CONSTRAINT CK_MESSAGES_self CHECK (from_num <> to_num) ); GO -- 받은 쪽지함이 최신순이므로 이 순서로 둡니다(3.5). CREATE NONCLUSTERED INDEX IX_MESSAGES_to ON Member.MESSAGES (to_num, reg_date DESC); -- 이름이 붙은 것을 확인합니다. -- CK_MESSAGES_self · DF_MESSAGES_reg_date · FK_MESSAGES_FROM -- FK_MESSAGES_TO · PK_MESSAGES · IX_MESSAGES_to -- 표 하나를 두 번 가리키므로 FK 이름을 FK_MESSAGES_USERS 로 둘 수 없습니다. -- 이럴 때는 열 쪽을 이름에 넣습니다. DROP TABLE Member.MESSAGES;
2. 옮긴 표를 다시 읽게 합니다 난이도 중

Board 에 있던 표를 Member 로 옮겼더니 읽던 사람이 229 로 막힙니다. 무슨 일이 일어난 것이고, 무엇을 해야 합니까.

권한은 객체에 붙어 있었습니까, 스키마에 붙어 있었습니까.
-- 옮기면서 그 표에 직접 걸려 있던 권한이 지워졌습니다. SELECT COUNT(*) FROM sys.database_permissions WHERE major_id = OBJECT_ID('Member.T_PERM'); -- 0 -- 고르는 길이 둘입니다. -- (가) 그 사람에게 표 권한을 다시 줍니다. GRANT SELECT ON OBJECT::Member.T_PERM TO p_user; -- (나) Member 스키마 권한을 줍니다. 앞으로 Member 에 만드는 표까지 함께입니다. GRANT SELECT ON SCHEMA::Member TO p_user; -- (나)가 편하지만 그만큼 넓습니다. 회원 정보가 든 스키마라면 -- 정말 통째로 열어도 되는지 먼저 따지십시오. -- 옮기기 전에 "이 표를 누가 읽고 있었는가" 를 적어 두는 편이 안전합니다.

이 단원이 만든 것은 예제 안에서 그때그때 지웠습니다. 남아 있는 것이 없는지 확인하려면 아래를 실행하십시오. 실습 데이터베이스의 여섯 표만 나와야 합니다.
SELECT s.name + '.' + t.name FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id;

요약
  • 스키마는 데이터베이스 안을 나누는 영역입니다. 같은 이름의 표를 여러 스키마에 둘 수 있습니다.
  • 이름은 늘 Board.POSTS 처럼 두 부분으로 적으십시오. 스키마를 빼면 실행하는 사람의 기본 스키마에서 찾습니다. 없으면 208 이고, 있으면 사람마다 다른 표가 열립니다.
  • GRANT … ON SCHEMA::나중에 만드는 표에도 적용됩니다. 스키마를 나누는 기준은 업무 영역이 아니라 권한 경계입니다.
  • ALTER SCHEMA … TRANSFER 로 옮기면 외래 키와 인덱스는 따라오지만 그 표에 직접 준 권한은 지워집니다. 제약 이름도 그대로 남습니다.
  • 이름 규칙은 정답이 없고 하나를 정해 지키는 것이 중요합니다. 실습 데이터베이스는 PK_ · FK_자식_부모 · IX_표_용도 · UX_ · CK_ · DF_ 를 예외 없이 따릅니다.
  • 이름에 형식을 넣지 마십시오. 형식을 바꾸면 이름이 거짓말이 됩니다.
  • 대조가 CI 라 이름의 대소문자는 무시됩니다. 틀리게 적어도 오류가 나지 않을 뿐이고, CS 서버로 옮기면 그때 깨집니다.
  • USER · KEY · ORDER 는 예약어입니다. 복수로 적으면 피해 갑니다.