При разработке в системе «1С:Предприятие» начинающие специалисты часто сталкиваются с ситуацией, когда код работает некорректно или сильно замедляет систему при увеличении объемов данных. Типичный пример — попытка получить остатки товаров на складе по определенным параметрам. На первый взгляд задача кажется простой, но в процессе ее реализации можно совершить целый комплекс грубых ошибок: от неоптимальных запросов до грубых нарушений в проектировании базы данных.
В этой статье мы подробно разберем практический кейс, проанализируем ошибки в коде и архитектуре, а также рассмотрим правильные шаблоны проектирования и написания запросов для получения остатков в соответствии со стандартами разработки фирмы «1С».
Рассмотрим фрагмент кода, с которым возникли сложности у автора темы:
Процедура КоманднаяПанель2Действие9(Кнопка)
Для каждого стр Из Материалы Цикл
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Склад41000Центральный.ДоговорСбыта КАК Договор,
| Склад41000Центральный.Количество
|ИЗ
| РегистрНакопления.Склад41000Центральный КАК Склад41000Центральный
|ГДЕ
| Склад41000Центральный.Артикул.Артикул = &Артик
| И Склад41000Центральный.ДоговорСбыта = &Договор";
Запрос.УстановитьПараметр("Артик", СокрЛП(стр.АртикулМатериала));
Запрос.УстановитьПараметр("Договор",СокрЛП(ДоговорСбытаСКоторогоСписываем));
Результат = Запрос.Выполнить.Выбрать;
Пока Результат.Следующий Цикл
стр.КоличествоНаСкладеПКИ = Результат.Количество ;
КонецЦикла;
Сообщить(Результат.договор);
НеХватаетНаСкладе;
КонецЦикла;
Записать;
КонецПроцедуры
Проанализируем этот код. В нем содержится сразу несколько критических уязвимостей и ошибок, которые приводят к неработоспособности алгоритма и потенциальному зависанию всей информационной базы. Выясним причины этих проблем пошагово.
---
Конструкция Для каждого стр Из Материалы Цикл, внутри которой инициализируется и выполняется Новый Запрос, является злейшим врагом производительности 1С. Это классический антипаттерн.
Когда мы выполняем запрос в цикле, происходит следующее:
Материалы будет хотя бы 100 строк, система совершит 100 отдельных обращений к базе данных. При работе нескольких пользователей это мгновенно приведет к критическому снижению производительности и блокировкам таблиц.Правильное решение: Собрать все необходимые параметры из табличной части в массив или таблицу значений, передать их в один общий запрос, выполнить его один раз, а затем обработать полученный массив данных.
---
В тексте запроса автор обращается к таблице РегистрНакопления.Склад41000Центральный напрямую. Это физическая таблица, в которой хранятся все движения (приходы и расходы) за все время существования базы данных.
Пытаясь получить остаток таким способом, мы заставляем СУБД сканировать всю историю движений по указанным артикулам и договорам. На реальной базе с тысячами документов этот запрос будет выполняться чрезвычайно долго и вернет не актуальный текущий остаток, а детальные записи обо всех операциях.
Правильное решение: Для получения остатков необходимо всегда использовать виртуальную таблицу Остатки (например, РегистрНакопления.Склад41000Центральный.Остатки). Платформа 1С оптимизирует обращение к этой таблице на уровне СУБД, используя предварительно рассчитанные итоги на конец каждого месяца, что позволяет получить результат мгновенно.
---
В параметрах запроса автор использует функцию СокрЛП():
Запрос.УстановитьПараметр("Договор", СокрЛП(ДоговорСбытаСКоторогоСписываем));
Функция СокрЛП преобразует переданный в нее объект в тип Строка. При этом измерение ДоговорСбыта в регистре накопления имеет ссылочный тип (например, СправочникСсылка.ДоговорыКонтрагентов).
В СУБД ссылки хранятся в виде уникальных 16-байтных двоичных идентификаторов (UUID). Когда СУБД пытается сравнить уникальный идентификатор ссылки с текстовой строкой (например, с наименованием договора "Договор №12"), сравнение всегда возвращает ложь (Ложь). В результате запрос всегда будет возвращать пустой результат, и остатки никогда не будут найдены.
Правильное решение: Передавать в параметры запроса чистые ссылки без какого-либо преобразования в текст. Очистите код от лишних функций форматирования строк при работе с объектными типами данных.
---
В секции условий мы видим следующую конструкцию:
Склад41000Центральный.Артикул.Артикул = &Артик
Здесь происходит обращение к реквизиту через два уровня вложенности. При трансляции запроса в SQL каждое такое обращение «через точку» преобразуется в операцию левого соединения (LEFT OUTER JOIN) с таблицей соответствующего справочника.
Если поле имеет составной тип данных, СУБД выполнит соединения со всеми таблицами, входящими в этот тип. Это колоссально перегружает оптимизатор запросов СУБД. Кроме того, дублирование имени (Артикул.Артикул) говорит о некорректной структуре данных — артикул должен храниться непосредственно в измерении регистра или в реквизитах номенклатуры без лишней вложенности.
---
Название регистра Склад41000Центральный указывает на фундаментальную ошибку в архитектуре конфигурации. Создание отдельных регистров под конкретные физические склады — это тупиковый путь развития системы.
Представьте, что завтра у компании появится "Склад42000Западный". Разработчику придется создавать новый регистр в конфигураторе, переписывать все документы, отчественность и логику проведения.
Правильное решение: Создать один универсальный регистр накопления (например, ТоварыНаСкладах) и добавить в него измерение Склад с типом ссылки на справочник Склады. Это позволит масштабировать систему без изменения метаданных и программного кода.
---
Разберем, как правильно переписать данный алгоритм, избавившись от всех вышеописанных проблем. Применим паттерн проектирования с использованием временных таблиц.
Вместо обхода табличной части в цикле, мы выгрузим ее данные в отдельную таблицу значений. Это позволит нам передать все строки одним пакетом в запрос.
// Выгружаем только необходимые колонки из табличной части для оптимизации памяти
ТаблицаИсходныхДанных = Материалы.Выгрузить( , "АртикулМатериала");
Мы напишем пакетный запрос. В первом пакете мы поместим наши исходные данные во временную таблицу. Во втором пакете мы соединим эту временную таблицу с виртуальной таблицей остатков нашего регистра.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ИсходныеДанные.АртикулМатериала КАК Артикул
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТаблицаИсходныхДанных КАК ИсходныеДанные
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Артикул КАК Артикул,
| ЕСТЬNULL(ТоварыОстатки.КоличествоОстаток, 0) КАК КоличествоНаСкладе
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(
| &Период,
| Номенклатура.Артикул В (ВЫБРАТЬ ВТ_ИсходныеДанные.Артикул ИЗ ВТ_ИсходныеДанные)
| И ДоговорСбыта = &ДоговорСбыта
| ) КАК ТоварыОстатки
| ПО ВТ.Артикул = ТоварыОстатки.Номенклатура.Артикул";
Проанализируем важные детали этого запроса:
ПОМЕСТИТЬ ВТ_ИсходныеДанные создает временную таблицу в оперативной памяти СУБД, что позволяет быстро связывать данные.Остатки.ГДЕ. Это заставляет СУБД рассчитывать остатки только по нужным нам позициям, существенно экономя ресурсы.ЕСТЬNULL, чтобы в случае отсутствия остатка на складе система вернула нам числовой ноль (0), а не значение Null, с которым невозможно выполнять математические операции.При получении остатков в процессе проведения документа критически важно правильно указать момент времени, на который эти остатки запрашиваются. Если передать просто дату документа, остатки будут получены без учета движений самого этого документа. Для решения этой проблемы мы используем объект Граница.
// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("ТаблицаИсходныхДанных", ТаблицаИсходныхДанных);
// Передаем чистую ссылку на договор без использования строковых функций
Запрос.УстановитьПараметр("ДоговорСбыта", ДоговорСбытаСКоторогоСписываем);
// Формируем границу для получения остатков на момент проведения документа
ГраницаОстатков = Новый Граница(МоментВремени(), ВидГраницы.Исключая);
Запрос.УстановитьПараметр("Период", ГраницаОстатков);
После выполнения запроса мы получаем готовую выборку, которую можем быстро обойти и сопоставить с исходной табличной частью документа.
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Для быстрого поиска по табличной части создадим индекс
Материалы.Индексировать("АртикулМатериала");
Пока Выборка.Следующий() Цикл
// Ищем строки в табличной части по артикулу
ПараметрыПоиска = Новый Структура("АртикулМатериала", Выборка.Артикул);
НайденныеСтроки = Материалы.НайтиСтроки(ПараметрыПоиска);
Для каждого СтрокаТЧ Из НайденныеСтроки Цикл
СтрокаТЧ.КоличествоНаСкладеПКИ = Выборка.КоличествоНаСкладе;
КонецЦикла;
КонецЦикла;
---
Для того чтобы ваши решения всегда работали быстро и без сбоев, придерживайтесь следующих золотых правил разработки:
.Остатки) и оборотов (.Обороты). Забудьте про обращение к физическим таблицам регистров для этих задач.ГДЕ после обращения к виртуальной таблице сводит на нет все оптимизационные механизмы платформы.СокрЛП. Передавайте в параметры запроса только объекты соответствующих ссылочных типов.Соблюдение этих несложных правил позволит вам писать качественный, легко масштабируемый и быстродействующий код, который без проблем выдержит работу сотен пользователей в высоконагруженных информационных базах.