한국어 검색이 LIKE ‘%키워드%’로 버틸 때, pg_bigm과 전문검색 비교

게시판 검색창에 LIKE ‘%키워드%’ 조건을 넣어두고 서비스를 시작했는데, 글이 몇만 건쯈 쌓이자 검색 버튼만 누르면 화면이 몇 초씩 멈추는 경험을 겪어보신 분들이 적지 않을 겁니다. 한국어 검색이 LIKE ‘%키워드%’로 버티는 구간은 의외로 금방 끝나는데, 이때 바로 풀 텍스트 검색 엔진으로 갈아타기보다 pg_bigm 확장을 올려서 기존 LIKE 쿼리에 인덱스만 태우는 선택지를 먼저 검토해 볼 만합니다. 이 … Read more

deadlock detected 로그를 받았을 때 잠금 순서 복원하는 법

결론부터 말씀드리면, deadlock detected 로그를 받았을 때 가장 먼저 할 일은 에러 메시지를 지우거나 재시도 로직을 추가하는 게 아니라 로그에 적힌 두 트랜잭션의 잠금 순서를 그대로 복원하는 것입니다. 어느 트랜잭션이 어떤 리소스를 먼저 잠그고 어떤 리소스를 기다리다가 되돌아갔는지를 종이에 그려보면, 코드에서 잠금 순서를 어디부터 맞춰야 하는지가 보입니다. 이 글에서는 MySQL InnoDB와 PostgreSQL을 기준으로 로그를 읽는 … Read more

페이지가 뒤로 갈수록 느려질 때 쓰는 커서 페이지네이션

게시판이나 상품 목록에서 1페이지는 빠른데 50페이지, 100페이지로 넘어갈수록 응답이 눈에 띄게 느려지는 현상은 대부분 OFFSET을 쓰는 쿼리 구조 때문에 생깁니다. 페이지가 뒤로 갈수록 느려지는 이유는 간단합니다. DB가 매 요청마다 앞쪽 행을 전부 읽어서 세고, 그중 필요 없는 부분을 버린 다음에야 실제로 원하는 20개를 돌려주기 때문입니다. 이 글에서는 왜 이런 구조가 생기는지, 그리고 커서(키셋) 페이지네이션으로 바꾸면 … Read more

로그 테이블이 수억 행일 때, 파티셔닝이 정말 이득인지 따져봅니다

결론부터 말씀드리면, 로그 테이블이 수억 행 규모로 커졌다고 해서 파티셔닝부터 꺼내 드는 게 항상 정답은 아닙니다. 조회 조건이 거의 매번 날짜 범위로 걸리고, 오래된 로그를 주기적으로 지워야 하는 구조라면 파티셔닝이 확실한 효과를 냅니다. 반대로 전체 기간을 가로지르는 집계 쿼리가 많거나 파티션 관리를 자동화할 여력이 없다면, 이득보다 운영 부담이 먼저 커질 수 있습니다. 수억 행 로그 … Read more

JSONB에 다 넣기 전에 점검할 GIN 인덱스·TOAST 갈림길

테이블 설계를 할 때 속성을 JSONB에 다 넣기로 정하면 당장은 편합니다. 하지만 조회량이 늘어나는 순간부터 정규 컬럼과 체감 성능 차이가 벌어지기 시작합니다. 자주 필터링하거나 정렬하는 값을 JSONB 안에 묻어두면 GIN 인덱스가 생각보다 커지고, 값 하나가 조금만 길어져도 TOAST 테이블로 분리 저장되면서 읽기 비용이 따라붙습니다. 이 글은 PostgreSQL 12 이상, 기본 페이지 크기(8KB) 환경을 기준으로 정규 … Read more

SQLite 파일을 cp로 백업하면 깨지는 이유, VACUUM INTO와 백업 API로 해결하기

“SQLite는 파일 하나짜리 DB니까 cp 명령으로 복사만 해두면 백업이 끝난다”고 생각하는 분이 많습니다. 하지만 SQLite 파일을 cp로 백업하면 깨지는 경우가 실제로 있고, 그 이유는 cp가 SQLite 엔진의 잠금 상태나 WAL(Write-Ahead Log) 파일을 전혀 알지 못하기 때문입니다. 결과만 먼저 말씀드리면, 쓰기 작업이 전혀 없는 완전히 닫힌 DB를 복사할 때는 cp로도 문제가 없지만, 서비스가 돌아가는 중이거나 체크포인트가 … Read more

백업은 있는데 복구를 못 할 때, pg_dump와 PITR 중 무엇을 써야 할까요

백업은 있는데 복구를 못 하는 상황은 대부분 백업 방식과 복구 시간(RTO)을 미리 맞춰보지 않아서 생깁니다. pg_dump로 받은 백업은 논리적 스냅숏이라 특정 시점을 골라서 복구할 수 없고, PITR(Point-In-Time Recovery)은 원하는 시점을 고를 수 있는 대신 장애가 나기 전부터 WAL 아카이빙을 켜두지 않았다면 아예 쓸 수 없습니다. 이 글은 PostgreSQL 13 이상에서 recovery.signal / standby.signal 방식을 쓰는 … Read more

행을 지웠는데 디스크가 그대로인 이유, 데드 튜플과 오토배큠 튜닝으로 해결하기

DELETE 쿼리를 수십만 건 실행했는데 df -h로 확인한 디스크 사용량이 줄지 않아서 검색하셨다면, 결론은 간단합니다. PostgreSQL은 행을 지워도 그 공간을 바로 운영체제에 돌려주지 않습니다. 삭제된 행은 “데드 튜플”로 테이블 파일 안에 그대로 남아 있고, 이걸 치우는 오토배큠(autovacuum)이 제대로 돌지 않으면 테이블 파일 크기는 줄어들지 않습니다. 이 글은 PostgreSQL 13 이상, 단일 테이블 기준 autovacuum 파라미터 … Read more

기본키를 UUID로 했더니 느려질 때 — UUIDv4·UUIDv7·BIGSERIAL 비교

기본키를 UUID로 했더니 인서트가 느려질 수 있다는 이야기, 실제로 겪어보면 사실일까요? 조건만 맞으면 사실입니다. 완전히 무작위로 생성되는 UUIDv4를 기본키로 쓰면 B-Tree 인덱스에 값이 뒤죽박죽 꽂히면서 페이지 분할과 캐시 미스가 늘어나고, 테이블이 수백만 행을 넘기는 시점부터 그 차이가 체감될 만큼 커집니다. 이 문제는 시간 순서를 담은 UUIDv7이나 애초에 정수를 쓰는 BIGSERIAL로 기본키를 바꾸면 상당 부분 줄어듭니다. … Read more

인덱스를 만들었는데 Seq Scan이 뜨는 이유, EXPLAIN으로 좁히기

인덱스를 만들었는데 Seq Scan이 그대로 뜨는 상황 때문에 이 글을 검색하셨다면, 답은 대부분 “인덱스가 안 걸려서”가 아니라 “플래너가 비용 계산상 인덱스 쪽이 더 비싸다고 판단해서”입니다. PostgreSQL의 쿼리 플래너는 인덱스 존재 여부와 상관없이 통계 정보, 테이블 크기, 선택도를 종합해 더 저렴한 경로를 고릅니다. 그래서 인덱스를 지우는 것보다 EXPLAIN으로 플래너가 어떤 수치를 보고 있는지 먼저 확인하는 쪽이 … Read more