전문 검색
인덱스를 만들고 CONTAINS 로 찾습니다. 언어마다 단어를 나누는 방식이 다른 점도 봅니다.
이 단원만 환경이 다릅니다
전문 검색은 SQL Server 를 설치할 때 따로 고르는 기능입니다. 실습에 사용한 LocalDB 에는 들어 있지 않습니다.
SELECT SERVERPROPERTY('IsFullTextInstalled'); -- 1 이어야 합니다
| 환경 | 값 |
|---|---|
| LocalDB | 0 |
| 전문 검색을 고른 설치 | 1 |
0 이라면 읽기만 하고 넘어가셔도 됩니다. 직접 해보려면
설치 관리자에서 "전체 텍스트 및 의미 체계 추출" 을 추가하고
SQL Full-text Filter Daemon Launcher 서비스가
도는지 확인하십시오. 아래 실측은 20만 행짜리 표에서 잡은 것입니다.
본문 검색은 언제나 다 읽습니다
5.4 에서 LIKE N'%값%' 은 어떻게 해도 인덱스로
들어가지 못한다고 했습니다. 앞이 정해지지 않아 어디서 시작할지 알
수 없기 때문입니다.
SET STATISTICS IO ON; SET STATISTICS TIME ON; -- 20만 건 가운데 한 건만 나오는 검색입니다. SELECT num FROM Board.POSTS_BIG WHERE content LIKE N'%12345번%';
| 논리적 읽기 | CPU | |
|---|---|---|
| LIKE | 3,487 | 234 밀리초 |
읽는 양보다 CPU 가 문제입니다. 20만 개의 본문에서 문자열을 하나씩 맞춰 봐야 하고, 사람이 검색어를 넣을 때마다 이 일이 일어납니다. 게시판 검색이 느린 곳은 대개 이 모양입니다.
낱말을 미리 뽑아 둡니다
전문 인덱스는 글마다 어떤 낱말이 들어 있는지를 미리 뽑아 정리해 둡니다. 검색할 때는 그 목록에서 낱말을 찾고, 딸려 있는 글 번호를 가져옵니다.
-- 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,000 | 1 |
| 156,000 | 1 |
| 200,000 | 0 |
무엇이 들어 있는지 볼 수 있습니다
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 |
한국어는 조사를 떼어 냅니다
-- 단어 분리기가 문장을 어떻게 쪼개는지 직접 볼 수 있습니다. 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장이 됩니다
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,487 | 234 밀리초 |
| CONTAINS(content, N'"12345번"') | 3 | 0 밀리초 |
결과가 많으면 차이가 줄어듭니다. 25,000건이 나오는 검색에서는 3,487장과 1,085장으로 3배쯤이었습니다. 찾는 것이 드물수록 전문 검색이 유리합니다 — 그리고 실제 검색은 대개 그렇습니다.
무엇을 적을 수 있습니까
-- 낱말 하나 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 을 씁니다.
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;
| num | RANK | 제목 |
|---|---|---|
| 8 | 64 | 글 제목 8 인덱스 |
| 16 | 64 | 글 제목 16 인덱스 |
| 24 | 64 | 글 제목 24 인덱스 |
| 함수 | 쓰는 자리 | 내는 것 |
|---|---|---|
| CONTAINS | WHERE | 맞는지 아닌지 |
| CONTAINSTABLE | FROM 에 조인 | KEY 와 RANK |
| FREETEXT | WHERE | 뜻이 비슷한 것까지 |
| FREETEXTTABLE | FROM 에 조인 | KEY 와 RANK |
RANK 는 0~1000 사이의 상대값입니다.
문장 안에서만 뜻이 있고 다른 검색의 RANK 와 견줄 수 없습니다.
정렬에만 쓰고 화면에 점수로 내보이지 마십시오.
FREETEXT 는 형태소 분석기를 함께 사용해
활용형까지 찾습니다. 다만 언어와 서버 구성에 따라
오류 30053 으로 끝나는 경우가 있습니다. 실측 환경에서도
한국어 FREETEXT 가 그렇게 끝났습니다.
쓰기 전에 그 환경에서 도는지 반드시 확인하십시오.
못 하는 것 둘
전문 검색은 LIKE 의 상위 호환이 아닙니다.
바꾸면 잃는 것이 있습니다.
(1) 낱말 가운데로는 찾지 못합니다
-- "인덱스" 의 가운데 토막으로 찾아 봅니다. 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) 넣자마자 찾아지지 않습니다
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'%가나다라마바사%';
| 언제 | CONTAINS | LIKE |
|---|---|---|
| 넣은 직후 | 0 | 1 |
| 5초 뒤 | 1 | 1 |
전문 인덱스는 트랜잭션 밖에서 따로 갱신됩니다. 글을 쓰고 바로 검색 결과에 나와야 하는 화면이라면 그 사실을 알고 만들어야 합니다. 방금 쓴 글은 목록 맨 위에 있으므로 대개 문제가 되지 않습니다.
| 갱신 방식 | 언제 반영됩니까 | 쓰는 자리 |
|---|---|---|
| CHANGE_TRACKING AUTO | 바뀐 뒤 잠시 후 | 대부분 |
| CHANGE_TRACKING MANUAL | 직접 부를 때 | 정해진 시각에 모아 반영 |
| CHANGE_TRACKING OFF | 전체를 다시 만들 때만 | 거의 바뀌지 않는 자료 |
무엇을 씁니까
| 찾는 것 | 쓸 것 |
|---|---|
| 앞이 정해진 문자열 | LIKE N'값%' 과 인덱스(5.4) |
| 본문에서 낱말 | CONTAINS |
| 얼마나 잘 맞는지까지 | CONTAINSTABLE 의 RANK |
| 낱말 가운데도 걸려야 | 전문 검색으로는 안 됩니다 |
| 오타·비슷한 말까지 | 검색 엔진을 따로 두는 편이 낫습니다 |
전문 검색은 SQL Server 안에서 끝난다는 것이 가장 큰 장점입니다. 자료를 따로 옮기지 않아도 되고, 조인·조건·정렬을 평소처럼 함께 적을 수 있습니다. 검색이 서비스의 핵심이 아니라면 대개 이것으로 충분합니다.
검색 자체가 서비스의 핵심이라면 전용 검색 엔진을 보십시오. 오타 교정, 동의어, 형태소 분석의 품질, 색인 갱신 속도에서 차이가 큽니다. 다만 자료를 두 곳에 두는 일이 되므로 그 값을 치를 만한지 먼저 따지십시오.
직접 해보기
사람이 넣은 검색어로 CONTAINS 검색을 하는
프로시저를 만듭니다. 4.9 에서 본 문제가 여기서도 생깁니다.
안전하게 적어 보세요.
"어떤 말은 검색되고 어떤 말은 안 됩니다" 라는 신고입니다. 무엇을 어떤 순서로 확인하겠습니까.
실습을 마쳤으면 정리합니다.
DROP FULLTEXT INDEX ON Board.POSTS_BIG;
DROP FULLTEXT CATALOG FT_LAB;
DROP PROCEDURE Board.P_POST_SEARCH;
재고 나서 고칩니다
5부는 계획을 읽는 법으로 시작해서 인덱스 · 통계 · 조건 모양 · 잠금 · 페이징 · 대량 처리 · 검색을 지났습니다. 여덟 단원이 되풀이해서 말한 것은 하나입니다.
| 단원 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 5.1 | 인덱스 없는 열로 7,333장 | 있는 열로 3장 |
| 5.2 | 키 조회 76,618장 | 커버링 151장 |
| 5.3 | 스니핑으로 4,535장 | RECOMPILE 로 5장 |
| 5.4 | 함수로 감싸 994장 | 범위로 42장 |
| 5.5 | 4,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밀리초. 같은 검색이CONTAINS로 3장에 0밀리초입니다.- 만드는 순서는 카탈로그 → 전문 인덱스이고,
KEY INDEX에 한 열짜리 고유 인덱스를 지정합니다. - 한국어 자료에는
LANGUAGE 1042를 지정하십시오. 그래야인덱스를에서인덱스를 떼어 냅니다. sys.dm_fts_parser로 검색어가 어떤 낱말로 쪼개지는지,sys.dm_fts_index_keywords로 색인에 무엇이 들어 있는지 볼 수 있습니다. 검색이 안 될 때 먼저 볼 곳입니다.- 연산자는
AND·OR·AND NOT·NEAR· 구 검색("…") · 접두어("값*") 입니다. - 순위가 필요하면
CONTAINSTABLE의RANK를 조인합니다. 다른 검색의 RANK 와는 견줄 수 없습니다. - 못 하는 것 둘입니다. 낱말 가운데로는 찾지 못하고(
덱스→ 0건), 넣자마자 찾아지지도 않습니다(직후 0건, 5초 뒤 1건). - 검색어는 따옴표로 감싸 넘기십시오. 안에 든
AND나*가 연산자로 읽히는 것을 막습니다.CONTAINS는 변수를 받으므로 동적 SQL 로 이어 붙일 까닭이 없습니다(4.9).