중복 쿼리 목록의 대부분은 하나의 형태로 수렴합니다. 포스트 목록을 돌면서 각 포스트의 메타를 읽는 코드입니다. 겉보기에는 아무 문제가 없습니다.
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로 줄었는가, 그리고 총 응답 시간이 실제로 움직였는가.
두 번째 확인이 중요합니다. 반복이 사라졌는데 응답 시간이 그대로인 경우가 있습니다 — 그 쿼리들이 원래 아주 빨랐고 병목은 다른 곳이었다는 뜻이며, 그 사실도 그대로 기록에 남깁니다. 개선되지 않았다는 결과 역시 다음 사람에게는 정보입니다.
목록 화면의 쿼리 설계를 다루는 글은 성능 최적화 아카이브에 있고, 사이트 전체의 쿼리 구조를 한 번에 점검받고 싶다면 최적화 지원 사업의 진단에 포함돼 있습니다.
다음 회차
프라이밍은 한 요청 안의 반복을 없앱니다. 요청과 요청 사이에도 유지하려면 영속 오브젝트 캐시가 필요합니다 — 다음 회차에서 Redis 를 붙였을 때 실제로 줄어드는 것과 줄어들지 않는 것을 나눠 봅니다.