В практике разработчика 1С часто возникает задача вывода данных в иерархическом виде: например, сгруппировать записи по сотрудникам, а внутри каждого сотрудника — по транспортным средствам (ТС). Стандартным решением является использование конструкции ИТОГИ в тексте запроса и последующий обход выборки с типом ОбходРезультатаЗапроса.ПоГруппировкам. Однако существует альтернативный, более легковесный метод — СледующийПоЗначениюПоля. В этой статье мы разберем, как он работает, почему он может выдавать «кашу» из данных и как правильно организовать вложенные циклы.
Рассмотрим типичную ситуацию, с которой столкнулся автор темы. Имеется плоская выборка запроса (без итогов), содержащая поля Сотрудник, ТС, ВидВремени и другие. Разработчик пытается построить вложенную структуру, используя три цикла:
Пока ВыборкаДанныхОВремени.СледующийПоЗначениюПоля("Сотрудник") Цикл
Пока ВыборкаДанныхОВремени.СледующийПоЗначениюПоля("ТС") Цикл
Пока ВыборкаДанныхОВремени.Следующий() Цикл
// Обработка данных
КонецЦикла;
КонецЦикла;
КонецЦикла;
На первый взгляд логика верна, но на практике в отчет попадают дубли или данные перемешиваются. Проанализируем причину. Метод СледующийПоЗначениюПоля работает с плоским результатом запроса и перемещает курсор выборки до тех пор, пока значение в указанном поле совпадает с текущим. Если данные в выборке не упорядочены должным образом, система воспримет повторное появление того же сотрудника (после другого сотрудника) как новую группу.
Важнейшее правило, которое часто забывают: метод СледующийПоЗначениюПоля корректно работает только в том случае, если результат запроса жестко упорядочен по тем полям, которые используются для группировки. В нашем примере в тексте запроса обязательно должна присутствовать секция:
УПОРЯДОЧИТЬ ПО
Сотрудник,
ТранспортноеСредство
Без этой сортировки, если в выборке сначала идет «Иванов», затем «Петров», а потом снова «Иванов», цикл по сотрудникам сработает трижды, и вы получите разорванные данные по Иванову. Именно это произошло у автора вопроса, когда он увидел повторяющиеся строки для одного и того же сотрудника и ТС.
Разберем по шагам, что происходит с курсором (указателем текущей строки) при выполнении вложенных циклов. Это поможет нам понять, почему Следующий() во внутреннем цикле может привести к ошибкам.
СледующийПоЗначениюПоля("Сотрудник") позиционирует выборку на первой строке нового сотрудника.СледующийПоЗначениюПоля("ТС") внутри первого цикла позиционирует выборку на первой строке нового ТС для этого сотрудника.Следующий() внутри самого глубокого цикла начинает перебирать записи. Проблема в том, что Следующий() просто двигает курсор на одну позицию вперед. Если вы используете его без дополнительных проверок, он может «проглотить» записи, которые должны были принадлежать следующей итерации цикла по ТС.Проанализируем ситуацию: когда внутренний цикл Пока Выборка.Следующий() заканчивается? Он закончится только тогда, когда выборка будет полностью исчерпана до самого конца! Это означает, что внешние циклы по ТС и Сотруднику больше не получат ни одной записи, так как курсор уже стоит в конце результата запроса.
Если мы хотим использовать именно этот метод, нам нужно пересмотреть логику. На самом деле, для обработки данных внутри группы по ТС нам часто не нужен третий цикл Следующий(). В момент нахождения внутри цикла СледующийПоЗначениюПоля("ТС"), выборка уже стоит на первой записи этой группы.
Рассмотрим правильный подход к организации кода:
// В запросе обязательно: УПОРЯДОЧИТЬ ПО Сотрудник, ТС
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.СледующийПоЗначениюПоля("Сотрудник") Цикл
// Здесь мы в начале группы по Сотруднику
ТекущийСотрудник = Выборка.Сотрудник;
Пока Выборка.СледующийПоЗначениюПоля("ТС") Цикл
// Здесь мы в начале группы по ТС для текущего сотрудника
ТекущееТС = Выборка.ТС;
// ВАЖНО: Мы уже стоим на строке с данными!
// Если нам нужно собрать все записи внутри этой группы "Сотрудник + ТС",
// мы можем использовать метод СледующийПоЗначениюПоля еще раз или
// собирать данные, пока значения полей не изменятся.
// Однако чаще всего внутри ТС идут детальные записи (например, по датам).
// Чтобы не потерять управление, можно использовать такой прием:
Пока Истина Цикл
// Обрабатываем детальные данные текущей строки
Сообщить("Сотрудник: " + Выборка.Сотрудник + ", ТС: " + Выборка.ТС + ", Дата: " + Выборка.Дата);
// Пытаемся перейти к следующей строке, НО только в рамках текущего ТС
Если Не Выборка.Следующий() Тогда
Прервать; // Конец всей выборки
КонецЕсли;
// Если ТС или Сотрудник изменились - выходим во внешний цикл
Если Выборка.ТС <> ТекущееТС Или Выборка.Сотрудник <> ТекущийСотрудник Тогда
Прервать;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецЦикла;
Автор темы в итоге пришел к выводу, что использование ИТОГИ в запросе работает быстрее и надежнее. Проанализируем, почему это так. При использовании ИТОГИ сервер 1С берет на себя построение древовидной структуры. Нам остается лишь правильно вызвать метод Выбрать().
// Текст запроса: ... ИТОГИ ПО Сотрудник, ТС
РезультатЗапроса = Запрос.Выполнить();
ВыборкаСотрудник = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Сотрудник");
Пока ВыборкаСотрудник.Следующий() Цикл
ВыборкаТС = ВыборкаСотрудник.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ТС");
Пока ВыборкаТС.Следующий() Цикл
ВыборкаДетальные = ВыборкаТС.Выбрать(); // Прямой обход детальных записей
Пока ВыборкаДетальные.Следующий() Цикл
// Здесь обрабатываем конкретные Дни и Часы
КонецЦикла;
КонецЦикла;
КонецЦикла;
Преимущества этого метода:
Многие опытные разработчики предпочитают обходить выборку в одном цикле Следующий(), самостоятельно отслеживая смену ключевых полей. Это самый быстрый способ с точки зрения производительности, так как нет накладных расходов на вложенные вызовы методов выборки.
Выборка = Запрос.Выполнить().Выбрать();
ПредСотрудник = Неопределено;
ПредТС = Неопределено;
Пока Выборка.Следующий() Цикл
Если Выборка.Сотрудник <> ПредСотрудник Тогда
// Сменился сотрудник - выводим заголовок группировки
ПредСотрудник = Выборка.Сотрудник;
ПредТС = Неопределено; // Сбрасываем ТС для нового сотрудника
КонецЕсли;
Если Выборка.ТС <> ПредТС Тогда
// Сменилось ТС
ПредТС = Выборка.ТС;
КонецЕсли;
// Обработка детальной строки
КонецЦикла;
Выясним, в каких случаях этот метод всё же выигрывает у ИТОГИ. Основной аргумент — экономия ресурсов. На огромных объемах данных (десятки и сотни тысяч строк) построение итогов в запросе может существенно нагрузить сервер БД и оперативную память сервера приложений 1С. Метод СледующийПоЗначениюПоля работает с «плоским» потоком данных, не создавая дополнительных временных таблиц для расчета итогов. Если вам нужно просто последовательно прочитать данные и сгруппировать их «на лету» без расчета сложных агрегатов, этот метод будет идеален.
Подведем итоги:
УПОРЯДОЧИТЬ ПО в запросе при использовании СледующийПоЗначениюПоля.Следующий() или СледующийПоЗначениюПоля() необратимо двигает курсор вперед.ИТОГИ или ручное сравнение значений в одном цикле.