서버 처리 시간이 범인으로 좁혀졌다면 다음은 쿼리입니다. Query Monitor 같은 프로파일러를 켜면 한 요청이 만든 쿼리가 전부 나열되는데, 그 목록을 총 개수만 보고 판단하면 대개 틀립니다. 200개짜리 목록이 300개짜리 목록보다 느릴 수 있습니다.
봐야 할 것은 두 종류이고, 둘은 원인도 처방도 다릅니다.
패널을 읽는 순서
프로파일러 화면은 위에서 아래로 읽습니다. 개요에서 총 쿼리 수 · 총 쿼리 시간 · 최대 메모리 세 값을 먼저 봅니다. 여기서 중요한 것은 총 쿼리 시간이 응답 시간의 일부일 뿐이라는 사실입니다. 총 쿼리 시간이 작은데 응답이 느리다면 병목은 DB 가 아니라 PHP 실행 — 외부 API 호출 · 이미지 처리 · 과한 훅 — 쪽입니다.
그다음 쿼리 목록을 소요 시간으로 정렬해 상위 몇 건을 봅니다. 여기에 잡히는 것이 느린 쿼리입니다. 이어서 중복 패널로 옮겨 같은 SQL 이 몇 번 실행됐는지를 봅니다. 이 둘을 각각 보지 않고 목록을 통째로 훑으면, 시간의 대부분을 차지하는 한 건과 개수의 대부분을 차지하는 반복이 서로를 가립니다.
모든 프로파일러에서 가장 값진 열은 호출자(caller) 입니다. 어떤 SQL 인지보다 누가 그것을 불렀는지가 고칠 위치를 알려 줍니다. 같은 SQL 이 30번 나오는데 호출자가 전부 같은 함수라면 그 함수 안에 루프가 있다는 뜻이고, 호출자가 제각각이라면 캐시가 아예 비어 있다는 뜻입니다.
프로파일러를 설치할 수 없을 때
Query Monitor 는 개발 · 스테이징 전용입니다. 쿼리와 훅 정보를 화면에 노출하는 데다 db.php 드롭인을 설치해 모든 쿼리에 오버헤드를 얹으므로, 프로덕션에 켜 두면 진단하려던 대상을 진단 도구가 느리게 만듭니다.
플러그인을 넣기 어려운 환경이라면 코어 상수 하나로 같은 데이터를 얻을 수 있습니다.
// wp-config.php — 스테이징에서만
define( 'SAVEQUERIES', true );
이 상수가 켜지면 $wpdb->queries 에 [SQL, 소요시간, 호출 스택] 이 쌓입니다. 요청 끝에서 정렬 한 번, 집계 한 번이면 위의 두 목록이 그대로 나옵니다.
add_action( 'shutdown', function () {
global $wpdb;
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
return;
}
$rows = $wpdb->queries;
// ① 느린 쿼리 — 소요시간 내림차순
usort( $rows, fn( $a, $b ) => $b[1] <=> $a[1] );
error_log( 'slowest: ' . round( $rows[0][1] * 1000 ) . 'ms ' . $rows[0][0] );
// ② 중복 쿼리 — 같은 SQL 이 몇 번인가
$dupes = array_count_values( array_column( $rows, 0 ) );
arsort( $dupes );
error_log( 'top repeat: ' . reset( $dupes ) . 'x ' . key( $dupes ) );
} );
스무 줄이 안 되는 코드지만, 이 두 줄의 로그가 “무엇을 고칠 것인가” 를 정합니다. 느린 쿼리가 하나 크게 튄다면 그 쿼리를 고쳐야 하고 — 캐시를 얹어도 첫 요청은 여전히 그 시간을 냅니다 — 같은 SQL 이 수십 번이라면 캐시 프라이밍으로 대부분 사라집니다.
숫자를 해석할 때의 함정
쿼리 수가 줄었다고 반드시 빨라지지는 않습니다. 반대로 쿼리 수가 그대로여도 느린 한 건을 고치면 크게 빨라집니다. 개수는 목표가 아니라 단서입니다. 개선을 보고할 때도 응답 시간과 쿼리 수를 함께 적고, 둘 중 하나만 좋아졌다면 그 사실을 그대로 적습니다.
또 하나, 관리자 화면의 프로파일과 방문자 화면의 프로파일을 섞지 않습니다. 관리 화면은 목록 테이블 · 알림 · 업데이트 확인 때문에 원래 쿼리가 많고, 그 숫자를 방문자 페이지의 기준으로 삼으면 엉뚱한 곳을 파게 됩니다.
쿼리 구조와 인덱스를 다루는 글은 성능 최적화 아카이브에 있고, 진단부터 개선까지 한 번에 맡기는 경로는 최적화 지원 사업에 정리돼 있습니다.
다음 회차
중복 쿼리 쪽이 압도적이라면 원인은 대개 하나입니다. 다음 회차는 N+1 — 루프 안의 get_post_meta() 가 만드는 비용과, 코어가 이미 갖고 있는 프라이밍 함수들입니다.