Как устроен язык запросов 1С и как писать на нём быстрые выборки
Содержание
Язык запросов 1С — это диалект SQL только для чтения данных информационной базы. Пишут на нём через объект Запрос: текст запроса, параметры, затем Выполнить(). Таблицы называются не именами из СУБД, а именами объектов метаданных: Справочник.Номенклатура, Документ.РеализацияТоваровУслуг, РегистрНакопления.ТоварыНаСкладах.Остатки.
Платформа сама переводит такой запрос в SQL нужной СУБД: файловой базы, MS SQL Server, PostgreSQL. Поэтому один и тот же текст работает везде. Ключевые слова есть и на русском, и на английском: ВЫБРАТЬ и SELECT равноправны, но в типовых конфигурациях принят русский вариант.
Чем язык запросов 1С отличается от обычного SQL
Синтаксис узнаваем для любого, кто писал на SQL. Различия лежат в модели данных и в ограничениях.
🔹 Только чтение. Нет INSERT, UPDATE, DELETE. Данные меняют объектами встроенного языка: СправочникОбъект, ДокументОбъект, наборы записей регистров. Единственное, что запрос может «создать», — временную таблицу.
🔹 Объектная модель. Поле-ссылку можно разыменовать через точку: Продажи.Контрагент.ИНН. Платформа сама добавит соединение с таблицей контрагентов.
🔹 Виртуальные таблицы. У регистров есть готовые таблицы с параметрами: Остатки, Обороты, ОстаткиИОбороты, СрезПоследних, СрезПервых. Это не хранимые таблицы, а запросы, которые платформа собирает за вас.
🔹 Табличные части как таблицы. Строки документа читаются как Документ.РеализацияТоваровУслуг.Товары, а поле Ссылка ведёт на шапку.
🔹 Права доступа
. Ограничения на уровне записей (RLS) применяются к запросу автоматически. Слово РАЗРЕШЕННЫЕ после ВЫБРАТЬ отбрасывает недоступные записи вместо ошибки доступа.
🔹 Свои функции. Вместо COALESCE — ЕСТЬNULL, вместо CASE — ВЫБОР КОГДА … ТОГДА … ИНАЧЕ … КОНЕЦ, для дат — НАЧАЛОПЕРИОДА, КОНЕЦПЕРИОДА, РАЗНОСТЬДАТ, ДОБАВИТЬКДАТЕ.
| Задача | SQL | Язык запросов 1С |
|---|---|---|
| Первые N строк | TOP 10 / LIMIT 10 | ПЕРВЫЕ 10 |
| Без повторов | DISTINCT | РАЗЛИЧНЫЕ |
| Условие на группу | HAVING | ИМЕЮЩИЕ |
| Объединение выборок | UNION ALL | ОБЪЕДИНИТЬ ВСЕ |
| Пустое значение | COALESCE(x, 0) | ЕСТЬNULL(x, 0) |
| Временная таблица | SELECT … INTO #t | ВЫБРАТЬ … ПОМЕСТИТЬ ВТ |
| Приведение типа | CAST | ВЫРАЗИТЬ(x КАК Справочник.Контрагенты) |
Новичку не нужно набирать текст вручную. В Конфигураторе и в EDT есть конструктор запроса: в модуле щёлкните правой кнопкой и выберите «Конструктор запроса». Он соберёт текст по галочкам, а потом его можно править руками.
Структура запроса ВЫБРАТЬ: из чего состоит выборка
Порядок секций фиксирован. Лишние можно опускать, переставлять нельзя.
ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 100
Номенклатура.Ссылка КАК Номенклатура,
Номенклатура.Артикул КАК Артикул,
ЕСТЬNULL(Номенклатура.ЕдиницаИзмерения.Наименование, "") КАК Единица
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
НЕ Номенклатура.ПометкаУдаления
И НЕ Номенклатура.ЭтоГруппа
И Номенклатура.Наименование ПОДОБНО &Шаблон СПЕЦСИМВОЛ "~"
УПОРЯДОЧИТЬ ПО
Номенклатура.Наименование
Что здесь важно:
КАКзадаёт псевдонимы таблиц и полей. Без псевдонимов поля в результате получат автоматические имена, к которым неудобно обращаться из кода.ГДЕподдерживаетИ,ИЛИ,НЕ,В (…),МЕЖДУ … И …,ЕСТЬ NULL,ПОДОБНО,В ИЕРАРХИИ.ПОДОБНОработает с шаблонами%и_.СПЕЦСИМВОЛнужен, чтобы искать сами эти символы в тексте.- Сравнивать с конкретными элементами удобно через
ЗНАЧЕНИЕ(…):ЗНАЧЕНИЕ(Перечисление.ВидыДоговоровКонтрагентов.СПокупателем),ЗНАЧЕНИЕ(Справочник.Валюты.ПустаяСсылка).
Выполнение из кода выглядит так:
Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("Шаблон", "%болт%");
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Артикул);
КонецЦикла;
Выбрать() читает результат построчно и бережёт память. Выгрузить() отдаёт всю таблицу значений сразу — удобно для небольших выборок и для передачи в другие процедуры.
Виртуальные таблицы регистров
Самая частая задача — остатки на дату. Отбор передают в параметры виртуальной таблицы, а не в ГДЕ:
ВЫБРАТЬ
Остатки.Номенклатура,
Остатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата, Склад = &Склад) КАК Остатки
Если написать то же условие в ГДЕ, платформа сначала посчитает остатки по всем складам, а потом отбросит лишнее. На большой базе разница во времени — в разы. Условие по измерениям регистра ставьте в параметры виртуальной таблицы.
Соединения таблиц в запросах 1С
Поддерживаются четыре вида соединений:
| Соединение | Что вернёт | Когда брать |
|---|---|---|
ВНУТРЕННЕЕ СОЕДИНЕНИЕ | только строки с парой в обеих таблицах | продажи и цены, где цена обязательна |
ЛЕВОЕ СОЕДИНЕНИЕ | все строки левой таблицы + совпадения справа | номенклатура и остатки (товар без остатка тоже нужен) |
ПРАВОЕ СОЕДИНЕНИЕ | зеркально левому | редко; конструктор обычно перестраивает его в левое |
ПОЛНОЕ СОЕДИНЕНИЕ | все строки обеих таблиц | сверка двух источников: план и факт |
Отдельного CROSS JOIN нет. Декартово произведение получают через ВНУТРЕННЕЕ СОЕДИНЕНИЕ … ПО ИСТИНА. Пользуйтесь этим осторожно: 1000 × 1000 строк — это уже миллион.
Пример левого соединения: справочник товаров с остатками и ценами.
ВЫБРАТЬ
Номенклатура.Ссылка КАК Товар,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток,
ЕСТЬNULL(Цены.Цена, 0) КАК Цена
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата) КАК Остатки
ПО Номенклатура.Ссылка = Остатки.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, ТипЦен = &ТипЦен) КАК Цены
ПО Номенклатура.Ссылка = Цены.Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
Имена регистров и их измерений в примере условные: в вашей конфигурации они могут называться иначе. Посмотрите их в дереве метаданных или в конструкторе запроса.
После левого соединения поля правой таблицы бывают NULL. Арифметика с NULL даёт NULL, а сравнение NULL = 0 не выполняется никогда. Оборачивайте такие поля в ЕСТЬNULL.
Неявные соединения через точку
Запись Документ.Контрагент.ОсновнойДоговор.Валюта выглядит безобидно, но каждая точка — это ещё одно соединение. Хуже всего с полями составного типа. Если Регистратор может быть ссылкой на 40 видов документов, обращение Регистратор.Дата превратится в соединение с 40 таблицами.
Решение — явно указать тип:
ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Дата
Проверить тип ссылки в условии можно так: Движения.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг.
Временные таблицы и пакетные запросы
Сложную выборку удобнее разбить на шаги. Каждый шаг кладёт результат во временную таблицу, следующий читает её. Запросы в одном тексте разделяют точкой с запятой — это пакет.
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
СУММА(Продажи.Количество) КАК Количество
ПОМЕСТИТЬ ВТ_Продажи
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК Продажи
ГДЕ
Продажи.Ссылка.Дата МЕЖДУ &Начало И &Конец
И Продажи.Ссылка.Проведен
СГРУППИРОВАТЬ ПО
Продажи.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Номенклатура,
ВТ.Количество,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток
ИЗ
ВТ_Продажи КАК ВТ
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(
&Конец,
Номенклатура В (ВЫБРАТЬ Т.Номенклатура ИЗ ВТ_Продажи КАК Т)) КАК Остатки
ПО ВТ.Номенклатура = Остатки.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
УНИЧТОЖИТЬ ВТ_Продажи
Приёмы, которые стоит знать:
- 📌
ИНДЕКСИРОВАТЬ ПОимеет смысл для полей, по которым временная таблица потом соединяется. На таблице из сотни строк индекс не нужен. - 📌
УНИЧТОЖИТЬосвобождает место вtempdbили в памяти сервера сразу, а не при завершении запроса. - 📌
Запрос.ВыполнитьПакет()возвращает массив результатов всех запросов пакета. Это удобно при отладке: видно, что лежит в каждой временной таблице. - 📌 Чтобы временная таблица пережила запрос и была доступна в следующем, задайте менеджер:
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;и передавайте его в новые запросы.
Вложенный запрос в ИЗ или в соединении тоже работает, но на больших данных он часто проигрывает временной таблице: оптимизатор СУБД хуже оценивает число строк в подзапросе. Если отчёт тормозит, первым делом вынесите подзапросы во временные таблицы.
Группировка, агрегатные функции и ИМЕЮЩИЕ
Агрегатных функций пять: СУММА, КОЛИЧЕСТВО, МАКСИМУМ, МИНИМУМ, СРЕДНЕЕ. Для подсчёта уникальных значений есть КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле).
Правило то же, что в SQL: каждое поле в ВЫБРАТЬ либо стоит внутри агрегатной функции, либо перечислено в СГРУППИРОВАТЬ ПО. Иначе запрос не пройдёт синтаксический контроль.
ВЫБРАТЬ
Продажи.Ссылка.Контрагент КАК Контрагент,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Продажи.Ссылка) КАК ЧислоДокументов,
СУММА(Продажи.Сумма) КАК Выручка
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК Продажи
ГДЕ
Продажи.Ссылка.Проведен
И Продажи.Ссылка.Дата МЕЖДУ &Начало И &Конец
СГРУППИРОВАТЬ ПО
Продажи.Ссылка.Контрагент
ИМЕЮЩИЕ
СУММА(Продажи.Сумма) > 100000
УПОРЯДОЧИТЬ ПО
Выручка УБЫВ
Разница между ГДЕ и ИМЕЮЩИЕ:
ГДЕотбирает исходные строки до группировки. Сюда — период, проведённость, склад.ИМЕЮЩИЕотбирает готовые группы по значению агрегата. Сюда — «выручка больше 100 000», «больше 5 документов».
Условие, которое можно проверить в ГДЕ, не переносите в ИМЕЮЩИЕ: группировка пройдёт по лишним строкам.
Итоги
У языка запросов 1С есть то, чего нет в стандартном SQL, — секция ИТОГИ. Она добавляет к результату строки промежуточных итогов:
ИТОГИ СУММА(Выручка) ПО ОБЩИЕ, Контрагент
Обходят такой результат через Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам). В отчётах на СКД итоги считает сама система компоновки, а секция ИТОГИ нужна скорее в коде обработок.
Параметры запроса вместо склейки строк
Значения в запрос передают через параметры с амперсандом: &Склад, &Начало. Подставлять их конкатенацией строк — плохая практика, и вот почему.
Пример опасного кода:
// Так делать нельзя
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Контрагенты
|ГДЕ Наименование ПОДОБНО ""%" + СтрокаПоиска + "%""";
Если пользователь введёт в поле поиска кавычку и кусок текста запроса, он изменит условие. Изменить данные запросом нельзя: язык только читающий. Но расширить выборку и увидеть больше, чем задумано, — можно, особенно если код выполняется в привилегированном режиме, где ограничения прав не действуют.
Правильно:
Запрос.Текст = "ВЫБРАТЬ
| Контрагенты.Ссылка
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Наименование ПОДОБНО &Шаблон СПЕЦСИМВОЛ ""~""";
Запрос.УстановитьПараметр("Шаблон", "%" + ЭкранироватьСпецсимволы(СтрокаПоиска) + "%");
Здесь ЭкранироватьСпецсимволы — ваша функция, которая ставит ~ перед %, _, [, ], ^ и самим ~ во введённой строке. В БСП подобная функция есть в общих модулях; найдите её поиском по слову «ПОДОБНО» в общих модулях вашей конфигурации.
Плюсы параметров не только в безопасности:
- ✅ Типы передаются как есть: дата остаётся датой, ссылка — ссылкой. Не нужно форматировать значения в строку.
- ✅ Текст запроса не меняется от вызова к вызову. СУБД может повторно использовать план выполнения.
- ✅ Параметром можно передать массив или список значений:
ГДЕ Склад В (&Склады). - ✅ Параметром передают и таблицу значений:
ВЫБРАТЬ … ПОМЕСТИТЬ ВТ ИЗ &Таблица КАК Т. Колонки такой таблицы должны иметь заданные типы, иначе запрос не выполнится.
Склеивать текст допустимо только из фрагментов, которые вы написали сами: например, добавить условие по складу, если склад заполнен. Пользовательский ввод в такой текст не попадает.
Как ускорить запрос: практические правила
Медленный запрос в 1С почти всегда вызван одной из нескольких причин.
| Признак | Причина | Что сделать |
|---|---|---|
| Остатки считаются минуты | отбор по измерениям в ГДЕ, а не в параметрах таблицы | перенести условие в параметры виртуальной таблицы |
| В плане десятки соединений | разыменование поля составного типа | ВЫРАЗИТЬ к нужному типу |
| Запрос внутри цикла | на каждую строку отдельное обращение к базе | один запрос с В (&Массив) или соединение с временной таблицей |
| Тормозит соединение с подзапросом | неудачная оценка строк в подзапросе | вынести подзапрос во временную таблицу с индексом |
Условие ИЛИ по разным полям | СУБД не использует индексы | разбить на два запроса через ОБЪЕДИНИТЬ ВСЕ |
Ещё одна привычка опытных разработчиков — не выбирать лишнего. Не берите ВЫБРАТЬ *: перечисляйте поля. Разница особенно заметна для табличных частей и полей неограниченной длины.
Узнать, где тормозит, помогает замер производительности в режиме отладки и технологический журнал. На клиент-серверной базе можно посмотреть и сам текст SQL, который сгенерировала платформа.
Частые ошибки в запросах 1С
- ❌ Сравнение с
NULLчерез=. УсловиеПоле = NULLне выполняется никогда. ПишитеПоле ЕСТЬ NULL. - ❌ Путаница пустой ссылки и
NULL. У незаполненного реквизита — пустая ссылка, а неNULL.NULLпоявляется после левого соединения без пары. Проверка:= ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка). - ❌ Упорядочивание во вложенном запросе.
УПОРЯДОЧИТЬ ПОвнутри вложенного запроса безПЕРВЫЕсмысла не имеет. Сортируйте итоговую выборку. - ❌ Удвоение сумм при соединении. Соединили шапку с двумя табличными частями сразу — каждая строка товаров размножилась на число строк услуг. Сначала сгруппируйте каждую табличную часть, потом соединяйте.
- ❌ Даты без конца дня.
МЕЖДУ &Начало И &Конецпри&Конец= 31.03 00:00:00 потеряет документы за 31 марта. ПередавайтеКонецДня(Дата)из кода илиКОНЕЦПЕРИОДА(&Конец, ДЕНЬ)в запросе. - ❌ Одинаковое имя временной таблицы при общем менеджере. Повторное
ПОМЕСТИТЬ ВТ_Продажив том же менеджере вызовет ошибку: таблица уже существует. Уничтожьте её перед повторным созданием.
Частые вопросы
Можно ли запросом 1С изменить или удалить данные?
Чем `ОБЪЕДИНИТЬ` отличается от `ОБЪЕДИНИТЬ ВСЕ`?
ОБЪЕДИНИТЬ убирает повторяющиеся строки, на что тратит время. ОБЪЕДИНИТЬ ВСЕ оставляет все строки и работает быстрее. Если дублей быть не может, берите второй вариант.Как вывести представление ссылки, а не ссылку?
ПРЕДСТАВЛЕНИЕ(Поле) или поле Поле.Представление вернут строку. Для вывода пользователю удобно, но для соединений и отборов используйте саму ссылку.Можно ли писать запросы на английском?
SELECT, FROM, WHERE, LEFT JOIN. Оба варианта можно даже смешивать, но читать такой текст неудобно.