Как устроен язык запросов 1С и как писать на нём быстрые выборки

· 10 мин

Содержание

Язык запросов 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(Номенклатура.ЕдиницаИзмерения.Наименование, "") КАК Единица
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    НЕ Номенклатура.ПометкаУдаления
    И НЕ Номенклатура.ЭтоГруппа
    И Номенклатура.Наименование ПОДОБНО &Шаблон СПЕЦСИМВОЛ "~"
УПОРЯДОЧИТЬ ПО
    Номенклатура.Наименование

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

  1. КАК задаёт псевдонимы таблиц и полей. Без псевдонимов поля в результате получат автоматические имена, к которым неудобно обращаться из кода.
  2. ГДЕ поддерживает И, ИЛИ, НЕ, В (…), МЕЖДУ … И …, ЕСТЬ NULL, ПОДОБНО, В ИЕРАРХИИ.
  3. ПОДОБНО работает с шаблонами % и _. СПЕЦСИМВОЛ нужен, чтобы искать сами эти символы в тексте.
  4. Сравнивать с конкретными элементами удобно через ЗНАЧЕНИЕ(…): ЗНАЧЕНИЕ(Перечисление.ВидыДоговоровКонтрагентов.СПокупателем), ЗНАЧЕНИЕ(Справочник.Валюты.ПустаяСсылка).

Выполнение из кода выглядит так:

Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("Шаблон", "%болт%");

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

Выбрать() читает результат построчно и бережёт память. Выгрузить() отдаёт всю таблицу значений сразу — удобно для небольших выборок и для передачи в другие процедуры.

Виртуальные таблицы регистров

Самая частая задача — остатки на дату. Отбор передают в параметры виртуальной таблицы, а не в ГДЕ:

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

Если написать то же условие в ГДЕ, платформа сначала посчитает остатки по всем складам, а потом отбросит лишнее. На большой базе разница во времени — в разы. Условие по измерениям регистра ставьте в параметры виртуальной таблицы.

Соединения таблиц в запросах 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 марта. Передавайте КонецДня(Дата) из кода или КОНЕЦПЕРИОДА(&Конец, ДЕНЬ) в запросе.
  • ❌ Одинаковое имя временной таблицы при общем менеджере. Повторное ПОМЕСТИТЬ ВТ_Продажи в том же менеджере вызовет ошибку: таблица уже существует. Уничтожьте её перед повторным созданием.
Проверка запроса перед сдачей
Выполнено: 0 / 7

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

Можно ли запросом 1С изменить или удалить данные?
Нет. Язык запросов 1С только читает данные. Изменения вносят объектами встроенного языка: записью элемента, проведением документа, записью набора регистра.
Чем `ОБЪЕДИНИТЬ` отличается от `ОБЪЕДИНИТЬ ВСЕ`?
ОБЪЕДИНИТЬ убирает повторяющиеся строки, на что тратит время. ОБЪЕДИНИТЬ ВСЕ оставляет все строки и работает быстрее. Если дублей быть не может, берите второй вариант.
Как вывести представление ссылки, а не ссылку?
Функция ПРЕДСТАВЛЕНИЕ(Поле) или поле Поле.Представление вернут строку. Для вывода пользователю удобно, но для соединений и отборов используйте саму ссылку.
Можно ли писать запросы на английском?
Да. Каждое ключевое слово имеет английский аналог: SELECT, FROM, WHERE, LEFT JOIN. Оба варианта можно даже смешивать, но читать такой текст неудобно.
Где потренироваться в запросах на рабочей базе?
Используйте внешнюю обработку «Консоль запросов» в копии базы. Запрос только читает данные, но тяжёлая выборка в рабочее время заметно нагрузит сервер и замедлит работу других пользователей.