검색엔진 후보를 DB 로 재인가할 때, 엔진 문서의 인덱스 키로 펜스 치기
검색엔진이 돌려준 후보를 DB 에서 다시 권한 검사하는데 인덱스 선두 키가 없어 느렸습니다. 엔진 문서가 이미 가진 키 쌍을 서브쿼리 출처로 끼워 비용을 채널 크기에서 후보 수로 바꾼 과정입니다.
파일 검색은 검색엔진(ES)에 맡기고, 돌려받은 후보 수십 건을 DB 에서 다시 권한 검사하는 구조입니다. 엔진 결과는 노출 후보일 뿐이고 최종 판정은 DB 가 합니다.
후보의 키는 file_id 인데, 1억 행이 넘는 링크 테이블에는 file_id 로 시작하는 인덱스가 없었습니다. 인덱스는 (channel_id, post_id, ...) 뿐입니다.
인덱스가 있는데 왜 느릴까요?
file_id = ANY(...) 만으로 거르면 플래너가 테이블 전체를 순차 스캔합니다. 수십 초가 걸려 타임아웃이 났습니다.
그래서 후보가 나온 채널을 channel_id = ANY(...) 로 같이 걸었습니다. 인덱스는 타는데, 그 채널의 링크 행을 전 기간 전부 heap 에서 읽고 나서 file_id 를 사후 필터합니다. 비용이 결과 수가 아니라 채널 크기에 비례합니다. 큰 채널이 섞이면 콜드 캐시에서 수십 초가 됩니다.
게다가 멤버십 EXISTS 를 플래너가 1행으로 잘못 추정해서, 사용자가 속한 채널 수천 개를 구동 측으로 펼치고 채널마다 인덱스를 탐색했습니다.
엔진 문서가 이미 키를 들고 있습니다
그래서 엔진 문서가 인덱스 선두 키를 이미 갖고 있는지 확인했습니다. 색인기가 링크 행 하나당 문서 하나를 만들고, channel_id 와 post_id 를 그 행에서 그대로 싣고 있었습니다. 그렇다면 후보의 (channel_id, post_id) 쌍이 곧 인덱스 선두 두 컬럼입니다.
그 쌍으로 먼저 행을 뽑는 서브쿼리를 출처(FROM)로 끼웁니다. 추가 조건(AND)으로 넣으면 안 됩니다. 여기에 OFFSET 0(또는 DISTINCT ON)을 붙여 서브쿼리가 풀리지 않게 하면, 바깥 권한 조인보다 먼저 작은 집합이 확정됩니다.
권한 게이트(멤버십, 가시성)는 바깥에 그대로 둡니다. 바뀐 건 출처뿐이라 인가가 약해지지 않습니다.
SELECT l.*
FROM (SELECT DISTINCT ON (x.link_id) x.*
FROM (
SELECT e.*
FROM unnest($1::bigint[], $2::bigint[]) AS p(chan, post)
JOIN file_links e ON e.channel_id = p.chan AND e.post_id = p.post
WHERE e.file_id = ANY($3::bigint[])
UNION ALL -- 엔진이 안 준 행 중 꼭 필요한 것(예: 즐겨찾기)은 PK 로
SELECT e.*
FROM file_links e
WHERE e.link_id = ANY($4::bigint[]) AND e.file_id = ANY($3::bigint[])
) x
ORDER BY x.link_id
OFFSET 0) l
WHERE EXISTS (SELECT 1 FROM memberships m
WHERE m.channel_id = l.channel_id AND m.user_id = $5);같은 결과를 내는 요청 기준으로 이렇게 줄었습니다.
| 항목 | 이전 | 이후 |
|---|---|---|
| heap 방문 행 | 8,895 | 44 |
| 공유 버퍼 | 5,365 | 1,413 |
채널 크기에 비례하던 비용이 후보 수에 비례하게 바뀐 겁니다. 이미 있는 인덱스를 타게 하는 쪽으로 풀었습니다.
쓰기 전에 확인할 것
- 전제는 엔진 문서와 테이블 행이 같은 키를 쓴다는 점입니다. 색인기 코드로 직접 확인해야 합니다. 엔진이 첨부당 문서 하나로 접거나 다른 번호를 주면, 펜스는 그 행을 조용히 놓칩니다. 확인되지 않은 백엔드는 이전 방식으로 내려 둡니다.
- 쌍 중 하나라도 키가 없으면 펜스를 쓰지 않습니다. 쌍만 골라내고 진행하면 그 후보가 결과에서 사라집니다.
- 의미가 조금 달라집니다. 이전 방식은 같은 채널의 아무 글에 걸린 같은 파일을 대체 후보로 삼았지만, 펜스는 엔진이 돌려준 글만 봅니다. 차이는 엔진 결과 창 밖으로 밀린 글에서만 납니다.