
(이것은 충격 실화입니다...)
때는 어제.
회사에서 진행중인 웹 서비스의 홈페이지 트래픽이 기하급수적으로 늘어날 위기에 처했고,
그래서 늘어날 트래픽에 대비해 해당 페이지에서 요청하는 API를 모두 점검했다.

적잖은 API와 API가 거쳐가는 수많은 쿼리를 하나씩 점검하던 중...

예?
무려 30만짜리 쿼리 비용을 가진 초대형 쿼리를 발견했다.
SELECT
*
FROM
"Post"
WHERE
"published" = TRUE AND "publishedAt" < NOW()
ORDER BY
"createdAt" NULLS LAST DESC AND "id" DESC
OFFSET
X
LIMIT
Y;
대충 쿼리 로직은 이러했다.
그닥 복잡하지 않았고 어려울 게 없었다.

이미 인덱스가 주어졌으나,
publishedAt이 아닌 createdAt으로 생성되었기 때문에 인덱스를 타지 못했다.
그래서 단순하게
(published, publishedAt)과 (createdAt, id) 복합 인덱스를 설정했다.
DB에 스키마를 직접 때려넣고 실행해 본 결과,
비용은 줄어들지 않았다...

(도대체 왜 ㅠ.ㅠ)
우선은 (당연히) GPT를 갈궜다.
왜 인덱스를 추가했는데도 비용이 줄어들지 않냐고.
왜 이 문제를 해결해주지 못하냐고.
다 너 때문이라고... 책임지라고...
그래서 결국 몇 가지 원인을 찾았는데,
그 중 진짜 원인은 NULLS LAST 조건이었다.
인덱스는 주어진 조건에 따라 데이터를 미리 정렬해서 저장한다.
그래서 ORDER BY에서 인덱스를 타게 되면 정렬을 수행하지 않고 데이터를 읽어올 수 있다.
그리고 나는 정렬에 쓰이는 컬럼을 빠짐없이 createdAt, id 순서대로 인덱스에 추가했다.
(왜 순서를 맞춰야 하는지에 대한 의문이 생긴다면 이 글을 참고하길 바란다)
그러나 NULLS LAST는 미리 결정된 정렬 기준을 바꿀 수도 있다.
DESC였다면
{ 3, 2, 1, NULL ... }이 그대로 유지되지만
ASC였다면{ NULL, 1, 2, 3 ... }이{ 1, 2, 3, ... NULL }로 바뀐다.
그리고 나는 createdAt과 id를 DESC로 인덱스를 생성했다.
그러면 DESC는 순서가 바뀌지 않으므로 NULLS LAST와 별개로 인덱스를 타야 하지만,
그렇지 않았다...
반대로 NULLS LAST를 제거하자마자 인덱스를 타서

1/10000로 줄고 말았다.
결국 정확한 원인은 파악하지 못했지만,
NULLS LAST를 명시하게 되면 인덱스와 별개로 정렬이 발생하는 것 같다는 가설만 가지고 있다.

(그래서?)
ORDER BY 정렬 조건에 NULLS FIRST, NULLS LAST 등을 추가할 경우
인덱스를 타는 데 제약이 있다.
그래서 내 경우와 동일한 DESC NULLS LAST는 NULLS LAST 조건을 제외할 수 있다.
다른 경우에는 처음부터 인덱스를 설정할 때 명시적으로 지정해도 좋을 듯하다.