Часто в практике разработки на платформе 1С 7.7 возникает классическая задача: получить из регистра остатки товаров, по которым за выбранный период были движения (приход или расход). На первый взгляд, задача кажется тривиальной, однако при попытке решить ее штатным «черным» запросом разработчики сталкиваются с тем, что расчет остатков ломается. Рассмотрим подробнее, почему так происходит, и разберем по шагам рабочие способы решения этой проблемы.
Давайте проанализируем ситуацию на примере классического кода. Многие разработчики пытаются написать примерно следующий штатный запрос:
|Период С ДатаНач По ДатаОконч;
|Товар = Регистр.Товары.Номенклатура;
|Количество = Регистр.Товары.Количество;
|Функция ПриходК = Приход(Количество);
|Функция РасходК = Расход(Количество);
|Функция КонОстК = КонОст(Количество);
|Условие((Запрос.РасходК<>0) ИЛИ (Запрос.ПриходК<>0));
При выполнении этого кода мы видим, что остатки на конец периода (КонОстК) перестают выводиться корректно или вовсе обнуляются. Выясним причину такого поведения.
Архитектура штатного механизма запросов 1С 7.7 устроена так, что функции (например, Приход или Расход) и расчет остатков (КонОст) вычисляются «на лету» в момент перебора физических записей таблиц итогов и движений. Если в тексте запроса мы накладываем условие на функцию оборота, парсер 1С применяет этот фильтр на самом нижнем уровне выборки. В результате отсекаются те физические записи, которые критически необходимы движку для математического расчета правильного начального и конечного остатка. В штатном запросе 7.7 технически невозможно разделить фильтрацию уже готового конечного результата от фильтрации исходных данных для расчета. Итоговые цифры неизбежно искажаются.
Разберем первый рабочий способ, который не требует использования сторонних SQL-компонент. Суть метода заключается в том, чтобы разнести получение данных и их фильтрацию на два этапа.
КонОст посчитается абсолютно верно.ТаблицаЗначений.Важно помнить, что при удалении строк из таблицы значений в цикле, обход нужно делать с конца (от максимального индекса к первому), чтобы не сбить нумерацию строк. Рассмотрим пример логики:
ТабРез = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТабРез, 0, 0);
Для Индекс = -ТабРез.КоличествоСтрок() По -1 Цикл
ТекСтрока = -Индекс;
ТабРез.ПолучитьСтрокуПоНомеру(ТекСтрока);
Если (ТабРез.ПриходК = 0) И (ТабРез.РасходК = 0) Тогда
ТабРез.УдалитьСтроку(ТекСтрока);
КонецЕсли;
КонецЦикла;
Если вам необходимы итоги по вышестоящим группам справочника (сохранение иерархии), мы можем делать выгрузку с итогами по группам. После удаления пустых строк потребуется дополнительно пересчитать итоги для вышестоящих групп или построить дерево программно.
Рассмотрим более производительный вариант первого способа. Если в базе установлено расширение 1С++, мы можем использовать объект ИндексированнаяТаблица (ИТЗ). В отличие от обычного перебора, ИТЗ позволяет сделать фильтрацию практически мгновенно.
ПриходК).Этот способ отлично спасает, когда база данных работает в формате DBF, и использование полноценных SQL-запросов затруднено, но требуется высокая скорость обработки.
Проанализируем самый мощный и быстрый инструмент — прямые SQL-запросы через компоненты 1С++ или 1SQLite. Переход на прямые запросы позволяет решить задачу элегантно и одним запросом.
В компоненте 1С++ реализован механизм виртуальных таблиц, аналогичный тому, что используется в современных версиях 1С 8.х. Мы можем использовать макрос виртуальной таблицы, например $РегистрОстаткиОбороты.Товары(...). В параметрах этой таблицы задаются условия объединения, периодичность и нужные фильтры. Компонента самостоятельно сгенерирует оптимальный SQL-код, который обратится к физическим таблицам итогов (RG) и движений (RA), и вернет правильные остатки и обороты.
Главное преимущество SQL — мы можем применить секцию HAVING или обернуть результат во вложенный запрос и отфильтровать итоговые данные WHERE (Приход <> 0 OR Расход <> 0). При этом расчет остатков не пострадает, так как SQL-сервер сначала произведет агрегацию (группировку) данных, а уже затем отбросит строки с нулевыми оборотами.
Автор обсуждаемой темы провел тестирование на реальной базе за месяц. Результаты показали значительное ускорение:
Штатный черный запрос выполнялся около 18 секунд.
Прямой запрос SQLite выполнился всего за 3.4 - 4.2 секунды.
При тестировании прямых запросов вы можете заметить, что количество строк в результате SQLite и штатного запроса немного различается, хотя итоговые суммы сходятся до копейки. Разберем основные причины таких расхождений:
По ДатаОконч автоматически раздвигает границу расчета до конца дня (23:59:59) или до Точки Актуальности (ТА). Разработчики прямых запросов часто забывают программно сместить границу периода на конец дня, из-за чего могут «выпасть» последние документы периода.Подводя итог, можно уверенно сказать: если перед вами стоит задача сложных фильтраций по регистрам в 1С 7.7 — лучшим выбором будет использование прямых запросов. Если же использование внешних DLL недопустимо — смело выгружайте результат штатного запроса в ТаблицаЗначений и фильтруйте данные программно.