framework/skills/bsl-practices/query-patterns/SKILL.md
При написании новых запросов 1С и параметров
npx skillsauth add steelmorgan/1c-agent-based-dev-framework query-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ключевой принцип: Каждый запрос к базе — сетевой round-trip. Минимизация количества запросов и объёма возвращаемых данных — приоритет.
Для N итераций — N * (латентность сети + время выполнения). 1000 элементов * 5 мс = 5 секунд только на ожидание сети.
Стандарт ИТС: «Запросы к базе данных — ограничение на использование запросов в цикле».
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
| Номенклатура.СтавкаНДС КАК СтавкаНДС
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка В (&МассивНоменклатуры)";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
СоответствиеДанных = Новый Соответствие;
Пока Выборка.Следующий() Цикл
СоответствиеДанных.Вставить(Выборка.Ссылка,
Новый Структура("Наименование, ЕдиницаИзмерения, СтавкаНДС",
Выборка.Наименование, Выборка.ЕдиницаИзмерения, Выборка.СтавкаНДС));
КонецЦикла;
Для Каждого СтрокаТоваров Из Документ.Товары Цикл
ДанныеНоменклатуры = СоответствиеДанных.Получить(СтрокаТоваров.Номенклатура);
Если ДанныеНоменклатуры <> Неопределено Тогда
СтрокаТоваров.ЕдиницаИзмерения = ДанныеНоменклатуры.ЕдиницаИзмерения;
КонецЕсли;
КонецЦикла;
Разбиение на этапы: читаемость, отладка поэтапно, индексация промежуточных результатов.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК ДокументСсылка,
| Реализация.Контрагент КАК Контрагент,
| Реализация.СуммаДокумента КАК Сумма
|ПОМЕСТИТЬ втРеализации
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
| И Реализация.Проведен
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| втРеализации.Контрагент КАК Контрагент,
| СУММА(втРеализации.Сумма) КАК ОбщаяСумма,
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ втРеализации.ДокументСсылка) КАК КоличествоДокументов
|ПОМЕСТИТЬ втИтогиПоКонтрагентам
|ИЗ
| втРеализации
|
|СГРУППИРОВАТЬ ПО
| втРеализации.Контрагент
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| втИтоги.Контрагент КАК Контрагент,
| втИтоги.ОбщаяСумма КАК ОбщаяСумма,
| втИтоги.КоличествоДокументов КАК КоличествоДокументов,
| ВзаиморасчетыОстатки.СуммаОстаток КАК Задолженность
|ИЗ
| втИтогиПоКонтрагентам КАК втИтоги
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки КАК ВзаиморасчетыОстатки
| ПО втИтоги.Контрагент = ВзаиморасчетыОстатки.Контрагент
|
|УПОРЯДОЧИТЬ ПО
| ОбщаяСумма УБЫВ";
Правила: префикс вт (стандарт); ИНДЕКСИРОВАТЬ ПО для полей соединения; использовать МенеджерВременныхТаблиц для управления жизненным циклом.
Подстановка значений в текст: уязвимость, невозможность кэширования плана СУБД, ошибки форматирования.
// Правильно — через параметры
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ Товары.Наименование
|ИЗ Справочник.Номенклатура КАК Товары
|ГДЕ Товары.ВидНоменклатуры = &ВидНоменклатуры
| И Товары.Цена >= &МинимальнаяЦена";
Запрос.УстановитьПараметр("ВидНоменклатуры", Перечисления.ВидыНоменклатуры.Товар);
Запрос.УстановитьПараметр("МинимальнаяЦена", 1000);
NULL + 100 = NULL, NULL > 0 = ЛОЖЬ. Необработанный NULL приводит к неверным расчётам и потерянным строкам в условиях.
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура,
| ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0) КАК Остаток,
| ЕСТЬNULL(ОстаткиТоваров.СуммаОстаток, 0) КАК СуммаОстатка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК ОстаткиТоваров
| ПО Номенклатура.Ссылка = ОстаткиТоваров.Номенклатура
|ГДЕ
| ЕСТЬNULL(ОстаткиТоваров.КоличествоОстаток, 0) > 0";
// ПЛОХО: ГДЕ ОстаткиТоваров.КоличествоОстаток > 0
// → NULL > 0 = ЛОЖЬ → строки без остатков пропадут (LEFT JOIN превращается в INNER)
Виртуальные таблицы (Остатки, Обороты, СрезПоследних) — параметризованные функции СУБД. Параметры внутри — оптимальный план. Параметры в WHERE — СУБД сначала вычислит все данные, потом отфильтрует. Разница — в тысячи раз.
Стандарт ИТС: «Использование виртуальных таблиц».
// Правильно — параметры внутри виртуальной таблицы
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.Склад КАК Склад,
| Остатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &ДатаОстатков,
| Номенклатура В (&СписокНоменклатуры)
| И Склад = &Склад
| ) КАК Остатки";
// ПЛОХО: СУБД вычислит остатки по ВСЕЙ номенклатуре на ВСЕХ складах, потом отфильтрует
"ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|ГДЕ Остатки.Номенклатура В (&СписокНоменклатуры)"
Передача массива данных во временную таблицу через Запрос.УстановитьПараметр("ВТ", ТаблицаЗначений) — один запрос вместо цикла.
ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Штрихкод", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(13)));
ТаблицаДанных.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
Для Каждого СтрокаИмпорта Из ДанныеИмпорта Цикл
НоваяСтрока = ТаблицаДанных.Добавить();
НоваяСтрока.Штрихкод = СтрокаИмпорта.Штрихкод;
НоваяСтрока.Количество = СтрокаИмпорта.Количество;
КонецЦикла;
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДанныеИмпорта", ТаблицаДанных);
Запрос.Текст =
"ВЫБРАТЬ
| Данные.Штрихкод КАК Штрихкод,
| Данные.Количество КАК Количество
|ПОМЕСТИТЬ втДанныеИмпорта
|ИЗ
| &ДанныеИмпорта КАК Данные
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Штрихкоды.Номенклатура КАК Номенклатура,
| втДанные.Количество КАК Количество
|ИЗ
| втДанныеИмпорта КАК втДанные
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ШтрихкодыНоменклатуры КАК Штрихкоды
| ПО втДанные.Штрихкод = Штрихкоды.Штрихкод";
| Способ | Когда использовать |
|--------|-------------------|
| Выбрать()/Следующий() | Последовательная обработка, экономия памяти |
| Выбрать(ПоГруппировкам) | Иерархические данные |
| Выгрузить() → ТаблицаЗначений | Произвольный доступ, поиск, передача в другую процедуру, данных < 10 000 строк |
// Выборка — данные загружаются порциями
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// обработка
КонецЦикла;
// Выгрузка — всё в память
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
НайденнаяСтрока = ТаблицаРезультат.Найти(ИскомаяНоменклатура, "Номенклатура");
Индексируйте: поля в условиях ГДЕ, поля в условиях соединения (ОН/ON), поля упорядочивания.
Индекс временной таблицы не добавляется "на всякий случай". Он оправдан, когда временная таблица большая и дальше многократно участвует в соединениях/отборах, либо есть измерение/план запроса, показывающий выигрыш. Для маленьких одноразовых ВТ индекс может стоить дороже, чем последующее чтение.
Запрос.Текст =
"ВЫБРАТЬ
| ДанныеЗаказов.Номенклатура КАК Номенклатура,
| ДанныеЗаказов.Количество КАК Количество
|ПОМЕСТИТЬ втЗаказы
|ИЗ
| &ТаблицаЗаказов КАК ДанныеЗаказов
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| втЗаказы.Номенклатура,
| втЗаказы.Количество КАК Заказано,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК НаСкладе
|ИЗ
| втЗаказы
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(,
| Номенклатура В (ВЫБРАТЬ втЗаказы.Номенклатура ИЗ втЗаказы)) КАК Остатки
| ПО втЗаказы.Номенклатура = Остатки.Номенклатура";
В конфигураторе/EDT: Реквизит -> Свойства -> «Индексировать».
Избыточные поля: лишний трафик, невозможность покрывающего индекса, хрупкость при добавлении реквизитов.
// Правильно — явный список полей
"ВЫБРАТЬ
| Контрагенты.Ссылка,
| Контрагенты.Наименование,
| Контрагенты.ИНН
|ИЗ Справочник.Контрагенты КАК Контрагенты"
Запрос без ограничения может вернуть миллионы строк и исчерпать память.
"ВЫБРАТЬ ПЕРВЫЕ 100
| Товары.Наименование, Товары.Код
|ИЗ Справочник.Номенклатура КАК Товары
|ГДЕ Товары.Наименование ПОДОБНО &СтрокаПоиска
|УПОРЯДОЧИТЬ ПО Товары.Наименование"
Для отображения списков используйте динамические списки — они реализуют пагинацию автоматически.
РАЗЛИЧНЫЕ требует сортировки/хеширования всех строк. Если дубликаты из-за лишнего JOIN — исправьте запрос.
// Правильно — подзапрос вместо JOIN + РАЗЛИЧНЫЕ
"ВЫБРАТЬ Контрагенты.Ссылка, Контрагенты.Наименование
|ИЗ Справочник.Контрагенты КАК Контрагенты
|ГДЕ Контрагенты.Ссылка В
| (ВЫБРАТЬ РАЗЛИЧНЫЕ Реализация.Контрагент
| ИЗ Документ.РеализацияТоваровУслуг КАК Реализация
| ГДЕ Реализация.Дата >= &ДатаНачала)"
Если поле имеет составной тип (напр. «Регистратор»), СУБД делает LEFT JOIN ко всем таблицам составного типа. ВЫРАЗИТЬ(Поле КАК Тип) ограничивает JOIN до одной таблицы.
Стандарт ИТС: «Использование конструкции ВЫРАЗИТЬ в запросах».
// Правильно — JOIN только с одной таблицей
"ВЫБРАТЬ
| Движения.Период,
| ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Контрагент КАК Контрагент,
| Движения.Количество
|ИЗ РегистрНакопления.ТоварыНаСкладах КАК Движения
|ГДЕ Движения.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг"
// ПЛОХО: Движения.Регистратор.Контрагент без ВЫРАЗИТЬ
// Если Регистратор может быть 20 видами документов — 20 LEFT JOIN!
ПО фильтрует правую таблицу ДО соединения — строки левой без пары остаются с NULLГДЕ фильтрует ПОСЛЕ — строки с NULL отбрасываются, превращая LEFT JOIN в INNER JOIN// Правильно — фильтр правой таблицы в параметрах виртуальной таблицы / в ON
"ВЫБРАТЬ
| Номенклатура.Наименование,
| ЕСТЬNULL(Цены.Цена, 0) КАК Цена
|ИЗ Справочник.Номенклатура КАК Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаЦен,
| ВидЦены = &ВидЦены) КАК Цены
| ПО Номенклатура.Ссылка = Цены.Номенклатура"
// Все товары в результате, даже без цены
// ПЛОХО: ГДЕ Цены.ВидЦены = &ВидЦены
// → строки без цены (NULL) отфильтруются — LEFT JOIN стал INNER JOIN
Эти проверки не заменяют оптимизацию, а ловят style/safety дефекты до измерений производительности.
| Проверка | Правило |
|----------|---------|
| Имена | Вложенные запросы, временные таблицы и наборы СКД имеют осмысленные имена. Не оставляй ВложенныйЗапрос, ВТ1, НаборДанных1 в рабочем коде. |
| Составные типы | Для разыменования реквизита составного типа используй ВЫРАЗИТЬ(... КАК ...) и/или ССЫЛКА, чтобы не получить неявные JOIN ко всем возможным типам. |
| Предопределенные значения | Для пустых ссылок и предопределенных значений используй ЗНАЧЕНИЕ(...), когда значение является частью модели, а не внешним параметром. |
| Лишние поля | Не выбирай поля, которые дальше не используются. |
| Строковые константы | Значения, которые меняются от вызова к вызову, передавай параметрами, а не строковыми литералами в тексте запроса. |
| RLS | Не убирай РАЗРЕШЕННЫЕ ради производительности без явного решения по безопасности. |
| Вычисления | Сложные вычисляемые поля сначала считай в ВЫБРАТЬ/ВТ, затем фильтруй по готовому полю, если это снижает повторные вычисления. |
| ПО vs ГДЕ | Условия соединения держи в ПО; условия отбора результата - в ГДЕ. Смешивай только с осознанной причиной. |
| Dot dereference | Избегай длинных цепочек через точку в ГДЕ; вынеси нужные значения в ВТ или используй явное соединение. |
| Сборка текста | Не собирай произвольный текст запроса конкатенацией в бизнес-логике. Динамические фрагменты должны быть заранее определены, промаркированы и ограничены белым списком таблиц/полей. |
| Длинный запрос | Длинный запрос выноси в отдельную функцию, которая возвращает только текст. Выполнение, параметры и обработка результата остаются отдельно. |
| Порядок | Сначала установи Запрос.Текст, затем вызывай Запрос.УстановитьПараметр(...). |
| Пустой результат | Не делай отдельный РезультатЗапроса.Пустой() перед обычным обходом, если достаточно первого Выборка.Следующий(). |
| Булевы поля | Для булевого результата используй булеву алгебру (Количество > 0 КАК ЕстьОстаток), а не ВЫБОР ... ТОГДА ИСТИНА ИНАЧЕ ЛОЖЬ. |
| АВТОНОМЕРЗАПИСИ | Не строь бизнес-логику на ожидании, что автонумерация записей гарантированно идет с единицы и без пропусков. |
В (...) не является универсальным решениемДля маленьких списков параметров В (&Список) нормально. Для больших наборов, подзапросов и критичных участков проверь вариант через временную таблицу и явное соединение: так легче управлять планом, индексами и объемом данных.
Не переписывай В механически. Меняй форму запроса только когда есть ожидаемый выигрыш, читаемость или evidence от плана/замера.
ИТОГИ генерирует дополнительные строки-суммы. Они нужны только при иерархическом обходе Выбрать(ПоГруппировкам). Для плоской выборки — СГРУППИРОВАТЬ ПО, без ИТОГИ.
// Плоская выборка сумм по контрагентам — СГРУППИРОВАТЬ ПО, без ИТОГИ
"ВЫБРАТЬ Контрагент, СУММА(Сумма) КАК Итог
|ИЗ ...
|СГРУППИРОВАТЬ ПО Контрагент"
development
1C server maintenance webhooks: container restart and external component cache cleanup
development
Interactive DAP debugging of a single BSL procedure
tools
Rules for using RLM tools for project search and navigation in 1C/BSL
development
Creates web applications and routes on Winow (a web server on OneScript and Autumn). Use when working with a web server on OneScript, routing, or Winow controllers.