Technote

성능 최적화 실무

연재 워드프레스 성능 프로파일링 8부 중 3부

N+1 쿼리 — 루프 안의 get_post_meta 와 캐시 프라이밍

WP_Query 는 자기 결과에 대해서만 캐시를 채웁니다. 손으로 만든 ID 목록에는 그 혜택이 따라오지 않고, 그 차이가 쿼리 수를 결정합니다.

중복 쿼리 목록의 대부분은 하나의 형태로 수렴합니다. 포스트 목록을 돌면서 각 포스트의 메타를 읽는 코드입니다. 겉보기에는 아무 문제가 없습니다.

foreach ( $ids as $id ) {
    $price = get_post_meta( $id, 'price', true );
}

이 코드가 만드는 쿼리 수는 캐시가 채워져 있느냐에 따라 완전히 달라집니다. 채워져 있으면 0건이고, 비어 있으면 포스트 하나당 한 건씩 나갑니다. 같은 코드가 화면에 따라 다르게 동작하는 것이 이 문제를 어렵게 만듭니다.

WP_Query 는 자기 결과만 채웁니다

WP_Query 는 결과를 가져온 뒤 그 포스트들의 메타와 텀을 한 번에 조회해 오브젝트 캐시에 넣습니다. 그래서 일반적인 루프 안의 get_post_meta() 는 쿼리를 만들지 않습니다 — 이미 캐시에 있기 때문입니다. 워드프레스가 처음부터 이 문제를 고려해 만들어졌다는 증거입니다.

문제는 그 경로를 벗어난 목록입니다. 세 가지가 대표적입니다.

프라이밍이 따라오는 목록과 따라오지 않는 목록

특히 세 번째가 함정입니다. 'update_post_meta_cache' => false 는 메타를 전혀 쓰지 않는 목록에서 쿼리 하나를 아끼는 최적화인데, 성능 팁으로 돌아다니다 보니 메타를 쓰는 루프에도 그대로 복사됩니다. 그러면 한 건을 아끼려다 N 건을 새로 만듭니다.

코어가 이미 갖고 있는 프라이밍 함수

직접 캐시를 다룰 필요는 없습니다. 코어에 함수가 있습니다.

$ids = $wpdb->get_col(
    $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s", 'featured' )
);

// 포스트 · 텀 · 메타를 각각 한 번의 쿼리로 채운다
_prime_post_caches( $ids, true, true );

foreach ( $ids as $id ) {
    $price = get_post_meta( $id, 'price', true ); // 캐시 히트
}

메타만 필요하면 update_postmeta_cache( $ids ), 텀만 필요하면 update_object_term_cache( $ids, 'product' ) 를 씁니다. 사용자 목록에는 cache_users( $user_ids ) 가 같은 역할을 합니다. 어느 쪽이든 N 건의 조회가 1 건으로 접힙니다.

한 가지 자주 오해되는 지점 — get_post_meta( $id, 'price', true )키 하나가 아니라 그 포스트의 메타 전체를 읽어 캐시에 넣습니다. 그래서 같은 포스트에서 메타 키를 다섯 개 읽어도 쿼리는 한 건입니다. N+1 의 N 은 메타 키 수가 아니라 포스트 수입니다.

고쳤는지 확인하는 법

프라이밍을 넣었으면 앞 회차의 프로파일로 돌아가 같은 URL 을 같은 조건에서 다시 봅니다. 확인할 것은 두 가지입니다. 그 SQL 의 반복 횟수가 1로 줄었는가, 그리고 총 응답 시간이 실제로 움직였는가.

N+1 을 걷어내는 한 바퀴 — 마지막 칸을 건너뛰면 고쳤는지 알 수 없다

두 번째 확인이 중요합니다. 반복이 사라졌는데 응답 시간이 그대로인 경우가 있습니다 — 그 쿼리들이 원래 아주 빨랐고 병목은 다른 곳이었다는 뜻이며, 그 사실도 그대로 기록에 남깁니다. 개선되지 않았다는 결과 역시 다음 사람에게는 정보입니다.

목록 화면의 쿼리 설계를 다루는 글은 성능 최적화 아카이브에 있고, 사이트 전체의 쿼리 구조를 한 번에 점검받고 싶다면 최적화 지원 사업의 진단에 포함돼 있습니다.

다음 회차

프라이밍은 한 요청 안의 반복을 없앱니다. 요청과 요청 사이에도 유지하려면 영속 오브젝트 캐시가 필요합니다 — 다음 회차에서 Redis 를 붙였을 때 실제로 줄어드는 것과 줄어들지 않는 것을 나눠 봅니다.

이 주제의 다른 글

노하우 목록으로

성능 최적화 실무

태그 하나가 더하는 밀리초는 누가 냅니까

추적 스크립트는 내려받고 끝나지 않습니다. 파싱과 실행이 메인 스레드를 점유하는 동안 페이지는 손가락에 반응하지 않습니다. 그 비용은 직접 잴 수 있습니다.

마케터 3분 읽기

₩270,000 · 신청하기