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

로그와 보관 정책

기록을 어디에 쌓고 언제 지울지 정합니다.

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

로그 표는 다릅니다

지금까지 만든 표는 읽고 쓰는 비율이 비슷했습니다. 로그는 다릅니다.

보통 표로그 표
쓰기가끔끊임없이
읽기끊임없이가끔, 대개 최근 것만
고치기합니다하지 않습니다
지우기드물게주기적으로 대량
한 건의 값큽니다작습니다

그래서 5.2 의 판단이 뒤집힙니다. 인덱스를 더하면 조회가 빨라지지만, 로그는 조회보다 쓰기가 수천 배 많습니다.

SQL
-- 같은 로그 표를 둘 만들고 한쪽에만 인덱스를 겁니다.
CREATE TABLE Board.T_LOG1 (
    num       int IDENTITY PRIMARY KEY,
    cont_type int NOT NULL,
    cont_num  int NOT NULL,
    val       int NOT NULL,
    reg_num   int NULL,
    reg_addr  varchar(50) NOT NULL,
    reg_date  datetime2(0) NOT NULL
);

-- T_LOG2 는 같은 모양에 인덱스 셋을 더 답니다.
CREATE INDEX IX_LOG2_cont ON Board.T_LOG2 (cont_type, cont_num);
CREATE INDEX IX_LOG2_reg  ON Board.T_LOG2 (reg_date);
CREATE INDEX IX_LOG2_user ON Board.T_LOG2 (reg_num);
결과 — 각 3회
인덱스10만 행 넣기한 건 조회
기본 키만69~71 밀리초598장
셋을 더 검351~357 밀리초2장
쓰기는 5배 느려지고 조회는 300배 빨라집니다. 어느 쪽이 많은지가 답을 정합니다.

하루에 로그를 100만 건 쓰고 조회를 100번 한다면 인덱스를 더하는 쪽이 훨씬 손해입니다. 쓰기에서 잃는 것이 조회에서 얻는 것보다 수천 배 큽니다.

그렇다고 인덱스를 아예 두지 않으면 조회가 못 씁니다. 실제로 하는 조회를 먼저 정하고 그것 하나만 받치는 인덱스를 두십시오. "언젠가 필요할지 모른다" 로 더하지 마십시오.

설계

무엇을 어디에 쌓습니까

로그를 업무 데이터베이스에 두지 마십시오. 이 저장소는 MAYYANET(업무)과 LOGS(기록)를 데이터베이스부터 나눠 두었습니다.

나누면 얻는 것까닭
백업 계획을 달리 둡니다로그는 SIMPLE 로, 업무는 FULL 로(6.2)
디스크를 나눕니다로그가 디스크를 채워도 업무가 멈추지 않습니다
권한을 나눕니다로그만 보는 계정을 둘 수 있습니다(6.1)
지울 때 영향이 없습니다대량 삭제가 업무 표를 잠그지 않습니다(5.7)

기록마다 성격이 다릅니다

무엇보관 기간지울 수 있습니까
조회수·좋아요 같은 행위 기록며칠~몇 주집계한 뒤 지웁니다
관리자 작업 기록몇 년지우면 안 됩니다
로그인·접속 기록법으로 정해진 기간그 전에는 못 지웁니다
오류·디버그 기록며칠지웁니다
개인 정보가 든 기록목적을 다하면지워야 합니다

보관 기간을 정하는 것은 기술이 아니라 정책입니다. 법으로 정해진 것이 있고, 업무 쪽이 정할 것이 있습니다. 정해지지 않은 채로 쌓기 시작하면 나중에 지울 수 없습니다 — 무엇을 지워도 되는지 아무도 모르기 때문입니다.

개인 정보는 반대로 "언제까지 지워야 하는가" 가 정해져 있습니다. 접속 아이피도 개인 정보로 봅니다. 목적을 다한 뒤에도 남겨 두면 보관하지 않아야 할 것을 보관한 것이 됩니다.

방식

어떻게 지웁니까

오래된 것을 치우는 방법이 셋 있습니다. 지우는 값이 크게 다릅니다.

방식지우는 법
한 표에 쌓고 지우기배치 DELETE(5.7)비쌉니다
기간별 표로 나누기TRUNCATE 또는 DROP거의 공짜입니다
파티션파티션 전환거의 공짜지만 Enterprise 입니다
10만 행을 비우는 값
방법걸린 시간
DELETE54 밀리초
TRUNCATE1 밀리초
10만 행이라 이 정도지만, 수천만 행이면 분과 초로 갈립니다(5.7).

기간별로 표를 나누면 지우는 일이 TRUNCATE 하나가 됩니다. 조건을 걸어 찾을 필요도, 배치로 나눌 필요도, 잠금을 걱정할 필요도 없습니다.

실제로 돌고 있는 방식

이 사이트의 LOGS 데이터베이스는 요일별 표 일곱 개를 돌려 씁니다.

표 — Logging.WEEK1 부터 WEEK7 까지
CREATE TABLE Logging.WEEK1 (
    num       INT IDENTITY NOT NULL
        CONSTRAINT PK_WEEK1 PRIMARY KEY (num DESC),
    cont_type INT NOT NULL,             -- 1:게시판, 2:스도쿠 …
    [type]    INT NOT NULL              -- 1:조회, 2:추천, 4:신고 …
        CONSTRAINT CK_WEEK1_type CHECK ([type] IN (1, 2, 4, 8, 16)),
    cont_num  INT NOT NULL,
    val       INT NOT NULL,
    reg_num   INT NULL,
    reg_addr  VARCHAR(50) NOT NULL
        CONSTRAINT DF_WEEK1_reg_addr DEFAULT (''),
    reg_date  DATETIME NOT NULL
        CONSTRAINT DF_WEEK1_reg_date DEFAULT (GETDATE())
);

-- 인덱스는 실제로 하는 조회 하나만 받칩니다.
CREATE INDEX IX_WEEK1 ON Logging.WEEK1 (cont_type, [type], cont_num, usable)
    INCLUDE (val);
저장 — 요일로 표를 고릅니다
CREATE PROCEDURE Logging.P_WEEK_SAVE
    @cont_type INT, @type INT, @cont_num INT, @val INT,
    @reg_num INT = NULL, @reg_addr VARCHAR(50) = '',
    @rst INT = 0 OUTPUT
AS
    SET NOCOUNT ON;

    DECLARE @week INT = DATEPART(WEEKDAY, GETDATE()),   -- 1~7
            @sql NVARCHAR(MAX), @sqlp NVARCHAR(MAX);

    -- 표 이름만 이어 붙이고, 값은 매개 변수로 넘깁니다(4.9).
    SET @sql = N'INSERT INTO Logging.WEEK' + CONVERT(NCHAR(1), @week) + N' (
        [type], cont_num, val, reg_num, reg_addr
    ) VALUES (@type, @cont_num, @val, @reg_num, @reg_addr);
    SET @rst = @@ROWCOUNT;';

    SET @sqlp = N'@type INT, @cont_num INT, @val INT,
        @reg_num INT, @reg_addr VARCHAR(50), @rst INT OUTPUT';

    EXEC sp_executesql @sql, @sqlp,
        @type = @type, @cont_num = @cont_num, @val = @val,
        @reg_num = @reg_num, @reg_addr = @reg_addr, @rst = @rst OUTPUT;
이 방식이 주는 것
지우는 일이 없습니다. 일주일 뒤 같은 요일이 오면 그 표에 다시 씁니다. 덮어쓰기 전에 TRUNCATE 한 번이면 끝납니다. 한 표가 커지지 않습니다. 7일치만 담기므로 조회도 그 안에서만 일어납니다. 표 하나가 잠겨도 나머지 엿새는 멀쩡합니다.
동적 SQL 이지만 이어 붙인 것은 표 이름(숫자 하나)뿐이고 값은 전부 매개 변수입니다.

이 방식의 값은 소유권 체인이 끊긴다는 것입니다(6.1). 동적 SQL 이라 표 권한을 사용자가 직접 가지고 있어야 합니다. 로그 데이터베이스라 업무 표와 나뉘어 있어 감당할 만한 선택입니다.

비교

어느 방식을 고릅니까

방식어울리는 곳
한 표에 쌓고 배치 삭제양이 적거나 기간이 들쭉날쭉할 때지우는 값이 계속 듭니다
요일·월별 표 돌려쓰기보관 기간이 고정일 때표 이름을 정하는 코드가 필요합니다
기간별 표를 계속 만들기오래 보관해야 할 때표가 계속 늘고 조회가 번거롭습니다
파티션아주 크고 Enterprise 일 때에디션과 설계 비용
월별 표를 만들어 가는 방식
-- 매달 초에 다음 달 표를 미리 만듭니다.
DECLARE @name sysname = N'ACCESS_' + FORMAT(DATEADD(month, 1, GETDATE()), 'yyyyMM');

IF NOT EXISTS (SELECT 1 FROM sys.tables
                WHERE schema_id = SCHEMA_ID('Logging') AND name = @name)
BEGIN
    DECLARE @sql nvarchar(MAX) = N'CREATE TABLE Logging.' + QUOTENAME(@name) + N' (…)';
    EXEC sp_executesql @sql;
END

-- 보관 기간이 지난 표를 지웁니다. 이름으로 찾습니다.
DECLARE @cut nvarchar(6) = FORMAT(DATEADD(month, -13, GETDATE()), 'yyyyMM');

SELECT N'DROP TABLE Logging.' + QUOTENAME(name) + N';'
FROM sys.tables
WHERE schema_id = SCHEMA_ID('Logging')
  AND name LIKE 'ACCESS_%'
  AND RIGHT(name, 6) < @cut;
읽을 것
지우는 문장을 바로 실행하지 않고 만들어만 두었습니다. 읽어 보고 손으로 실행하는 편이 안전합니다. 이름 규칙을 정확히 지켜야 합니다. ACCESS_202601 처럼 자리 수가 같아야 문자열 비교가 맞습니다.
여러 달을 한 번에 조회하려면 뷰를 하나 두고 UNION ALL 로 묶습니다(3.4).

표를 계속 만들어 가는 방식은 조회가 번거로워집니다. 넉 달치를 보려면 표 넷을 UNION ALL 해야 하고, 그 뷰를 매달 고쳐야 합니다. 보관 기간이 고정이라면 돌려쓰기가 훨씬 간단합니다.

집계

지우기 전에 요약합니다

로그를 지우면 그 기간의 통계도 함께 사라집니다. 지우기 전에 필요한 것만 요약해 따로 남겨 두어야 합니다.

SQL
-- 원본 로그: 하루 100만 건
-- 요약: 하루 몇백 건
CREATE TABLE Statistics.DAILY_VIEW (
    stat_date date NOT NULL,
    cont_type int  NOT NULL,
    cont_num  int  NOT NULL,
    view_cnt  int  NOT NULL,
    CONSTRAINT PK_DAILY_VIEW PRIMARY KEY CLUSTERED (stat_date, cont_type, cont_num)
);
GO

-- 새벽에 어제치를 요약합니다.
DECLARE @day date = CAST(DATEADD(day, -1, GETDATE()) AS date);

-- 다시 돌려도 안전하도록 그날 것을 먼저 지웁니다(6.3).
DELETE FROM Statistics.DAILY_VIEW WHERE stat_date = @day;

INSERT INTO Statistics.DAILY_VIEW (stat_date, cont_type, cont_num, view_cnt)
SELECT @day, cont_type, cont_num, COUNT(*)
FROM Logging.WEEK1
WHERE reg_date >= @day AND reg_date < DATEADD(day, 1, @day)
  AND [type] = 1                       -- 조회만
GROUP BY cont_type, cont_num;
지키는 것까닭
요약을 먼저, 지우기를 나중에순서가 바뀌면 그날 통계가 없습니다
다시 돌려도 같게배치가 실패해 다시 도는 일이 생깁니다(6.3)
어제치까지만오늘 것은 아직 쌓이는 중입니다
요약이 끝난 것만 지웁니다요약 실패를 눈치채지 못하면 자료를 잃습니다

요약이 성공했는지 확인하고 지우십시오. 요약 배치가 조용히 실패한 채로 삭제 배치만 돌면 그 기간이 통째로 사라집니다. 집계 표를 실제로 채우는 방법은 6.8 에서 한 벌로 만듭니다.

연습

직접 해보기

1. 접속 기록 표를 설계합니다 난이도 하

하루 200만 건이 쌓이는 접속 기록 표를 설계하십시오. 보관은 6개월, 조회는 "특정 회원의 최근 접속" 하나뿐 입니다.

-- 월별 표로 나눕니다. 보관이 6개월이니 표가 일곱 개를 넘지 않습니다. CREATE TABLE Logging.ACCESS_202609 ( num bigint IDENTITY NOT NULL, -- 하루 200만이면 int 가 3년에 넘칩니다 user_num int NULL, ip varchar(45) NOT NULL, -- IPv6 까지 담깁니다 url nvarchar(500) NOT NULL, reg_date datetime2(0) NOT NULL CONSTRAINT DF_ACCESS_202609_reg DEFAULT SYSDATETIME(), CONSTRAINT PK_ACCESS_202609 PRIMARY KEY CLUSTERED (num) ); -- 인덱스는 실제로 하는 조회 하나만 받칩니다. CREATE INDEX IX_ACCESS_202609_user ON Logging.ACCESS_202609 (user_num, reg_date DESC); -- 이렇게 정한 까닭 -- 1. bigint 하루 200만 × 365 = 7억 3천만. int(21억)로 3년입니다. -- 보관이 6개월이라 int 로도 되지만 IDENTITY 는 지운다고 -- 되감기지 않습니다. 처음부터 bigint 가 안전합니다. -- 2. 인덱스 하나 (user_num, reg_date DESC) 로 그 조회를 그대로 받습니다. -- 5.2 의 등호 → 정렬 순서입니다. -- 3. url 에 인덱스를 두지 않습니다. 조회하지 않으니까요. -- 4. 월별 표 지울 때 DROP TABLE 한 번입니다. -- 하지 않은 것 -- ip 에 인덱스 — "이 아이피가 누구인가" 를 조회하지 않습니다. -- 필요해지면 그때 더합니다. -- 외래 키 — 회원을 지워도 접속 기록은 남아야 합니다. -- 그리고 로그 표의 외래 키는 쓰기마다 확인 비용이 듭니다. -- 함께 정할 것 -- ip 는 개인 정보입니다. 6개월이 지나면 반드시 지워야 합니다. -- 보관 기간을 코드 주석이 아니라 문서로 남기십시오.
2. 로그 표가 디스크를 채웁니다 난이도 중

로그 표 하나가 3억 행까지 자라 디스크가 거의 찼습니다. 서비스를 멈추지 않고 정리하는 순서를 적어 보세요. 지금은 한 표에 계속 쌓는 구조입니다.

DELETE 로 3억 행을 지워도 디스크는 바로 돌아오지 않습니다. 그리고 지우는 동안 무슨 일이 생깁니까(5.7).
-- 0. 먼저 확인합니다. SELECT SUM(row_count) AS 행수, SUM(reserved_page_count) * 8 / 1024 AS MB FROM sys.dm_db_partition_stats WHERE object_id = OBJECT_ID('Logging.ACCESS'); -- 복구 모델도 봅니다(6.2). FULL 이면 지우는 만큼 로그가 커집니다. SELECT recovery_model_desc, log_reuse_wait_desc FROM sys.databases WHERE name = 'LOGS'; -- 1. 급하면 먼저 숨 쉴 공간을 만듭니다. -- 로그 백업을 뜨거나(FULL), 안 쓰는 인덱스를 지웁니다(5.2). -- 데이터 파일을 줄이는 것(SHRINK)은 마지막입니다 — 조각이 심해집니다. -- 2. 남길 것을 먼저 요약합니다. 지운 뒤에는 못 합니다. INSERT INTO Statistics.DAILY_ACCESS (…) SELECT … FROM Logging.ACCESS WHERE reg_date < @cut GROUP BY …; -- 3. 배치로 지웁니다. 한 문장으로 하면 표가 잠깁니다(5.7). DECLARE @n int = 1, @loop int = 0; WHILE @n > 0 AND @loop < 100000 BEGIN DELETE TOP (3000) FROM Logging.ACCESS WHERE reg_date < @cut; SET @n = @@ROWCOUNT; SET @loop += 1; IF @n > 0 WAITFOR DELAY '00:00:00.100'; END -- 3억 행이면 10만 번입니다. 며칠에 나눠 도는 것이 낫습니다. -- reg_date 에 인덱스가 있어야 합니다. 없으면 배치마다 표를 훑습니다. -- 4. 되풀이되지 않게 구조를 바꿉니다. 이것이 진짜 해결입니다. -- (가) 월별 표로 나눕니다. -- 새 표를 만들고 응용 프로그램이 그리로 쓰게 합니다. -- 옛 표는 조회용으로 두었다가 기간이 지나면 DROP 합니다. -- (나) 보관 기간을 정하고 매달 도는 작업을 만듭니다. -- (다) 무엇을 남길지 다시 봅니다. -- 3억 행 가운데 실제로 조회하는 것이 얼마나 됩니까. -- 5. 디스크를 실제로 돌려받으려면 -- DELETE 는 페이지를 비울 뿐 파일을 줄이지 않습니다. -- DBCC SHRINKFILE 로 줄일 수 있지만 조각이 심해지므로 -- 줄인 뒤 인덱스를 다시 만들어야 합니다. -- 월별 표라면 DROP TABLE 로 공간이 바로 돌아옵니다. -- 배울 것 -- "보관 기간을 정하지 않고 쌓기 시작한 것" 이 원인입니다. -- 표를 만들 때 지우는 방법까지 함께 정해 두십시오.
요약
  • 로그 표는 다릅니다. 쓰기가 압도적으로 많고 조회는 가끔이라 5.2 의 판단이 뒤집힙니다.
  • 인덱스 셋을 더하면 10만 행 넣기가 70밀리초에서 354밀리초가 되고, 조회는 598장에서 2장이 됩니다. 비율이 답을 정합니다.
  • 실제로 하는 조회를 먼저 정하고 그것 하나만 받치는 인덱스를 두십시오. "언젠가 필요할지 모른다" 로 더하지 마십시오.
  • 로그를 업무 데이터베이스에 두지 마십시오. 나누면 백업·디스크·권한·삭제가 서로 영향을 주지 않습니다.
  • 보관 기간은 기술이 아니라 정책입니다. 정하지 않고 쌓기 시작하면 나중에 무엇을 지워도 되는지 아무도 모릅니다.
  • 개인 정보는 지워야 하는 기한이 있습니다. 접속 아이피도 개인 정보입니다.
  • 지우는 방법에 따라 값이 크게 다릅니다 — 10만 행에서 DELETE 54밀리초, TRUNCATE 1밀리초입니다.
  • 기간별로 표를 나누면 지우는 일이 TRUNCATEDROP 하나가 됩니다. 이 사이트는 요일별 표 일곱 개를 돌려 씁니다.
  • 돌려쓰기는 보관 기간이 고정일 때 가장 간단합니다. 오래 보관해야 하면 기간별 표를 만들어 가되 조회가 번거로워지는 값을 함께 따지십시오.
  • 지우기 전에 요약하십시오. 순서가 바뀌면 그 기간의 통계가 사라집니다. 요약이 성공했는지 확인한 뒤에 지웁니다.