Перейти к содержимому

Постраничная навигация сбоит - разбор причин

Список разбит на страницы и ведёт себя неверно. Элементы повторяются на соседних страницах, последняя страница пуста, а число страниц не сходится. Разбираем причины по убыванию частоты, начиная с проверки, которая делит их пополам.

С чего начать

Сверяем общее число с суммой по страницам:

$total = CIBlockElement::GetList([], $filter, []); // подсчёт группировкой: одно число
$seen = [];
for ($page = 1; $page <= (int)ceil($total / 20); $page++) {
$res = CIBlockElement::GetList(['SORT' => 'ASC'], $filter, false,
['nPageSize' => 20, 'iNumPage' => $page], ['ID']);
while ($row = $res->Fetch()) { $seen[] = $row['ID']; }
}
printf("всего %d, собрано %d, уникальных %d\n", $total, count($seen), count(array_unique($seen)));

Три числа отвечают на главный вопрос сразу. Совпадение общего и собранного при меньшем числе уникальных означает неустойчивый порядок строк между запросами. Расхождение самих счётчиков говорит о другом: подсчёт и выборка идут по разным правилам, и виноват уже не порядок.

Проверяем устойчивость сортировки:

$order = ['SORT' => 'ASC', 'ID' => 'ASC']; // второй ключ делает порядок однозначным
$res = CIBlockElement::GetList($order, $filter, false,
['nPageSize' => 20, 'iNumPage' => 2], ['ID', 'NAME', 'SORT']);
// без второго ключа строки с равным SORT база возвращает в произвольном порядке

Тысяча элементов с одинаковым значением поля сортировки не имеет однозначного порядка. База вправе вернуть такие строки по-разному на каждый запрос, а каждая страница списка - это отдельный запрос. Отсюда и берётся товар, который виден и на первой странице, и на второй.

Ищем дубли строк от множественного свойства:

$res = CIBlockElement::GetList([], $filter, false, false, ['ID', 'PROPERTY_COLOR']);
$rows = 0; $ids = [];
while ($row = $res->Fetch()) { $rows++; $ids[$row['ID']] = true; }
printf("строк %d, элементов %d\n", $rows, count($ids));
// строк больше - множественное свойство размножило элемент; тогда берут GetNextElement()

Старое ядро отдаёт по строке на каждое значение множественного свойства при первом варианте хранения. Размер страницы режет строки, а не элементы, поэтому на странице их оказывается меньше заказанного, а подсчёт для навигации даёт завышенное число страниц.

То же на выборке ORM со связями:

use Bitrix\Main\ORM\Query\QueryHelper;
$query = \Bitrix\Iblock\ElementTable::query()
->setSelect(['ID', 'NAME'])->setOrder(['ID' => 'ASC'])
->setLimit(20)->setOffset(20); // вторая страница по двадцать
$rows = QueryHelper::decompose($query, true); // честный предел по элементам

Ограничение в новом ядре считает строки объединения, а не записи сущности. Помощник разбирает запрос на части и выбирает связи отдельно, после чего предел снова означает число элементов.

Смотрим номер страницы в ключе кэша:

$page = (int)($_REQUEST['PAGEN_1'] ?? 1);
$cacheId = 'list_' . $sectionId . '_' . $page; // номер страницы обязан быть в ключе
printf("PAGEN_1=%d, ключ %s\n", $page, $cacheId);
// одинаковый ключ на разных страницах - все они отдаются из записи первой

Кэш ничего не знает про адрес, пока номер страницы не попал в ключ. Симптом при этом обманчив: первая страница верна, а остальные повторяют её содержимое целиком и списываются на поисковых роботов.

Сравниваем выдачу с проверкой прав и без:

$f = ['IBLOCK_ID' => $iblockId, 'ACTIVE' => 'Y'];
printf("с правами %d, без прав %d\n",
CIBlockElement::GetList([], $f, []),
CIBlockElement::GetList([], $f + ['CHECK_PERMISSIONS' => 'N'], []));
// разные числа означают, что часть элементов закрыта правами посетителя

Разные числа означают, что счётчик и вывод работают в разном контексте прав. Отключать проверку прав стоит только там, где результат не уходит посетителю: на витрине это открывает элементы закрытых разделов.

Причины

  1. Порядок строк не задан однозначно примерно 28% случаев

    ПризнакОдин и тот же элемент виден на соседних страницах, а другой пропал совсем.

    ПроверкаСобираем идентификаторы со всех страниц и сравниваем их число с числом уникальных.

    Что делатьДобавляем в сортировку второй ключ с уникальными значениями, обычно идентификатор элемента.

  2. Страница режет строки соединения, а не элементы примерно 20% случаев

    ПризнакНа странице меньше элементов, чем задано размером, и часть из них повторяется.

    ПроверкаСчитаем строки выборки и уникальные идентификаторы в одном проходе по результату.

    Что делатьУбираем множественное свойство из выборки, а значения дочитываем отдельным вызовом по списку.

  3. Номер страницы не входит в ключ кэша примерно 18% случаев

    ПризнакПервая страница верна, а вторая и следующие показывают ровно её содержимое.

    ПроверкаПечатаем ключ кэша списка и сравниваем его на первой и на второй странице.

    Что делатьДобавляем текущий номер страницы в ключ кэша рядом с разделом и фильтром.

  4. Параметр номера теряется в адресе или занят соседом примерно 14% случаев

    ПризнакСсылки навигации листают не тот список либо возвращают на первую страницу.

    ПроверкаСмотрим адрес ссылки и порядок вызовов компонентов с навигацией на этой странице.

    Что делатьРазводим списки по разным номерам навигации и передаём параметры отбора в ссылки.

  5. Подсчёт и вывод идут по разным условиям примерно 12% случаев

    ПризнакПоследняя страница пуста, а общее число элементов не делится на размер страницы.

    ПроверкаПечатаем фильтр подсчёта и фильтр вывода рядом и сравниваем их посимвольно.

    Что делатьСобираем условие один раз в переменную и передаём её в обе выборки без правок.

  6. Права сокращают выдачу после подсчёта примерно 8% случаев

    ПризнакАдминистратор видит все страницы целиком, а обычный посетитель - пустой хвост списка.

    ПроверкаСравниваем количество элементов с проверкой прав и с явно отключённой проверкой.

    Что делатьСчитаем и выводим в одном контексте прав, а закрытые элементы отсекаем самим фильтром.

Частые вопросы

Почему независимо от номера страницы выводятся одни и те же первые записи?

Номер страницы не доходит до выборки: его не передали в параметры навигации либо перекрыл кэш. Проверяют значение параметра в адресе, в вызове выборки и в ключе кэша по очереди.

Почему неправильно формируется ссылка пагинации?

Чаще всего на странице несколько компонентов с навигацией, и номера назначаются по порядку вызовов. Ссылка второго списка при этом собирается с чужим номером и листает первый.

Количество выводимых элементов не совпадает с общим счётчиком, где искать?

Сначала сравнивают фильтр подсчёта с фильтром вывода, потом смотрят отсев в шаблоне. Счётчик считает строки запроса, а на страницу попадает то, что осталось после всех проверок.

Товары дублируются в списке каталога, это ошибка навигации?

Нет, обычно это множественное свойство: каждое значение даёт свою строку выборки. Навигация только делает дубли заметными, потому что режет результат по строкам.

Как быстро понять, дело в кэше или в самой выборке?

Повторить выборку витрины из консольного скрипта с теми же фильтром и номером страницы. Совпадение с браузером снимает подозрение с кэша и переводит разбор на запрос.

Смежное

Первоисточник