- AI와 빅데이터로 건설업계의 디지털 혁신을 이끄는 (주)산군의 Jun Kang입니다. 대규모 데이터 처리, DB 최적화, 확장 가능한 시스템 구축에 관심이 많으며, 개발팀의 건강하고 지속 가능한 성장 문화를 문화를 만들어가고 있습니다.
- 주요 관심사: Backend Development, Cloud Computing, DevOps, Database, AI, 개발 문화 형성
[PostgreSQL] 파티셔닝이 성능을 올리지 않는 경우
“테이블이 커서 파티셔닝했다"는 말을 자주 듣는다. 그리고 그 뒤에 “그런데 안 빨라졌다"가 따라오는 경우도 자주 본다. 파티셔닝은 쿼리를 빠르게 하는 기능이 아니다. 조건에 맞는 파티션만 읽게 하는 기능이다. 그 조건이 안 갖춰지면 이득은 없고 비용만 남는다. 파티션 프루닝 주문 테이블을 월별로 나눴다고 하자. CREATE TABLE orders ( id bigint, created_at timestamptz NOT NULL, ... ) PARTITION BY RANGE (created_at); CREATE TABLE orders_2026_08 PARTITION OF orders FOR VALUES FROM ('2026-08-01') TO ('2026-09-01'); CREATE TABLE orders_2026_09 PARTITION OF orders FOR VALUES FROM ('2026-09-01') TO ('2026-10-01'); 이 쿼리는 9월 파티션 하나만 읽는다. ...
[PostgreSQL] 인덱스가 있는데 안 쓰는 진짜 이유
시리즈 마지막이다. 앞의 두 편에서 행 수 추정을 맞추는 이야기를 했다. 이번엔 추정이 맞아도 인덱스를 안 쓰는 경우다. 인덱스를 만들었는데 EXPLAIN에 여전히 Seq Scan이 나온다. 이때 먼저 가를 것이 있다. 플래너가 인덱스를 못 쓰는 건지, 안 쓰는 건지. 못 쓰는 경우 조건의 모양이 인덱스와 안 맞으면 플래너는 선택지 자체가 없다. 컬럼에 함수를 씌웠다. WHERE lower(email) = 'a@b.com' -- email 인덱스 못 씀 WHERE date(created_at) = '2026-09-01' -- created_at 인덱스 못 씀 WHERE created_at + interval '1 day' > now() 인덱스는 email의 값으로 정렬돼 있지 lower(email)의 값으로 정렬돼 있지 않다. 표현식 인덱스를 만들거나, 조건을 컬럼이 그대로 남는 형태로 바꾼다. ...
[PostgreSQL] 두 컬럼이 서로 얽혀 있으면 플래너는 수백 배 틀린다
지난 편은 컬럼 하나의 통계가 틀리는 경우였다. 이번 편은 컬럼 하나하나는 정확한데 함께 쓰면 틀리는 경우다. 이쪽이 더 자주, 더 크게 빗나간다. 독립 가정 주소 테이블이 있다. sido(시도)와 sigungu(시군구) 컬럼이 있다. SELECT * FROM addresses WHERE sido = '서울특별시' AND sigungu = '강남구'; 플래너는 이렇게 계산한다. sido = '서울특별시'인 비율: 최빈값 목록에서 찾음. 20%라고 하자. sigungu = '강남구'인 비율: 최빈값 목록에서 찾음. 2%라고 하자. 둘 다 만족하는 비율: 20% × 2% = 0.4% 그런데 강남구는 서울에만 있다. sigungu = '강남구'인 행은 전부 sido = '서울특별시'다. 실제 비율은 2%다. 플래너는 5배 적게 추정했다. ...
[PostgreSQL] 플래너는 통계를 보고 결정한다 — 그 통계는 언제 틀리나
EXPLAIN ANALYZE를 보면 두 숫자가 나란히 있다. rows=100과 actual rows=1500000. 플래너는 백 행이 나올 줄 알고 계획을 짰는데 실제로는 백오십만 행이 나왔다. 그 계획은 백 행에 맞는 계획이었을 테니, 결과는 느린 쿼리다. 플래너가 왜 백 행이라고 생각했는지를 따라가면 통계에 닿는다. 이 시리즈는 그 통계 이야기다. 통계는 표본이다 플래너는 실행 전에 테이블을 읽지 않는다. ANALYZE가 미리 만들어둔 요약을 본다. ANALYZE는 테이블 전체를 읽지 않는다. 표본을 뽑는다. 표본 크기는 default_statistics_target × 300이고, 기본값이 100이라 3만 행이다. 테이블이 천 행이든 십억 행이든 3만 행이다. ...
[PostgreSQL] SKIP LOCKED로 작업 큐 만들기
잠금 시리즈 마지막 편이다. 앞의 두 편이 잠금 때문에 멈추는 이야기였다면, 이번엔 잠금을 이용해서 일을 나누는 이야기다. 문제 작업 테이블이 있다. 워커 여러 대가 여기서 하나씩 집어가서 처리한다. CREATE TABLE jobs ( id bigserial PRIMARY KEY, status text NOT NULL DEFAULT 'pending', payload jsonb, created_at timestamptz NOT NULL DEFAULT now() ); 순진하게 짜면 이렇게 된다. SELECT * FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1; UPDATE jobs SET status = 'running' WHERE id = ?; 워커 둘이 동시에 SELECT하면 같은 행을 본다. 둘 다 UPDATE하고 둘 다 처리한다. 중복이다. ...