При работе в платформе 1С:Предприятие 7.7 (в среде разработчиков часто называемой «клюшками») программисты регулярно сталкиваются с ограничениями встроенного языка запросов. Одним из наиболее классических затруднений является попытка отсечь нулевые остатки товаров непосредственно в самом тексте запроса. В отличие от современных версий платформы (1С 8.x), где встроенный язык запросов максимально приближен к стандарту SQL, в «семерке» действуют совершенно иные механизмы.
В этой статье мы подробно разберем, почему возникает популярная ошибка Ошибка в выражении 'КонОстКол', проанализируем внутреннее поведение движка запросов 1С 7.7 и рассмотрим несколько надежных способов получения только актуальных (ненулевых) остатков товаров.
Для начала давайте проанализируем исходный код запроса, с которым обратился автор темы:
ТекстЗапроса = "
|ПЕРИОД С ДатаОтчета По ДатаОтчета;
|Склад = Регистр.ОстаткиТоваров.Склад;
|Товар = Регистр.ОстаткиТоваров.Товар;
|Кол = Регистр.ОстаткиТоваров.ОстатокТовара;
|Функция КонОстКол = КонОст(Кол);//
|Условие (КонОстКол>0);
|Условие (Товар в ВыбТовары);
|Условие (Склад в ВыбСклады);
|Группировка Товар без групп все;";
ЗапросОстатки.Выполнить(ТекстЗапроса);
При попытке выполнить этот код система выдает ошибку компиляции: Условие (КонОстКол <<?>> >0); Запрос[7] : Ошибка в выражении 'КонОстКол'. Здесь кроются сразу две ключевые проблемы — одна синтаксическая, а вторая глубокая архитектурная.
Проблема №1: Синтаксическая ошибка (пропущена точка с запятой)
В строке описания функции Функция КонОстКол = КонОст(Кол) в самом конце отсутствует обязательный символ точки с запятой ;. Из-за этого парсер запросов 1С склеивает эту строку со следующей, теряет контекст и спотыкается на операторе Условие.
Проблема №2: Использование агрегатной функции в секции «Условие»
Даже если мы поставим точку с запятой, запрос все равно выдаст ошибку. Дело в том, что в стандартном («черном») запросе 1С 7.7 нельзя использовать имя переменной, рассчитываемой через агрегатную функцию (в данном случае КонОстКол), внутри оператора Условие(...).
Давайте выясним причину этого поведения. Секция Условие в языке запросов 1С 7.7 обрабатывается на этапе выборки исходных строк из физических таблиц базы данных (это аналог предложения WHERE в языке SQL). В этот момент система считывает отдельные записи (движения или итоги регистра). Агрегатная функция конечного остатка КонОст() вычисляется только на этапе группировки данных, когда все строки уже прочитаны и сгруппированы по товарам и складам. На этапе работы фильтра Условие конечного остатка просто физически еще не существует в памяти, поэтому платформа выдает ошибку обращения к неизвестной переменной.
---
Рассмотрим несколько рабочих подходов к решению этой проблемы — от простых базовых исправлений до профессиональной оптимизации.
Наиболее надежный, методологически верный и производительный способ в рамках штатных механизмов 1С 7.7 — это перенос фильтрации нулевых итогов из самого текста запроса непосредственно в цикл обхода его результатов.
Давайте посмотрим, как будет выглядеть правильный программный код:
// Формируем чистый и корректный запрос
ТекстЗапроса = "
|ПЕРИОД С ДатаОтчета По ДатаОтчета;
|Склад = Регистр.ОстаткиТоваров.Склад;
|Товар = Регистр.ОстаткиТоваров.Товар;
|Кол = Регистр.ОстаткиТоваров.ОстатокТовара;
|Функция КонОстКол = КонОст(Кол); // Точка с запятой обязательна!
|Условие (Товар в ВыбТовары);
|Условие (Склад в ВыбСклады);
|Группировка Товар без групп; // Убрали ключевое слово 'все'
|";
ЗапросОстатки = СоздатьОбъект("Запрос");
Если ЗапросОстатки.Выполнить(ТекстЗапроса) = 1 Тогда
// Начинаем обход результатов запроса
Пока ЗапросОстатки.Группировка(1) = 1 Цикл
// Отсекаем нулевые и отрицательные остатки прямо в цикле
Если ЗапросОстатки.КонОстКол <= 0 Тогда
Продолжить; // Пропускаем эту номенклатуру и идем к следующей
КонецЕсли;
// Здесь выполняется полезный код (вывод в печатную форму, заполнение ТЗ)
Сообщить("Товар: " + ЗапросОстатки.Товар + " | Остаток: " + ЗапросОстатки.КонОстКол);
КонецЦикла;
КонецЕсли;
Преимущества этого способа:
---
Обратите внимание на строку группировки в нашем примере: Группировка Товар без групп;. Мы намеренно убрали ключевое слово все (в исходном запросе автора было Группировка Товар без групп все;).
Выясним разницу:
Если ЗапросОстатки.КонОстКол <= 0 Тогда Продолжить; в цикле обхода остается строго необходимой.---
В обсуждении на форуме (сообщение №6) был предложен интересный обходной путь:
|Условие (ЗапросОстатки.КонОстКол > 0);
Давайте проанализируем, как это работает и почему опытные разработчики относятся к этому приему с крайней осторожностью. Когда мы пишем внутри запроса условие, обращающееся к самому объекту запроса через точку, платформа выполняет расчет этого условия динамически в процессе прохода по записям. Такой запрос действительно скомпилируется без ошибок и выдаст результат.
Существенные минусы «лайфхака»:
НачОст) или приход с расходом, такое условие сломает всю математику. Движок запросов начнет отфильтровывать записи на промежуточных этапах, что приведет к абсолютно хаотичным и неверным цифрам в оборотах.Резюме по Способу Б: Использовать данный трюк можно только в самых простых и некритичных отчетах, где получается исключительно конечный остаток, а объем данных минимален.
---
Если ваша информационная база 1С 7.7 работает в файловом режиме (DBF), то Способ А является для вас единственным верным решением. Однако, если вы администрируете и разрабатываете крупную систему на базе MS SQL Server, штатные запросы 1С 7.7 могут стать «бутылочным горлышком» системы.
Штатный механизм запросов 1С 7.7 транслирует «черные» запросы в SQL крайне неэффективно, зачастую вытягивая огромные массивы данных на сторону клиента для локальной обработки. В профессиональной среде для решения задач получения остатков без нулевых значений используют внешние компоненты, такие как 1С++.
Прямой SQL-запрос к таблице итогов регистра накопления (например, к таблице остатков RG*) позволяет выполнить моментальную фильтрацию данных силами самого сервера СУБД. На стороне SQL это решается использованием конструкции HAVING SUM(...) > 0.
Рассмотрим пример построения прямого SQL-запроса через класс ODBCRecordset компоненты 1С++:
// Пример использования внешней компоненты 1С++ для прямого запроса
БазаДанных = СоздатьОбъект("ODBCRecordset");
// Строим эффективный SQL-запрос к таблице итогов регистра ОстаткиТоваров
ТекстSQL = "
|SELECT
| Рег.SP_Склад as [Склад $Справочник.Склады],
| Рег.SP_Товар as [Товар $Справочник.Номенклатура],
| SUM(Рег.SP_Количество) as Остаток
|FROM
| $РегистрИтоги.ОстаткиТоваров Рег
|WHERE
| Рег.Period = :ВыбПериод
|GROUP BY
| Рег.SP_Склад, Рег.SP_Товар
|HAVING
| SUM(Рег.SP_Количество) > 0
|";
БазаДанных.УстановитьТекстовыйПараметр("ВыбПериод", НачалоМесяца(ДатаОтчета));
ТаблицаРезультатов = БазаДанных.ВыполнитьИнструкцию(ТекстSQL);
// В ТаблицаРезультатов окажутся только товары с остатком строго больше 0
Такой запрос на больших базах данных выполняется в десятки и сотни раз быстрее стандартного, так как СУБД передает на клиентский компьютер только готовую отфильтрованную таблицу без лишнего «мусора».
---
Разработчикам на платформе 1С 7.7 важно помнить ограничения встроенного языка запросов. Для безопасного и быстрого получения остатков товаров без нулевых значений придерживайтесь следующих правил:
Условие на агрегатные функции (КонОст, НачОст, Приход, Расход) непосредственно в теле запроса.все из параметров группировки.Если <...> <= 0 Тогда Продолжить; КонецЕсли; при обходе выборки в цикле.