В практике разработки на платформе 1С:Предприятие 8 одной из самых частых задач является расчет себестоимости списываемого товара и контроль его наличия на складе. Часто начинающие разработчики пытаются решить эту задачу одним сложным запросом, что приводит к снижению производительности системы. В этой статье мы проанализируем типичные ошибки и разберем по шагам, как построить оптимальный алгоритм работы с данными в процедуре ОбработкаПроведения.
Рассмотрим ситуацию, когда в запросе происходит прямое соединение табличной части документа с виртуальной таблицей Остатки регистра накопления. Проанализируем, почему такой подход (представленный в начале обсуждения) является неоптимальным:
ЛЕВОЕ СОЕДИНЕНИЕ с виртуальной таблицей остатков заставляет СУБД выполнять вложенный запрос для каждой строки, что крайне неэффективно.Сумма / Количество) создает лишнюю нагрузку и может привести к ошибкам округления или дублированию строк, если данные в ТЧ документа не свернуты.Первым делом нам необходимо подготовить данные из табличной части документа. Важно свернуть их по номенклатуре, чтобы избежать дублей и лишних расчетов. Мы будем использовать менеджер временных таблиц (МенеджерВременныхТаблиц) для передачи данных между этапами запроса.
Рассмотрим пример кода для первой временной таблицы ВТТабличнаяЧасть:
ВЫБРАТЬ
ПереченьНоменклатуры.Номенклатура КАК Номенклатура,
СУММА(ПереченьНоменклатуры.Количество) КАК Количество
ПОМЕСТИТЬ ВТТабличнаяЧасть
ИЗ
Документ.ПеремещениеТоваров.ПереченьНоменклатуры КАК ПереченьНоменклатуры
ГДЕ
ПереченьНоменклатуры.Ссылка = &Ссылка
СГРУППИРОВАТЬ ПО
ПереченьНоменклатуры.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
Важный момент: Мы добавили инструкцию ИНДЕКСИРОВАТЬ ПО. Индексирование временной таблицы по полям связи (в данном случае по Номенклатура) значительно ускоряет последующие соединения, так как оптимизатор SQL сможет использовать более эффективные алгоритмы поиска.
Теперь проанализируем, как правильно получить данные из регистра. Вместо соединения в параметрах виртуальной таблицы мы используем условие В (ВЫБРАТЬ ...) из нашей первой временной таблицы. Это позволяет системе получить остатки только по тем позициям, которые реально присутствуют в документе.
Также добавим проверку на деление на ноль с помощью конструкции ВЫБОР:
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
ВЫБОР
КОГДА Остатки.КоличествоОстаток = 0
ТОГДА 0
ИНАЧЕ Остатки.СуммаОстаток / Остатки.КоличествоОстаток
КОНЕЦ КАК СебестоимостьЕдиницы
ПОМЕСТИТЬ ВТОстатки
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&МоментВремени,
Склад = &СкладОтправитель
И Номенклатура В
(ВЫБРАТЬ
ВТТабличнаяЧасть.Номенклатура
ИЗ
ВТТабличнаяЧасть)) КАК Остатки
;
На последнем этапе мы соединяем наши временные таблицы. Обратите внимание на использование функции ЕСТЬNULL. Если товара нет на остатках, соединение вернет NULL, и расчет суммы превратится в пустое значение. Мы должны явно заменить его на 0.
ВЫБРАТЬ
ВТТабличнаяЧасть.Номенклатура КАК Номенклатура,
ВТТабличнаяЧасть.Количество КАК Количество,
ЕСТЬNULL(ВТОстатки.СебестоимостьЕдиницы, 0) * ВТТабличнаяЧасть.Количество КАК Сумма
ИЗ
ВТТабличнаяЧасть КАК ВТТабличнаяЧасть
ЛЕВОЕ СОЕДИНЕНИЕ ВТОстатки КАК ВТОстатки
ПО ВТТабличнаяЧасть.Номенклатура = ВТОстатки.Номенклатура
Выясним, как правильно интегрировать этот запрос в модуль документа. Мы будем использовать классическую (старую) методику проведения: расчет -> запись движений -> контроль отрицательных остатков.
Рассмотрим структуру процедуры ОбработкаПроведения:
Процедура ОбработкаПроведения(Отказ, Режим)
// Подготовка запроса и установка параметров
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = "... (текст запросов из Шагов 1-3) ...";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("СкладОтправитель", Отправитель);
Запрос.УстановитьПараметр("МоментВремени", МоментВремени());
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// 1. Формирование движений
Движения.ТоварыНаСкладах.Очистить();
Пока Выборка.Следующий() Цикл
// Расход со склада-отправителя
Движение = Движения.ТоварыНаСкладах.ДобавитьРасход();
ЗаполнитьЗначенияСвойств(Движение, Выборка);
Движение.Период = Дата;
Движение.Склад = Отправитель;
// Приход на склад-получатель
Движение = Движения.ТоварыНаСкладах.ДобавитьПриход();
ЗаполнитьЗначенияСвойств(Движение, Выборка);
Движение.Период = Дата;
Движение.Склад = Получатель;
КонецЦикла;
// 2. Запись движений в БД
Движения.ТоварыНаСкладах.Записывать = Истина;
Движения.Записать();
// 3. Контроль отрицательных остатков
// Используем Границу, чтобы проверить итог ПОСЛЕ наших движений
ГраницаКонтроля = Новый Граница(МоментВремени(), ВидГраницы.Включая);
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура.Наименование КАК НоменклатураПредставление,
| -Остатки.КоличествоОстаток КАК Дефицит
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&МоментВремени, Склад = &СкладОтправитель) КАК Остатки
|ГДЕ
| Остатки.КоличествоОстаток < 0";
Запрос.УстановитьПараметр("МоментВремени", ГраницаКонтроля);
ОшибкиОстатков = Запрос.Выполнить().Выбрать();
Пока ОшибкиОстатков.Следующий() Цикл
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара: " + ОшибкиОстатков.НоменклатураПредставление;
Сообщение.Сообщить();
КонецЦикла;
КонецПроцедуры
Для создания действительно надежной системы стоит учитывать еще несколько важных факторов, которые часто остаются за рамками простых примеров:
БлокировкаДанных). Это предотвратит ситуацию, когда другой пользователь спишет тот же товар в параллельной транзакции между вашим чтением и записью.Таким образом, мы проанализировали процесс перехода от «грязного» запроса к чистому и производительному коду. Использование временных таблиц, правильная работа с индексами и контроль исключительных ситуаций (таких как деление на ноль) — залог стабильной работы конфигурации 1С.