Постраничная навигация сбоит - разбор причин
Список разбит на страницы и ведёт себя неверно. Элементы повторяются на соседних страницах, последняя страница пуста, а число страниц не сходится. Разбираем причины по убыванию частоты, начиная с проверки, которая делит их пополам.
С чего начать
Сверяем общее число с суммой по страницам:
$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'], []));// разные числа означают, что часть элементов закрыта правами посетителяРазные числа означают, что счётчик и вывод работают в разном контексте прав. Отключать проверку прав стоит только там, где результат не уходит посетителю: на витрине это открывает элементы закрытых разделов.
Причины
-
Порядок строк не задан однозначно примерно 28% случаев
ПризнакОдин и тот же элемент виден на соседних страницах, а другой пропал совсем.
ПроверкаСобираем идентификаторы со всех страниц и сравниваем их число с числом уникальных.
Что делатьДобавляем в сортировку второй ключ с уникальными значениями, обычно идентификатор элемента.
-
Страница режет строки соединения, а не элементы примерно 20% случаев
ПризнакНа странице меньше элементов, чем задано размером, и часть из них повторяется.
ПроверкаСчитаем строки выборки и уникальные идентификаторы в одном проходе по результату.
Что делатьУбираем множественное свойство из выборки, а значения дочитываем отдельным вызовом по списку.
-
Номер страницы не входит в ключ кэша примерно 18% случаев
ПризнакПервая страница верна, а вторая и следующие показывают ровно её содержимое.
ПроверкаПечатаем ключ кэша списка и сравниваем его на первой и на второй странице.
Что делатьДобавляем текущий номер страницы в ключ кэша рядом с разделом и фильтром.
-
Параметр номера теряется в адресе или занят соседом примерно 14% случаев
ПризнакСсылки навигации листают не тот список либо возвращают на первую страницу.
ПроверкаСмотрим адрес ссылки и порядок вызовов компонентов с навигацией на этой странице.
Что делатьРазводим списки по разным номерам навигации и передаём параметры отбора в ссылки.
-
Подсчёт и вывод идут по разным условиям примерно 12% случаев
ПризнакПоследняя страница пуста, а общее число элементов не делится на размер страницы.
ПроверкаПечатаем фильтр подсчёта и фильтр вывода рядом и сравниваем их посимвольно.
Что делатьСобираем условие один раз в переменную и передаём её в обе выборки без правок.
-
Права сокращают выдачу после подсчёта примерно 8% случаев
ПризнакАдминистратор видит все страницы целиком, а обычный посетитель - пустой хвост списка.
ПроверкаСравниваем количество элементов с проверкой прав и с явно отключённой проверкой.
Что делатьСчитаем и выводим в одном контексте прав, а закрытые элементы отсекаем самим фильтром.
Частые вопросы
Почему независимо от номера страницы выводятся одни и те же первые записи?
Номер страницы не доходит до выборки: его не передали в параметры навигации либо перекрыл кэш. Проверяют значение параметра в адресе, в вызове выборки и в ключе кэша по очереди.
Почему неправильно формируется ссылка пагинации?
Чаще всего на странице несколько компонентов с навигацией, и номера назначаются по порядку вызовов. Ссылка второго списка при этом собирается с чужим номером и листает первый.
Количество выводимых элементов не совпадает с общим счётчиком, где искать?
Сначала сравнивают фильтр подсчёта с фильтром вывода, потом смотрят отсев в шаблоне. Счётчик считает строки запроса, а на страницу попадает то, что осталось после всех проверок.
Товары дублируются в списке каталога, это ошибка навигации?
Нет, обычно это множественное свойство: каждое значение даёт свою строку выборки. Навигация только делает дубли заметными, потому что режет результат по строкам.
Как быстро понять, дело в кэше или в самой выборке?
Повторить выборку витрины из консольного скрипта с теми же фильтром и номером страницы. Совпадение с браузером снимает подозрение с кэша и переводит разбор на запрос.
Смежное
- Выборки из инфоблоков - оглавление подтемы
- Постраничная навигация: страницы в выборке, свой вызов, кэш - как навигация собирается правильно
- Выборки из инфоблоков: GetList, ORM и разделы - параметры выборки и способы подсчёта
- Кэширование своей выборки: ключ, теги, сброс - что обязано входить в ключ кэша
- Элемент не появляется в списке: разбор причин - когда пропал один элемент, а не страница
- Сложный фильтр выборки: ИЛИ, вложенные условия, подзапросы - как собрать одно условие для подсчёта и вывода
- Выборка ORM возвращает не то: разбор причин - ограничение при связях и произведение отношений
- Подгрузка списка порциями: кнопка «Показать ещё» и прокрутка - та же выборка без перезагрузки страницы
- Дубли страниц каталога: канонический адрес, пагинация, фильтр - страницы навигации в поисковой выдаче
- Медленный запрос к базе: поиск, план, индекс - когда подсчёт страниц стал тяжёлым
- Инфоблоки - устройство инфоблоков целиком