Язык запросов 1С 8.3: от объекта Запрос до оптимизации выборки
Содержание
Чтобы получить данные из базы 1С в коде, создают объект Запрос, кладут в его свойство Текст запрос на языке запросов и вызывают метод Выполнить(). Дальше работа с запросом в 1С сводится к трём шагам: написать текст на конструкции ВЫБРАТЬ … ИЗ … ГДЕ, передать параметры и обойти результат выборкой. Всё остальное — временные таблицы, соединения, группировки и оптимизация — надстраивается поверх этой схемы.
Язык запросов читает данные, но не меняет их: записать документ или провести движения запросом нельзя, для этого есть объектная модель. Ниже — рабочие конструкции с примерами на встроенном языке и приёмы, которые отличают быстрый запрос от того, что кладёт сервер под нагрузкой.
Объект «Запрос» и первый текст на языке запросов
Минимальная выборка выглядит так:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Наименование);
КонецЦикла;
Разберём по частям:
- 🔹
ВЫБРАТЬ— перечень полей. Псевдоним черезКАКзадаёт имя колонки в результате. - 🔹
ИЗ— источник: таблица объекта (Справочник.,Документ.,РегистрНакопления.) с обязательным псевдонимом. - 🔹
ГДЕ— условие отбора строк. - 🔹 вертикальная черта
|— всего лишь разделитель строк многострочной константы в коде, к языку запросов она отношения не имеет.
Текст необязательно писать руками. В модуле поставьте курсор в строку запроса, нажмите правую кнопку → Конструктор запроса (горячая клавиша Ctrl+Q): визуальный конструктор соберёт текст по выбранным таблицам и полям и вернёт готовую конструкцию. Удобно для сложных соединений, где легко ошибиться в условии связи.
Полезные операторы выборки:
| Оператор | Что делает |
|---|---|
ПЕРВЫЕ 100 | ограничивает число строк |
РАЗЛИЧНЫЕ | убирает дубли строк |
УПОРЯДОЧИТЬ ПО | сортировка результата |
В ИЕРАРХИИ (&Группа) | отбор по группе справочника со вложенностью |
Поле через точку (Номенклатура.ЕдиницаИзмерения.Наименование) — это обращение к реквизиту связанной таблицы. Для ссылочных полей платформа сама подставит нужное соединение, но у реквизитов составного типа такая «точка» порождает лишние левые соединения — об этом в разделе про оптимизацию.
Параметры запроса и защита от подстановки значений
Значения в запрос передают не склейкой строк, а через параметры. В тексте параметр обозначают амперсандом (&Организация), а значение подставляют методом УстановитьПараметр:
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Номенклатура КАК Номенклатура,
| Товары.Количество КАК Количество,
| Товары.Сумма КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК Товары
|ГДЕ
| Товары.Ссылка.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И Товары.Ссылка.Организация = &Организация";
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ДатаС));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(ДатаПо));
Запрос.УстановитьПараметр("Организация", Организация);
Почему именно параметры, а не вставка значений прямо в текст:
- Склейка пользовательского ввода в текст запроса — это дыра, через которую в выборку попадают чужие данные. Параметр платформа передаёт отдельно от текста, подставить через него произвольную конструкцию нельзя.
- Один и тот же текст с разными параметрами сервер кэширует как один план — это быстрее.
- Типы приводятся корректно: дату не нужно форматировать в строку.
МЕЖДУ &НачалоПериода И &КонецПериода включает обе границы. Если в параметр «конец периода» передать дату без времени, строки за этот день после 00:00:00 не попадут — оборачивайте в КонецДня().
В параметр можно передать не только одиночное значение, но и список — тогда работает условие В:
Запрос.Текст =
"ВЫБРАТЬ Товары.Номенклатура, Товары.Количество
|ИЗ Документ.РеализацияТоваровУслуг.Товары КАК Товары
|ГДЕ Товары.Номенклатура В (&СписокНоменклатуры)";
Запрос.УстановитьПараметр("СписокНоменклатуры", МассивТоваров);
В параметр уходит массив ссылок или таблица значений. Для отбора по группе и всем её вложенным элементам применяют В ИЕРАРХИИ (&Группа). Эти же параметры принимают и виртуальные таблицы регистров в скобках — и вот это уже принципиально для скорости.
Временные таблицы и пакетные запросы
Когда промежуточный результат нужно использовать дальше — например, сгруппировать продажи, а потом соединить с остатками, — его помещают во временную таблицу оператором ПОМЕСТИТЬ. Несколько запросов, разделённых точкой с запятой, образуют пакет:
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Номенклатура КАК Номенклатура,
| СУММА(Товары.Количество) КАК Количество
|ПОМЕСТИТЬ втПродажи
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК Товары
|СГРУППИРОВАТЬ ПО
| Товары.Номенклатура
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|////////////////////////////////////////
|ВЫБРАТЬ
| втПродажи.Номенклатура КАК Номенклатура,
| втПродажи.Количество КАК Количество
|ИЗ
| втПродажи КАК втПродажи";
Результат = Запрос.ВыполнитьПакет();
Выборка = Результат[1].Выбрать();
Что здесь важно:
ВыполнитьПакет()возвращает массив результатов — по одному на каждый запрос пакета. Временные таблицы внутри пакета менеджер создаёт и удаляет сам.ИНДЕКСИРОВАТЬ ПОна временной таблице резко ускоряет последующие соединения и отборы по этому полю.- Если временную таблицу нужно передать между разными объектами
Запрос, им назначают один общийМенеджерВременныхТаблиц:
МВТ = Новый МенеджерВременныхТаблиц;
Запрос1.МенеджерВременныхТаблиц = МВТ;
Запрос2.МенеджерВременныхТаблиц = МВТ; // увидит втПродажи из Запрос1
Временные таблицы — главный инструмент против тяжёлых вложенных запросов. Соединять физическую таблицу с подзапросом дорого: оптимизатор СУБД плохо оценивает его размер. Вынесите подзапрос в проиндексированную временную таблицу — и соединение становится предсказуемым.
Соединения таблиц: левое, внутреннее и полное
Соединение (JOIN) связывает строки двух таблиц по условию в секции ПО. Тип соединения определяет, что делать со строками без пары:
| Тип | Что возвращает | Когда применяют |
|---|---|---|
ВНУТРЕННЕЕ СОЕДИНЕНИЕ | только строки, у которых есть пара в обеих таблицах | нужны совпадения, остальное отбрасываем |
ЛЕВОЕ СОЕДИНЕНИЕ | все строки левой таблицы, из правой — что нашлось (иначе NULL) | остатки + цены: товар остаётся, даже если цены нет |
ПОЛНОЕ СОЕДИНЕНИЕ | все строки обеих таблиц | сверка двух источников, где важны обе «половины» |
Левое соединение — самое частое. Пример: остатки на складах плюс последняя цена из регистра сведений.
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.КоличествоОстаток КАК Остаток,
| Цены.Цена КАК Цена
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&НаДату, ) КАК Остатки
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ) КАК Цены
| ПО Остатки.Номенклатура = Цены.Номенклатура"
При левом соединении незаполненная правая часть возвращает NULL, а не ноль и не пустую строку. Оборачивайте такие поля в ЕСТЬNULL(Цены.Цена, 0), иначе арифметика и сравнения дадут неожиданный результат.
Два практических правила по соединениям:
- 🔸 Условие
ИЛИв секцииПОилиГДЕломает использование индексов. Там, где напрашиваетсяИЛИ, чаще выгоднее два запроса сОБЪЕДИНИТЬ ВСЕили временная таблица. - 🔸 Соединять с виртуальными таблицами регистров (
.Остатки,.Обороты,.СрезПоследних) нужно, передавая отбор прямо в их параметры (в скобках), а не фильтровать результат вГДЕ— тогда СУБД отберёт меньше строк на раннем этапе.
Группировка, агрегатные функции и отбор по итогам
Агрегатные функции сворачивают строки в итоги. Основные: СУММА, КОЛИЧЕСТВО, СРЕДНЕЕ, МАКСИМУМ, МИНИМУМ. Как только в выборке появляется агрегат, все неагрегированные поля обязаны попасть в СГРУППИРОВАТЬ ПО.
"ВЫБРАТЬ
| Продажи.Контрагент КАК Контрагент,
| СУММА(Продажи.СуммаДокумента) КАК Выручка,
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Продажи.Ссылка) КАК ЧислоПродаж
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Продажи
|ГДЕ
| Продажи.Проведен
|СГРУППИРОВАТЬ ПО
| Продажи.Контрагент
|ИМЕЮЩИЕ
| СУММА(Продажи.СуммаДокумента) > 100000
|УПОРЯДОЧИТЬ ПО
| Выручка УБЫВ"
Ключевое отличие — где фильтровать:
ГДЕотбирает строки до группировки (здесь — только проведённые документы).ИМЕЮЩИЕ(HAVING) отбирает после группировки, по значению агрегата (контрагенты с выручкой больше 100 000).
Поставить СУММА(...) > 100000 в ГДЕ нельзя — на этом этапе суммы ещё нет. КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …) считает уникальные значения: так получают число документов, а не число строк табличной части.
Для итогов с подытогами и общим итогом есть секция ИТОГИ … ПО. Она строит дерево результата (Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам)), удобное для отчётов без ручного суммирования в коде.
Оптимизация и анализ производительности запроса
Запрос, который летает на тестовой базе, может встать на боевой с миллионами строк. Разница почти всегда в нескольких типовых приёмах.
Что делать:
- Берите только нужные поля.
ВЫБРАТЬ *тащит лишние колонки и мешает серверу использовать покрывающие индексы. - Фильтруйте в параметрах виртуальных таблиц. Отбор по складу и номенклатуре для
Остаткиуказывайте в скобках таблицы, а не вГДЕпосле выборки. - Убирайте вложенные запросы из соединений — переносите их во временные таблицы с
ИНДЕКСИРОВАТЬ ПО. - Избавляйтесь от
ИЛИв условиях связи и отбора. - Типизируйте составные поля через
ВЫРАЗИТЬ(Регистратор КАК Документ.РеализацияТоваровУслуг)— тогда «точка» к реквизиту не породит соединения со всеми типами сразу. - Не обращайтесь к реквизитам через точку в цикле выборки — то, что нужно, добавьте в сам запрос.
Чем мерить. Самый доступный инструмент — внешняя обработка Консоль запросов (есть в типовых и на диске ИТС): она показывает время выполнения и результат, позволяет менять текст без перекомпиляции модуля. Для серверного анализа смотрят технологический журнал по событию DBMSSQL и план запроса на стороне СУБД — там видно, где идёт сканирование таблицы вместо поиска по индексу.
Оптимизируйте по замерам, а не по догадке. Сначала найдите самый долгий запрос (журнал регистрации, APDEX, замер в консоли), потом правьте именно его — переписывать «на всякий случай» то, что и так работает за миллисекунды, смысла нет.
Пример типизации составного поля. Регистратор движений — поле составного типа (любой документ), и обращение Движения.Регистратор.Номер без уточнения типа заставит платформу соединиться со всеми таблицами документов. Правильно так:
"ВЫБРАТЬ
| ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Номер КАК Номер
|ИЗ
| РегистрНакопления.Продажи КАК Движения
|ГДЕ
| ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг) ЕСТЬ НЕ NULL"
Инструменты для написания и проверки запроса
Писать текст запроса вслепую в строковой константе неудобно и легко ошибиться в имени таблицы. Разработчику помогают три инструмента.
Конструктор запроса (Ctrl+Q на строке с текстом или правая кнопка → соответствующий пункт). Выбираете таблицы, перетаскиваете поля, задаёте соединения, группировки, условия — конструктор собирает корректный текст и подсвечивает ошибки связи. Открыть уже написанный запрос в конструкторе можно тем же Ctrl+Q: это проверка, что текст синтаксически цел.
Конструктор запроса с обработкой результата (пункт в том же контекстном меню) вставляет не только текст, но и готовый каркас кода: создание объекта Запрос, выполнение и цикл обхода выборки. Экономит рутину при разовых обработках.
Консоль запросов — внешняя обработка (входит в типовые конфигурации и есть на диске ИТС). В ней запрос запускают без правки и перекомпиляции модуля, задают параметры в таблице, видят результат и время выполнения. Это основной полигон для отладки и замеров перед тем, как переносить текст в код.
Когда запрос нужно собирать динамически (разный набор полей и условий в зависимости от настроек), текст не склеивают строками, а используют объект СхемаЗапроса: он разбирает и собирает запрос программно, не ломая синтаксис. Для простых случаев достаточно условно включать части текста и управлять ими через параметры.
Несколько независимых выборок объединяют в один результат оператором ОБЪЕДИНИТЬ ВСЕ (оставляет все строки, в том числе дубли) или ОБЪЕДИНИТЬ (убирает полные дубли, но стоит дороже из-за сортировки). Число и типы полей во всех частях объединения должны совпадать.
Частые вопросы
Чем `Выбрать()` отличается от `Выгрузить()`?
Результат.Выбрать() даёт курсор для построчного обхода в цикле Пока … Следующий() — память расходуется экономно. Результат.Выгрузить() возвращает всю выборку таблицей значений целиком: удобно, когда данные нужны как таблица, но на больших объёмах дороже по памяти.Можно ли запросом изменить или записать данные?
Ссылка.ПолучитьОбъект()), поменяйте реквизиты и запишите; движения регистров проводят документом или набором записей.Почему пустое поле после соединения ведёт себя странно в условиях?
NULL — особое значение «нет данных». NULL не равен ни нулю, ни пустой строке, а сравнение с ним через = всегда даёт ложь. Используйте ЕСТЬNULL(Поле, ЗначениеПоУмолчанию) или проверку Поле ЕСТЬ NULL.Когда брать виртуальную таблицу `Остатки`, а когда `ОстаткиИОбороты`?
Остатки — если нужно состояние на дату. Обороты — приход и расход за период. ОстаткиИОбороты — когда в одной выборке нужны и начальный остаток, и обороты, и конечный остаток; отдельно соединять три источника не придётся.Зачем псевдоним через `КАК`, если и так работает?
УПОРЯДОЧИТЬ ПО.