#DB
「DB」 태그가 붙은 글 41편이다.
글 목록
41편 중 1–41
느린 쿼리 1등을 고쳤는데 왜 그대로일까
느린 쿼리 1등을 3초에서 0.5초로 줄였는데 서비스 체감은 그대로였다. 하루 500만 번 도는 50ms 쿼리가 총 69시간. 진짜 짐은 그쪽이었다.
쿼리는 그대로인데 조인이 100배 느려졌다
쿼리도 인덱스도 그대로인데 리포트 쿼리가 어느 날부터 100배 느려졌다. EXPLAIN 을 보니 조인 순서가 뒤집혀 있었다. 뒤집힌 이유는 조인이 아니라 행 수 추정에 있었다.
Index Scan 이라고 찍혔는데 왜 3초나 걸릴까
EXPLAIN 에 인덱스 스캔이 찍혔는데도 쿼리가 3초씩 걸린다. 인덱스를 타는 건 시작일 뿐이다. 시간이 어디로 새는지는 계획 아래 숫자 몇 개가 말해 준다.
다시 만들었더니 인덱스가 절반이 됐다, 인덱스 팽창
테이블은 그대로인데 인덱스만 몇 달에 걸쳐 조금씩 커진다. 다시 만들어 보면 크기가 절반으로 줄기도 한다. 그동안 늘린 서버 비용이 사실은 빈 공간 값이었다.
COUNT(*) 는 결국 다 세는 일이다
목록 스무 개는 10ms 에 나오는데 전체 개수를 세는 데는 3초가 걸린다. 인덱스를 더해도 크게 안 준다. 이 비용은 구조에서 나온다. 고칠 곳은 쿼리보다 화면 쪽일 때가 많다.
NOT IN 결과가 0건, 범인은 NULL 하나
NOT IN 으로 걸렀는데 에러도 없이 결과가 0건이다. 데이터는 분명히 있는데. NULL 하나가 결과를 통째로 비우고, DB 를 옮기면 정렬 순서까지 바뀐다.
OR 하나 붙였더니 인덱스를 안 탄다
검색 조건에 OR 하나를 더했을 뿐인데 쿼리가 갑자기 느려진다. AND 로 바꾸면 멀쩡하다. 어떤 OR 은 괜찮고 어떤 OR 만 탈이 나는지는 같은 컬럼이냐 다른 컬럼이냐로 갈린다.
왜 하필 여섯 번째 실행부터 쿼리가 느려질까
같은 쿼리가 다섯 번까지는 빠르다가 여섯 번째부터 느려지고, 서버를 재시작하면 다시 빨라진다. 우연 같은 이 숫자는 PostgreSQL 옵티마이저가 정해 둔 규칙이다.
트래픽 몰릴 때만 느린 정렬, work_mem 이 곱해진다
정렬이 들어간 쿼리가 평소엔 빠른데 트래픽이 몰리는 시간에만 수십 배 느려진다. 혼자 재현하면 멀쩡하다. 메모리 설정을 크게 잡으면 낫겠다 싶지만, 그러다 서버가 죽기도 한다.
인덱스에 다 넣었는데 PostgreSQL 이 테이블을 또 읽는다
필요한 컬럼을 전부 인덱스에 넣었는데 PostgreSQL 이 여전히 테이블을 읽는다. 대량 갱신 직후엔 느리다가 한참 지나면 저절로 빨라진다. 그 「한참」 사이에 VACUUM 이 하는 일이 있다.
LIMIT 10 은 0.1초인데 LIMIT 100 은 왜 12초일까
LIMIT 10 을 붙였더니 오히려 느려졌다. LIMIT 10 은 0.1초인데 LIMIT 100 은 12초가 걸리기도 한다. 옵티마이저가 LIMIT 값을 보고 계획을 통째로 바꿨다.
EXPLAIN ANALYZE 는 추정이 빗나간 곳을 보라고 있다
느린 쿼리를 잡으려고 EXPLAIN 을 열어도 숫자가 무슨 뜻인지 모르겠다. 인덱스 스캔인데도 느리면 더 막막하다. 먼저 볼 건 추정 행 수와 실제 행 수다.
readOnly 를 안 붙이면 읽기 복제본이 논다
읽기 복제본을 붙였는데 주 서버 부하가 그대로였다. 라우팅 설정도 맞았다. 코드에 SELECT 만 있어도 읽기 전용이라고 선언하지 않으면 쓰기로 취급된다.
백만 건 배치를 트랜잭션 하나로 감쌌더니 두 시간이 날아갔다
백만 건 야간 배치를 트랜잭션 하나로 감쌌다. 도는 두 시간 동안 서비스가 느려졌고, 중간에 실패하면 처음부터 다시 두 시간이었다.
낙관적 락이냐 비관적 락이냐, 충돌률이 정한다
낙관적 잠금은 빠르고 비관적 잠금은 안전하다는 말, 둘 다 맞게 들려서 더 헷갈린다. 실제로는 충돌이 얼마나 자주 나느냐에 따라 유불리가 뒤집힌다.
따로 보이던 장애 셋, 범인은 긴 트랜잭션 하나였다
커넥션 부족, 테이블 팽창, 락 경합이 한꺼번에 났다. 셋을 하나씩 따로 조사했는데 알고 보니 원인은 하나였다. 트랜잭션이 길다는 것.
알림은 왔는데 주문이 없다, 아웃박스 패턴
주문을 저장하고 바로 알림 이벤트를 보냈다. 가끔 알림 서비스가 「그런 주문 없는데요」라고 했다. 취소된 주문 알림이 나가기도 했다. 순서를 바꿔도 안 풀린다.
워커를 늘려도 그대로인 DB 큐, SKIP LOCKED 로 푼다
DB 테이블로 만든 작업 큐에 워커를 다섯으로 늘렸다. 처리량은 한 대일 때와 똑같았다. 다섯이 전부 맨 앞 작업 하나 앞에 줄을 서고 있었다.
CPU 도 DB 도 한가한데 앱이 멈췄다, REQUIRES_NEW
로그만은 남기려고 저장 메서드를 별도 트랜잭션으로 뺐다. 그런데 트래픽이 오르자 CPU 도 DB 도 한가한데 앱이 멈췄다. 요청 하나가 커넥션 둘을 쥐고 있었다.
@Transactional 을 붙였는데 롤백이 안 된다
트랜잭션 애너테이션을 붙였는데 예외가 나도 롤백이 안 된다. 오타도 없고 임포트도 맞다. 로그를 켜 보면 트랜잭션이 아예 시작되지 않았다.
쓰기 스큐, 각자는 옳은데 합치면 틀리다
당직은 최소 한 명이 남아야 하는데, 두 사람이 동시에 해제를 눌렀더니 0명이 됐다. 검사도 있었고 트랜잭션도 걸려 있었다. 어디가 틀렸을까?
두 번 적립했는데 포인트가 한 번만 들어갔다
포인트 적립 요청이 두 번 다 성공했는데 잔액에는 한 번만 들어가 있다. 에러가 하나도 없어서, 정산 때가 돼서야 알게 되는 종류의 사고다.
결제사가 느려졌을 뿐인데 우리 서비스가 멈췄다
결제사가 조금 느려졌을 뿐인데 결제와 상관없는 화면까지 우리 서비스가 통째로 멈췄다. 원인은 트랜잭션 안에서 부른 외부 API 였다.
JSON 컬럼은 스키마를 미루는 빚이다
요구사항이 자주 바뀌어 JSON 컬럼에 속성을 몰아넣었더니, 1년 뒤엔 어떤 키가 들어 있는지 아무도 모른다. 조회도 느려졌다. 빨리 가려던 선택에 이자가 붙었다.
대량 적재 뒤에만 느려지는 쿼리, 범인은 낡은 통계
데이터를 왕창 넣은 직후부터 쿼리가 느려졌다가 시간이 지나면 저절로 낫는다. 그래서 원인을 못 찾고, 다음 적재 때 똑같이 또 느려진다.
인덱스가 분명히 있는데 순차 스캔이 뜬다
인덱스를 만들었는데 쿼리는 그대로 느리다. 개발 DB 에선 잘 타던 인덱스를 운영에서만 안 탄다. 그런데 옵티마이저가 인덱스를 안 쓰는 게 옳은 판단일 때가 생각보다 많다.
천만 행 DELETE 한 번에 복제가 수십 분 밀렸다
오래된 로그를 치우려고 천만 행을 한 번에 지웠더니, 복제 지연이 수십 분으로 뛰고 디스크는 오히려 늘었다. 지우는 일도 결국 기록을 쓰는 일이었다.
deleted_at 만 채웠더니 2년 뒤 테이블이 느려졌다
지운 데이터를 실제로 지우지 않고 표시만 해 뒀더니, 2년 뒤 테이블의 70%가 삭제된 행이 됐다. 조회는 점점 느려지고, 탈퇴한 이메일로는 재가입도 막힌다.
서머타임 전환일에 크론 배치가 두 번 돈 이유
한국은 서머타임이 없지만, 해외 결제사 리포트를 받는 순간 1년에 두 번 배치가 건너뛰거나 두 번 돈다. 상대가 자르는 하루가 그날만 23시간이거나 25시간이다.
같은 REPEATABLE READ 인데 DB 를 옮기니 결과가 달라졌다
같은 REPEATABLE READ 인데 MySQL 과 PostgreSQL 에서 동작이 다르다. DB 를 옮기면 코드는 그대로인데 결과가 바뀐다. 이름은 표준이 정했지만 구현은 DB 마다 제각각이다.
안 쓰는 인덱스, 지워도 되는지 어떻게 알까
느릴 때마다 인덱스를 하나씩 더하다 보니 테이블 하나에 열두 개가 됐다. 조회는 그대로인데 쓰기만 느려졌다. 그렇다고 사용 횟수 0 이라는 숫자를 그대로 믿으면 곤란하다.
CREATE INDEX 하나에 주문이 몇 분 멈췄다
운영 중에 인덱스 하나를 추가했더니 몇 분 동안 주문이 안 들어왔다. 스테이징에선 1초 만에 끝난 작업이다. 락은 그때도 똑같이 걸렸다. 아무도 못 느꼈을 뿐이다.
pgbouncer transaction 모드로 바꿨더니 SET 이 사라진다
커넥션을 아끼려고 pgbouncer 설정 하나를 바꿨더니 앱이 가끔 이상해졌다. 다시 보내면 성공해서 재현도 안 된다. 세션에 걸어 둔 설정이 에러도 없이 사라지고 있었다.
PostgreSQL 이 쓰기를 거부할 때, XID wraparound
어느 날 PostgreSQL 이 모든 쓰기를 거부했다. 디스크도 CPU 도 멀쩡한데 DB 가 스스로 문을 닫았다. 그런데 이 일은 몇 주 전부터 지표에 다 보이고 있었다.
1초짜리 ALTER TABLE 이 서비스를 몇 분 세웠다
1초면 끝나는 컬럼 추가 마이그레이션을 돌렸는데 서비스 전체가 몇 분간 멈췄다. 마이그레이션은 시작도 못 한 상태였다. 락을 기다리는 줄 뒤에 평범한 조회까지 서 있었다.
다른 상품을 샀는데 왜 서로 기다릴까, SELECT FOR UPDATE
재고를 잠그는 쿼리가 개발 DB 에선 멀쩡했다. 그런데 프로모션 날 주문 API 가 통째로 멈췄다. 서로 다른 상품을 산 사람끼리 줄을 서고 있었다.
100페이지만 가면 타임아웃, OFFSET 페이지네이션
목록 1페이지는 10ms 인데 100페이지쯤 가면 타임아웃이 난다. 그 깊이까지 넘기는 건 사람이 아니라 크롤러다. OFFSET 은 건너뛰는 척하면서 사실 다 읽고 있다.
운영에서만 느린 목록 API, 범인은 ORM N+1
목록 API 가 로컬에선 50ms 인데 운영에선 1.5초가 걸렸다. 느린 쿼리는 하나도 없었다. 범인은 쿼리 하나가 아니라 101번이라는 개수였다.
MySQL 의 utf8 은 UTF-8 이 아니다
닉네임에 이모지를 넣었더니 500 에러가 났다. 인코딩은 분명 utf8 로 맞췄는데도. MySQL 의 utf8 은 이름만 UTF-8 이고, 문자 하나에 3바이트까지만 담는다.
인덱스를 걸었는데 LIKE '%검색어%' 가 느리다
검색을 LIKE '%키워드%' 로 만들었더니 데이터가 늘면서 몇 초씩 걸렸다. 인덱스를 걸어 뒀는데도. 앞이 열린 패턴 앞에서는 B-tree 가 손쓸 방법이 없다.
컬럼은 있는데 인덱스를 안 탄다면, 복합 인덱스 순서 문제
(status, created_at) 인덱스를 만들었는데 created_at 만으로 찾으면 인덱스를 안 탄다. 그래서 인덱스를 자꾸 더 만들게 된다. 순서 기준을 알면 오히려 개수가 준다.
조건에 맞는 글이 없다. 브루니가 숨었나 보다.