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

· 9 мин

Содержание

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

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

Объект «Запрос» и первый текст на языке запросов

Минимальная выборка выглядит так:

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

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

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

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

Текст необязательно писать руками. В модуле поставьте курсор в строку запроса, нажмите правую кнопку → Конструктор запроса (горячая клавиша Ctrl+Q): визуальный конструктор соберёт текст по выбранным таблицам и полям и вернёт готовую конструкцию. Удобно для сложных соединений, где легко ошибиться в условии связи.

Полезные операторы выборки:

ОператорЧто делает
ПЕРВЫЕ 100ограничивает число строк
РАЗЛИЧНЫЕубирает дубли строк
УПОРЯДОЧИТЬ ПОсортировка результата
В ИЕРАРХИИ (&Группа)отбор по группе справочника со вложенностью

Поле через точку (Номенклатура.ЕдиницаИзмерения.Наименование) — это обращение к реквизиту связанной таблицы. Для ссылочных полей платформа сама подставит нужное соединение, но у реквизитов составного типа такая «точка» порождает лишние левые соединения — об этом в разделе про оптимизацию.

Параметры запроса и защита от подстановки значений

Значения в запрос передают не склейкой строк, а через параметры. В тексте параметр обозначают амперсандом (&Организация), а значение подставляют методом УстановитьПараметр:

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

Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ДатаС));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(ДатаПо));
Запрос.УстановитьПараметр("Организация", Организация);

Почему именно параметры, а не вставка значений прямо в текст:

  1. Склейка пользовательского ввода в текст запроса — это дыра, через которую в выборку попадают чужие данные. Параметр платформа передаёт отдельно от текста, подставить через него произвольную конструкцию нельзя.
  2. Один и тот же текст с разными параметрами сервер кэширует как один план — это быстрее.
  3. Типы приводятся корректно: дату не нужно форматировать в строку.

МЕЖДУ &НачалоПериода И &КонецПериода включает обе границы. Если в параметр «конец периода» передать дату без времени, строки за этот день после 00:00:00 не попадут — оборачивайте в КонецДня().

В параметр можно передать не только одиночное значение, но и список — тогда работает условие В:

Запрос.Текст =
    "ВЫБРАТЬ Товары.Номенклатура, Товары.Количество
    |ИЗ Документ.РеализацияТоваровУслуг.Товары КАК Товары
    |ГДЕ Товары.Номенклатура В (&СписокНоменклатуры)";

Запрос.УстановитьПараметр("СписокНоменклатуры", МассивТоваров);

В параметр уходит массив ссылок или таблица значений. Для отбора по группе и всем её вложенным элементам применяют В ИЕРАРХИИ (&Группа). Эти же параметры принимают и виртуальные таблицы регистров в скобках — и вот это уже принципиально для скорости.

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

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

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

Результат = Запрос.ВыполнитьПакет();
Выборка = Результат[1].Выбрать();

Что здесь важно:

  • ВыполнитьПакет() возвращает массив результатов — по одному на каждый запрос пакета. Временные таблицы внутри пакета менеджер создаёт и удаляет сам.
  • ИНДЕКСИРОВАТЬ ПО на временной таблице резко ускоряет последующие соединения и отборы по этому полю.
  • Если временную таблицу нужно передать между разными объектами Запрос, им назначают один общий МенеджерВременныхТаблиц:
МВТ = Новый МенеджерВременныхТаблиц;
Запрос1.МенеджерВременныхТаблиц = МВТ;
Запрос2.МенеджерВременныхТаблиц = МВТ; // увидит втПродажи из Запрос1

Временные таблицы — главный инструмент против тяжёлых вложенных запросов. Соединять физическую таблицу с подзапросом дорого: оптимизатор СУБД плохо оценивает его размер. Вынесите подзапрос в проиндексированную временную таблицу — и соединение становится предсказуемым.

Соединения таблиц: левое, внутреннее и полное

Соединение (JOIN) связывает строки двух таблиц по условию в секции ПО. Тип соединения определяет, что делать со строками без пары:

ТипЧто возвращаетКогда применяют
ВНУТРЕННЕЕ СОЕДИНЕНИЕтолько строки, у которых есть пара в обеих таблицахнужны совпадения, остальное отбрасываем
ЛЕВОЕ СОЕДИНЕНИЕвсе строки левой таблицы, из правой — что нашлось (иначе NULL)остатки + цены: товар остаётся, даже если цены нет
ПОЛНОЕ СОЕДИНЕНИЕвсе строки обеих таблицсверка двух источников, где важны обе «половины»

Левое соединение — самое частое. Пример: остатки на складах плюс последняя цена из регистра сведений.

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

При левом соединении незаполненная правая часть возвращает NULL, а не ноль и не пустую строку. Оборачивайте такие поля в ЕСТЬNULL(Цены.Цена, 0), иначе арифметика и сравнения дадут неожиданный результат.

Два практических правила по соединениям:

  • 🔸 Условие ИЛИ в секции ПО или ГДЕ ломает использование индексов. Там, где напрашивается ИЛИ, чаще выгоднее два запроса с ОБЪЕДИНИТЬ ВСЕ или временная таблица.
  • 🔸 Соединять с виртуальными таблицами регистров (.Остатки, .Обороты, .СрезПоследних) нужно, передавая отбор прямо в их параметры (в скобках), а не фильтровать результат в ГДЕ — тогда СУБД отберёт меньше строк на раннем этапе.

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

Агрегатные функции сворачивают строки в итоги. Основные: СУММА, КОЛИЧЕСТВО, СРЕДНЕЕ, МАКСИМУМ, МИНИМУМ. Как только в выборке появляется агрегат, все неагрегированные поля обязаны попасть в СГРУППИРОВАТЬ ПО.

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

Ключевое отличие — где фильтровать:

  • ГДЕ отбирает строки до группировки (здесь — только проведённые документы).
  • ИМЕЮЩИЕ (HAVING) отбирает после группировки, по значению агрегата (контрагенты с выручкой больше 100 000).

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

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

Оптимизация и анализ производительности запроса

Запрос, который летает на тестовой базе, может встать на боевой с миллионами строк. Разница почти всегда в нескольких типовых приёмах.

Что делать:

  1. Берите только нужные поля. ВЫБРАТЬ * тащит лишние колонки и мешает серверу использовать покрывающие индексы.
  2. Фильтруйте в параметрах виртуальных таблиц. Отбор по складу и номенклатуре для Остатки указывайте в скобках таблицы, а не в ГДЕ после выборки.
  3. Убирайте вложенные запросы из соединений — переносите их во временные таблицы с ИНДЕКСИРОВАТЬ ПО.
  4. Избавляйтесь от ИЛИ в условиях связи и отбора.
  5. Типизируйте составные поля через ВЫРАЗИТЬ(Регистратор КАК Документ.РеализацияТоваровУслуг) — тогда «точка» к реквизиту не породит соединения со всеми типами сразу.
  6. Не обращайтесь к реквизитам через точку в цикле выборки — то, что нужно, добавьте в сам запрос.
Чек-лист перед вводом запроса в работу
Выполнено: 0 / 7

Чем мерить. Самый доступный инструмент — внешняя обработка Консоль запросов (есть в типовых и на диске ИТС): она показывает время выполнения и результат, позволяет менять текст без перекомпиляции модуля. Для серверного анализа смотрят технологический журнал по событию DBMSSQL и план запроса на стороне СУБД — там видно, где идёт сканирование таблицы вместо поиска по индексу.

Оптимизируйте по замерам, а не по догадке. Сначала найдите самый долгий запрос (журнал регистрации, APDEX, замер в консоли), потом правьте именно его — переписывать «на всякий случай» то, что и так работает за миллисекунды, смысла нет.

Пример типизации составного поля. Регистратор движений — поле составного типа (любой документ), и обращение Движения.Регистратор.Номер без уточнения типа заставит платформу соединиться со всеми таблицами документов. Правильно так:

"ВЫБРАТЬ
|    ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Номер КАК Номер
|ИЗ
|    РегистрНакопления.Продажи КАК Движения
|ГДЕ
|    ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг) ЕСТЬ НЕ NULL"

Инструменты для написания и проверки запроса

Писать текст запроса вслепую в строковой константе неудобно и легко ошибиться в имени таблицы. Разработчику помогают три инструмента.

Конструктор запроса (Ctrl+Q на строке с текстом или правая кнопка → соответствующий пункт). Выбираете таблицы, перетаскиваете поля, задаёте соединения, группировки, условия — конструктор собирает корректный текст и подсвечивает ошибки связи. Открыть уже написанный запрос в конструкторе можно тем же Ctrl+Q: это проверка, что текст синтаксически цел.

Конструктор запроса с обработкой результата (пункт в том же контекстном меню) вставляет не только текст, но и готовый каркас кода: создание объекта Запрос, выполнение и цикл обхода выборки. Экономит рутину при разовых обработках.

Консоль запросов — внешняя обработка (входит в типовые конфигурации и есть на диске ИТС). В ней запрос запускают без правки и перекомпиляции модуля, задают параметры в таблице, видят результат и время выполнения. Это основной полигон для отладки и замеров перед тем, как переносить текст в код.

Когда запрос нужно собирать динамически (разный набор полей и условий в зависимости от настроек), текст не склеивают строками, а используют объект СхемаЗапроса: он разбирает и собирает запрос программно, не ломая синтаксис. Для простых случаев достаточно условно включать части текста и управлять ими через параметры.

Несколько независимых выборок объединяют в один результат оператором ОБЪЕДИНИТЬ ВСЕ (оставляет все строки, в том числе дубли) или ОБЪЕДИНИТЬ (убирает полные дубли, но стоит дороже из-за сортировки). Число и типы полей во всех частях объединения должны совпадать.

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

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