Как устроена выборка данных в запросах 1С

· 9 мин

Содержание

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

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

Объект Выборка и как получить данные запроса

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

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

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

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

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

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

Проверить, пустой ли результат, можно до обхода: Если Результат.Пустой() Тогда. Это быстрее, чем заводить цикл ради одной проверки.

Один объект Запрос умеет больше, чем кажется. Когда текст состоит из нескольких запросов через ;, метод ВыполнитьПакет() вернёт массив результатов — по одному на каждый запрос пакета. Это удобно, когда промежуточные наборы нужны не только для соединения, но и сами по себе: например, отдельно получить шапку документа и отдельно его строки за одно обращение к серверу.

Как платформа исполняет запрос

Текст запроса 1С не исполняет сама. Она транслирует его в SQL и отдаёт СУБД — PostgreSQL, MS SQL или встроенной файловой базе. СУБД строит план выполнения, читает страницы данных, применяет индексы и возвращает платформе набор строк.

Этапов, по сути, четыре:

  1. 🧩 Разбор текста — платформа проверяет синтаксис и имена таблиц/полей метаданных.
  2. 🔁 Трансляция в SQL — объектные имена (Справочник.Номенклатура) превращаются в имена таблиц СУБД, виртуальные таблицы — в подзапросы к физическим таблицам регистров.
  3. ⚙️ Выполнение на стороне СУБД — именно здесь работают индексы и план запроса; львиная доля времени тратится тут.
  4. 📥 Возврат выборки — платформа отдаёт объект Выборка, по которому вы идёте в коде.

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

В клиент-серверном варианте запрос всегда выполняется на сервере 1С, который и общается с СУБД. На клиент приходит уже готовая выборка.

Из чего состоит текст запроса

Текст читается сверху вниз по секциям. Обязательны только ВЫБРАТЬ и ИЗ, остальные добавляют по надобности.

СекцияЗачем нужна
ВЫБРАТЬперечень полей; РАЗЛИЧНЫЕ убирает дубли, ПЕРВЫЕ 100 ограничивает число строк
ИЗисточник: таблица справочника, документа, регистра или временная таблица
ГДЕусловие отбора строк
СГРУППИРОВАТЬ ПОсвёртка строк с агрегатами СУММА, КОЛИЧЕСТВО, МАКСИМУМ
ИМЕЮЩИЕусловие уже по сгруппированным данным
УПОРЯДОЧИТЬ ПОсортировка результата
ИТОГИ ... ПОподведение итогов по группировкам (нужно для иерархического обхода)

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

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

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

Если поле может быть составного типа (например, Регистратор регистра), приводите его к нужному типу через ВЫРАЗИТЬ(Поле КАК Документ.РеализацияТоваровУслуг). Иначе обращение через точку породит лишние соединения.

Как перебрать результат: прямой обход и по группировкам

У метода Выбрать() есть параметр — способ обхода результата.

Прямой обход (ОбходРезультатаЗапроса.Прямой, он же по умолчанию) идёт по строкам подряд. Подходит для плоских списков.

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

Выборка = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока Выборка.Следующий() Цикл              // уровень организации
    Сообщить("Организация: " + Выборка.Организация);
    ВыборкаТоваров = Выборка.Выбрать();    // детальные строки группы
    Пока ВыборкаТоваров.Следующий() Цикл
        Сообщить("  " + ВыборкаТоваров.Товар + " — " + ВыборкаТоваров.Сумма);
    КонецЦикла;
КонецЦикла;

Третий способ, ПоГруппировкамСИерархией, нужен для иерархических справочников — он разворачивает группы и подгруппы по уровням вложенности.

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

  • 🔹 Количество() — сколько строк в результате (или в текущей группе).
  • 🔹 Сбросить() — вернуть указатель в начало, чтобы пройти выборку заново.
  • 🔹 НайтиСледующий() — перейти к строке с нужным значением поля.
  • 🔹 Уровень() и ТипЗаписи() — пригодятся при разборе иерархического обхода.

Соединения таблиц и виртуальные таблицы

Чаще всего данные лежат в разных таблицах, и их соединяют.

Четыре вида соединений:

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

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

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

Здесь строки продаж сохранятся все, а остаток подтянется там, где он есть; где товара на складе нет, ЕСТЬNULL вернёт ноль вместо NULL.

Отдельная и важная тема — виртуальные таблицы регистров. Это не физические таблицы, а готовые наборы, которые платформа собирает «на лету»: остатки, обороты, срезы.

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

Условие отбора для виртуальной таблицы задают прямо в её скобках — параметрами. Это принципиально: платформа учтёт отбор ещё при сборке набора и прочитает меньше данных.

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

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

Для временных таблиц нужен менеджер:

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

Несколько запросов, разделённых ;, образуют пакетный запрос. Они выполняются последовательно и видят временные таблицы друг друга. Результат последнего и возвращает Выполнить(); получить все результаты пакета можно через ВыполнитьПакет().

Два приёма, которые реально ускоряют соединения:

  • 📌 ИНДЕКСИРОВАТЬ ПО на тех полях временной таблицы, по которым будет соединение.
  • 📌 перенос тяжёлого подзапроса во временную таблицу вместо соединения с вложенным запросом — СУБД строит план лучше, когда соединяет две таблицы, а не таблицу с подзапросом.

Как ускорить выборку: приёмы оптимизации

Запрос работает долго почти всегда по одним и тем же причинам. Что помогает:

  1. Берите только нужные поля. ВЫБРАТЬ * читает лишнее. Явный список полей меньше нагружает и СУБД, и канал до сервера.
  2. Отбор — в виртуальную таблицу, не в ГДЕ. .Остатки(&Дата, Склад = &Склад) отработает быстрее, чем .Остатки(&Дата) с последующим ГДЕ Остатки.Склад = &Склад.
  3. Не обращайтесь к реквизитам через точку в тяжёлых местах. Продажи.Ссылка.Контрагент.ИНН — это цепочка неявных левых соединений. Если поле нужно часто, заведите его отдельным соединением или временной таблицей.
  4. Избегайте подзапросов в условиях соединения и в ГДЕ. Подзапрос лучше вынести в отдельную временную таблицу.
  5. Заменяйте ИЛИ и ПОДОБНО "%текст%". Условие с ИЛИ и поиск с процентом в начале шаблона не дают СУБД использовать индекс. ИЛИ часто заменяется на В (&СписокЗначений) или на объединение.
  6. Никаких запросов в цикле. Самая частая и дорогая ошибка: вместо запроса на каждой итерации соберите все нужные данные одним запросом до цикла.
  7. Соединяйте через временную таблицу, а не вложенный запрос. План соединения двух таблиц СУБД строит предсказуемо; соединение таблицы с подзапросом нередко выливается в полный перебор.

Ещё одна тонкость — соединение двух виртуальных таблиц напрямую. Платформа умеет его оптимизировать, но если таблиц много и у каждой свой отбор, надёжнее собрать каждую во временную таблицу, проиндексировать и соединять уже их. Так контроль над планом остаётся за вами, а не за эвристиками транслятора.

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

Частые ошибки в запросах

Ниже — именно ошибки логики и результата, а не повтор советов по скорости.

  • ❌ Не обработан NULL после левого соединения. Количество * Цена при NULL в одном из полей даст NULL, а не ноль. Оборачивайте в ЕСТЬNULL.
  • ❌ Поле составного типа без ВЫРАЗИТЬ. Обращение Регистратор.Номер без приведения типа вернёт пусто для документов «не того» вида и замедлит запрос.
  • ❌ СГРУППИРОВАТЬ ПО без всех неагрегатных полей. Любое поле из ВЫБРАТЬ, к которому не применён агрегат, обязано быть в группировке — иначе запрос не выполнится.
  • ❌ Дубли строк после соединения по табличным частям. Если соединяете два документа по их строкам, строки перемножаются. Сначала сверните каждую часть во временной таблице, потом соединяйте.
  • ❌ ПЕРВЫЕ N без УПОРЯДОЧИТЬ ПО. Без сортировки «первые» строки — любые, порядок не гарантирован.
Проверьте себя
Где правильнее задать отбор по складу для виртуальной таблицы остатков?
Что возвращает метод Следующий() объекта Выборка?
Какой обход результата нужен, чтобы перебрать группы и внутри них детальные строки?

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

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