При разработке сложных отчетов или обработок на платформе 1С:Предприятие 8 программисты часто сталкиваются с необходимостью сопоставления данных из двух разных выборок. Один из стандартных механизмов для этого — использование метода НайтиСледующий() объекта ВыборкаИзРезультатаЗапроса. Однако у этого метода есть нюансы работы, связанные с позиционированием курсора, которые могут привести к логическим ошибкам и отсутствию данных в результате — для их выявления поможет инструмент отладки кода 1С в режиме Предприятие. В этой статье мы подробно разберем, как работает этот механизм и почему вызов метода Сбросить() является обязательным условием в большинстве сценариев.
Представим типичную ситуацию: у нас есть два запроса. Первый запрос выбирает заголовки документов, а второй — строки табличных частей этих документов. Мы обходим документы в цикле и для каждого документа хотим найти соответствующие ему строки во второй выборке. Рассмотрим пример кода, который часто вызывает вопросы у начинающих разработчиков:
ВыборкаПоДокументам = РезультатЗапроса[0].Выбрать();
ВыборкаПоТЧ = РезультатЗапроса[1].Выбрать();
Пока ВыборкаПоДокументам.Следующий() Цикл
// Нужно ли здесь сбрасывать ВыборкаПоТЧ?
СтруктураПоиска = Новый Структура("Ссылка", ВыборкаПоДокументам.Ссылка);
Пока ВыборкаПоТЧ.НайтиСледующий(СтруктураПоиска) Цикл
// Обработка найденных строк табличной части
КонецЦикла;
КонецЦикла;
Главный вопрос здесь заключается в том, продолжит ли НайтиСледующий() поиск с начала выборки при следующей итерации внешнего цикла или нам нужно принудительно «вернуть» систему в исходное состояние. Для отладки подобных алгоритмов и тестирования кода обработки выборок можно использовать консоль запросов с возможностью выполнения произвольного кода 1С.
Для того чтобы понять необходимость метода Сбросить(), выясним, как платформа 1С управляет позицией в выборке. Объект ВыборкаИзРезультатаЗапроса — это последовательный курсор. Методы Следующий() и НайтиСледующий() работают по схожему принципу:
Ложь (запись не найдена), это означает, что от текущей позиции до самого конца выборки подходящих под структуру строк больше нет. Курсор при этом перемещается в позицию за последней записью выборки.Теперь проанализируем ситуацию без использования метода Сбросить(). В первой итерации внешнего цикла (по первому документу) внутренняя выборка будет просмотрена до конца. Как только НайтиСледующий() вернет Ложь, курсор ВыборкаПоТЧ окажется в самом конце. Когда начнется вторая итерация внешнего цикла (для второго документа), вызов НайтиСледующий() снова попытается найти данные, начиная с текущей (последней) позиции. Естественно, он ничего не найдет, даже если данные для этого документа в выборке присутствуют.
Единственный надежный способ гарантировать, что поиск будет выполнен по всей выборке для каждого элемента внешней петли — это использование метода Сбросить(). Этот метод устанавливает курсор в позицию перед первой записью выборки. Посмотрим на доработанный и правильный пример кода:
ВыборкаПоДокументам = РезультатЗапроса[0].Выбрать();
ВыборкаПоТЧ = РезультатЗапроса[1].Выбрать();
Пока ВыборкаПоДокументам.Следующий() Цикл
// Обязательно сбрасываем позицию перед началом поиска для нового документа
ВыборкаПоТЧ.Сбросить();
СтруктураПоиска = Новый Структура("Ссылка", ВыборкаПоДокументам.Ссылка);
Пока ВыборкаПоТЧ.НайтиСледующий(СтруктураПоиска) Цикл
// Теперь мы гарантированно найдем все строки,
// так как поиск каждый раз начинается с начала выборки
КонецЦикла;
КонецЦикла;
Важный момент: Обратите внимание, что внутри вложенного цикла Пока ВыборкаПоТЧ.НайтиСледующий(...) метод Сбросить() вызывать нельзя. Если вы вызовете его внутри, вы создадите бесконечный цикл, так как НайтиСледующий() всегда будет находить одну и ту же первую подходящую строку и никогда не дойдет до конца.
Несмотря на то что НайтиСледующий() является рабочим инструментом, в среде профессиональной разработки 1С существуют более эффективные подходы — для их реализации есть консоль запросов и кода для разработчика 1С. Рассмотрим их подробнее, чтобы понимать, когда стоит отказаться от последовательного поиска в выборке.
1. Использование итогов в запросе (Группировки)
Это наиболее «правильный» с точки зрения производительности путь. Вместо двух раздельных запросов и ручного сопоставления, мы можем использовать ключевое слово ИТОГИ. В этом случае иерархия данных (Документ -> Строки) строится на уровне СУБД. Обход в коде будет выглядеть как вложенный цикл по группировкам, что работает значительно быстрее, так как платформе не нужно выполнять поиск — данные уже структурированы. Для интерактивного составления и тестирования сложных запросов с группировками подойдёт инженерная консоль запросов для 1С.
2. Выгрузка в Таблицу Значений и метод НайтиСтроки()
Если данные второго запроса не слишком объемны, их можно выгрузить в ТаблицаЗначений. Основное преимущество здесь — возможность использования индексов. При использовании НайтиСтроки() по индексированной колонке поиск выполняется практически мгновенно. В то время как НайтиСледующий() в выборке — это всегда линейный перебор, что при больших объемах данных (например, тысячи документов и десятки тысяч строк) превращает алгоритм в крайне медленный процесс со сложностью O(N * M).
3. Потоковое чтение и память
Единственный случай, когда НайтиСледующий() со Сбросить() оправдан — это работа с экстремально большими выборками, которые могут не поместиться в оперативную память сервера в виде ТаблицыЗначений. Выборка читает данные порциями (потоково), что позволяет экономить память, хотя и жертвует скоростью из-за постоянных повторных пробегов курсора.
Мы выяснили, что логика работы методов Следующий() и НайтиСледующий() неразрывно связана с текущим положением курсора в наборе данных. Выясним основные правила работы с ними:
Сбросить() для внутренней выборки, если внешний критерий поиска изменился.НайтиСледующий() не использует индексы, это последовательный перебор строк.Сбросить() переводит выборку в состояние «перед первой записью», подготавливая её к новому полному циклу обхода.Соблюдение этих простых правил позволит вам избежать трудноуловимых ошибок, когда данные «чудесным образом» пропадают из обработки при переходе к следующему объекту.