Как устроена выборка данных в запросах 1С
Содержание
Выборка в 1С — это объект, который возвращает метод Выбрать() у результата запроса. Через него перебирают строки по одной: Пока Выборка.Следующий() Цикл. Сам отбор данных задаёт текст запроса на встроенном языке запросов — секции ВЫБРАТЬ, ИЗ, ГДЕ, УПОРЯДОЧИТЬ ПО и другие.
Путаница обычно в том, что словом «выборка» называют две вещи: сам процесс извлечения данных запросом и конкретный объект РезультатЗапроса.Выбрать(). Ниже разберём оба смысла — как платформа исполняет запрос, какими способами обходят результат, когда брать виртуальные и временные таблицы и что ускоряет тяжёлый запрос, а что его тормозит.
Объект Выборка и как получить данные запроса
Любая работа с данными начинается с объекта Запрос. Ему задают текст, выполняют и получают результат, а из результата — выборку для построчного обхода.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Товар,
| Номенклатура.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Товар.Наименование);
КонецЦикла;
Следующий() сдвигает указатель на очередную строку и возвращает Истина, пока строки есть. К полям обращаются по именам из секции ВЫБРАТЬ — поэтому так полезны псевдонимы через КАК.
Если данные нужны все сразу и порядок обхода неважен, вместо выборки берут Результат.Выгрузить() — он вернёт таблицу значений. Но для больших объёмов выгрузка держит всё в памяти, а выборка идёт по строке за раз и экономнее.
Проверить, пустой ли результат, можно до обхода: Если Результат.Пустой() Тогда. Это быстрее, чем заводить цикл ради одной проверки.
Один объект Запрос умеет больше, чем кажется. Когда текст состоит из нескольких запросов через ;, метод ВыполнитьПакет() вернёт массив результатов — по одному на каждый запрос пакета. Это удобно, когда промежуточные наборы нужны не только для соединения, но и сами по себе: например, отдельно получить шапку документа и отдельно его строки за одно обращение к серверу.
Как платформа исполняет запрос
Текст запроса 1С не исполняет сама. Она транслирует его в SQL и отдаёт СУБД — PostgreSQL, MS SQL или встроенной файловой базе. СУБД строит план выполнения, читает страницы данных, применяет индексы и возвращает платформе набор строк.
Этапов, по сути, четыре:
- 🧩 Разбор текста — платформа проверяет синтаксис и имена таблиц/полей метаданных.
- 🔁 Трансляция в SQL — объектные имена (
Справочник.Номенклатура) превращаются в имена таблиц СУБД, виртуальные таблицы — в подзапросы к физическим таблицам регистров. - ⚙️ Выполнение на стороне СУБД — именно здесь работают индексы и план запроса; львиная доля времени тратится тут.
- 📥 Возврат выборки — платформа отдаёт объект
Выборка, по которому вы идёте в коде.
Отсюда главный вывод: скорость запроса определяется тем, какой SQL получится и какие индексы сможет использовать СУБД, а не тем, как вы потом обходите результат в цикле. Оптимизируют текст запроса, а не обход.
В клиент-серверном варианте запрос всегда выполняется на сервере 1С, который и общается с СУБД. На клиент приходит уже готовая выборка.
Из чего состоит текст запроса
Текст читается сверху вниз по секциям. Обязательны только ВЫБРАТЬ и ИЗ, остальные добавляют по надобности.
| Секция | Зачем нужна |
|---|---|
ВЫБРАТЬ | перечень полей; РАЗЛИЧНЫЕ убирает дубли, ПЕРВЫЕ 100 ограничивает число строк |
ИЗ | источник: таблица справочника, документа, регистра или временная таблица |
ГДЕ | условие отбора строк |
СГРУППИРОВАТЬ ПО | свёртка строк с агрегатами СУММА, КОЛИЧЕСТВО, МАКСИМУМ |
ИМЕЮЩИЕ | условие уже по сгруппированным данным |
УПОРЯДОЧИТЬ ПО | сортировка результата |
ИТОГИ ... ПО | подведение итогов по группировкам (нужно для иерархического обхода) |
Параметры подставляют через &ИмяПараметра, а значение задают в коде: Запрос.УстановитьПараметр("Склад", ВыбранныйСклад). Склеивать значения прямо в текст нельзя — это и медленнее, и ломается на специальных символах.
Отдельно стоит ОБЪЕДИНИТЬ. Он склеивает результаты нескольких запросов в один набор строк — например, когда данные лежат в разных регистрах или документах, но должны попасть в одну выборку. ОБЪЕДИНИТЬ убирает полные дубли строк, а ОБЪЕДИНИТЬ ВСЕ оставляет всё как есть и работает быстрее. Число и порядок полей в объединяемых запросах должны совпадать.
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Номенклатура КАК Товар,
| СУММА(Продажи.Количество) КАК Количество
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК Продажи
|ГДЕ
| Продажи.Ссылка.Дата МЕЖДУ &Начало И &Конец
|СГРУППИРОВАТЬ ПО
| Продажи.Номенклатура
|УПОРЯДОЧИТЬ ПО
| Количество УБЫВ";
Если поле может быть составного типа (например, Регистратор регистра), приводите его к нужному типу через ВЫРАЗИТЬ(Поле КАК Документ.РеализацияТоваровУслуг). Иначе обращение через точку породит лишние соединения.
Как перебрать результат: прямой обход и по группировкам
У метода Выбрать() есть параметр — способ обхода результата.
Прямой обход (ОбходРезультатаЗапроса.Прямой, он же по умолчанию) идёт по строкам подряд. Подходит для плоских списков.
Обход по группировкам (ОбходРезультатаЗапроса.ПоГруппировкам) превращает выборку в двухуровневую: сначала перебираете группы, а внутри каждой — её детальные строки. Работает только если в запросе есть секция ИТОГИ ... ПО.
Выборка = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока Выборка.Следующий() Цикл // уровень организации
Сообщить("Организация: " + Выборка.Организация);
ВыборкаТоваров = Выборка.Выбрать(); // детальные строки группы
Пока ВыборкаТоваров.Следующий() Цикл
Сообщить(" " + ВыборкаТоваров.Товар + " — " + ВыборкаТоваров.Сумма);
КонецЦикла;
КонецЦикла;
Третий способ, ПоГруппировкамСИерархией, нужен для иерархических справочников — он разворачивает группы и подгруппы по уровням вложенности.
Полезные методы самой выборки:
- 🔹
Количество()— сколько строк в результате (или в текущей группе). - 🔹
Сбросить()— вернуть указатель в начало, чтобы пройти выборку заново. - 🔹
НайтиСледующий()— перейти к строке с нужным значением поля. - 🔹
Уровень()иТипЗаписи()— пригодятся при разборе иерархического обхода.
Соединения таблиц и виртуальные таблицы
Чаще всего данные лежат в разных таблицах, и их соединяют.
Четыре вида соединений:
ВНУТРЕННЕЕ СОЕДИНЕНИЕ— только строки, у которых есть пара в обеих таблицах.ЛЕВОЕ СОЕДИНЕНИЕ— все строки левой таблицы плюс совпавшие из правой; где пары нет, поля будутNULL.ПРАВОЕ СОЕДИНЕНИЕ— зеркально левому, применяют редко.ПОЛНОЕ СОЕДИНЕНИЕ— все строки обеих таблиц.
Значения NULL после левого соединения обрабатывают через ЕСТЬNULL(Поле, 0), иначе арифметика и сравнения дадут неожиданный результат.
ВЫБРАТЬ
Продажи.Номенклатура КАК Товар,
Продажи.Количество КАК Продано,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(&НаДату, ) КАК Остатки
ПО Остатки.Номенклатура = Продажи.Номенклатура
ГДЕ
Продажи.Ссылка = &Документ
Здесь строки продаж сохранятся все, а остаток подтянется там, где он есть; где товара на складе нет, ЕСТЬNULL вернёт ноль вместо NULL.
Отдельная и важная тема — виртуальные таблицы регистров. Это не физические таблицы, а готовые наборы, которые платформа собирает «на лету»: остатки, обороты, срезы.
| Виртуальная таблица | Что отдаёт |
|---|---|
.Остатки(Период, Условие) | остатки регистра накопления/бухгалтерии на дату |
.Обороты(Начало, Конец, Период, Условие) | обороты за интервал |
.ОстаткиИОбороты(...) | и то и другое одним запросом |
.СрезПоследних(Период, Условие) | последние значения регистра сведений на дату |
.СрезПервых(Период, Условие) | первые значения на дату |
ВЫБРАТЬ
Остатки.Номенклатура КАК Товар,
Остатки.КоличествоОстаток КАК Остаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&НаДату, Склад = &Склад) КАК Остатки
Условие отбора для виртуальной таблицы задают прямо в её скобках — параметрами. Это принципиально: платформа учтёт отбор ещё при сборке набора и прочитает меньше данных.
Временные таблицы и пакетные запросы
Когда логика сложная, запрос разбивают на шаги через временные таблицы. Промежуточный результат помещают в таблицу оператором ПОМЕСТИТЬ, а дальше соединяют с ней как с обычным источником.
Для временных таблиц нужен менеджер:
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| Строки.Номенклатура КАК Товар
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК Строки
|ГДЕ
| Строки.Ссылка = &Документ
|ИНДЕКСИРОВАТЬ ПО
| Товар
|;
|ВЫБРАТЬ
| ВТ_Товары.Товар,
| Остатки.КоличествоОстаток
|ИЗ
| ВТ_Товары КАК ВТ_Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(&НаДату, ) КАК Остатки
| ПО Остатки.Номенклатура = ВТ_Товары.Товар";
Несколько запросов, разделённых ;, образуют пакетный запрос. Они выполняются последовательно и видят временные таблицы друг друга. Результат последнего и возвращает Выполнить(); получить все результаты пакета можно через ВыполнитьПакет().
Два приёма, которые реально ускоряют соединения:
- 📌
ИНДЕКСИРОВАТЬ ПОна тех полях временной таблицы, по которым будет соединение. - 📌 перенос тяжёлого подзапроса во временную таблицу вместо соединения с вложенным запросом — СУБД строит план лучше, когда соединяет две таблицы, а не таблицу с подзапросом.
Как ускорить выборку: приёмы оптимизации
Запрос работает долго почти всегда по одним и тем же причинам. Что помогает:
- Берите только нужные поля.
ВЫБРАТЬ *читает лишнее. Явный список полей меньше нагружает и СУБД, и канал до сервера. - Отбор — в виртуальную таблицу, не в
ГДЕ..Остатки(&Дата, Склад = &Склад)отработает быстрее, чем.Остатки(&Дата)с последующимГДЕ Остатки.Склад = &Склад. - Не обращайтесь к реквизитам через точку в тяжёлых местах.
Продажи.Ссылка.Контрагент.ИНН— это цепочка неявных левых соединений. Если поле нужно часто, заведите его отдельным соединением или временной таблицей. - Избегайте подзапросов в условиях соединения и в
ГДЕ. Подзапрос лучше вынести в отдельную временную таблицу. - Заменяйте
ИЛИиПОДОБНО "%текст%". Условие сИЛИи поиск с процентом в начале шаблона не дают СУБД использовать индекс.ИЛИчасто заменяется наВ (&СписокЗначений)или на объединение. - Никаких запросов в цикле. Самая частая и дорогая ошибка: вместо запроса на каждой итерации соберите все нужные данные одним запросом до цикла.
- Соединяйте через временную таблицу, а не вложенный запрос. План соединения двух таблиц СУБД строит предсказуемо; соединение таблицы с подзапросом нередко выливается в полный перебор.
Ещё одна тонкость — соединение двух виртуальных таблиц напрямую. Платформа умеет его оптимизировать, но если таблиц много и у каждой свой отбор, надёжнее собрать каждую во временную таблицу, проиндексировать и соединять уже их. Так контроль над планом остаётся за вами, а не за эвристиками транслятора.
Включите технологический журнал или замер производительности в конфигураторе и посмотрите реальное время по строкам кода — оптимизировать на глаз бессмысленно, узкое место почти всегда не там, где кажется.
Частые ошибки в запросах
Ниже — именно ошибки логики и результата, а не повтор советов по скорости.
- ❌ Не обработан
NULLпосле левого соединения.Количество * ЦенаприNULLв одном из полей дастNULL, а не ноль. Оборачивайте вЕСТЬNULL. - ❌ Поле составного типа без
ВЫРАЗИТЬ. ОбращениеРегистратор.Номербез приведения типа вернёт пусто для документов «не того» вида и замедлит запрос. - ❌
СГРУППИРОВАТЬ ПОбез всех неагрегатных полей. Любое поле изВЫБРАТЬ, к которому не применён агрегат, обязано быть в группировке — иначе запрос не выполнится. - ❌ Дубли строк после соединения по табличным частям. Если соединяете два документа по их строкам, строки перемножаются. Сначала сверните каждую часть во временной таблице, потом соединяйте.
- ❌
ПЕРВЫЕ NбезУПОРЯДОЧИТЬ ПО. Без сортировки «первые» строки — любые, порядок не гарантирован.
Частые вопросы
Чем выборка отличается от выгрузки результата?
Выбрать() даёт объект для построчного обхода и экономит память на больших объёмах. Выгрузить() сразу возвращает таблицу значений целиком — удобно для небольших наборов и когда нужен произвольный доступ к строкам.Можно ли пройти выборку повторно?
Выборка.Сбросить() — указатель вернётся в начало, и можно обойти строки заново без повторного выполнения запроса.Почему запрос быстрый в файловой базе и медленный в клиент-серверной?
Как в запросе получить остатки на конкретную дату?
РегистрНакопления.ИмяРегистра.Остатки(&НаДату, Условие). Дату и отбор передавайте параметрами в скобках самой таблицы.Зачем индексировать временную таблицу?
ИНДЕКСИРОВАТЬ ПО соединение большой временной таблицы с регистром может замедлить запрос в разы.