본문 바로가기
빌드노트 검색

빌드노트

PostgreSQL에서 날짜 내림차순인데 NULL 행이 맨 위에 나온다

PostgreSQL의 DESC는 기본적으로 NULL을 앞에 둔다. 네 개의 합성 행으로 ASC·DESC·NULLS LAST와 LIMIT 1을 비교하고, 동률 보조 키와 날짜 없는 행을 제외할 조건을 정한다.

PostgreSQL에서 ORDER BY updated_at DESC LIMIT 1로 최신 기록을 골랐는데 날짜가 없는 행이 나온다면 NULL 배치를 확인한다. PostgreSQL의 DESC 정렬은 기본적으로 NULL을 앞에 둔다. 날짜가 있는 행을 먼저 고르려면 NULLS LAST를 적고, 같은 시각의 행이 여러 개라면 그중 하나를 고를 보조 키도 정해야 한다.

현재 조회의 정렬과 제한 부분을 다음처럼 바꾸면, 날짜가 있는 행 중 최신 시각을 먼저 놓고 같은 시각에서는 큰 ID를 고른다.

ORDER BY updated_at DESC NULLS LAST, id DESC
LIMIT 1;

여기서 id는 조회 후보 안에서 NULL이 없고 유일한 키여야 한다. 큰 ID가 실제로 더 늦게 수정됐다는 뜻은 아니다. 시각이 같을 때 한 행을 선택하기 위해 정한 규칙이며, 업무에서 다른 우선순위를 쓴다면 그 기준으로 바꾼다.

PostgreSQL 18의 ORDER BY 문서 (새 탭에서 열림)에 따르면 기본 NULL 배치는 DESC에서 FIRST, ASC에서 LAST다. DESC만 보고 “최신 날짜가 무조건 첫 행”이라고 해석하면 이 기본값을 놓친다. NULLS LAST는 날짜 값의 내림차순은 유지하면서 NULL의 위치를 뒤로 지정한다.

네 행으로 방향과 NULL 배치를 비교했다

별도 임시 컨테이너의 PostgreSQL 18.6에서 다음 입력을 실행했다. 직접 비교하려면 PostgreSQL에 연결된 psql이나 SQL 콘솔에서 같은 연결을 유지하며 실행한다. 임시 테이블을 만들 권한이 필요하고, sort_lab이라는 임시 테이블이 없는 연결을 사용한다. 시각은 시간대 없는 timestamp이며, 2번과 4번은 같은 최신 시각이다.

다음 입력은 과거·최근·NULL·같은 시각의 네 행을 준비한다.

CREATE TEMP TABLE sort_lab (
  id integer PRIMARY KEY,
  updated_at timestamp
);
INSERT INTO sort_lab VALUES
  (1, TIMESTAMP '2026-01-01 09:00:00'),
  (2, TIMESTAMP '2026-10-20 09:00:00'),
  (3, NULL),
  (4, TIMESTAMP '2026-10-20 09:00:00');

같은 입력에서 아래 세 조회로 날짜 방향과 NULL 배치를 비교한다. id DESC는 같은 날짜끼리의 순서만 고정한다.

SELECT id, updated_at FROM sort_lab
ORDER BY updated_at ASC, id DESC;

SELECT id, updated_at FROM sort_lab
ORDER BY updated_at DESC, id DESC;

SELECT id, updated_at FROM sort_lab
ORDER BY updated_at DESC NULLS LAST, id DESC;

실제 결과에서 ID 순서만 옮기면 다음과 같다. SQL 도구에 따라 NULL이 빈칸으로 보일 수 있으므로, 빈칸 자체를 빈 문자열이라고 판단하지 않는다.

  • ASC, id DESC: 1 → 4 → 2 → 3. 과거 날짜부터 나오고 NULL은 마지막이다.
  • DESC, id DESC: 3 → 4 → 2 → 1. NULL이 맨 앞이고 그다음이 최신 날짜다.
  • DESC NULLS LAST, id DESC: 4 → 2 → 1 → 3. 최신 날짜부터 나오며 NULL은 마지막이다.

명시적인 ASC NULLS LAST와 DESC NULLS FIRST도 각각 실행했고, 기본 ASC 및 DESC와 같은 순서를 확인했다. 방향과 NULL 배치는 따로 선택하는 옵션이다. NULL을 뒤로 보내려고 ASC로 바꾸면 실제 날짜까지 과거순으로 바뀐다.

LIMIT 1은 동률까지 해결해 주지 않는다

같은 테이블에서 문제의 조회와 수정한 조회가 고르는 한 행을 비교한다.

SELECT id, updated_at FROM sort_lab
ORDER BY updated_at DESC LIMIT 1;

SELECT id, updated_at FROM sort_lab
ORDER BY updated_at DESC NULLS LAST, id DESC LIMIT 1;

첫 조회는 id=3, updated_at=NULL, 두 번째는 id=4, updated_at=2026-10-20 09:00:00을 반환했다. LIMIT 1은 앞에 놓인 행 하나만 남길 뿐, NULL을 건너뛰거나 최신성을 새로 판단하지 않는다.

보조 키 없이 ORDER BY updated_at DESC NULLS LAST LIMIT 1만 실행했을 때는 2번이 나왔다. 이 한 번의 결과가 같은 시각이면 작은 ID를 고른다는 보장은 아니다. PostgreSQL 18의 LIMIT 문서 (새 탭에서 열림)는 제한된 결과를 예측하려면 ORDER BY로 유일한 순서를 정해야 한다고 설명한다.

보조 키를 id ASC로 정한 조회에서는 2번, id DESC로 정한 조회에서는 4번이 선택됐다. 삽입 순서를 뒤집은 별도 비교에서도 DESC NULLS LAST, id DESC는 4번을 골랐다. NULL 배치를 고치는 결정과 같은 시각의 우선순위를 정하는 결정이 모두 필요하다.

NULL을 뒤로 보낼지, 후보에서 뺄지

NULLS LAST는 NULL 행을 제거하지 않는다. 네 행의 날짜를 모두 NULL로 바꾼 추가 비교에서는 수정한 정렬도 id=4, updated_at=NULL을 반환했다. 날짜가 있는 후보가 하나도 없으면 NULL 행이 첫 행이 될 수 있다.

“날짜가 있는 기록 중 최신 하나”만 허용하고, 전부 NULL이면 결과가 없어야 한다면 필터를 추가한다. 아래 조회는 기존 입력에서 4번을 반환했고, 모든 날짜를 NULL로 바꾼 입력에서는 0행이었다.

SELECT id, updated_at FROM sort_lab
WHERE updated_at IS NOT NULL
ORDER BY updated_at DESC, id DESC
LIMIT 1;

자기 쿼리에 적용할 때는 기존 WHERE 조건에 AND updated_at IS NOT NULL을 결합하고, 조회 대상의 날짜 열과 유일한 보조 키를 사용한다. 목록 끝에 날짜 없는 행을 남겨야 하면 NULLS LAST, 날짜가 있는 후보만 허용하면 IS NOT NULL이라는 차이를 먼저 정한다.

수정 뒤에도 예상한 기록이 안 나오면 LIMIT을 잠시 빼고, 실제 필터를 통과한 후보의 날짜와 보조 키를 함께 본다. 기대한 행이 필터에 걸렸는지, 같은 시각의 행이 있는지, 바깥 쿼리가 다시 정렬하는지 확인한다. 이때도 NULL 표시는 도구마다 다를 수 있지만 선택된 ID로 결과를 비교할 수 있다.

이번 관찰은 네 개의 합성 행으로 전체 결과의 첫 행을 고른 단일 연결 실험이다. 그룹별 대표행 선택, 시간대 변환, 동시 변경에 따른 결과 차이와 실제 테이블의 인덱스 성능은 검증하지 않았다.

← 전체 글 보기