숏컷 코드
SELECT id, title, created_at
FROM posts
WHERE published = true
ORDER BY created_at DESC;문법
SELECT 절에 열을 명시하면 불필요한 데이터 전송을 줄인다
SELECT * 는 테이블의 모든 컬럼을 읽어 클라이언트로 전송합니다. 컬럼 수가 많거나 TEXT, JSONB 같은 대용량 타입이 포함된 테이블에서는 실제로 필요하지 않은 데이터까지 읽어 네트워크 전송량과 메모리 사용량이 늘어납니다. 필요한 컬럼만 명시하면 커버링 인덱스(Index Only Scan)를 활용할 수 있는 기회도 생깁니다. 즉 SELECT id, title FROM posts WHERE published = true 처럼 조회하고 (published, id, title) 인덱스가 있으면 테이블 힙을 읽지 않고 인덱스만으로 결과를 만들 수 있습니다.
-- 필요한 컬럼만 명시
SELECT id, title, created_at
FROM posts
WHERE published = true;
-- 커버링 인덱스 활용 예시
CREATE INDEX idx_posts_published_cover
ON posts (published, id, title, created_at)
WHERE published = true;SELECT, WHERE, ORDER BY는 조회에서 각각 다른 역할을 맡는다
SELECT는 어떤 열을 가져올지, WHERE는 어떤 행만 남길지, ORDER BY는 결과를 어떤 순서로 보여줄지를 정합니다. 이 세 절은 자주 함께 나오지만 평가 역할이 다르기 때문에 섞어 생각하면 쿼리 의도가 흐려집니다. "열 선택", "행 필터", "결과 정렬"을 따로 구분해 쓰는 습관이 기본입니다.
SELECT id, title -- 열 선택
FROM posts
WHERE published = true -- 행 필터
ORDER BY created_at DESC; -- 결과 정렬WHERE, ORDER BY, LIMIT은 조회 단계가 다르다
WHERE는 행 수를 줄이고, ORDER BY는 남은 행의 순서를 정하고, LIMIT은 그중 앞부분만 잘라냅니다. LIMIT 20이 있다고 해서 먼저 20행을 뽑은 뒤 정렬하는 것이 아닙니다. 특히 페이징 쿼리는 필터 조건과 정렬 기준이 먼저 안정적으로 정해져 있어야 결과가 흔들리지 않습니다.
-- 최근 공개 글 20개
SELECT id, title, created_at
FROM posts
WHERE published = true
ORDER BY created_at DESC, id DESC
LIMIT 20;WHERE 조건의 선택성이 실행 계획을 결정한다
WHERE 절에 쓰인 조건의 선택성(전체 행 대비 조건을 만족하는 행의 비율)이 낮을수록 인덱스 스캔이 유리하고, 선택성이 높으면(대부분의 행이 조건을 통과하면) Sequential Scan 이 더 효율적입니다. PostgreSQL 옵티마이저는 ANALYZE 로 수집된 통계를 기반으로 이 판단을 합니다. 통계가 오래됐거나 ANALYZE 가 실행되지 않았다면 옵티마이저가 잘못된 계획을 선택할 수 있습니다.
-- 실행 계획 확인
EXPLAIN ANALYZE
SELECT id, title
FROM posts
WHERE published = true AND user_id = 42;
-- 복합 조건에 맞는 인덱스
CREATE INDEX idx_posts_user_published
ON posts (user_id, published);ORDER BY 정렬은 인덱스 순서와 일치할 때 별도 정렬 비용이 없다
ORDER BY 가 있을 때 인덱스의 정렬 방향(ASC/DESC)이 일치하면 PostgreSQL 은 인덱스를 순서대로 읽는 것만으로 정렬을 완료합니다. 이 경우 실행 계획에 별도 Sort 노드가 나타나지 않습니다. 반면 인덱스가 없거나 정렬 방향이 다르면 결과를 메모리 또는 디스크에서 정렬하는 추가 비용이 생깁니다. 대용량 테이블에서 정렬 기준 컬럼에 인덱스가 없으면 work_mem 초과로 디스크 정렬이 발생할 수 있습니다.
-- 인덱스 정렬 방향과 ORDER BY 방향 맞추기
CREATE INDEX idx_posts_created_desc ON posts (created_at DESC);
-- NULL 정렬 위치 명시 (NULLS FIRST / NULLS LAST)
SELECT id, title, published_at
FROM posts
ORDER BY published_at DESC NULLS LAST;LIMIT 과 OFFSET 의 조합은 대용량 페이징에서 성능 함정이 된다
LIMIT n OFFSET m 은 m 번째 행까지 모두 읽은 뒤 버리고 n 개만 반환합니다. OFFSET 이 커질수록 버리는 행이 많아져 실제로 페이지를 넘길수록 점점 느려집니다. 대용량 페이징에는 마지막으로 읽은 행의 정렬 기준값을 WHERE 절로 넘기는 커서 기반 페이징(keyset pagination)이 성능상 우월합니다.
-- OFFSET 기반 (페이지가 뒤로 갈수록 느려짐)
SELECT id, title FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 10000;
-- 커서 기반 페이징 (성능 일정)
SELECT id, title FROM posts
WHERE created_at < '2024-01-01 00:00:00'
ORDER BY created_at DESC
LIMIT 20;선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 모든 컬럼이 필요할 때 | SELECT * (개발 편의 목적에 한정) |
| 특정 컬럼만 필요할 때 | 컬럼 명시 + 커버링 인덱스 검토 |
| 선택성이 낮은 조건 필터링 | 해당 컬럼에 인덱스 추가 |
| 정렬 비용을 없애고 싶을 때 | 정렬 방향과 일치하는 인덱스 사용 |
| 대용량 데이터 페이징 | OFFSET 대신 커서 기반 페이징 |
주의할 점
SELECT * 는 컬럼이 추가되거나 대용량 타입이 포함될 때 예상보다 많은 데이터를 읽고 전송합니다.
ORDER BY 는 인덱스 정렬 방향과 맞지 않으면 별도 정렬 비용이 발생하고, 대용량에서는 디스크 정렬로 이어질 수 있습니다.
LIMIT ... OFFSET 은 페이지가 뒤로 갈수록 선형적으로 느려지므로 대용량 페이징에는 커서 기반 패턴을 써야 합니다.
SELECT id, title
FROM posts
WHERE published = true;ORDER BY가 없으면 반환 순서는 보장되지 않습니다. 지금은 우연히 삽입 순서처럼 보여도, 인덱스 사용 여부나 실행 계획이 바뀌면 결과 순서가 달라질 수 있습니다.
SELECT id, title, created_at
FROM posts
WHERE published = true
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;created_at 값이 같은 행이 많으면 페이지를 넘길 때 중복이나 누락이 생길 수 있습니다. 페이징 정렬은 ORDER BY created_at DESC, id DESC처럼 항상 안정적인 보조 키를 함께 두는 편이 안전합니다.
참고 링크
1 sources