- TL;DR
- Dấu hiệu nhận biết “ăn RAM”
- Nguyên nhân phổ biến
- Chiến thuật bắt “thủ phạm” với backtrace
- 1) Đo RAM ± backtrace tại chỗ (bao vây điểm nghi ngờ)
- 2) Gài hook toàn cục vào the_posts (tự log mọi WP_Query)
- 3) “Đo từ gốc” – gài ngay khi WP sắp thực thi SQL
- Sau khi bắt được thủ phạm – fix kiểu gì?
- Ví dụ tối ưu nhanh (trước → sau)
- Mẹo khi đọc backtrace
- Kết
Nếu bạn từng thấy trang vẫn cache nhanh nhưng mỗi request PHP lại tiêu tốn vài MB, hoặc template/shortcode chạy ổn ở dev nhưng vỡ RAM ở production, thì đây là checklist chẩn đoán dành cho bạn.
- Hiểu những tình huống khiến
WP_Queryđội RAM. - Log backtrace gọn để chỉ ra đoạn code gây phình bộ nhớ.
- Áp dụng bộ tối ưu “cắm là đỡ”:
fields ="ids", tắt meta/term cache, giới hạn bản ghi, và chiến lược sort hợp lý.
TL;DR
- WP_Query ngốn RAM khi bạn kéo về quá nhiều post objects + meta/term cache, hoặc meta query/sort phức tạp buộc WP phải tải & xử lý nhiều dữ liệu trong bộ nhớ.
- Cách bắt quả tang: đo RAM trước/sau mỗi query, và in backtrace rút gọn để biết đoạn code nào tạo ra truy vấn phình to.
Dấu hiệu nhận biết “ăn RAM”
- Trang load vẫn nhanh (cache HTML), nhưng mỗi request PHP ăn nhiều MB → dễ vỡ trên hosting yếu.
- Bật debug thấy
Fatal error: Allowed memory size ...ở trang nhiều nội dung. - Một vòng lặp dùng
WP_Querytrong template/shortcode chạy ổn ở dev, nhưng vỡ RAM ở production khi dữ liệu lớn.
Nguyên nhân phổ biến
- Tải cả post object (title, content, meta, terms) cho rất nhiều bài (vd:
posts_per_page = -1). - Meta Query nặng:
- Dùng
LIKE/NOT LIKE/ nhiềuOR. ORDER BY meta_valuekhông có index, phải lôi toàn bộ rows ra để sort.
- Dùng
- Không tắt prefetch meta/term:
update_post_meta_cache,update_post_term_cachevẫntruedù không cần. - Không dùng
fields => 'ids'khi chỉ cần ID. no_found_rows=falsedù không phân trang → thêm 1COUNT(*)vô ích.- ACF/meta lớn: một meta chứa JSON to; load 100 bài = load 100 JSON vào RAM.
- Template lồng nhiều query (N+1 queries), mỗi query lại không tối ưu.
Chiến thuật bắt “thủ phạm” với backtrace
1) Đo RAM ± backtrace tại chỗ (bao vây điểm nghi ngờ)
Dùng khi bạn nghi một query cụ thể phình RAM (vd loop trong template).
<?php
function init_dbg_bytes($bytes) {
$units = ['B','KB','MB','GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units)-1) { $bytes /= 1024; $i++; }
return round($bytes, 2) . ' ' . $units[$i];
}
function init_probe_wp_query($args, $label = 'probe') {
$m0 = memory_get_usage();
$q = new WP_Query($args);
$m1 = memory_get_usage();
$diff = $m1 - $m0;
error_log("INIT_PROBE {$label}: +" . init_dbg_bytes($diff) . " (after: " . init_dbg_bytes($m1) . ")");
if ($diff > 1 * 1024 * 1024) { // >1MB thì in backtrace rút gọn
ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 8);
$trace = ob_get_clean();
error_log("INIT_PROBE {$label} TRACE:\n" . $trace);
}
return $q;
}
// Ví dụ:
// $q = init_probe_wp_query([ 'post_type' => 'post', 'posts_per_page' => 50 ], 'home-loop');
- Khi nào dùng: muốn “bắt quả tang” 1 query cụ thể.
- Lợi ích: biết RAM tăng bao nhiêu do query đó và gọi từ file/hàm nào.
2) Gài hook toàn cục vào the_posts (tự log mọi WP_Query)
Dùng khi bạn không biết query nào nặng – để hệ thống tự log tất cả.
<?php
if (!function_exists('init_dbg_bytes')) {
function init_dbg_bytes($bytes) {
$units = ['B','KB','MB','GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units)-1) { $bytes /= 1024; $i++; }
return round($bytes, 2) . ' ' . $units[$i];
}
}
add_filter('the_posts', function($posts, $query) {
static $last_mem = null;
$before = memory_get_usage();
$result = $posts; // giữ nguyên
// Sau khi WP đã tạo $posts (post objects) xong
$after = memory_get_usage();
$diff = $after - $before;
// Bỏ các query nhỏ
if ($diff < 512 * 1024) return $result;
$tag = [];
$tag[] = $query->is_main_query() ? 'main' : 'secondary';
if (!empty($query->query['post_type'])) $tag[] = 'pt:' . (is_array($query->query['post_type']) ? implode(',', $query->query['post_type']) : $query->query['post_type']);
if (!empty($query->query['meta_query'])) $tag[] = 'meta';
if (!empty($query->query['orderby'])) $tag[] = 'orderby:' . $query->query['orderby'];
if (isset($query->query_vars['posts_per_page'])) $tag[] = 'ppp:' . $query->query_vars['posts_per_page'];
error_log('INIT_TRACE the_posts [' . implode('|', $tag) . ']: +' . init_dbg_bytes($diff) . ' after:' . init_dbg_bytes($after) . ' count:' . count($posts));
// Backtrace rút gọn khi bùng >2MB
if ($diff > 2 * 1024 * 1024) {
ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 8);
$trace = ob_get_clean();
error_log("INIT_TRACE backtrace:\n" . $trace);
}
$last_mem = $after;
return $result;
}, 999, 2);
Xem log ở đâu: bật WP_DEBUG_LOG để ghi vào wp-content/debug.log.
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);
3) “Đo từ gốc” – gài ngay khi WP sắp thực thi SQL
Muốn soi truy vấn SQL nào dẫn tới RAM tăng, log thêm posts_request:
<?php
add_filter('posts_request', function($sql, $query){
// Đánh dấu query đáng ngờ (nhiều JOIN meta, ORDER BY meta_value)
if (false !== stripos($sql, 'postmeta') || false !== stripos($sql, 'ORDER BY meta_value')) {
error_log("INIT_SQL suspicious:\n" . $sql);
ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6);
$trace = ob_get_clean();
error_log("INIT_SQL trace:\n" . $trace);
}
return $sql;
}, 10, 2);
Sau khi bắt được thủ phạm – fix kiểu gì?
Checklist tối ưu WP_Query (áp dụng theo nhu cầu):
- Chỉ lấy ID khi có thể
'fields' => 'ids', - Tắt những thứ không cần
'no_found_rows' => true, // nếu không phân trang 'update_post_meta_cache' => false, // nếu không cần meta ngay 'update_post_term_cache' => false, // nếu không render taxonomy ngay - Giới hạn số lượng
'posts_per_page' => 20, - Giảm độ phức tạp meta_query
- Tránh
LIKE/NOT LIKEnếu có thể chuyển sang=,IN,BETWEEN+typephù hợp (NUMERIC,DECIMAL…). - Nếu cần sort theo meta lớn, cân nhắc đưa dữ liệu ra custom table để index.
- Tránh
- Sort rẻ hơn
- Tránh
ORDER BY meta_valuetrên dataset lớn; dùngdate,menu_order, hoặc precompute field có index.
- Tránh
- Chia nhỏ và lazy-load (lấy ID trước, rồi load meta theo lô).
- Cache thông minh (transient/object cache cho danh sách ID hoặc kết quả đã chuẩn hóa).
Ví dụ tối ưu nhanh (trước → sau)
Trước (ngốn RAM):
$q = new WP_Query([
'post_type' => 'manga',
'posts_per_page' => -1,
'meta_query' => [
['key' => 'score', 'value' => 7, 'compare' => '>=', 'type' => 'NUMERIC'],
],
'orderby' => 'meta_value',
'meta_key' => 'score',
'order' => 'DESC',
]);
Sau (nhẹ RAM hơn nhiều):
// 1) Chỉ lấy ID (nhẹ hơn rất nhiều)
$q = new WP_Query([
'post_type' => 'manga',
'fields' => 'ids',
'posts_per_page' => 50,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'meta_query' => [
['key' => 'score', 'value' => 7, 'compare' => '>=', 'type' => 'NUMERIC'],
],
'orderby' => 'meta_value_num',
'meta_key' => 'score',
'order' => 'DESC',
]);
// 2) Tùy nhu cầu, load meta theo lô sau (get_post_meta cho từng ID)
Mẹo khi đọc backtrace
- Tập trung vào template file hoặc plugin file trong 5–8 frame đầu tiên.
- Nếu thấy call từ shortcode, kiểm tra function render của shortcode đó.
- Nếu thấy call qua
pre_get_posts, kiểm tra hook có áp dụng “đại trà” không (vô tình tác động mọi query).
Kết
- Đo RAM trước/sau + backtrace rút gọn là cách nhanh & chuẩn để biết đoạn code nào gây phình bộ nhớ khi chạy
WP_Query. - Fix theo checklist (đặc biệt
fields => 'ids', tắt meta/term cache,no_found_rows) thường giảm RAM tức thì. - Case cực lớn hoặc cần sort meta phức tạp → custom table + index là con đường bền vững.
Bình luận