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