MSSQL LAB
MSSQL 6.3 · 6부. 운영과 실전

스키마 변경과 배포

운영 중인 테이블을 고치는 순서와 되돌릴 수 있게 두는 방법입니다.

예상 학습 시간 22분 난이도 고급
준비

50만 행짜리 사본에 걸어 봅니다

스키마 변경은 표가 클수록 값이 커집니다. 507행에서는 무엇을 해도 순식간이라 차이가 드러나지 않습니다.

SQL
SELECT num, board_num, user_num, title, hit_count, reg_date
INTO Board.T_ALTER FROM Board.POSTS_BIG;

ALTER TABLE Board.T_ALTER ADD CONSTRAINT PK_T_ALTER PRIMARY KEY CLUSTERED (num);
GO
개념 설명

고치는 값이 둘로 갈립니다

어떤 변경은 목록만 고치고 끝나고, 어떤 변경은 50만 행을 전부 다시 씁니다. 문법은 비슷하게 생겼는데 값이 수백 배 다릅니다.

SQL
DECLARE @t datetime2(7) = SYSDATETIME();
ALTER TABLE Board.T_ALTER ADD memo1 nvarchar(100) NULL;
PRINT CONCAT(DATEDIFF(millisecond, @t, SYSDATETIME()), ' ms');
결과 — 50만 행에서
변경걸린 시간무엇을 합니까
ADD 열 (NULL 허용)1 밀리초목록에 이름만 적습니다
ALTER COLUMN 길이 늘리기1 밀리초목록만 고칩니다
DROP COLUMN2 밀리초목록에서 빼기만 합니다
sp_rename 열 이름179 밀리초이름만 바꿉니다
ADD 열 (NOT NULL + 기본값)260~420 밀리초모든 행에 값을 채웁니다
ALTER COLUMN 길이 줄이기393 밀리초모든 행을 검사합니다
ALTER COLUMN int → bigint429 밀리초모든 행을 다시 씁니다
위 넷과 아래 셋이 다릅니다. 50만 행이라 400밀리초지만, 5천만 행이면 40초입니다.

DROP COLUMN 이 2밀리초인 것에 주의하십시오. 열을 목록에서 뺐을 뿐 자리는 그대로 남아 있습니다. 공간을 실제로 돌려받으려면 인덱스를 다시 만들어야 합니다 (ALTER INDEX … REBUILD).

기본값 없는 NOT NULL 은 아예 막힙니다

SQL
ALTER TABLE Board.T_ALTER ADD cnt2 int NOT NULL;
결과
메시지 4901, 수준 16, 상태 1 ALTER TABLE only allows columns to be added that can contain nulls, or have a DEFAULT definition specified, … Column 'cnt2' cannot be added to non-empty table 'T_ALTER'.
이미 있는 50만 행에 무엇을 넣을지 알 수 없기 때문입니다.

기본값을 붙이거나, NULL 허용으로 넣고 채운 뒤 NOT NULL 로 바꿉니다. 뒤쪽이 세 문장이지만 각각이 짧아 잠금을 오래 잡지 않습니다.

SQL
-- 1. NULL 허용으로 넣습니다. 1밀리초입니다.
ALTER TABLE Board.T_ALTER ADD cnt2 int NULL;

-- 2. 배치로 나누어 채웁니다(5.7).
DECLARE @n int = 1;
WHILE @n > 0
BEGIN
    UPDATE TOP (3000) Board.T_ALTER SET cnt2 = 0 WHERE cnt2 IS NULL;
    SET @n = @@ROWCOUNT;
END

-- 3. 다 찼으면 조인다.
ALTER TABLE Board.T_ALTER ADD CONSTRAINT DF_cnt2 DEFAULT 0 FOR cnt2;
ALTER TABLE Board.T_ALTER ALTER COLUMN cnt2 int NOT NULL;

에디션에 따라 다릅니다. Enterprise 는 기본값 있는 NOT NULL 열 추가를 목록만 고치는 것으로 처리합니다. 위 실측은 Express 라 400밀리초가 나왔습니다. 배포할 서버의 에디션에서 재 보십시오.

잠금

고치는 동안 아무도 못 씁니다

ALTER TABLE스키마 수정 잠금(Sch-M) 을 겁니다. 5.5 에서 본 X 잠금보다 강합니다 — NOLOCK 으로 읽는 것까지 막습니다.

잠금언제막는 것
Sch-S (스키마 안정)문장을 실행하는 동안Sch-M 만 막습니다
Sch-M (스키마 수정)DDL 을 실행하는 동안읽기까지 전부 막습니다

그래서 걸리는 시간이 곧 멈추는 시간입니다. 1밀리초짜리 변경은 아무도 눈치채지 못하지만, 400밀리초짜리는 그동안 들어온 요청이 전부 기다립니다. 5천만 행에서 40초라면 장애로 신고됩니다.

더 위험한 것은 기다리는 쪽입니다. DDL 이 오래 도는 트랜잭션 뒤에서 Sch-M 을 기다리고 있으면, 그 뒤의 모든 조회가 DDL 뒤에 줄을 섭니다. 읽기끼리는 서로 막지 않는데 그 사이에 낀 DDL 하나가 전부를 세웁니다. DDL 은 한산한 시간에, 짧게 끊어서 하십시오.

인덱스는 만드는 동안 표를 잠급니다

SQL
CREATE INDEX IX_ALTER_reg ON Board.T_ALTER (reg_date) WITH (ONLINE = ON);
결과 — Express 에서
메시지 1712, 수준 16, 상태 3 Online index operations can only be performed in Enterprise edition of SQL Server or Azure SQL Edge.
Enterprise 가 아니면 인덱스를 만드는 동안 그 표를 쓸 수 없습니다.

ONLINE = ON 을 쓸 수 없다면 인덱스를 만드는 시간이 곧 멈추는 시간입니다. 5.2 에서 50만 행에 인덱스 하나가 수백 밀리초였으니 감당할 만하지만, 수천만 행이라면 미리 재 보고 시간을 잡으십시오.

Enterprise 라면 RESUMABLE = ON 도 함께 볼 만합니다. 중간에 멈췄다가 이어서 만들 수 있습니다. 작업 시간이 모자랄 때 되돌리지 않고 다음 날 이어 가는 방법입니다.

제약

검사할지 말지를 고를 수 있습니다

SQL
-- 있는 행을 전부 검사합니다.
ALTER TABLE Board.T_ALTER WITH CHECK
    ADD CONSTRAINT CK_ALTER_hit CHECK (hit_count >= 0);

-- 검사하지 않고 붙이기만 합니다. 앞으로 들어오는 것만 봅니다.
ALTER TABLE Board.T_ALTER WITH NOCHECK
    ADD CONSTRAINT CK_ALTER_hit CHECK (hit_count >= 0);
결과
방법걸린 시간is_not_trusted
WITH CHECK62 밀리초0
WITH NOCHECK0 밀리초1
나중에 검사시키기38 밀리초0
NOCHECK 로 붙이면 그 자리에서는 공짜지만 "신뢰하지 않음" 상태로 남습니다.
SQL
-- 한산한 시간에 검사시켜 신뢰 상태로 돌립니다.
ALTER TABLE Board.T_ALTER WITH CHECK CHECK CONSTRAINT CK_ALTER_hit;

-- 신뢰하지 않는 제약 조건을 찾습니다.
SELECT name AS 제약, OBJECT_NAME(parent_object_id) ASFROM sys.check_constraints WHERE is_not_trusted = 1
UNION ALL
SELECT name, OBJECT_NAME(parent_object_id)
FROM sys.foreign_keys WHERE is_not_trusted = 1;

신뢰하지 않는 제약 조건은 최적화에 쓰이지 않습니다. 엔진이 "이 조건이 참이다" 를 전제로 계획을 줄일 수 있는데, 검사하지 않은 제약은 믿을 수 없어 그 전제를 쓰지 못합니다. WITH NOCHECK 로 붙였으면 반드시 나중에 검사시키십시오.

배포

넓히고, 옮기고, 좁힙니다

운영 중인 표를 고칠 때 가장 어려운 것은 문법이 아니라 순서입니다. 코드와 스키마가 함께 바뀌어야 하는데 둘을 같은 순간에 바꿀 수는 없습니다.

그래서 세 번에 나누어 배포합니다. 각 단계 사이에는 옛 코드와 새 코드가 함께 돌아도 괜찮은 상태가 유지됩니다.

단계무엇을그동안
1. 넓히기새 열을 NULL 허용으로 추가옛 코드는 그 열을 모릅니다. 아무 일 없습니다
2. 채우기배치로 값을 채웁니다(5.7)양쪽 다 옛 열을 봅니다
3. 옮기기새 코드를 배포합니다새 코드는 새 열, 옛 코드는 옛 열
4. 좁히기옛 열을 지웁니다옛 코드가 남아 있지 않은 것을 확인한 뒤

열 이름을 바꾸는 일이 가장 흔한 예입니다. sp_rename 한 줄로 바꾸면 그 순간 옛 코드가 전부 깨집니다. 나누면 그런 일이 없습니다.

SQL
-- 배포 1 — 넓힙니다.
ALTER TABLE Board.POSTS ADD view_count int NULL;
GO

-- 두 열을 함께 채우는 트리거를 잠시 둡니다(4.7).
-- 옛 코드가 hit_count 에 쓰면 view_count 도 따라옵니다.
CREATE OR ALTER TRIGGER Board.TR_POSTS_SYNC
ON Board.POSTS AFTER INSERT, UPDATE
AS
BEGIN
    SET NOCOUNT ON;
    IF UPDATE(hit_count)
        UPDATE p SET view_count = i.hit_count
        FROM Board.POSTS p JOIN inserted i ON i.num = p.num;
END
GO

-- 배포 2 — 옛 자료를 채웁니다. 배치로 나눕니다.
DECLARE @n int = 1;
WHILE @n > 0
BEGIN
    UPDATE TOP (3000) Board.POSTS
    SET view_count = hit_count WHERE view_count IS NULL;
    SET @n = @@ROWCOUNT;
END
GO

-- 배포 3 — 새 코드를 올립니다. view_count 를 읽고 씁니다.
-- 이때도 트리거가 양쪽을 맞춰 주므로 되돌릴 수 있습니다.

-- 배포 4 — 며칠 지켜본 뒤 좁힙니다.
DROP TRIGGER Board.TR_POSTS_SYNC;
ALTER TABLE Board.POSTS DROP COLUMN hit_count;

4단계를 서두르지 마십시오. 옛 열을 지우는 순간 되돌릴 수 없습니다. 배포한 코드가 전부 새 열을 보고 있는지 확인하고, 문제가 없다는 것을 며칠 지켜본 뒤에 지웁니다. 그동안 열 하나가 더 있는 것은 값이 싼 쪽입니다.

되돌릴 수 있게 적습니다

이렇게 적으면되돌릴 때
열을 추가지우면 됩니다
열을 지움자료가 이미 없습니다
열 이름을 바꿈다시 바꾸면 되지만 그 사이 코드가 깨집니다
형식을 좁힘잘린 값이 돌아오지 않습니다

더하는 변경은 안전하고 빼는 변경은 위험합니다. 배포 하나에 더하는 것만 담고, 빼는 것은 다음 배포로 미루면 언제든 되돌릴 수 있습니다.

스크립트

몇 번을 돌려도 같아야 합니다

배포 스크립트는 실패해서 다시 돌리는 일이 반드시 생깁니다. 그때 앞부분이 다시 실행되어도 문제가 없어야 합니다.

SQL
-- 열이 없을 때만 추가합니다.
IF NOT EXISTS (SELECT 1 FROM sys.columns
                WHERE object_id = OBJECT_ID('Board.POSTS') AND name = 'view_count')
    ALTER TABLE Board.POSTS ADD view_count int NULL;
GO

-- 인덱스도 마찬가지입니다.
IF NOT EXISTS (SELECT 1 FROM sys.indexes
                WHERE object_id = OBJECT_ID('Board.POSTS') AND name = 'IX_POSTS_view')
    CREATE INDEX IX_POSTS_view ON Board.POSTS (view_count);
GO

-- 프로시저·뷰·함수는 CREATE OR ALTER 하나로 됩니다(4.2).
CREATE OR ALTER PROCEDURE Board.P_POST_LISTGO

-- 표를 지우는 것은 이렇게 적습니다.
DROP TABLE IF EXISTS Board.T_OLD;
GO
지켜야 할 것까닭
몇 번 돌려도 결과가 같게실패 후 다시 돌리는 일이 생깁니다
DDL 과 자료 이동을 나눠서자료 이동이 오래 걸려 DDL 잠금을 잡고 있으면 안 됩니다
긴 것은 배치로한 문장으로 하면 표가 잠깁니다(5.7)
되돌리는 스크립트를 함께되돌릴 방법이 없으면 배포하지 않습니다
시험 환경에서 먼저걸리는 시간을 미리 알아야 시간을 잡습니다

DDL 을 트랜잭션으로 감싸는 것은 신중해야 합니다. SQL Server 는 DDL 도 롤백되지만, 그 트랜잭션이 도는 내내 Sch-M 이 유지 됩니다. 표 다섯 개를 한 트랜잭션에 고치면 다섯 개가 동시에 잠깁니다. 되돌릴 필요가 정말 있는지 따져 보고, 아니면 하나씩 나누는 편이 낫습니다.

연습

직접 해보기

1. 이 변경들의 값을 매겨 보세요 난이도 하

5천만 행짜리 표에 아래를 하려 합니다. 어느 것이 순식간이고 어느 것이 표를 다시 쓰는지 나누고, 위험한 것은 어떻게 나눌지 적어 보세요.

SQL
ALTER TABLE Log.ACCESS ADD ip_v6 varchar(45) NULL;
ALTER TABLE Log.ACCESS ALTER COLUMN url nvarchar(500) NULL;   -- 200 에서
ALTER TABLE Log.ACCESS ADD is_bot bit NOT NULL DEFAULT 0;
ALTER TABLE Log.ACCESS ALTER COLUMN user_num bigint NOT NULL;  -- int 에서
DROP INDEX IX_ACCESS_url ON Log.ACCESS;
-- 순식간에 끝나는 것 (목록만 고칩니다) -- ADD ip_v6 varchar(45) NULL 1밀리초쯤 -- ALTER COLUMN url nvarchar(500) 늘리는 것이라 목록만 -- DROP INDEX 인덱스를 버립니다 -- 표를 다시 쓰는 것 -- ADD is_bot bit NOT NULL DEFAULT 0 Express 면 5천만 행에 값을 채웁니다 -- Enterprise 면 목록만 고칩니다 -- ALTER COLUMN user_num bigint 5천만 행을 전부 다시 씁니다 -- 나누는 방법 -- is_bot (Enterprise 가 아니라면) -- 1. ALTER TABLE Log.ACCESS ADD is_bot bit NULL; -- 1밀리초 -- 2. 배치로 0 을 채웁니다. -- WHILE 문 + UPDATE TOP (3000) … WHERE is_bot IS NULL -- 3. DEFAULT 를 붙이고 NOT NULL 로 조입니다. -- user_num int → bigint -- 이것은 나눌 수 없습니다. 한 문장이 표 전체를 다시 씁니다. -- 5천만 행이면 분 단위이고 그동안 표가 잠깁니다. -- 방법 (가) 서비스를 세울 수 있는 시간에 합니다. -- 방법 (나) 새 열을 두고 옮깁니다. -- ALTER TABLE Log.ACCESS ADD user_num_new bigint NULL; -- 배치로 채우고, 트리거로 양쪽을 맞추고, -- 코드를 배포한 뒤, 옛 열을 지웁니다(확장·축소). -- 먼저 할 것 -- 시험 환경에 같은 규모를 만들어 재 보십시오. -- "몇 초 걸립니다" 를 말할 수 있어야 시간을 잡을 수 있습니다.
2. 배포하다 절반에서 실패했습니다 난이도 중

배포 스크립트가 중간에서 실패했습니다. 다시 돌려도 안전하도록 고쳐 보세요. 어디까지 실행되었는지 모르는 상태입니다.

SQL
ALTER TABLE Board.POSTS ADD view_count int NULL;
GO
UPDATE Board.POSTS SET view_count = hit_count;
GO
CREATE INDEX IX_POSTS_view ON Board.POSTS (view_count);
GO
ALTER TABLE Board.POSTS ALTER COLUMN view_count int NOT NULL;
GO
각 문장이 두 번째 실행에서 어떻게 되는지 하나씩 따져 보십시오. 그리고 UPDATE 는 몇 행을 한 번에 건드립니까.
-- 지금 스크립트의 문제 -- 1행: 이미 있으면 오류 2705 로 멈춥니다. -- 2행: 다시 돌아도 결과는 같지만 표 전체를 잠급니다(5.7). -- 3행: 이미 있으면 오류 1913 으로 멈춥니다. -- 4행: NULL 이 하나라도 있으면 오류 515 로 멈춥니다. -- 인덱스가 걸린 열이라면 4922 로 먼저 막힙니다. -- 고친 것 IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('Board.POSTS') AND name = 'view_count') ALTER TABLE Board.POSTS ADD view_count int NULL; GO -- 배치로 채웁니다. 아직 안 채워진 것만 보므로 다시 돌려도 안전합니다. DECLARE @n int = 1, @loop int = 0; WHILE @n > 0 AND @loop < 10000 BEGIN UPDATE TOP (3000) Board.POSTS SET view_count = hit_count WHERE view_count IS NULL; SET @n = @@ROWCOUNT; SET @loop += 1; IF @n > 0 WAITFOR DELAY '00:00:00.100'; END GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID('Board.POSTS') AND name = 'IX_POSTS_view') CREATE INDEX IX_POSTS_view ON Board.POSTS (view_count); GO -- 남은 NULL 이 없을 때만 조입니다. IF NOT EXISTS (SELECT 1 FROM Board.POSTS WHERE view_count IS NULL) AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('Board.POSTS') AND name = 'view_count' AND is_nullable = 1) BEGIN ALTER TABLE Board.POSTS ADD CONSTRAINT DF_POSTS_view DEFAULT 0 FOR view_count; ALTER TABLE Board.POSTS ALTER COLUMN view_count int NOT NULL; END GO -- 되돌리는 스크립트도 함께 둡니다. -- DROP INDEX IX_POSTS_view ON Board.POSTS; -- ALTER TABLE Board.POSTS DROP CONSTRAINT DF_POSTS_view; -- ALTER TABLE Board.POSTS DROP COLUMN view_count; -- 3단계 배포라면 여기까지가 "넓히기" 이고 -- 코드를 배포한 뒤 다음 배포에서 hit_count 를 지웁니다.

실습에서 만든 사본을 지웁니다.
DROP TABLE Board.T_ALTER;

요약
  • 스키마 변경은 목록만 고치는 것과 표를 다시 쓰는 것으로 갈립니다. 50만 행에서 1밀리초와 400밀리초입니다.
  • 순식간에 끝나는 것 — NULL 허용 열 추가 · 열 길이 늘리기 · 열 지우기. 표를 다시 쓰는 것 — NOT NULL 열 추가 · 길이 줄이기 · 형식 바꾸기.
  • DROP COLUMN 은 2밀리초지만 공간은 그대로 남습니다. 돌려받으려면 인덱스를 다시 만들어야 합니다.
  • 기본값 없는 NOT NULL 열 추가는 4901 로 막힙니다. NULL 로 넣고 채운 뒤 조이는 것이 안전합니다.
  • ALTER TABLESch-M 잠금을 겁니다. NOLOCK 읽기까지 막으므로 걸리는 시간이 곧 멈추는 시간입니다.
  • 에디션에 따라 다릅니다. ONLINE = ON 은 Enterprise 전용이고(1712), 기본값 있는 NOT NULL 열 추가도 Enterprise 에서만 목록 수정으로 끝납니다.
  • WITH NOCHECK 로 붙인 제약 조건은 신뢰하지 않음으로 남아 최적화에 쓰이지 않습니다. 나중에 WITH CHECK CHECK CONSTRAINT 로 돌리십시오.
  • 운영 중 변경은 넓히고 · 채우고 · 옮기고 · 좁히는 네 번으로 나눕니다. 각 단계 사이에 옛 코드와 새 코드가 함께 돌아도 되는 상태가 유지됩니다.
  • 더하는 변경은 안전하고 빼는 변경은 위험합니다. 한 배포에 더하는 것만 담으면 언제든 되돌릴 수 있습니다.
  • 배포 스크립트는 몇 번을 돌려도 같아야 합니다. IF NOT EXISTS · CREATE OR ALTER · DROP … IF EXISTS 를 쓰고, 되돌리는 스크립트를 함께 두십시오.