Язык запросов 1С: механизм выполнения и приёмы ускорения
Содержание
Запрос в 1С — это способ быстро прочитать данные из базы: остатки по складу, продажи за период, список контрагентов с долгом. Пишется он на встроенном языке запросов, похожем на SQL, но с русскими ключевыми словами и своими объектами — виртуальными таблицами регистров.
Главное, что нужно держать в голове: язык запросов 1С умеет только читать данные. Изменить, удалить или записать что-либо через запрос нельзя — для этого есть объектная модель (документы, регистры, их методы Записать()). Запрос возвращает выборку, а что с ней делать — решает уже код обработки или отчёт.
Ниже — как запрос проходит путь от текста до результата, из каких конструкций он собирается, чем левое соединение отличается от внутреннего, зачем нужны временные таблицы и почему один и тот же запрос может выполняться доли секунды или минуты.
Что происходит, когда запрос выполняется
Запрос в коде создаётся объектом Запрос:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Наименование КАК Товар,
| Номенклатура.Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Товар);
КонецЦикла;
Что происходит под капотом по шагам:
- Разбор текста. Платформа парсит текст запроса на языке 1С и строит внутреннее представление.
- Трансляция в SQL. Текст переводится на диалект SQL той СУБД, с которой работает база — PostgreSQL, MS SQL или встроенная файловая СУБД. Имена метаданных («Справочник.Номенклатура») превращаются в имена реальных таблиц вроде
_Reference123. - Выполнение на стороне СУБД. Сервер базы данных выполняет SQL, использует индексы, строит план запроса. Вся тяжёлая работа идёт именно здесь, а не в коде 1С.
- Возврат результата. СУБД отдаёт выборку платформе, платформа — вашему коду как объект
РезультатЗапроса.
Отсюда вывод для производительности: запрос тем быстрее, чем меньше данных просеивает СУБД и чем лучше ложится на индексы. Оптимизация запроса — это почти всегда «помочь СУБД взять данные по индексу и не читать лишнего».
В файловом варианте базы роль СУБД играет сама платформа, поэтому тяжёлые запросы в файловой базе заметно медленнее, чем в клиент-серверной на том же железе.
Из чего собирается запрос: базовый синтаксис
Минимальный запрос состоит из двух секций — что выбрать и откуда:
ВЫБРАТЬ
Товар.Наименование,
Товар.ЕдиницаИзмерения
ИЗ
Справочник.Номенклатура КАК Товар
Основные секции в порядке их записи:
| Секция | Назначение | Аналог в SQL |
|---|---|---|
ВЫБРАТЬ | перечень полей результата | SELECT |
ИЗ | источники данных (таблицы) | FROM |
ГДЕ | условие отбора строк | WHERE |
СГРУППИРОВАТЬ ПО | группировка для агрегатов | GROUP BY |
ИМЕЮЩИЕ | отбор по агрегатам | HAVING |
УПОРЯДОЧИТЬ ПО | сортировка | ORDER BY |
ИТОГИ ПО | итоги по группировкам | — (своё) |
Несколько приёмов, которые встречаются в каждом втором запросе:
- 🔹
ВЫБРАТЬ РАЗЛИЧНЫЕ— убрать дубли строк (аналог DISTINCT). - 🔹
ВЫБРАТЬ ПЕРВЫЕ 10— вернуть только первые N строк (аналог TOP). - 🔹
КАК Товар— псевдоним таблице или полю; по псевдониму потом обращаемся к полю через точку. - 🔹 Параметры пишутся через
&:ГДЕ Товар.Цена > &МинЦена. Значение задаётся в коде:Запрос.УстановитьПараметр("МинЦена", 100);.
Условный расчёт поля делается через ВЫБОР (аналог CASE):
ВЫБРАТЬ
Заказ.Номер,
ВЫБОР
КОГДА Заказ.СуммаДокумента > 100000 ТОГДА "Крупный"
КОГДА Заказ.СуммаДокумента > 10000 ТОГДА "Средний"
ИНАЧЕ "Мелкий"
КОНЕЦ КАК Категория
ИЗ
Документ.ЗаказКлиента КАК Заказ
Соединения таблиц и чем они различаются
Когда данные лежат в двух таблицах — например, строки заказа и справочник номенклатуры — их соединяют. Тип соединения определяет, какие строки попадут в результат.
ВНУТРЕННЕЕ СОЕДИНЕНИЕ— только строки, для которых нашлось совпадение в обеих таблицах.ЛЕВОЕ СОЕДИНЕНИЕ— все строки левой таблицы; из правой подтягивается совпадение, а если его нет —NULL.ПРАВОЕ СОЕДИНЕНИЕ— зеркально левому, все строки правой таблицы.ПОЛНОЕ СОЕДИНЕНИЕ— все строки обеих таблиц, несовпавшие дополняютсяNULL.
На практике в 90% случаев используется левое соединение: берём документы и подтягиваем к ним справочные данные.
ВЫБРАТЬ
СтрокиЗаказа.Номенклатура,
СтрокиЗаказа.Количество,
ЕСТЬNULL(Цены.Цена, 0) КАК Цена
ИЗ
Документ.ЗаказКлиента.Товары КАК СтрокиЗаказа
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО СтрокиЗаказа.Номенклатура = Цены.Номенклатура
После левого соединения в несовпавших строках будет NULL. Если такое поле попадёт в арифметику (Количество * Цена), результат тоже станет NULL. Поэтому поля из присоединённой таблицы почти всегда оборачивают в ЕСТЬNULL(Поле, 0).
Чем соединения опасны для скорости:
- Каждое соединение — это дополнительная работа для СУБД. Чем больше таблиц, тем сложнее план.
- Декартово произведение — если забыть условие
ПОили задать его неверно, каждая строка левой таблицы умножится на все строки правой. Запрос вернёт миллионы строк и «повиснет». - Соединение с подзапросом или виртуальной таблицей дороже, чем с обычной таблицей по индексированному полю.
Агрегатные функции и группировка
Чтобы посчитать суммы, количества и средние, поля делят на две группы: по которым группируем и которые агрегируем.
Агрегатные функции языка запросов:
СУММА(Поле)— сумма;КОЛИЧЕСТВО(Поле)— число непустых значений,КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле)— число уникальных;МАКСИМУМ(Поле),МИНИМУМ(Поле)— экстремумы;СРЕДНЕЕ(Поле)— среднее.
Правило: все поля, которые не внутри агрегатной функции, обязаны быть в СГРУППИРОВАТЬ ПО. Иначе запрос не выполнится.
ВЫБРАТЬ
Продажи.Контрагент,
СУММА(Продажи.Сумма) КАК Оборот,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Продажи.Ссылка) КАК ЧислоСделок
ИЗ
Документ.РеализацияТоваровУслуг КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
ИМЕЮЩИЕ
СУММА(Продажи.Сумма) > 50000
Разница между ГДЕ и ИМЕЮЩИЕ: ГДЕ отбирает строки до группировки (по обычным полям), ИМЕЮЩИЕ — после (по агрегатам). Если условие можно поставить в ГДЕ, ставьте туда — так СУБД отсеет лишнее раньше и быстрее.
Отдельная конструкция 1С — ИТОГИ. Она не группирует строки, а добавляет к детальным строкам итоговые по уровням иерархии:
...
ИТОГИ
СУММА(Сумма)
ПО
Контрагент
Итоги удобно читать выборкой по группировкам: Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам).
Подзапросы и временные таблицы
Когда логику в один проход не уложить, есть два инструмента.
Вложенный подзапрос — запрос внутри секции ИЗ или условия. Подходит для небольших наборов данных:
ВЫБРАТЬ
Товар.Наименование
ИЗ
Справочник.Номенклатура КАК Товар
ГДЕ
Товар.Ссылка В
(ВЫБРАТЬ РАЗЛИЧНЫЕ Строки.Номенклатура
ИЗ Документ.ЗаказКлиента.Товары КАК Строки)
Временная таблица — результат промежуточного запроса, сохранённый в памяти и доступный следующим запросам пакета. Создаётся через ПОМЕСТИТЬ:
ВЫБРАТЬ
Строки.Номенклатура,
СУММА(Строки.Количество) КАК Количество
ПОМЕСТИТЬ ВтТовары
ИЗ
Документ.ЗаказКлиента.Товары КАК Строки
СГРУППИРОВАТЬ ПО
Строки.Номенклатура
;
ВЫБРАТЬ
ВтТовары.Номенклатура,
ВтТовары.Количество,
Остатки.КоличествоОстаток
ИЗ
ВтТовары КАК ВтТовары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
ПО ВтТовары.Номенклатура = Остатки.Номенклатура
Запросы в пакете разделяются ;. Чтобы временные таблицы жили между запросами, запросу назначают менеджер временных таблиц:
Менеджер = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = Менеджер;
Когда что выбирать:
- 🔹 Подзапрос — набор маленький, используется один раз.
- 🔹 Временная таблица — набор большой или нужен в нескольких местах запроса; результат объёмный и к нему идёт несколько соединений. СУБД индексирует временную таблицу и работает с ней быстрее, чем с повторяющимся подзапросом.
Отдельно стоит ОБЪЕДИНИТЬ и ОБЪЕДИНИТЬ ВСЕ — они складывают результаты двух запросов «в столбик». ОБЪЕДИНИТЬ убирает дубли (и потому медленнее), ОБЪЕДИНИТЬ ВСЕ оставляет всё как есть — используйте его, если дублей заведомо нет.
Виртуальные таблицы регистров — чем они особенные
В 1С данные учёта лежат в регистрах, и к ним обращаются не напрямую, а через виртуальные таблицы. Это главное отличие языка запросов 1С от чистого SQL.
Для регистра накопления типовые виртуальные таблицы:
.Остатки(&Период, Отбор)— остатки на дату;.Обороты(&Начало, &Конец, Периодичность, Отбор)— обороты за период;.ОстаткиИОбороты(...)— и то, и другое сразу.
Для регистра сведений — .СрезПоследних(&Период, Отбор) и .СрезПервых(...).
Ключевой приём оптимизации: отбор виртуальной таблицы задавайте в её параметрах, а не в секции ГДЕ. Параметры виртуальной таблицы сужают выборку внутри, до расчёта итогов. Условие в ГДЕ применяется уже к готовому результату — платформа посчитает остатки по всему регистру, а потом отбросит лишнее.
Правильно:
ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(
&НаДату, Склад = &Склад) КАК Остатки
Медленно (отбор снаружи):
ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(&НаДату, ) КАК Остатки
ГДЕ Остатки.Склад = &Склад
Как ускорить медленный запрос
Если отчёт «думает» минуту, проблема почти всегда в одном из пунктов ниже.
- Обращение к полям через точку по составному типу. Запись
Регистратор.ДатаилиКонтрагент.ИНН, где поле — ссылка составного типа, платформа разворачивает в неявные левые соединения с каждой таблицей типа. Один «безобидный».может добавить десяток соединений. Лечится черезВЫРАЗИТЬ(Поле КАК Документ.РеализацияТоваровУслуг).Дата— явно указываем один тип. - Отбор виртуальной таблицы в
ГДЕвместо параметров — см. предыдущий раздел. - Соединение с подзапросом. Если соединяете таблицу с вложенным
(ВЫБРАТЬ ...), вынесите подзапрос во временную таблицу — СУБД её проиндексирует. - Отбор по неиндексированному полю. Отбирайте по измерениям регистров и реквизитам, по которым есть индексы. Отбор по реквизиту без индекса заставляет СУБД читать всю таблицу.
ОБЪЕДИНИТЬтам, где достаточноОБЪЕДИНИТЬ ВСЕ. Снятие дублей — лишняя сортировка.- Получение лишних данных. Не тяните поля, которые не нужны результату, не выбирайте строки, которые потом отбросите в коде.
Самая дорогая ошибка живёт не в самом запросе, а вокруг него — запрос в цикле. Когда для каждой строки документа выполняется отдельный запрос к базе, 500 строк превращаются в 500 обращений к СУБД.
Запрос внутри цикла по строкам — главная причина тормозов в доработках. Правильно: одним запросом получить все нужные данные сразу (соединив таблицы или через временную таблицу), а в цикле уже работать с готовой выборкой в памяти.
Посмотреть, что реально происходит, помогает технологический журнал и замер времени: включите в конфигураторе режим Отладка → Замер производительности или анализируйте план запроса средствами СУБД.
Практические запросы под типовые задачи
Несколько шаблонов, которые закрывают большинство реальных задач.
Остатки товаров на складе на дату:
ВЫБРАТЬ
Остатки.Номенклатура,
Остатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&НаДату, Склад = &Склад) КАК Остатки
ГДЕ
Остатки.КоличествоОстаток > 0
Долг контрагентов (взаиморасчёты):
ВЫБРАТЬ
Расчёты.Контрагент,
Расчёты.СуммаОстаток КАК Долг
ИЗ
РегистрНакопления.ВзаиморасчётыСКонтрагентами.Остатки(&НаДату, ) КАК Расчёты
ГДЕ
Расчёты.СуммаОстаток <> 0
УПОРЯДОЧИТЬ ПО
Долг УБЫВ
Продажи по менеджерам за период с итогами:
ВЫБРАТЬ
Продажи.Менеджер,
Продажи.Номенклатура,
СУММА(Продажи.ВыручкаОборот) КАК Выручка
ИЗ
РегистрНакопления.Продажи.Обороты(&Начало, &Конец, , ) КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Менеджер,
Продажи.Номенклатура
ИТОГИ
СУММА(Выручка)
ПО
Менеджер
Результат такого запроса выгружают в табличный документ отчёта, в дерево значений или обходят выборкой. Для разовых проверок удобно отлаживать текст в консоли запросов (внешняя обработка «Консоль запросов» или аналог из состава разработческих инструментов).
Частые вопросы о запросах в 1С
Можно ли запросом изменить или удалить данные?
Записать() или Удалить(). Это сделано намеренно — чтобы изменения проходили через бизнес-логику проведения и проверки.Чем виртуальная таблица отличается от обычной?
Почему запрос быстрый в консоли, но медленный в отчёте?
Когда брать временную таблицу вместо подзапроса?
Что делать с NULL после соединений?
ЕСТЬNULL(Поле, 0) для чисел и ЕСТЬNULL(Поле, "") для строк. Иначе NULL в арифметике и сравнениях даст неожиданный результат.