Приветствуем вас, уважаемые разработчики! Сегодня мы разберем классическую, но весьма каверзную проблему, с которой сталкивались практически все, кому доводилось работать со встроенным языком запросов платформы 1С:Предприятие 7.7. Речь пойдет о так называемых «черных запросах» и жестком ограничении платформы при попытке объединить и сгруппировать несколько реквизитов из шапки одного и того же документа в одну общую переменную запроса.
Представьте стандартную ситуацию: у нас есть документ, например, ЭДО_Поступление. В его шапке определены два реквизита типа справочник Сотрудники — это Сотрудник1 и Сотрудник2. Нам необходимо построить один запрос, который сформирует единую группировку Сотрудник, куда попадут значения из обоих реквизитов. На первый взгляд кажется, что платформа должна поддерживать перечисление через запятую в описании переменной запроса. Однако на практике разработчики сталкиваются с тем, что в результат попадает только первое заполненное значение. Давайте детально выясним причину, почему так происходит, и рассмотрим три надежных способа решения этой задачи.
Для начала проанализируем поведение движка запросов 1С 7.7. Когда мы пишем в теле запроса следующую конструкцию:
Сотрудник = Документ.ЭДО_Поступление.Сотрудник1, Документ.ЭДО_Поступление.Сотрудник2;
мы интуитивно ожидаем, что система выполнит операцию, аналогичную UNION ALL в терминах SQL, то есть «раздвоит» физическую строку документа в выборке. Но на самом деле этот механизм в 1С 7.7 спроектирован исключительно для работы с полиморфными переменными.
Перечисление реквизитов через запятую создано для того, чтобы собирать однотипные данные из разных видов документов или регистров для сквозного анализа. Например, если бы мы собирали сотрудников из документа Приказ и документа Договор, система отработала бы корректно, так как для каждой строки физической таблицы базы данных считывался бы только один реквизит. Но когда мы указываем реквизиты одной и той же таблицы (одного документа), парсер платформы обрабатывает их последовательно. Заполнив переменную Сотрудник первым успешно прочитанным значением (из реквизита Сотрудник1), движок запросов считает задачу выполненной для текущей записи и переходит к следующей строке таблицы. Значение реквизита Сотрудник2 в этой строке просто игнорируется.
К счастью, обойти это ограничение платформы можно несколькими способами. Рассмотрим их подробнее.
Этот способ является наиболее стандартным, безопасным и не требует использования сторонних внешних компонент. Если перед нами стоит задача получить общие итоги по сотрудникам без потери производительности на ручном переборе документов, мы выполняем два независимых быстрых запроса и объединяем их результаты в объекте ТаблицаЗначений.
Разберем этот алгоритм по шагам:
Сотрудник присваиваем значение реквизита Сотрудник1. Результат выгружаем в первую таблицу значений.Сотрудник присваиваем значение реквизита Сотрудник2. Результат выгружаем во вторую таблицу значений.Свернуть для суммирования показателей и получения уникального списка сотрудников.Посмотрим на практический пример реализации этого метода на встроенном языке 1С 7.7:
// Создаем таблицы значений для результатов
ТЗ1 = СоздатьОбъект("ТаблицаЗначений");
ТЗ2 = СоздатьОбъект("ТаблицаЗначений");
РезультирующаяТЗ = СоздатьОбъект("ТаблицаЗначений");
// Шаг 1: Первый запрос по Сотрудник1
Запрос1 = СоздатьОбъект("Запрос");
ТекстЗапроса1 =
"Период с ДатаНач по ДатаКон;
|ОбрабатыватьДокументы Все;
|Сотрудник = Документ.ЭДО_Поступление.Сотрудник1;
|Сумма = Документ.ЭДО_Поступление.СуммаДокумента;
|Группировка Сотрудник без групп;
|Функция ВсегоСумма = Сумма(Сумма);";
Если Запрос1.Выполнить(ТекстЗапроса1) = 1 Тогда
Запрос1.Выгрузить(ТЗ1, 1, 0);
КонецЕсли;
// Шаг 2: Второй запрос по Сотрудник2
Запрос2 = СоздатьОбъект("Запрос");
ТекстЗапроса2 =
"Период с ДатаНач по ДатаКон;
|ОбрабатыватьДокументы Все;
|Сотрудник = Документ.ЭДО_Поступление.Сотрудник2;
|Сумма = Документ.ЭДО_Поступление.СуммаДокумента;
|Группировка Сотрудник без групп;
|Функция ВсегоСумма = Сумма(Сумма);";
Если Запрос2.Выполнить(ТекстЗапроса2) = 1 Тогда
Запрос2.Выгрузить(ТЗ2, 1, 0);
КонецЕсли;
// Шаг 3: Объединение таблиц в одну структуру
ТЗ1.Выгрузить(РезультирующаяТЗ);
ТЗ2.ВыбратьСтроки();
Пока ТЗ2.ПолучитьСтроку() = 1 Цикл
РезультирующаяТЗ.НоваяСтрока();
РезультирующаяТЗ.Сотрудник = ТЗ2.Сотрудник;
РезультирующаяТЗ.ВсегоСумма = ТЗ2.ВсегоСумма;
КонецЦикла;
// Шаг 4: Свертка результатов для получения итогов
РезультирующаяТЗ.Свернуть("Сотрудник", "ВсегоСумма");
// Теперь в РезультирующаяТЗ у нас находится полный список сотрудников
// из обоих реквизитов с корректно просуммированными показателями.
РезультирующаяТЗ.ВыбратьСтроки();
Пока РезультирующаяТЗ.ПолучитьСтроку() = 1 Цикл
Сообщить("Сотрудник: " + РезультирующаяТЗ.Сотрудник + " - Итоговая сумма: " + РезультирующаяТЗ.ВсегоСумма);
КонецЦикла;
Этот способ работает очень быстро, так как оба запроса выполняются на уровне СУБД (или DBF-движка) с использованием индексов за указанный период, без медленного построчного перебора документов средствами встроенного языка.
Рассмотрим альтернативный вариант, предложенный в обсуждении. Мы можем составить запрос, группирующий данные по самим документам за период, а затем при обходе выборки вручную анализировать реквизиты каждого найденного документа.
Здесь важно избежать классической ошибки новичков — использования метода ВыбратьДокументы для полного перебора метаданных, что на больших базах данных гарантированно приводит к зависанию системы. Вместо этого мы заставим SQL-сервер или DBF-движок предварительно отфильтровать документы по периоду с помощью встроенного запроса, а затем точечно обработаем результат.
Посмотрим на пример кода:
ТекстЗапроса =
"Период с ДатаНач по ДатаКон;
|ОбрабатыватьДокументы Все;
|Док = Документ.ЭДО_Поступление.ТекущийДокумент;
|Группировка Док;";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
ТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗ.НоваяКолонка("Сотрудник");
ТЗ.НоваяКолонка("Сумма", "Число");
Пока Запрос.Группировка("Док") = 1 Цикл
ТекущийДок = Запрос.Док;
// Проверяем заполненность первого сотрудника в документе
Если ПустоеЗначение(ТекущийДок.Сотрудник1) = 0 Тогда
ТЗ.НоваяСтрока();
ТЗ.Сотрудник = ТекущийДок.Сотрудник1;
ТЗ.Сумма = ТекущийДок.СуммаДокумента;
КонецЕсли;
// Проверяем заполненность второго сотрудника в документе
Если ПустоеЗначение(ТекущийДок.Сотрудник2) = 0 Тогда
ТЗ.НоваяСтрока();
ТЗ.Сотрудник = ТекущийДок.Сотрудник2;
ТЗ.Сумма = ТекущийДок.СуммаДокумента;
КонецЕсли;
КонецЦикла;
// Сворачиваем таблицу по сотрудникам с суммированием числовых показателей
ТЗ.Свернуть("Сотрудник", "Сумма");
КонецЕсли;
Важное предупреждение о производительности: Этот метод уступает первому по скорости, поскольку при обращении к свойствам ТекущийДок.Сотрудник1 и ТекущийДок.Сотрудник2 платформа производит физическое позиционирование и чтение каждого объекта документа из базы данных (так называемое "обращение через точку к невыбранным реквизитам"). Если документов в периоде много (более нескольких тысяч), этот процесс может занять значительное время.
Если ваша информационная база 1С 7.7 работает под управлением Microsoft SQL Server (или если вы используете локальную версию с ВК SQLite), то самым элегантным и быстрым решением будет использование прямых SQL-запросов с помощью компоненты 1С++ и класса ODBCRecordset.
Использование прямого запроса позволяет выполнить операцию UNION ALL непосредственно на сервере базы данных. Это возвращает готовую выборку за один проход, сводя сетевой трафик и нагрузку на сервер к минимуму.
Давайте посмотрим на пример реализации:
// Инициализируем класс ODBCRecordset из ВК 1С++
ЗапросSQL = СоздатьОбъект("ODBCRecordset");
// Формируем текст SQL-запроса с типизацией метаданных 1С
ТекстSQL = "
|SELECT
| Doc.Сотрудник1 AS [Сотрудник $Справочник.Сотрудники],
| Doc.СуммаДокумента AS Сумма
|FROM
| $Документ.ЭДО_Поступление AS Doc
|WHERE
| Doc.IDDoc IN (SELECT IDDoc FROM _1SJourn WHERE Date_Time_IDDoc BETWEEN :НачДата AND :КонДата)
|
|UNION ALL
|
|SELECT
| Doc.Сотрудник2 AS [Сотрудник $Справочник.Сотрудники],
| Doc.СуммаДокумента AS Сумма
|FROM
| $Документ.ЭДО_Поступление AS Doc
|WHERE
| Doc.IDDoc IN (SELECT IDDoc FROM _1SJourn WHERE Date_Time_IDDoc BETWEEN :НачДата AND :КонДата)
|";
// Передаем параметры дат для фильтрации по журналу документов
ЗапросSQL.УстановитьТекстовыйПараметр("НачДата", НачДата);
ЗапросSQL.УстановитьТекстовыйПараметр("КонДата", КонДата);
// Выполняем запрос и получаем результат напрямую в Таблицу Значений
ТЗ_Результат = ЗапросSQL.ВыполнитьИнструкцию(ТекстSQL);
// Производим окончательную свертку полученной таблицы на стороне 1С
ТЗ_Результат.Свернуть("Сотрудник", "Сумма");
Этот метод демонстрирует феноменальную скорость работы (в десятки и сотни раз быстрее штатных средств платформы при больших объемах данных), так как вся тяжелая работа по выборке и объединению данных происходит на стороне СУБД, а в 1С возвращается уже подготовленный плоский массив данных.
Мы детально изучили ограничение платформы 1С 7.7, не позволяющее группировать данные по нескольким реквизитам одного документа в рамках одного стандартного "черного" запроса. Мы рассмотрели три жизнеспособных сценария решения этой проблемы:
Выбирайте наиболее подходящий способ в зависимости от требований вашего проекта к производительности и используемой архитектуры баз данных!