Skip to content
isdnetworks
Go back

인덱스가 아니라 N+1

상품 목록 화면이 느리다는 얘기가 계속 있었고 인덱스 문제로 보고 있었다. 조건이 복잡하고 테이블이 크니 그럴듯한 짐작이었다.

Table of contents

Open Table of contents

쿼리 수 측정

인덱스를 손대기 전에 한 페이지에 쿼리가 몇 개 나가는지부터 셌다. 20건짜리 1페이지를 여는 데 약 1,880개가 나갔고 건당 94개였다.

어디서 나오는지 나눠 보니 목록 조회 관련이 약 220개로 12%였고 나머지는 전부 접근자였다. 수수료율 접근자가 약 1,020개로 54%, 할인 접근자가 약 600개로 32%를 차지했다. 접근자가 86%였다.

곱셈 구조

컨트롤러를 보니 구조가 곱셈이었다.

foreach ($products as $product) {
    $product->distributions;
    $product->latestJobs;
    $product->placeStocks;

    foreach ($activeChannels as $channel) {
        $product->{"{$channel}_rate"};
        $product->{"{$channel}_discount"};
    }
}

상품 20건에 활성 채널이 약 27개이고 채널마다 접근자가 둘이므로 여기서만 1,080개가 나온다. 여기에 상품마다 세 번씩 도는 조회 60개와 목록 조회를 더하면 측정한 숫자와 대략 맞는다. 쿼리 수가 상품 수에 비례하는 것이 아니라 상품 수와 채널 수의 곱에 비례한다.

인덱스로 안 되는 이유

이 구조를 확인하고 나니 인덱스 튜닝으로는 체감이 개선되지 않는다는 결론이 나왔다. 쿼리 하나하나는 빠를 수 있고 1ms라고 해도 1,880개면 1.9초이며, 각각에 왕복이 있으므로 네트워크 지연이 0.5ms만 돼도 1초가 더 붙는다.

쿼리 하나를 빠르게 하는 문제가 아니라 개수를 줄이는 문제였다. 같은 증상을 두고 원인의 종류가 다르면 손댈 곳도 다르다.

개선 순서와 대상 경로

셋 중 무엇을 먼저 잡을지는 비중으로 정했다. 목록 조회를 배치화해도 12%라 체감이 안 나고, 수수료율과 할인 접근자를 잡으면 86%가 줄어 1,880개가 260개 수준이 된다. 비중이 큰 것부터 잡는다는 것은 당연한데 실제로는 고치기 쉬운 것부터 하고 싶어지므로 숫자를 먼저 보는 것이 중요했다.

같은 방식으로 다른 화면도 셌다. 마스터 목록이 약 1,880개인데 마켓 목록은 약 360개이고 상세와 수정 화면은 약 50개였다. 상세 화면 코드를 보니 관계를 미리 로드하고 있었다.

$product = Product::with(['distributions', 'options', 'media'])->find($id);

같은 코드베이스인데 화면마다 다르다. 누가 언제 손댔느냐에 따라 갈린 것인데, 잘 된 화면이 있으면 그것이 바로 참고 구현이 된다.

어느 화면을 고쳐야 하는지도 정확히 지목했다. 느린 것은 관리 콘솔 앱이 아니라 구 API를 호출하는 특정 경로였고 새 앱 쪽은 다른 API를 쓰고 있었다. 상품 화면이 느리다는 말을 경로 하나로 좁히지 않으면 엉뚱한 곳을 고친다.

인덱스 확인 결과

인덱스가 원인이 아니라고 결론지었지만 확인 자체는 했다. 실행 계획을 보니 OR 조건 때문에 옵티마이저가 인덱스를 고르지 않고 있었고, 강제로 지정하니 사용은 됐다.

FROM products FORCE INDEX (idx_provider_created)
-- rows: 32,692

사용은 가능한데 자동 선택은 안 되는 상태다. 지금 병목은 아니므로 고치지 않고 별도 항목으로 적어 뒀다. 조사 과정에서 확인한 사실은 원인이 아니어도 기록해 두면 다음 사람이 같은 확인을 반복하지 않는다.

정리


Share this post on:

Previous Post
도매 API 연동 시 Rate Limit 분산 전략
Next Post
백지에서 시작한 새 레코드