После того как обход каталога заменили одним запросом (первая часть), сохранение товара всё равно шло секунды. Оставшаяся работа выглядела дешёвой: у 82 адресатов пересобрать список совместимых товаров. Запросов к базе на это уходило 70, а времени — 4,69 секунды из 4,71. Запросы повторялись, и кэш WordPress не помогал ни разу. Причина — в том, что цикл сам писал в базу.
Это вторая часть цикла «Массовый импорт в WordPress».
Как выглядел цикл
foreach ($recipients as $rid) {
// соседи по группе в нужном порядке — одинаковы для многих адресатов
$group = get_posts([
'post_type' => 'katalog',
'include' => $ids,
'orderby' => 'meta_value_num',
'meta_key' => 'order',
'order' => 'ASC',
'numberposts'=> -1,
]);
update_post_meta($rid, 'fitsProductUnique', build_list($group, $rid));
clean_post_cache($rid);
}
Запрос get_posts внутри цикла почти всегда один и тот же. WordPress кэширует результаты запросов в объектном кэше, так что со второй итерации ответ должен приходить из памяти. Но не приходил.
Почему кэш не срабатывал
Ключ кэша у результатов WP_Query содержит метку last_changed группы posts. Любое изменение записи — clean_post_cache(), wp_update_post(), wp_insert_post() — ставит новую метку. Все ранее закэшированные результаты запросов остаются в памяти, но больше никогда не находятся: ключ у следующего запроса уже другой.
В нашем цикле clean_post_cache стоял в конце каждой итерации. Значит, кэш запросов обнулялся 82 раза подряд, и каждый get_posts шёл в базу заново — с сортировкой по мете и соединением таблиц.
Причём clean_post_cache — не единственный виновник. В современных версиях WordPress ту же метку двигают и added_post_meta, updated_post_meta, deleted_post_meta: в нашем цикле update_post_meta перед чисткой делал то же самое. Убрать одну строку было бы мало.
Общее правило: если цикл что-то пишет в записи или их мету — wp_update_post, update_post_meta, clean_post_cache, — то get_posts и WP_Query внутри того же цикла не кэшируются. Всё, что одинаково для всех итераций, собирайте один раз до цикла.
Лечение: таблица порядка один раз
Всё, что нужно для списков, — позиция товара, заголовок и общее название. Эти данные одинаковы для всех адресатов, поэтому их можно прочитать одним запросом на объединение всех участников, а списки собирать в памяти:
$table = build_order_table($allIds); // один запрос: ID → order, title, common_name
foreach ($recipients as $rid) {
$group = array_intersect_key($table, array_flip($idsFor[$rid]));
update_post_meta($rid, 'fitsProductUnique', build_list($group, $rid));
clean_post_cache($rid); // теперь не мешает: запросов в цикле больше нет
}
У этого приёма есть подвох: сортировка в PHP должна давать тот же порядок, что давала база. При одинаковых значениях order это не гарантировано — разбор в третьей части.
Результат
Замер по логу nginx на рабочем сайте до и после выката, вместе с правкой из первой части:
| Было | Стало | |
|---|---|---|
| товаров в минуту при обмене | 12 | 104 |
| среднее время записи товара | 5,00 с | 0,56 с |
| самый связный товар (101 связь) | 5–8 с | 0,95 с |
| товар, на который никто не ссылается (87 % каталога) | 0,5–0,7 с | 0,02 с |
На втором сайте с той же темой тот же перенос дал 14,2 → 2,0 секунды на тесте, а самый связный товар на рабочем сайте стал записываться за 1,64 секунды.
Где ещё встречается
- импорт, который после каждого товара пересчитывает «похожие» или «с этим покупают» через
WP_Query; - пересчёт счётчиков рубрик или фильтров в цикле по записям;
- миграции: «пройти все записи, обновить и для каждой найти соседей».
Признак везде один: запросов немного, они повторяются, а время растёт с размером каталога. Проверка — посчитать $wpdb->num_queries до и после цикла и сравнить с числом разных запросов. Если одинаковые запросы идут в базу повторно, кэш запросов кто-то обнуляет.
Импорт тормозит, хотя запросов мало?
Пришлите код обработчика — найду, что обнуляет кэш, и вынесу общее из цикла.
// загружаю комментарии…