Язык запросов 1С на практике: от простой выборки до оптимизации

· 9 мин

Содержание

Запрос в 1С начинается со слова ВЫБРАТЬ, перечисления полей, источника после ИЗ и отбора после ГДЕ. Это общий механизм платформы 8.3: язык запросов одинаков в Бухгалтерии, ЗУП, УТ и любой другой конфигурации, потому что работает на уровне платформы, а не прикладного решения.

Писать запрос можно вручную в модуле или собирать мышью через конструктор. Ниже — синтаксис, рабочие примеры на встроенном языке и приёмы, которые отделяют быстрый запрос от того, что вешает базу на минуту.

Из чего состоит запрос: ВЫБРАТЬ, ИЗ, ГДЕ

Минимальный запрос — три части: что выбрать, откуда и по какому условию. Платформа читает текст сверху вниз, но порядок секций фиксирован.

ВЫБРАТЬ
    Номенклатура.Наименование,
    Номенклатура.Артикул
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    Номенклатура.ПометкаУдаления = ЛОЖЬ

Разберём по строкам:

  • 📌 ВЫБРАТЬ — список полей. Через точку обращаемся к реквизитам и реквизитам реквизитов (Номенклатура.ЕдиницаИзмерения.Наименование).
  • 📌 ИЗ — источник данных. Это таблица объекта: Справочник.Номенклатура, Документ.РеализацияТоваровУслуг, регистр.
  • 📌 КАК Номенклатура — псевдоним таблицы. Короткое имя, по которому дальше ссылаемся на поля.
  • 📌 ГДЕ — условие отбора. Строки, не прошедшие условие, в результат не попадают.

Полный набор секций в порядке их следования:

СекцияНазначение
ВЫБРАТЬполя результата
ИЗисточники и соединения
ГДЕотбор строк
СГРУППИРОВАТЬ ПОгруппировка для итогов
ИМЕЮЩИЕотбор уже по сгруппированным данным
УПОРЯДОЧИТЬ ПОсортировка результата
ИТОГИ … ПОмногоуровневые итоги по иерархии

Служебные слова можно писать строчными или прописными — платформа не различает регистр ключевых слов. Прописные удобнее читать, поэтому так принято в типовых.

ВЫБРАТЬ РАЗЛИЧНЫЕ убирает дубли строк. Но включать его «на всякий случай» не нужно: уникализация стоит ресурсов, а при наличии ссылки на объект дублей обычно и так нет.

Как открыть конструктор запроса

Руками весь текст набирают редко. В модуле (обработка, отчёт, общий модуль) ставим курсор на строку с текстом запроса, правой кнопкой — Конструктор запроса. Если текста ещё нет, конструктор предложит создать новый.

Окно конструктора делится на вкладки:

  1. Таблицы и поля — слева дерево всех объектов базы, перетаскиваем нужные таблицы в среднюю колонку, поля — в правую.
  2. Связи — настройка соединений между таблицами.
  3. Группировка, Условия, Порядок, Итоги — остальные секции отдельными вкладками.

Кнопка ОК вставляет готовый текст в модуль. Обратно текст тоже читается: встали курсором на существующий запрос, открыли конструктор — он разобрал текст по вкладкам. Это удобно, когда правишь чужой код.

Конструктор не понимает текст, собранный из кусков строк через конкатенацию ("ВЫБРАТЬ " + ПолеПеременной). Для конструктора запрос должен быть цельным литералом.

Есть и второй конструктор — Конструктор запроса с обработкой результата. Он дополнительно генерирует код создания объекта Запрос, выполнения и обхода выборки. Для быстрого старта удобно, потом код правят под себя.

Параметры и условия отбора

Хардкодить даты и ссылки прямо в текст нельзя — запрос перестанет быть переиспользуемым. Вместо значения ставим параметр с амперсандом, а само значение передаём из кода.

ВЫБРАТЬ
    Продажи.Контрагент,
    Продажи.Сумма
ИЗ
    Документ.РеализацияТоваровУслуг КАК Продажи
ГДЕ
    Продажи.Дата МЕЖДУ &ДатаНачала И &ДатаКонца
    И Продажи.Организация = &Организация
    И Продажи.Проведен = ИСТИНА

Параметры &ДатаНачала, &ДатаКонца, &Организация задаём в коде перед выполнением:

Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаОтчета));
Запрос.УстановитьПараметр("ДатаКонца",  КонецДня(ДатаОтчета));
Запрос.УстановитьПараметр("Организация", ВыбраннаяОрганизация);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
    Сообщить(Выборка.Контрагент);
КонецЦикла;

Полезные операторы в секции ГДЕ:

  • МЕЖДУ … И … — диапазон (включая границы).
  • В (&СписокЗначений) — вхождение в список или массив, переданный параметром.
  • В ИЕРАРХИИ (&Группа) — элемент и все его подчинённые в иерархическом справочнике.
  • ПОДОБНО "%шуруп%" — поиск по шаблону, % заменяет любое число символов.
  • ССЫЛКА — проверка типа ссылочного поля: РегистраторТипа ССЫЛКА Документ.ПоступлениеТоваровУслуг.

Дату периода передавайте через НачалоДня и КонецДня. Если передать просто дату документа, время 00:00:00 отрежет все документы этого дня, кроме полночных. КонецДня ставит время 23:59:59.

Условие с ИЛИ всегда берите в скобки, когда оно соседствует с И: (А ИЛИ Б) И В и А ИЛИ (Б И В) дают разный результат, как в обычной логике.

Соединение таблиц: четыре вида связей

Данные редко лежат в одной таблице. Контрагент — в одном справочнике, его договоры — в другом, продажи — в документе. Соединения связывают их по общему полю.

ВЫБРАТЬ
    Товары.Номенклатура,
    Товары.Количество,
    Цены.Цена
ИЗ
    Документ.РеализацияТоваровУслуг.Товары КАК Товары
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
    ПО Товары.Номенклатура = Цены.Номенклатура

Четыре типа соединения отличаются тем, какие строки остаются, если пары не нашлось:

СоединениеЧто в результате
ВНУТРЕННЕЕ СОЕДИНЕНИЕтолько строки, у которых есть пара в обеих таблицах
ЛЕВОЕ СОЕДИНЕНИЕвсе строки левой таблицы; из правой — что нашлось, иначе NULL
ПРАВОЕ СОЕДИНЕНИЕзеркально левому: все строки правой
ПОЛНОЕ СОЕДИНЕНИЕвсе строки обеих таблиц, непарным ставится NULL

На практике 90% соединений — ЛЕВОЕ: берём все основные данные и подтягиваем к ним дополнительные. Правое почти не используют — его всегда можно переписать как левое, поменяв таблицы местами.

После левого соединения проверяйте поля правой таблицы на NULL. Если пары не нашлось, в поле будет не ноль и не пустая строка, а NULL, и арифметика с ним даст NULL. Оборачивайте такие поля в ЕСТЬNULL:

ВЫБРАТЬ
    Товары.Номенклатура,
    ЕСТЬNULL(Цены.Цена, 0) КАК Цена

ЕСТЬNULL(Поле, ЗначениеЕслиNULL) возвращает второй аргумент, когда первый — NULL. Это самая частая причина «неправильных сумм» в отчётах с соединениями.

Группировка и агрегатные функции

Когда нужны не строки, а итоги — сумма по контрагенту, количество документов за день, — включаем группировку. Агрегатная функция в ВЫБРАТЬ и секция СГРУППИРОВАТЬ ПО работают в паре.

ВЫБРАТЬ
    Продажи.Контрагент КАК Контрагент,
    СУММА(Продажи.Сумма) КАК ВсегоСумма,
    КОЛИЧЕСТВО(Продажи.Ссылка) КАК ЧислоДокументов
ИЗ
    Документ.РеализацияТоваровУслуг КАК Продажи
ГДЕ
    Продажи.Проведен = ИСТИНА
СГРУППИРОВАТЬ ПО
    Продажи.Контрагент

Доступные агрегатные функции:

  • СУММА(…) — сумма числового поля.
  • КОЛИЧЕСТВО(…) — число строк; КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле) — число уникальных значений.
  • МАКСИМУМ(…), МИНИМУМ(…) — крайние значения, работают и с датами.
  • СРЕДНЕЕ(…) — среднее по числовому полю.

Главное правило: каждое поле из ВЫБРАТЬ либо стоит под агрегатной функцией, либо перечислено в СГРУППИРОВАТЬ ПО. Иначе платформа не поймёт, какое из множества значений поля показать в строке итога, и вернёт ошибку.

Отбор по уже посчитанным итогам делается не в ГДЕ, а в ИМЕЮЩИЕ:

СГРУППИРОВАТЬ ПО
    Продажи.Контрагент
ИМЕЮЩИЕ
    СУММА(Продажи.Сумма) > 100000

ГДЕ отсеивает строки до группировки, ИМЕЮЩИЕ — группы после. Если фильтр по полю, которое не агрегируется (например, по организации), ставьте его в ГДЕ — он отработает раньше и быстрее.

Для многоуровневых итогов с промежуточными суммами есть секция ИТОГИ … ПО: она строит дерево результата (итог по контрагенту, внутри — по договору). Но её обходят через Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам), это уже тонкость обхода выборки.

Временные таблицы и пакетные запросы

Когда логика сложная, один запрос превращается в нечитаемую простыню с вложенными подзапросами. Разбить её помогают временные таблицы: промежуточный результат кладётся в таблицу ПОМЕСТИТЬ, а следующий запрос из неё читает.

ВЫБРАТЬ
    Заказы.Контрагент КАК Контрагент,
    СУММА(Заказы.Сумма) КАК СуммаЗаказов
ПОМЕСТИТЬ ВтЗаказы
ИЗ
    Документ.ЗаказПокупателя КАК Заказы
СГРУППИРОВАТЬ ПО
    Заказы.Контрагент
;

ВЫБРАТЬ
    ВтЗаказы.Контрагент,
    ВтЗаказы.СуммаЗаказов
ИЗ
    ВтЗаказы КАК ВтЗаказы
ГДЕ
    ВтЗаказы.СуммаЗаказов > &Порог

Несколько запросов, разделённых точкой с запятой, — это пакет. Чтобы пакет работал с временными таблицами, запросу нужен менеджер временных таблиц:

МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст = ТекстПакета;
Результат = Запрос.Выполнить();

Где временные таблицы реально выручают:

  • 🔹 Нужно соединить итоги двух регистров — сначала собираем каждый в свою ВТ, потом соединяем две компактные таблицы вместо тяжёлых виртуальных.
  • 🔹 Один и тот же набор данных используется в запросе несколько раз — считаем один раз в ВТ.
  • 🔹 Передать в запрос таблицу значений из кода — её кладут во временную таблицу через Запрос.УстановитьПараметр и источник &ТаблицаИзКода.

Если по полю временной таблицы потом идёт соединение или отбор, добавьте ИНДЕКСИРОВАТЬ ПО ИмяПоля после ПОМЕСТИТЬ. На больших таблицах индекс ускоряет следующий запрос в разы.

Частые ошибки и как ускорить запрос

Медленный запрос почти всегда упирается в одни и те же грабли. Разберём их и заодно — что делает запрос быстрым.

Отбор виртуальной таблицы в ГДЕ вместо параметров. Остатки и обороты — это виртуальные таблицы (РегистрНакопления.ОстаткиИОбороты, РегистрБухгалтерии.Остатки). У них есть параметры в скобках. Условие нужно передавать туда, а не в ГДЕ снаружи:

// медленно: отбор снаружи
ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
ГДЕ Остатки.Склад = &Склад

// быстро: отбор в параметрах виртуальной таблицы
ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки

Во втором варианте платформа отфильтрует данные до расчёта остатков, а не после. На регистре с миллионами записей разница — секунды против долей секунды.

Соединение с подзапросом. Соединять таблицу с вложенным (ВЫБРАТЬ …) дорого: у подзапроса нет индексов. Вынесите подзапрос во временную таблицу, проиндексируйте поле связи — и соединяйте уже с ней.

Забытый ЕСТЬNULL после левого соединения. Разобрали выше: NULL в сумме ломает итог. Это не про скорость, а про корректность, но встречается чаще всего.

Получение представлений через точку в цикле. Если в выборке дёргать Выборка.Контрагент.Наименование, платформа пойдёт в базу за каждым контрагентом отдельно. Берите ПРЕДСТАВЛЕНИЕ(Продажи.Контрагент) прямо в запросе — тогда имя придёт одним пакетом вместе с данными.

Работа с составными полями без типизации. Поле Регистратор или Субконто может хранить ссылки разных типов. Чтобы обратиться к реквизиту конкретного типа, приведите тип через ВЫРАЗИТЬ:

ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Договор

Короткий тест перед выполнением запроса на рабочей базе:

Проверка запроса перед запуском
Выполнено: 0 / 5

Проверьте, что поняли ключевые отличия — короткий тест:

Проверьте себя
Какое соединение вернёт все строки основной таблицы, даже если пары во второй не нашлось?
Куда передавать отбор по складу для таблицы остатков, чтобы запрос был быстрым?
Чем заменить NULL в числовом поле после левого соединения?

Частые вопросы

Чем язык запросов 1С отличается от SQL?
Он похож синтаксисом, но работает с объектами конфигурации, а не с таблицами СУБД напрямую. Платформа сама переводит запрос в SQL под конкретную базу. Поэтому в запросе 1С нет команд изменения данных — только выборка; писать и проводить данные запросом нельзя.
Можно ли запросом менять или удалять данные?
Нет. Язык запросов только читает. Для изменения используют объектную модель: получили объект, изменили реквизиты, записали. Запрос лишь отбирает, что именно обрабатывать.
Почему запрос в отчёте показывает суммы с NULL или пустые ячейки?
Почти всегда это левое соединение без ЕСТЬNULL. Там, где пара во второй таблице не нашлась, стоит NULL, и он протаскивается в итог. Оберните поля правой таблицы в ЕСТЬNULL(Поле, 0).
Как передать в запрос список значений для отбора?
Создайте в коде массив или список значений, передайте его параметром и используйте оператор В (&СписокОтбора). Для отбора по группе и всем подчинённым элементам — В ИЕРАРХИИ (&Группа).
Зачем нужны временные таблицы, если можно обойтись подзапросами?
Временную таблицу можно проиндексировать и переиспользовать в пакете несколько раз, а подзапрос считается заново при каждом обращении и не имеет индексов. На объёмах от десятков тысяч строк это заметная разница в скорости.