query-patterns

$npx mdskill add SteelMorgan/1c-agent-based-dev-framework/query-patterns

Optimizes 1C:Enterprise query patterns to minimize database round-trips and data volume for efficient agent operations.

  • Helps avoid performance issues from executing queries in loops, reducing network latency and execution time.
  • Integrates with 1C:Enterprise query language and uses structures like Соответствие for data handling.
  • Decides based on IT standards prioritizing single queries over iterative ones to enhance efficiency.
  • Presents results through structured data mappings for streamlined processing within agent workflows.
SKILL.md
.github/skills/query-patternsView on GitHub ↗
---
name: query-patterns
description: Паттерны запросов 1С. Этот навык учит агента правильно работать с языком запросов 1С:Предприятие.
---

# Паттерны запросов 1С

**Ключевой принцип:** Каждый запрос к базе — сетевой round-trip. Минимизация количества запросов и объёма возвращаемых данных — приоритет.

---

## Правило 1: НИКОГДА не выполняйте запросы в цикле

Для N итераций — N * (латентность сети + время выполнения). 1000 элементов * 5 мс = 5 секунд только на ожидание сети.

Стандарт ИТС: «Запросы к базе данных — ограничение на использование запросов в цикле».

### Правильно — один запрос + Соответствие для обработки

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

Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();

СоответствиеДанных = Новый Соответствие;
Пока Выборка.Следующий() Цикл
    СоответствиеДанных.Вставить(Выборка.Ссылка,
        Новый Структура("Наименование, ЕдиницаИзмерения, СтавкаНДС",
            Выборка.Наименование, Выборка.ЕдиницаИзмерения, Выборка.СтавкаНДС));
КонецЦикла;

Для Каждого СтрокаТоваров Из Документ.Товары Цикл
    ДанныеНоменклатуры = СоответствиеДанных.Получить(СтрокаТоваров.Номенклатура);
    Если ДанныеНоменклатуры <> Неопределено Тогда
        СтрокаТоваров.ЕдиницаИзмерения = ДанныеНоменклатуры.ЕдиницаИзмерения;
    КонецЕсли;
КонецЦикла;
```

---

## Правило 2: Временные таблицы для сложных запросов

Разбиение на этапы: читаемость, отладка поэтапно, индексация промежуточных результатов.

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

Правила: префикс `вт` (стандарт); `ИНДЕКСИРОВАТЬ ПО` для полей соединения; использовать `МенеджерВременныхТаблиц` для управления жизненным циклом.

---

## Правило 3: Параметризация запросов — не подставляйте значения в текст

Подстановка значений в текст: уязвимость, невозможность кэширования плана СУБД, ошибки форматирования.

```bsl
// Правильно — через параметры
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ Товары.Наименование
|ИЗ Справочник.Номенклатура КАК Товары
|ГДЕ Товары.ВидНоменклатуры = &ВидНоменклатуры
|   И Товары.Цена >= &МинимальнаяЦена";

Запрос.УстановитьПараметр("ВидНоменклатуры", Перечисления.ВидыНоменклатуры.Товар);
Запрос.УстановитьПараметр("МинимальнаяЦена", 1000);
```

---

## Правило 4: ЕСТЬNULL() при LEFT JOIN

`NULL + 100 = NULL`, `NULL > 0 = ЛОЖЬ`. Необработанный NULL приводит к неверным расчётам и потерянным строкам в условиях.

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

### Ловушка — фильтр без ЕСТЬNULL

```bsl
// ПЛОХО: ГДЕ ОстаткиТоваров.КоличествоОстаток > 0
// → NULL > 0 = ЛОЖЬ → строки без остатков пропадут (LEFT JOIN превращается в INNER)
```

---

## Правило 5: Виртуальные таблицы регистров — параметры внутрь

Виртуальные таблицы (`Остатки`, `Обороты`, `СрезПоследних`) — параметризованные функции СУБД. Параметры внутри — оптимальный план. Параметры в WHERE — СУБД сначала вычислит **все** данные, потом отфильтрует. Разница — в тысячи раз.

Стандарт ИТС: «Использование виртуальных таблиц».

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

### Неправильно — фильтрация через WHERE

```bsl
// ПЛОХО: СУБД вычислит остатки по ВСЕЙ номенклатуре на ВСЕХ складах, потом отфильтрует
"ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|ГДЕ Остатки.Номенклатура В (&СписокНоменклатуры)"
```

---

## Правило 6: Пакетные операции — ТаблицаЗначений как параметр запроса

Передача массива данных во временную таблицу через `Запрос.УстановитьПараметр("ВТ", ТаблицаЗначений)` — один запрос вместо цикла.

```bsl
ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Штрихкод", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(13)));
ТаблицаДанных.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));

Для Каждого СтрокаИмпорта Из ДанныеИмпорта Цикл
    НоваяСтрока = ТаблицаДанных.Добавить();
    НоваяСтрока.Штрихкод = СтрокаИмпорта.Штрихкод;
    НоваяСтрока.Количество = СтрокаИмпорта.Количество;
КонецЦикла;

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

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

---

## Правило 7: Обработка результатов — Выборка vs Выгрузка

| Способ | Когда использовать |
|--------|-------------------|
| `Выбрать()/Следующий()` | Последовательная обработка, экономия памяти |
| `Выбрать(ПоГруппировкам)` | Иерархические данные |
| `Выгрузить()` → ТаблицаЗначений | Произвольный доступ, поиск, передача в другую процедуру, данных < 10 000 строк |

```bsl
// Выборка — данные загружаются порциями
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // обработка
КонецЦикла;

// Выгрузка — всё в память
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
НайденнаяСтрока = ТаблицаРезультат.Найти(ИскомаяНоменклатура, "Номенклатура");
```

---

## Правило 8: Индексация — помогайте оптимизатору

Индексируйте: поля в условиях ГДЕ, поля в условиях соединения (ОН/ON), поля упорядочивания.

### Временные таблицы — индексируйте поля соединения

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

В конфигураторе/EDT: Реквизит -> Свойства -> «Индексировать».

---

## Правило 9: Только нужные поля (не ВЫБРАТЬ *)

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

```bsl
// Правильно — явный список полей
"ВЫБРАТЬ
|   Контрагенты.Ссылка,
|   Контрагенты.Наименование,
|   Контрагенты.ИНН
|ИЗ Справочник.Контрагенты КАК Контрагенты"
```

---

## Правило 10: ПЕРВЫЕ N для ограничения результата

Запрос без ограничения может вернуть миллионы строк и исчерпать память.

```bsl
"ВЫБРАТЬ ПЕРВЫЕ 100
|   Товары.Наименование, Товары.Код
|ИЗ Справочник.Номенклатура КАК Товары
|ГДЕ Товары.Наименование ПОДОБНО &СтрокаПоиска
|УПОРЯДОЧИТЬ ПО Товары.Наименование"
```

Для отображения списков используйте динамические списки — они реализуют пагинацию автоматически.

---

## Правило 11: Не маскируйте дубликаты через РАЗЛИЧНЫЕ

`РАЗЛИЧНЫЕ` требует сортировки/хеширования всех строк. Если дубликаты из-за лишнего JOIN — исправьте запрос.

```bsl
// Правильно — подзапрос вместо JOIN + РАЗЛИЧНЫЕ
"ВЫБРАТЬ Контрагенты.Ссылка, Контрагенты.Наименование
|ИЗ Справочник.Контрагенты КАК Контрагенты
|ГДЕ Контрагенты.Ссылка В
|       (ВЫБРАТЬ РАЗЛИЧНЫЕ Реализация.Контрагент
|        ИЗ Документ.РеализацияТоваровУслуг КАК Реализация
|        ГДЕ Реализация.Дата >= &ДатаНачала)"
```

---

## Правило 12: ВЫРАЗИТЬ для составных типов

Если поле имеет составной тип (напр. «Регистратор»), СУБД делает LEFT JOIN ко **всем** таблицам составного типа. `ВЫРАЗИТЬ(Поле КАК Тип)` ограничивает JOIN до одной таблицы.

Стандарт ИТС: «Использование конструкции ВЫРАЗИТЬ в запросах».

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

### Ловушка — обращение через составной тип без ВЫРАЗИТЬ

```bsl
// ПЛОХО: Движения.Регистратор.Контрагент без ВЫРАЗИТЬ
// Если Регистратор может быть 20 видами документов — 20 LEFT JOIN!
```

---

## Правило 13: ON vs WHERE в LEFT JOIN

- Условие в `ПО` фильтрует правую таблицу **ДО** соединения — строки левой без пары остаются с NULL
- Условие в `ГДЕ` фильтрует **ПОСЛЕ** — строки с NULL отбрасываются, превращая LEFT JOIN в INNER JOIN

```bsl
// Правильно — фильтр правой таблицы в параметрах виртуальной таблицы / в ON
"ВЫБРАТЬ
|   Номенклатура.Наименование,
|   ЕСТЬNULL(Цены.Цена, 0) КАК Цена
|ИЗ Справочник.Номенклатура КАК Номенклатура
|       ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаЦен,
|           ВидЦены = &ВидЦены) КАК Цены
|       ПО Номенклатура.Ссылка = Цены.Номенклатура"
// Все товары в результате, даже без цены
```

### Ловушка — фильтр правой таблицы в WHERE

```bsl
// ПЛОХО: ГДЕ Цены.ВидЦены = &ВидЦены
// → строки без цены (NULL) отфильтруются — LEFT JOIN стал INNER JOIN
```

---
depends_on: []
---
More from SteelMorgan/1c-agent-based-dev-framework