동기화 배치가 오래 걸려서 product 8만 건을 도는 데 40분이 걸렸다. 그런데 나가는 SELECT 하나하나는 전부 빨랐다.
Table of contents
Open Table of contents
큰 쪽을 기준으로 돌고 있었다
코드는 이랬다.
$products = $this->db->get('product')->result(); // 8만 건
foreach ($products as $p) {
$target = $this->db->where('product_no', $p->product_no)
->get('sync_target')->row();
if (!$target) continue; // 대상 아님
$this->sync($p, $target);
}
product 전체를 훑으면서 각각이 동기화 대상인지 확인한다. SELECT 하나의 문제가 아니라 조회 횟수의 문제였다.
대상 표를 세어 봤다.
SELECT COUNT(*) FROM sync_target;
-- 1,240
sync_target 이 1,240건이다. 8만 번 조회해서 1,240건을 찾고 있었고 작은 쪽을 기준으로 돌면 1,240번이면 된다.
기준을 바꿨다
$targets = $this->db->get('sync_target')->result(); // 1,240건
foreach ($targets as $t) {
$p = $this->db->where('product_no', $t->product_no)
->get('product')->row();
if (!$p) continue;
$this->sync($p, $t);
}
sync_target 을 바깥으로 두니 40분이 40초가 됐다. 같은 결과인데 훑는 방향만 달라진 것이다.
조인으로 더 줄였다
1,240번도 줄일 수 있었다.
SELECT p.*, t.last_sync, t.channel
FROM sync_target t
JOIN product p ON p.product_no = t.product_no
한 번에 가져오니 40초가 3초가 됐다.
JOIN 이 항상 나은 것은 아니어서 가져올 컬럼이 많고 행이 많으면 메모리를 쓴다. 한쪽에 여러 건이 달리는 관계를 이으면 행 자체가 곱해져서 나온다. 여기서는 sync_target 이 1,240건이라 괜찮았다.
판단 기준 — 어느 쪽이 작은가
이 일 뒤에 foreach 를 만들 때 양쪽 개수를 먼저 확인했다.
SELECT COUNT(*) FROM product; -- 82,041
SELECT COUNT(*) FROM sync_target; -- 1,240
두 줄이면 어느 쪽을 기준으로 돌지 정해진다. 개수 차이가 크면 작은 쪽이 명확히 낫고 비슷하면 JOIN 을 본다.
전체 개수만 보면 안 되는 경우도 있었다.
$products = $this->db->where('status', 'normal')
->where('update_date >=', $since)
->get('product')->result();
조건이 붙으면 8만이 아니라 몇백 건일 수 있어서 조건을 넣고 세어 봤다.
SELECT COUNT(*) FROM product WHERE status='normal' AND update_date >= '2016-07-10';
-- 340
340건이면 product 쪽을 기준으로 도는 것이 낫다. sync_target 1,240건보다 적으니 실제 조건을 넣고 세야 맞는 판단이 나온다.
검증 — 조치마다 시간 재기
340건을 가져오는 SELECT 자체가 느리면 소용없다.
EXPLAIN SELECT * FROM product WHERE status='normal' AND update_date >= '2016-07-10';
type: ALL rows: 82041
type 이 ALL 이라 전체를 훑는데 update_date 에 인덱스가 없었다.
ALTER TABLE product ADD INDEX idx_update (update_date);
다시 확인했다.
type: range key: idx_update rows: 412
추정 412건이고 실제 340건과 비슷하다. 매뉴얼도 InnoDB 에서 이 숫자가 추정이라고 적고 있으니 실제 건수는 따로 세는 편이 맞다.
여러 곳을 고쳤으니 어디서 얼마나 줄었는지도 정리했다.
처음 40분 12초
작은 쪽 기준으로 변경 0분 41초
조인으로 묶음 0분 03초
인덱스 추가(조건 조회) 0분 02초
첫 조치가 대부분을 줄였고 나머지는 소폭이다. 가장 큰 것을 먼저 찾으면 나머지는 안 해도 될 수 있다. 시간이 없으면 첫 번째만 해도 충분했다.
정리
- 조회가 전부 빠른데 전체가 느리면 횟수를 센다
- 반복문에서 어느 쪽을 기준으로 훑느냐가 조회 수를 정한다
- 양쪽 개수를 세어 보면 두 줄로 판단이 난다
- 조건이 붙으면 개수가 달라지니 실제 조건을 넣고 센다
- 개수 차이가 크면 작은 쪽이고 비슷하면
JOIN을 본다 JOIN은 가져올 양이 적을 때 낫다- 조건 조회 자체가 느리면
EXPLAIN을 본다 rows는 추정이라 실제 건수는 따로 센다- 조치마다 시간을 재면 어느 것이 대부분을 줄였는지 남는다