MSSQL 5.8 · 5부. 성능

전문 검색

인덱스를 만들고 CONTAINS 로 찾습니다. 언어마다 단어를 나누는 방식이 다른 점도 봅니다.

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

이 단원만 환경이 다릅니다

전문 검색은 SQL Server 를 설치할 때 따로 고르는 기능입니다. 실습에 사용한 LocalDB 에는 들어 있지 않습니다.

SQL
SELECT SERVERPROPERTY('IsFullTextInstalled');   -- 1 이어야 합니다
결과
환경
LocalDB0
전문 검색을 고른 설치1
0 이면 아래 문장들이 모두 오류로 끝납니다.

0 이라면 읽기만 하고 넘어가셔도 됩니다. 직접 해보려면 설치 관리자에서 "전체 텍스트 및 의미 체계 추출" 을 추가하고 SQL Full-text Filter Daemon Launcher 서비스가 도는지 확인하십시오. 아래 실측은 20만 행짜리 표에서 잡은 것입니다.

문제

본문 검색은 언제나 다 읽습니다

5.4 에서 LIKE N'%값%' 은 어떻게 해도 인덱스로 들어가지 못한다고 했습니다. 앞이 정해지지 않아 어디서 시작할지 알 수 없기 때문입니다.

SQL
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

-- 20만 건 가운데 한 건만 나오는 검색입니다.
SELECT num FROM Board.POSTS_BIG WHERE content LIKE N'%12345번%';
결과
논리적 읽기CPU
LIKE3,487234 밀리초
한 건을 찾으려고 20만 건의 본문을 하나씩 비교했습니다.

읽는 양보다 CPU 가 문제입니다. 20만 개의 본문에서 문자열을 하나씩 맞춰 봐야 하고, 사람이 검색어를 넣을 때마다 이 일이 일어납니다. 게시판 검색이 느린 곳은 대개 이 모양입니다.

개념 설명

낱말을 미리 뽑아 둡니다

전문 인덱스는 글마다 어떤 낱말이 들어 있는지를 미리 뽑아 정리해 둡니다. 검색할 때는 그 목록에서 낱말을 찾고, 딸려 있는 글 번호를 가져옵니다.

SQL
-- 1. 카탈로그를 만듭니다. 인덱스들을 담을 곳입니다.
CREATE FULLTEXT CATALOG FT_LAB AS DEFAULT;
GO

-- 2. 표에 전문 인덱스를 만듭니다.
--    KEY INDEX 에는 한 열짜리 고유 인덱스를 지정합니다. 대개 기본 키입니다.
--    LANGUAGE 1042 는 한국어입니다.
CREATE FULLTEXT INDEX ON Board.POSTS_BIG (
    title   LANGUAGE 1042,
    content LANGUAGE 1042)
    KEY INDEX PK_POSTS_BIG ON FT_LAB
    WITH CHANGE_TRACKING AUTO;
GO

-- 3. 다 만들어졌는지 봅니다. 20만 행에 1분 남짓 걸렸습니다.
SELECT FULLTEXTCATALOGPROPERTY('FT_LAB', 'ItemCount') AS 항목수,
       OBJECTPROPERTYEX(OBJECT_ID('Board.POSTS_BIG'),
                        'TableFullTextPopulateStatus') AS 채우는중;
결과 — 채우는 동안
항목수채우는중
64,0001
156,0001
200,0000
채우는중이 0 이 되어야 끝난 것입니다. 그 전에 검색하면 결과가 모자랍니다.

무엇이 들어 있는지 볼 수 있습니다

SQL
SELECT COUNT(*) AS 서로다른낱말
FROM sys.dm_fts_index_keywords(DB_ID(), OBJECT_ID('Board.POSTS_BIG'));

SELECT TOP (5) display_term AS 낱말, document_count AS 나온글수
FROM sys.dm_fts_index_keywords(DB_ID(), OBJECT_ID('Board.POSTS_BIG'))
ORDER BY document_count DESC;
결과
낱말나온 글 수
200,001
본문200,001
본문입니다200,001
글이고200,000
200,000
서로 다른 낱말이 600,090개입니다. 글 번호가 다 낱말로 들어가 이렇게 많습니다.

한국어는 조사를 떼어 냅니다

SQL
-- 단어 분리기가 문장을 어떻게 쪼개는지 직접 볼 수 있습니다.
SELECT display_term AS 낱말, occurrence AS 위치
FROM sys.dm_fts_parser(N'"인덱스를 만들면 조회가 빨라집니다"', 1042, 0, 0);
결과
낱말위치
인덱스를1
인덱스1
만들면2
조회가3
조회3
빨라집니다4
빨라4
집니다5
인덱스를 → 인덱스 · 조회가 → 조회. 조사를 뗀 것도 함께 담습니다.

그래서 "인덱스" 로 찾으면 "인덱스를" 이 든 글도 나옵니다. 한국어 단어 분리기(1042)를 지정한 값입니다. LANGUAGE 를 적지 않으면 서버 기본값이 쓰이므로 한국어 자료라면 반드시 1042 를 지정하십시오.

sys.dm_fts_parser검색이 안 될 때 가장 먼저 볼 곳입니다. 찾으려는 말이 실제로 어떤 낱말로 쪼개지는지 보면 대개 원인이 드러납니다.

실측

3,487장이 3장이 됩니다

SQL
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

SELECT num FROM Board.POSTS_BIG WHERE content LIKE N'%12345번%';
SELECT num FROM Board.POSTS_BIG WHERE CONTAINS(content, N'"12345번"');
결과 — 같은 한 건을 찾습니다
방법논리적 읽기CPU
LIKE N'%12345번%'3,487234 밀리초
CONTAINS(content, N'"12345번"')30 밀리초
1,162배입니다. 낱말 목록에서 찾아 글 번호를 얻고, 그 번호로 표를 읽었습니다.

결과가 많으면 차이가 줄어듭니다. 25,000건이 나오는 검색에서는 3,487장과 1,085장으로 3배쯤이었습니다. 찾는 것이 드물수록 전문 검색이 유리합니다 — 그리고 실제 검색은 대개 그렇습니다.

문법

무엇을 적을 수 있습니까

SQL
-- 낱말 하나
WHERE CONTAINS(content, N'인덱스')

-- 둘 다 든 글
WHERE CONTAINS(content, N'인덱스 AND 조회')

-- 하나라도 든 글
WHERE CONTAINS(content, N'인덱스 OR 커서')

-- 앞의 것은 있고 뒤의 것은 없는 글
WHERE CONTAINS(content, N'인덱스 AND NOT 조회')

-- 이어진 그대로 (구 검색)
WHERE CONTAINS(content, N'"조회가 빨라집니다"')

-- 그 글자로 시작하는 낱말
WHERE CONTAINS(content, N'"인덱*"')

-- 세 낱말 안쪽에 함께 나오는 글
WHERE CONTAINS(content, N'NEAR((인덱스, 조회), 3)')

-- 열을 가리지 않고 전문 인덱스에 든 모든 열에서
WHERE CONTAINS(*, N'인덱스')

순위를 매겨 가져옵니다

CONTAINS 는 맞는지 아닌지만 냅니다. 얼마나 잘 맞는지까지 필요하면 CONTAINSTABLE 을 씁니다.

SQL
SELECT TOP (5) p.num, ft.RANK, LEFT(p.title, 30) AS 제목
FROM Board.POSTS_BIG p
    JOIN CONTAINSTABLE(Board.POSTS_BIG, content, N'인덱스 OR 조회', 100) ft
      ON ft.[KEY] = p.num
ORDER BY ft.RANK DESC, p.num;
결과
numRANK제목
864글 제목 8 인덱스
1664글 제목 16 인덱스
2464글 제목 24 인덱스
마지막 인자 100 은 "상위 100건만" 이라는 뜻입니다. 빼면 전부 나옵니다.
함수쓰는 자리내는 것
CONTAINSWHERE맞는지 아닌지
CONTAINSTABLEFROM 에 조인KEY 와 RANK
FREETEXTWHERE뜻이 비슷한 것까지
FREETEXTTABLEFROM 에 조인KEY 와 RANK

RANK0~1000 사이의 상대값입니다. 문장 안에서만 뜻이 있고 다른 검색의 RANK 와 견줄 수 없습니다. 정렬에만 쓰고 화면에 점수로 내보이지 마십시오.

FREETEXT 는 형태소 분석기를 함께 사용해 활용형까지 찾습니다. 다만 언어와 서버 구성에 따라 오류 30053 으로 끝나는 경우가 있습니다. 실측 환경에서도 한국어 FREETEXT 가 그렇게 끝났습니다. 쓰기 전에 그 환경에서 도는지 반드시 확인하십시오.

제약

못 하는 것 둘

전문 검색은 LIKE 의 상위 호환이 아닙니다. 바꾸면 잃는 것이 있습니다.

(1) 낱말 가운데로는 찾지 못합니다

SQL
-- "인덱스" 의 가운데 토막으로 찾아 봅니다.
SELECT COUNT(*) FROM Board.POSTS_BIG WHERE content LIKE N'%덱스%';
SELECT COUNT(*) FROM Board.POSTS_BIG WHERE CONTAINS(content, N'덱스');
SELECT COUNT(*) FROM Board.POSTS_BIG WHERE CONTAINS(content, N'"덱스*"');
결과
방법찾은 건수
LIKE N'%덱스%'25,000
CONTAINS(content, N'덱스')0
CONTAINS(content, N'"덱스*"')0
접두어 검색(*)도 낱말의 시작을 기준으로 합니다. 가운데는 걸리지 않습니다.

낱말 단위로 색인했기 때문입니다. 목록에 있는 것은 인덱스 이지 덱스 가 아닙니다. "검색어가 낱말 가운데에 걸려도 나와야 한다" 는 요구가 있다면 전문 검색으로는 안 됩니다.

(2) 넣자마자 찾아지지 않습니다

SQL
INSERT INTO Board.POSTS_BIG (board_num, user_num, title, content, hit_count, reg_date)
VALUES (1, 1, N'방금 넣은 글', N'가나다라마바사 라는 말이 든 본문입니다.', 0, '2026-12-31');

SELECT COUNT(*) FROM Board.POSTS_BIG WHERE CONTAINS(content, N'가나다라마바사');
WAITFOR DELAY '00:00:05';
SELECT COUNT(*) FROM Board.POSTS_BIG WHERE CONTAINS(content, N'가나다라마바사');
SELECT COUNT(*) FROM Board.POSTS_BIG WHERE content LIKE N'%가나다라마바사%';
결과
언제CONTAINSLIKE
넣은 직후01
5초 뒤11
CHANGE_TRACKING AUTO 라도 반영이 즉시는 아닙니다.

전문 인덱스는 트랜잭션 밖에서 따로 갱신됩니다. 글을 쓰고 바로 검색 결과에 나와야 하는 화면이라면 그 사실을 알고 만들어야 합니다. 방금 쓴 글은 목록 맨 위에 있으므로 대개 문제가 되지 않습니다.

갱신 방식언제 반영됩니까쓰는 자리
CHANGE_TRACKING AUTO바뀐 뒤 잠시 후대부분
CHANGE_TRACKING MANUAL직접 부를 때정해진 시각에 모아 반영
CHANGE_TRACKING OFF전체를 다시 만들 때만거의 바뀌지 않는 자료
고르기

무엇을 씁니까

찾는 것쓸 것
앞이 정해진 문자열LIKE N'값%' 과 인덱스(5.4)
본문에서 낱말CONTAINS
얼마나 잘 맞는지까지CONTAINSTABLERANK
낱말 가운데도 걸려야전문 검색으로는 안 됩니다
오타·비슷한 말까지검색 엔진을 따로 두는 편이 낫습니다

전문 검색은 SQL Server 안에서 끝난다는 것이 가장 큰 장점입니다. 자료를 따로 옮기지 않아도 되고, 조인·조건·정렬을 평소처럼 함께 적을 수 있습니다. 검색이 서비스의 핵심이 아니라면 대개 이것으로 충분합니다.

검색 자체가 서비스의 핵심이라면 전용 검색 엔진을 보십시오. 오타 교정, 동의어, 형태소 분석의 품질, 색인 갱신 속도에서 차이가 큽니다. 다만 자료를 두 곳에 두는 일이 되므로 그 값을 치를 만한지 먼저 따지십시오.

연습

직접 해보기

1. 검색어를 안전하게 넘깁니다 난이도 하

사람이 넣은 검색어로 CONTAINS 검색을 하는 프로시저를 만듭니다. 4.9 에서 본 문제가 여기서도 생깁니다. 안전하게 적어 보세요.

CREATE OR ALTER PROCEDURE Board.P_POST_SEARCH @keyword nvarchar(100) AS BEGIN SET NOCOUNT ON; -- 검색어를 그대로 넣지 않습니다. -- 따옴표로 감싸 구 검색으로 만들고, -- 안에 든 따옴표는 두 번 적어 무력화합니다. DECLARE @term nvarchar(210) = N'"' + REPLACE(@keyword, N'"', N'""') + N'"'; SELECT TOP (50) p.num, p.title, p.reg_date, ft.RANK FROM Board.POSTS_BIG p JOIN CONTAINSTABLE(Board.POSTS_BIG, (title, content), @term, 50) ft ON ft.[KEY] = p.num ORDER BY ft.RANK DESC, p.num DESC; END GO EXEC Board.P_POST_SEARCH @keyword = N'인덱스'; -- CONTAINS 와 CONTAINSTABLE 은 검색식을 변수로 받습니다. -- 그러므로 동적 SQL 로 이어 붙일 까닭이 전혀 없습니다(4.9). -- N'... CONTAINS(content, ''' + @keyword + ''')' ← 하지 마십시오 -- 그래도 감싸는 까닭은 주입이 아니라 문법입니다. -- 검색어에 AND · OR · NEAR · * 가 들어 있으면 -- 그것이 연산자로 읽혀 뜻이 달라지거나 오류가 납니다. -- 따옴표로 감싸면 통째로 한 구가 됩니다. -- 빈 검색어와 너무 짧은 검색어는 미리 걸러 내십시오. -- IF LEN(LTRIM(RTRIM(@keyword))) < 2 RETURN;
2. 검색이 안 된다는 신고를 받았습니다 난이도 중

"어떤 말은 검색되고 어떤 말은 안 됩니다" 라는 신고입니다. 무엇을 어떤 순서로 확인하겠습니까.

검색어가 낱말로 어떻게 쪼개지는지, 그리고 그 낱말이 색인에 들어 있는지를 각각 볼 수 있습니다.
-- 1. 색인이 다 만들어졌는지 봅니다. 가장 흔한 원인입니다. SELECT OBJECTPROPERTYEX(OBJECT_ID('Board.POSTS_BIG'), 'TableFullTextPopulateStatus') AS 채우는중, FULLTEXTCATALOGPROPERTY('FT_LAB', 'ItemCount') AS 항목수; -- 채우는중이 0 이 아니면 아직 도는 중입니다. -- 2. 검색어가 어떤 낱말로 쪼개지는지 봅니다. SELECT display_term FROM sys.dm_fts_parser(N'"찾으려는 말"', 1042, 0, 0); -- 쪼갠 결과가 예상과 다르면 여기서 드러납니다. -- 3. 그 낱말이 색인에 있는지 봅니다. SELECT display_term, document_count FROM sys.dm_fts_index_keywords(DB_ID(), OBJECT_ID('Board.POSTS_BIG')) WHERE display_term = N'찾으려는말'; -- 없으면 색인이 안 된 것이고, 있으면 문장 쪽 문제입니다. -- 4. 중지 단어에 걸렸는지 봅니다. 너무 흔한 말은 색인하지 않습니다. SELECT COUNT(*) FROM sys.fulltext_system_stopwords WHERE language_id = 1042; -- 한국어는 0개입니다. 영어(1033)는 154개가 들어 있습니다. -- 한국어 자료라면 이 원인일 가능성은 낮습니다. -- 필요하면 중지 목록을 직접 만들어 붙일 수 있습니다. -- 5. 언어 설정을 봅니다. SELECT c.name AS 열, l.name AS 언어 FROM sys.fulltext_index_columns fic JOIN sys.columns c ON c.object_id = fic.object_id AND c.column_id = fic.column_id JOIN sys.fulltext_languages l ON l.lcid = fic.language_id WHERE fic.object_id = OBJECT_ID('Board.POSTS_BIG'); -- 한국어 자료에 English 가 걸려 있으면 조사를 떼지 못합니다. -- 자주 나오는 원인 셋 -- (가) 낱말 가운데로 찾고 있습니다. → 전문 검색으로는 안 됩니다 -- (나) 방금 쓴 글을 찾고 있습니다. → 반영에 시간이 걸립니다 -- (다) 검색어에 * 나 AND 가 들어 있습니다. → 연산자로 읽힙니다(연습 1)

실습을 마쳤으면 정리합니다.
DROP FULLTEXT INDEX ON Board.POSTS_BIG;
DROP FULLTEXT CATALOG FT_LAB;
DROP PROCEDURE Board.P_POST_SEARCH;

5부를 맺으며

재고 나서 고칩니다

5부는 계획을 읽는 법으로 시작해서 인덱스 · 통계 · 조건 모양 · 잠금 · 페이징 · 대량 처리 · 검색을 지났습니다. 여덟 단원이 되풀이해서 말한 것은 하나입니다.

단원고치기 전고친 뒤
5.1인덱스 없는 열로 7,333장있는 열로 3장
5.2키 조회 76,618장커버링 151장
5.3스니핑으로 4,535장RECOMPILE 로 5장
5.4함수로 감싸 994장범위로 42장
5.54,006밀리초 기다리다 실패스냅숏으로 0밀리초
5.6깊은 쪽 1,515장키셋으로 3장
5.7표 전체 잠금배치로 행 잠금
5.8본문 검색 3,487장전문 검색 3장

어느 것도 짐작으로 고친 것이 없습니다. 계획을 열어 Scan 인지 Seek 인지 보고, SET STATISTICS IO ON 으로 몇 장을 읽는지 세고, 고친 뒤 다시 세었습니다.

느리다는 말을 들으면 먼저 재십시오. 무엇이 느린지 모르는 채로 인덱스를 더하면 5.2 에서 본 대로 쓰기만 느려집니다.

6부에서는 이 데이터베이스를 실제로 운영하는 일을 봅니다 — 권한, 백업, 스키마 변경, 응용 프로그램에서 부르기입니다.

요약
  • 전문 검색은 따로 설치하는 기능입니다. SERVERPROPERTY('IsFullTextInstalled') 가 1 이어야 하고, LocalDB 에는 없습니다.
  • LIKE N'%값%'20만 건의 본문을 하나씩 비교합니다 — 3,487장에 CPU 234밀리초. 같은 검색이 CONTAINS3장에 0밀리초입니다.
  • 만드는 순서는 카탈로그 → 전문 인덱스이고, KEY INDEX 에 한 열짜리 고유 인덱스를 지정합니다.
  • 한국어 자료에는 LANGUAGE 1042 를 지정하십시오. 그래야 인덱스를 에서 인덱스 를 떼어 냅니다.
  • sys.dm_fts_parser검색어가 어떤 낱말로 쪼개지는지, sys.dm_fts_index_keywords색인에 무엇이 들어 있는지 볼 수 있습니다. 검색이 안 될 때 먼저 볼 곳입니다.
  • 연산자는 AND · OR · AND NOT · NEAR · 구 검색("…") · 접두어("값*") 입니다.
  • 순위가 필요하면 CONTAINSTABLERANK 를 조인합니다. 다른 검색의 RANK 와는 견줄 수 없습니다.
  • 못 하는 것 둘입니다. 낱말 가운데로는 찾지 못하고(덱스 → 0건), 넣자마자 찾아지지도 않습니다(직후 0건, 5초 뒤 1건).
  • 검색어는 따옴표로 감싸 넘기십시오. 안에 든 AND* 가 연산자로 읽히는 것을 막습니다. CONTAINS 는 변수를 받으므로 동적 SQL 로 이어 붙일 까닭이 없습니다(4.9).