В предыдущей статье "Проблемы производительности: Поля через несколько точек в условиях соединений" мы обсуждали, как соблюдение простых правил разработки ведёт к созданию быстрого и качественного кода. Сегодня мы разберём более злободневный реальный кейс, который наглядно показывает: писать «правильно» — не сложно, а результат оказывает колоссальное влияние на продуктивность и пользовательский опыт.
Структура технического разбора: от диагностики до решения
- Проблема:
Расчёт себестоимости «висит» сутками. В логах — запросы на 11 000 секунд и дольше. - Анализ:
Неявное разименование в условиях соединения таблиц. - Решение:
Меняем источник данных: вместо справочника используем специальный регистр сведений.
Результат: 11 000 с → 2,5 с (ускорение в 4500 раз). - Масштаб проблемы:
В коде — десятки похожих мест с тем же полем. Риск повторения проблемы высок. - Подарочный фикс от меня (5 мин, без правки кода):
Используем новые возможности платформы
Поиск проблемы
Наш заказчик столкнулся с критической проблемой: процесс расчёта себестоимости в системе работал непредсказуемо долго. Пользователи запускали расчёт и не могли понять — он всё ещё выполняется или система «зависла». Ожидание занимало более суток, что делало функционал практически бесполезным.

Первым шагом мы обратились к мониторингу длительных операций в базе данных. Анализ выявил целый набор «узких мест» (см рис. ниже), но для наглядности мы сфокусируемся на показательном случае — достаточно длительном запросе, время выполнения которого превышало 11 000 секунд (более 3 часов!).
Что же могло заставить один запрос работать три часа? Давайте разберём его «по косточкам», увидим типичные ошибки и последовательно применим правила эффективной разработки, чтобы исправить ситуацию.

Рис. 1 Список длительных замеров
Чтобы идентифицировать проблемный запрос, мы обратились к данным мониторинга. Контекст выполнения запроса приведен ниже:
ОбщийМодуль.ЗакрытиеМесяцаСервер.Модуль : 4128 : Обработки.ОперацииЗакрытияМесяца.ВыполнитьРасчетЭтапов(ПараметрыЗапуска);
Обработка.ОперацииЗакрытияМесяца.МодульМенеджера : 2285 : ОбщегоНазначения.ВыполнитьМетодКонфигурации(
ОбщийМодуль.ОбщегоНазначения.Модуль : 6119 : Выполнить ИмяМетода + "(" + ПараметрыСтрока + ")";
: 1 : РасчетСебестоимостиКорректировкаСтоимости.Выполнить_РасчетПартийИСебестоимости(Параметры[0])
ОбщийМодуль.РасчетСебестоимостиКорректировкаСтоимости.Модуль : 516 : РасчетСебестоимости.РассчитатьВсеВПопыткеИсключении(ПараметрыЗапуска);
ОбщийМодуль.РасчетСебестоимости.Модуль : 600 : РассчитатьВсе(ПараметрыЗапуска, ПараметрыРасчета, ПараметрыОтладки);
ОбщийМодуль.РасчетСебестоимости.Модуль : 335 : РасчетСебестоимостиПостатейныеЗатраты.РаспределениеДопРасходовМеждуПартиямиИТоварами(ПараметрыРасчета);
ОбщийМодуль.РасчетСебестоимостиПостатейныеЗатраты.Модуль : 440 : ПолучитьДанныеДляДополнительныхРасходов(ПараметрыРасчета);
ОбщийМодуль.РасчетСебестоимостиПостатейныеЗатраты.Модуль : 4627 : ТекстЗапросаДляРаспределенияДополнительныхРасходов(ПараметрыРасчета),
ОбщийМодуль.РасчетСебестоимостиПрикладныеАлгоритмы.Модуль : 7224 : ВыполнитьЗапросСЗамеромДляПротокола(ПараметрыРасчета, Запрос,,,, Пояснение);
ОбщийМодуль.РасчетСебестоимостиПрикладныеАлгоритмы.Модуль : 20025 : РезультатПодзапроса = Запрос.Выполнить();
Исходя из контекста, мы сфокусировались на модуле «РасчетСебестоимостиПрикладныеАлгоритмы». Но, вопреки ожиданиям, нужный запрос отсутствовал в этом модуле.
Потребовался более глубокий поиск. Прорывом стало обнаружение в тексте SQL-запроса уникального ключа — строки "ШаблонТекстаПервичныеПриемникиПоЗаказам_1". Именно эта метка, встроенная разработчиками, позволила нам быстро идентифицировать проблемный запрос в конфигурации ЕРП.
INSERT INTO pg_temp.tt1070 (_Q_000_F_000, _Q_000_F_001RRef, _Q_000_F_002, _Q_000_F_003_TYPE, _Q_000_F_003_RRRef, _Q_000_F_004_TYPE, _Q_000_F_004_RRRef, _Q_000_F_005_TYPE, _Q_000_F_005_RRRef, _Q_000_F_006_TYPE, _Q_000_F_006_RRRef, _Q_000_F_007_TYPE, _Q_000_F_007_RTRef, _Q_000_F_007_RRRef, _Q_000_F_008_TYPE, _Q_000_F_008_RRRef, _Q_000_F_009_TYPE, _Q_000_F_009_RTRef, _Q_000_F_009_RRRef, _Q_000_F_010_TYPE, _Q_000_F_010_RRRef, _Q_000_F_011, _Q_000_F_012, _Q_000_F_013, _Q_000_F_014, _Q_000_F_015, _Q_000_F_016, _Q_000_F_017) SELECT
'ШаблонТекстаПервичныеПриемникиПоЗаказам_1'::mvarchar,
T1._Q_000_F_000RRef,
TRUE,
'\\010'::bytea,
T4._Fld40018RRef,
'\\010'::bytea,
T4._Fld40017RRef,
'\\010'::bytea,
T4._Fld40019RRef,
'\\010'::bytea,
T4._Fld40020RRef,
T4._Fld73358_TYPE,
T4._Fld73358_RTRef,
T4._Fld73358_RRRef,
'\\010'::bytea,
T4._Fld73359RRef,
T4._Fld73360_TYPE,
T4._Fld73360_RTRef,
T4._Fld73360_RRRef,
'\\010'::bytea,
T4._Fld73361RRef,
T4._Fld40021,
T4._Fld40022,
T4._Fld40023,
T4._Fld40026,
T4._Fld85790,
T4._Fld148272,
T4._Fld40026
FROM pg_temp.tt1056 T1
INNER JOIN pg_temp.tt1064 T2
ON ('\\010'::bytea = T2._Q_000_F_001_TYPE AND CASE WHEN T1._Q_000_F_000RRef IS NOT NULL THEN CAST(0 AS NUMERIC) END = T2._Q_000_F_001_N AND T1._Q_000_F_000RRef = T2._Q_000_F_001_RRRef) AND (T2._Q_000_F_000 = CAST(1 AS NUMERIC))
INNER JOIN pg_temp.tt1057 T3
ON (T1._Q_000_F_005_TYPE = T3._Q_000_F_001_TYPE AND T1._Q_000_F_005_RTRef = T3._Q_000_F_001_RTRef AND T1._Q_000_F_005_RRRef = T3._Q_000_F_001_RRRef)
INNER JOIN _AccumRg40016 T4
LEFT OUTER JOIN _Reference183 T5
ON (T4._Fld40017RRef = T5._IDRRef) AND (T5._Fld1585 = CAST(0 AS NUMERIC))
ON (T3._Q_000_F_000TRef = T4._RecorderTRef AND T3._Q_000_F_000RRef = T4._RecorderRRef) AND (T3._Q_000_F_003RRef = T5._Fld4910RRef) AND (T3._Q_000_F_004RRef = T5._Fld4911RRef) AND (T3._Q_000_F_005RRef = T5._Fld73709RRef) AND ('\\010'::bytea = T5._Fld4913_TYPE AND T3._Q_000_F_006TRef = T5._Fld4913_RTRef AND T3._Q_000_F_006RRef = T5._Fld4913_RRRef) AND (T4._RecordKind = CAST(0 AS NUMERIC)) AND (T4._Period < '2025-01-01 00:00:00'::timestamp) AND (T4._Fld40020RRef = T1._Q_000_F_001RRef)
INNER JOIN _InfoRg32601 T6
ON (T4._Fld40017RRef = T6._Fld32606RRef)
WHERE (((T4._Fld1585 = CAST(0 AS NUMERIC))) AND (T6._Fld1585 = CAST(0 AS NUMERIC))) AND (((T4._Fld40021 <> CAST(0 AS NUMERIC)) OR (T4._Fld73378RRef IN ('\\206\\337m\\254X\\304;bA\\210)\\226\\377YR,'::bytea, '\\271*\\304L\\007\\2627\\262E@`P(\\256l\\373'::bytea, '\\203hM<\\012]<\\302J\\201}{\\002W\\\\\\023'::bytea, '\\260\\352g\\005/\\262\\2270E7\\335\\311\\031-\\037\\252'::bytea, '\\267\\015mM4\\367\\302\\323M\\327K3Kng\\031'::bytea, '\\223\\303#V\\213\\322\\307\\025F\\317\\310@,Sim'::bytea))))
UNION ALL SELECT
'ШаблонТекстаПервичныеПриемникиПоЗаказам_2'::mvarchar,
T7._Q_000_F_000RRef,
FALSE,
T10._Q_001_F_004_TYPE,
T10._Q_001_F_004_RRRef,
T10._Q_001_F_003_TYPE,
T10._Q_001_F_003_RRRef,
T10._Q_001_F_005_TYPE,
T10._Q_001_F_005_RRRef,
T10._Q_001_F_006_TYPE,
T10._Q_001_F_006_RRRef,
T10._Q_001_F_007_TYPE,
T10._Q_001_F_007_RTRef,
T10._Q_001_F_007_RRRef,
T10._Q_001_F_008_TYPE,
T10._Q_001_F_008_RRRef,
T10._Q_001_F_009_TYPE,
T10._Q_001_F_009_RTRef,
T10._Q_001_F_009_RRRef,
T10._Q_001_F_010_TYPE,
T10._Q_001_F_010_RRRef,
T10._Q_001_F_011,
T10._Q_001_F_012,
T10._Q_001_F_013,
T10._Q_001_F_022,
T10._Q_001_F_030,
T10._Q_001_F_037,
T10._Q_001_F_022
FROM pg_temp.tt1056 T7
INNER JOIN pg_temp.tt1064 T8
ON ('\\010'::bytea = T8._Q_000_F_001_TYPE AND CASE WHEN T7._Q_000_F_000RRef IS NOT NULL THEN CAST(0 AS NUMERIC) END = T8._Q_000_F_001_N AND T7._Q_000_F_000RRef = T8._Q_000_F_001_RRRef) AND (T8._Q_000_F_000 = CAST(1 AS NUMERIC))
INNER JOIN pg_temp.tt1057 T9
ON (T7._Q_000_F_005_TYPE = T9._Q_000_F_001_TYPE AND T7._Q_000_F_005_RTRef = T9._Q_000_F_001_RTRef AND T7._Q_000_F_005_RRRef = T9._Q_000_F_001_RRRef)
INNER JOIN pg_temp.tt993 T10
LEFT OUTER JOIN _Reference183 T11
ON (T10._Q_001_F_003_TYPE = '\\010'::bytea AND '\\000\\000\\000\\267'::bytea = '\\000\\000\\000\\267'::bytea AND T10._Q_001_F_003_RRRef = T11._IDRRef) AND (T11._Fld1585 = CAST(0 AS NUMERIC))
ON ('\\010'::bytea = T10._Q_001_F_001_TYPE AND T9._Q_000_F_000TRef = T10._Q_001_F_001_RTRef AND T9._Q_000_F_000RRef = T10._Q_001_F_001_RRRef) AND (T9._Q_000_F_003RRef = (T11._Fld4910RRef)) AND (T9._Q_000_F_004RRef = (T11._Fld4911RRef)) AND (T9._Q_000_F_005RRef = (T11._Fld73709RRef)) AND ('\\010'::bytea = (T11._Fld4913_TYPE) AND T9._Q_000_F_006TRef = (T11._Fld4913_RTRef) AND T9._Q_000_F_006RRef = (T11._Fld4913_RRRef)) AND T10._Q_001_F_000 = TRUE AND (T10._Q_001_F_006_TYPE = '\\010'::bytea AND T10._Q_001_F_006_RRRef = T7._Q_000_F_001RRef)
INNER JOIN _InfoRg32601 T12
ON (T10._Q_001_F_003_TYPE = '\\010'::bytea AND T10._Q_001_F_003_RRRef = T12._Fld32606RRef)
WHERE ((T12._Fld1585 = CAST(0 AS NUMERIC))) AND ((NOT (((T10._Q_001_F_011 = CAST(0 AS NUMERIC))))))
Отдельно хочется поблагодарить разработчиков за внедрение практики использования уникальных ключей в запросах. Это простое изменение существенно упрощает анализ производительности. Надеемся на продолжение улучшений в инструментах отладки.
В результате поиска мы локализовали проблемный запрос. Он находится в модуле "РасчетСебестоимостиПостатейныеЗатраты", в функции "ШаблонТекстаПервичныеПриемникиПоЗаказам". Проблема также воспроизводится в текущей стабильной версии платформы (ERP 2.5.22 LTS).
ВЫБРАТЬ
"ШаблонТекстаПервичныеПриемникиПоЗаказам_1" КАК ЗапросИсточник,
ДД.Ссылка КАК Регистратор,
ИСТИНА КАК ЭтоПартияПрошлогоПериода,
ИсточникБазы.РазделУчета КАК РазделУчета,
ИсточникБазы.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
ИсточникБазы.ВидЗапасов КАК ВидЗапасов,
ИсточникБазы.Организация КАК Организация,
ИсточникБазы.Партия КАК Партия,
ИсточникБазы.АналитикаУчетаПартий КАК АналитикаУчетаПартий,
ИсточникБазы.АналитикаФинансовогоУчета КАК АналитикаФинансовогоУчета,
ИсточникБазы.ВидДеятельностиНДС КАК ВидДеятельностиНДС,
ИсточникБазы.Количество КАК Количество,
ИсточникБазы.Стоимость КАК Стоимость,
ИсточникБазы.СтоимостьБезНДС КАК СтоимостьБезНДС,
ИсточникБазы.СтоимостьРегл КАК СтоимостьРегл,
ИсточникБазы.СтоимостьУпр КАК СтоимостьУпр,
ИсточникБазы.СтоимостьНДД КАК СтоимостьНДД,
ИсточникБазы.СтоимостьРегл КАК СтоимостьНУ
ПОМЕСТИТЬ ТаблицаДанных
ИЗ
Регистраторы КАК ДД
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ГруппыОбъектовДополнительныхРасходов КАК Группа
ПО ДД.Ссылка = Группа.Объект
И (Группа.НомерГруппы = &НомерГруппы)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ АналитикиЗаказов КАК АналитикиЗаказов
ПО ДД.АналитикаРасходов = АналитикиЗаказов.АналитикаРасходов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров КАК ИсточникБазы
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = ИсточникБазы.АналитикаУчетаНоменклатуры.Номенклатура)
И (АналитикиЗаказов.Характеристика = ИсточникБазы.АналитикаУчетаНоменклатуры.Характеристика)
И (АналитикиЗаказов.Назначение = ИсточникБазы.АналитикаУчетаНоменклатуры.Назначение)
И (АналитикиЗаказов.Склад = ИсточникБазы.АналитикаУчетаНоменклатуры.МестоХранения)
И (ИсточникБазы.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход))
И (ИсточникБазы.Период < &НачалоПериода)
И (ИсточникБазы.Организация = ДД.Организация)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаОтбора
ПО (ИсточникБазы.АналитикаУчетаНоменклатуры = АналитикаОтбора.КлючАналитики)
ГДЕ
(ИсточникБазы.Количество <> 0
ИЛИ ИсточникБазы.ТипЗаписи В (ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Партия), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Потребление), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.КорректировкаСтоимости), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.КорректировкаПриобретенияПрошлогоПериода), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Перемещение), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.ПеремещениеОбособленно)))
И &УсловиеСоединения
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"ШаблонТекстаПервичныеПриемникиПоЗаказам_2",
ДД.Ссылка,
ЛОЖЬ,
ИсточникБазы.РазделУчета,
ИсточникБазы.АналитикаУчетаНоменклатуры,
ИсточникБазы.ВидЗапасов,
ИсточникБазы.Организация,
ИсточникБазы.Партия,
ИсточникБазы.АналитикаУчетаПартий,
ИсточникБазы.АналитикаФинансовогоУчета,
ИсточникБазы.ВидДеятельностиНДС,
ИсточникБазы.Количество,
ИсточникБазы.Стоимость,
ИсточникБазы.СтоимостьБезНДС,
ИсточникБазы.СтоимостьРегл,
ИсточникБазы.СтоимостьУпр,
ИсточникБазы.СтоимостьНДД,
ИсточникБазы.СтоимостьРегл
ИЗ
Регистраторы КАК ДД
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ГруппыОбъектовДополнительныхРасходов КАК Группа
ПО ДД.Ссылка = Группа.Объект
И (Группа.НомерГруппы = &НомерГруппы)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ АналитикиЗаказов КАК АналитикиЗаказов
ПО ДД.АналитикаРасходов = АналитикиЗаказов.АналитикаРасходов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТКэшРасчетныеОборотыСебестоимостьТоваров КАК ИсточникБазы
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = ИсточникБазы.АналитикаУчетаНоменклатуры.Номенклатура)
И (АналитикиЗаказов.Характеристика = ИсточникБазы.АналитикаУчетаНоменклатуры.Характеристика)
И (АналитикиЗаказов.Назначение = ИсточникБазы.АналитикаУчетаНоменклатуры.Назначение)
И (АналитикиЗаказов.Склад = ИсточникБазы.АналитикаУчетаНоменклатуры.МестоХранения)
И (ИсточникБазы.СлужебноеВидДвиженияПриход)
И (ИсточникБазы.Организация = ДД.Организация)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаОтбора
ПО (ИсточникБазы.АналитикаУчетаНоменклатуры = АналитикаОтбора.КлючАналитики)
ГДЕ
НЕ ИсточникБазы.Количество = 0
И &УсловиеСоединения
Анализ
План запроса представлен на рисунке ниже:
https://explain.tensor.ru/archive/explain/67a42a80c5a26abc3b57e549ce181a55:0:2025-12-09#visio

Рис. 2 Графическое представление проблемного запроса
Как видно на рисунке с планом выполнения, ключевой проблемой стал оператор доступа к таблице справочника «Ключи аналитики номенклатуры». Несмотря на использование индекса, доступ организован крайне неэффективно. Это наглядно демонстрирует статистика: серверу пришлось просмотреть и отбросить почти 12 миллиардов записей (логических чтений), чтобы в итоге вернуть лишь 24 тысячи строк. Такое чудовищное соотношение — явный признак того, что выбранный для поиска индекс плохо соответствует условиям отбора, то есть является слишком «слабым» (low selectivity).
Однако возникает закономерный вопрос: в исходном тексте запроса явного соединения с этой таблицей нет. Откуда она взялась в плане?
Ответ становится очевидным при детальном анализе условий запроса. В одном из соединений используется конструкция, критически влияющая на производительность — обращение к полю через несколько точек:
...
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров КАК ИсточникБазы
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = ИсточникБазы.АналитикаУчетаНоменклатуры.Номенклатура)
И (АналитикиЗаказов.Характеристика = ИсточникБазы.АналитикаУчетаНоменклатуры.Характеристика)
И (АналитикиЗаказов.Назначение = ИсточникБазы.АналитикаУчетаНоменклатуры.Назначение)
И (АналитикиЗаказов.Склад = ИсточникБазы.АналитикаУчетаНоменклатуры.МестоХранения)
И (ИсточникБазы.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход))
...
Когда в условии используется поле через точку, платформа 1С выполняет неявное разименование. Для реквизита «АналитикаУчетаНоменклатуры» это означает: чтобы получить значение для сравнения, система в фоне подключает таблицу справочника «КлючиАналитикиНоменклатуры» (тип поля прямо указывает на это).
Чтобы увидеть реальную структуру запроса, которую обрабатывает СУБД, необходимо выполнить разименование составного поля явно. Результат этого преобразования представлен ниже. Для наглядности мы присвоили добавленной таблице псевдоним «АналитикаЧерезТочку».
ВЫБРАТЬ
"ШаблонТекстаПервичныеПриемникиПоЗаказам_1" КАК ЗапросИсточник,
ДД.Ссылка КАК Регистратор,
ИСТИНА КАК ЭтоПартияПрошлогоПериода,
ИсточникБазы.РазделУчета КАК РазделУчета,
ИсточникБазы.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
ИсточникБазы.ВидЗапасов КАК ВидЗапасов,
ИсточникБазы.Организация КАК Организация,
ИсточникБазы.Партия КАК Партия,
ИсточникБазы.АналитикаУчетаПартий КАК АналитикаУчетаПартий,
ИсточникБазы.АналитикаФинансовогоУчета КАК АналитикаФинансовогоУчета,
ИсточникБазы.ВидДеятельностиНДС КАК ВидДеятельностиНДС,
ИсточникБазы.Количество КАК Количество,
ИсточникБазы.Стоимость КАК Стоимость,
ИсточникБазы.СтоимостьБезНДС КАК СтоимостьБезНДС,
ИсточникБазы.СтоимостьРегл КАК СтоимостьРегл,
ИсточникБазы.СтоимостьУпр КАК СтоимостьУпр,
ИсточникБазы.СтоимостьНДД КАК СтоимостьНДД,
ИсточникБазы.СтоимостьРегл КАК СтоимостьНУ
ПОМЕСТИТЬ ТаблицаДанных
ИЗ
Регистраторы КАК ДД
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ГруппыОбъектовДополнительныхРасходов КАК Группа
ПО ДД.Ссылка = Группа.Объект
И (Группа.НомерГруппы = &НомерГруппы)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ АналитикиЗаказов КАК АналитикиЗаказов
ПО ДД.АналитикаРасходов = АналитикиЗаказов.АналитикаРасходов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров КАК ИсточникБазы
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.КлючиАналитикиУчетаНоменклатуры КАК АналитикаЧерезТочку
ПО (АналитикаЧерезТочку.Ссылка = ИсточникБазы.АналитикаУчетаНоменклатуры)
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = АналитикаЧерезТочку.Номенклатура)
И (АналитикиЗаказов.Характеристика = АналитикаЧерезТочку.Характеристика)
И (АналитикиЗаказов.Назначение = АналитикаЧерезТочку.Назначение)
И (АналитикиЗаказов.Склад = АналитикаЧерезТочку.МестоХранения)
И (ИсточникБазы.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход))
И (ИсточникБазы.Период < &НачалоПериода)
И (ИсточникБазы.Организация = ДД.Организация)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаОтбора
ПО (ИсточникБазы.АналитикаУчетаНоменклатуры = АналитикаОтбора.КлючАналитики)
ГДЕ
(ИсточникБазы.Количество <> 0
ИЛИ ИсточникБазы.ТипЗаписи В (ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Партия), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Потребление), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.КорректировкаСтоимости), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.КорректировкаПриобретенияПрошлогоПериода), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Перемещение), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.ПеремещениеОбособленно)))
И &УсловиеСоединения
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"ШаблонТекстаПервичныеПриемникиПоЗаказам_2",
ДД.Ссылка,
ЛОЖЬ,
ИсточникБазы.РазделУчета,
ИсточникБазы.АналитикаУчетаНоменклатуры,
ИсточникБазы.ВидЗапасов,
ИсточникБазы.Организация,
ИсточникБазы.Партия,
ИсточникБазы.АналитикаУчетаПартий,
ИсточникБазы.АналитикаФинансовогоУчета,
ИсточникБазы.ВидДеятельностиНДС,
ИсточникБазы.Количество,
ИсточникБазы.Стоимость,
ИсточникБазы.СтоимостьБезНДС,
ИсточникБазы.СтоимостьРегл,
ИсточникБазы.СтоимостьУпр,
ИсточникБазы.СтоимостьНДД,
ИсточникБазы.СтоимостьРегл
ИЗ
Регистраторы КАК ДД
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ГруппыОбъектовДополнительныхРасходов КАК Группа
ПО ДД.Ссылка = Группа.Объект
И (Группа.НомерГруппы = &НомерГруппы)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ АналитикиЗаказов КАК АналитикиЗаказов
ПО ДД.АналитикаРасходов = АналитикиЗаказов.АналитикаРасходов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТКэшРасчетныеОборотыСебестоимостьТоваров КАК ИсточникБазы
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.КлючиАналитикиУчетаНоменклатуры КАК АналитикаЧерезТочку
ПО (АналитикаЧерезТочку.Ссылка = ИсточникБазы.АналитикаУчетаНоменклатуры)
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = АналитикаЧерезТочку.Номенклатура)
И (АналитикиЗаказов.Характеристика = АналитикаЧерезТочку.Характеристика)
И (АналитикиЗаказов.Назначение = АналитикаЧерезТочку.Назначение)
И (АналитикиЗаказов.Склад = АналитикаЧерезТочку.МестоХранения)
И (ИсточникБазы.СлужебноеВидДвиженияПриход)
И (ИсточникБазы.Организация = ДД.Организация)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаОтбора
ПО (ИсточникБазы.АналитикаУчетаНоменклатуры = АналитикаОтбора.КлючАналитики)
ГДЕ
НЕ ИсточникБазы.Количество = 0
И &УсловиеСоединения
Замечание! Углубляясь в анализ, мы обнаружили ещё одну проблему. В ванильной конфигурации справочник «КлючиАналитикиНоменклатуры» не имеет индексов по своим реквизитам. Существующий в базе заказчика индекс — следствие локальной доработки, а не штатная возможность.
Таким образом, разработчики типового решения, по сути, закладывают предположение, что этот справочник будет использоваться исключительно для соединения по прямой ссылке. Его массовое использование в условиях отбора через составные поля, как в нашем случае, является архитектурной ошибкой, которая противоречит заложенной модели и гарантирует проблемы.

Решение
Итак, мы выяснили причину неэффективности. План выполнения показывает, что для доступа к справочнику «Ключи аналитики номенклатуры» используется индекс по полю «Назначение», который в данном контексте обладает низкой селективностью. Идеальным решением было бы создать более селективный составной индекс.
Однако здесь мы сталкиваемся с ограничением платформы 1С: для объектов типа «Справочник» можно создавать индексы, включающие только одно основное поле плюс системное поле «Ссылка» (см. документацию ИТС). Это архитектурное ограничение не позволяет нам просто добавить нужный индекс.
Что же делать? Вспоминаем о доступных инструментах.
В конфигурации существует объект, специально предназначенный для решения подобных задач — Регистр сведений «Аналитика учета номенклатуры». В отличие от справочников, для регистров сведений платформа позволяет создавать составные индексы, включающие множество полей. Более того, одна из ключевых задач этого регистра (как мы можем предположить) — как раз повышение эффективности запросов, связанных с аналитикой.
Таким образом, сама архитектура системы подсказывает нам решение: необходимо заменить обращение к справочнику на обращение к регистру сведений, сохранив при этом исходную логику запроса.
Эта замена выполняется буквально за несколько минут и не влечет потери функциональности.
ВЫБРАТЬ
"ШаблонТекстаПервичныеПриемникиПоЗаказам_1" КАК ЗапросИсточник,
ДД.Ссылка КАК Регистратор,
ИСТИНА КАК ЭтоПартияПрошлогоПериода,
ИсточникБазы.РазделУчета КАК РазделУчета,
ИсточникБазы.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
ИсточникБазы.ВидЗапасов КАК ВидЗапасов,
ИсточникБазы.Организация КАК Организация,
ИсточникБазы.Партия КАК Партия,
ИсточникБазы.АналитикаУчетаПартий КАК АналитикаУчетаПартий,
ИсточникБазы.АналитикаФинансовогоУчета КАК АналитикаФинансовогоУчета,
ИсточникБазы.ВидДеятельностиНДС КАК ВидДеятельностиНДС,
ИсточникБазы.Количество КАК Количество,
ИсточникБазы.Стоимость КАК Стоимость,
ИсточникБазы.СтоимостьБезНДС КАК СтоимостьБезНДС,
ИсточникБазы.СтоимостьРегл КАК СтоимостьРегл,
ИсточникБазы.СтоимостьУпр КАК СтоимостьУпр,
ИсточникБазы.СтоимостьНДД КАК СтоимостьНДД,
ИсточникБазы.СтоимостьРегл КАК СтоимостьНУ
ПОМЕСТИТЬ ТаблицаДанных
ИЗ
Регистраторы КАК ДД
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ГруппыОбъектовДополнительныхРасходов КАК Группа
ПО ДД.Ссылка = Группа.Объект
И (Группа.НомерГруппы = &НомерГруппы)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ АналитикиЗаказов КАК АналитикиЗаказов
ПО ДД.АналитикаРасходов = АналитикиЗаказов.АналитикаРасходов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров КАК ИсточникБазы
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаЧерезТочку
ПО (АналитикаЧерезТочку.КлючАналитики = ИсточникБазы.АналитикаУчетаНоменклатуры)
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = АналитикаЧерезТочку.Номенклатура)
И (АналитикиЗаказов.Характеристика = АналитикаЧерезТочку.Характеристика)
И (АналитикиЗаказов.Назначение = АналитикаЧерезТочку.Назначение)
И (АналитикиЗаказов.Склад = АналитикаЧерезТочку.МестоХранения)
И (ИсточникБазы.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход))
И (ИсточникБазы.Период < &НачалоПериода)
И (ИсточникБазы.Организация = ДД.Организация)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаОтбора
ПО (ИсточникБазы.АналитикаУчетаНоменклатуры = АналитикаОтбора.КлючАналитики)
ГДЕ
(ИсточникБазы.Количество <> 0
ИЛИ ИсточникБазы.ТипЗаписи В (ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Партия), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Потребление), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.КорректировкаСтоимости), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.КорректировкаПриобретенияПрошлогоПериода), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.Перемещение), ЗНАЧЕНИЕ(Перечисление.ТипыЗаписейПартий.ПеремещениеОбособленно)))
И &УсловиеСоединения
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"ШаблонТекстаПервичныеПриемникиПоЗаказам_2",
ДД.Ссылка,
ЛОЖЬ,
ИсточникБазы.РазделУчета,
ИсточникБазы.АналитикаУчетаНоменклатуры,
ИсточникБазы.ВидЗапасов,
ИсточникБазы.Организация,
ИсточникБазы.Партия,
ИсточникБазы.АналитикаУчетаПартий,
ИсточникБазы.АналитикаФинансовогоУчета,
ИсточникБазы.ВидДеятельностиНДС,
ИсточникБазы.Количество,
ИсточникБазы.Стоимость,
ИсточникБазы.СтоимостьБезНДС,
ИсточникБазы.СтоимостьРегл,
ИсточникБазы.СтоимостьУпр,
ИсточникБазы.СтоимостьНДД,
ИсточникБазы.СтоимостьРегл
ИЗ
Регистраторы КАК ДД
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ГруппыОбъектовДополнительныхРасходов КАК Группа
ПО ДД.Ссылка = Группа.Объект
И (Группа.НомерГруппы = &НомерГруппы)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ АналитикиЗаказов КАК АналитикиЗаказов
ПО ДД.АналитикаРасходов = АналитикиЗаказов.АналитикаРасходов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТКэшРасчетныеОборотыСебестоимостьТоваров КАК ИсточникБазы
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаЧерезТочку
ПО (АналитикаЧерезТочку.КлючАналитики = ИсточникБазы.АналитикаУчетаНоменклатуры)
ПО (АналитикиЗаказов.РегистраторСебестоимости = ИсточникБазы.Регистратор)
И (АналитикиЗаказов.Номенклатура = АналитикаЧерезТочку.Номенклатура)
И (АналитикиЗаказов.Характеристика = АналитикаЧерезТочку.Характеристика)
И (АналитикиЗаказов.Назначение = АналитикаЧерезТочку.Назначение)
И (АналитикиЗаказов.Склад = АналитикаЧерезТочку.МестоХранения)
И (ИсточникБазы.СлужебноеВидДвиженияПриход)
И (ИсточникБазы.Организация = ДД.Организация)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.АналитикаУчетаНоменклатуры КАК АналитикаОтбора
ПО (ИсточникБазы.АналитикаУчетаНоменклатуры = АналитикаОтбора.КлючАналитики)
ГДЕ
НЕ ИсточникБазы.Количество = 0
И &УсловиеСоединения
После внесённых изменений мы перезапустили расчёт себестоимости и проанализировали новые планы выполнения запросов. Результат говорит сам за себя: время выполнения проблемного запроса сократилось с более 11 000 секунд до ~2,5 секунд.
https://explain.tensor.ru/archive/explain/517ee459870c4b9fc00a707ef31b5be0:0:2025-12-17#visio

Рис. 3 План запроса, который мы оптимизировали
Анализ нового плана выполнения показывает, что его структура осталась практически неизменной. Основное отличие — источник данных: вместо таблицы справочника «Ключи аналитики номенклатуры» теперь используется таблица регистра сведений «Аналитика учета номенклатуры». Все условия запроса теперь полностью покрываются составным индексом регистра, что исключает лишние операции фильтрации («отброшенные записи»). В результате этот оператор выполняется максимально эффективно.
Заключение
В результате проведённой оптимизации производительность запроса увеличилась примерно в 4500 раз (11 000 с / 2,5 с).
Этот случай наглядно показывает, что следование best practices — это не «лишняя сложность», а прямая экономия времени и ресурсов на этапе эксплуатации.
Для исправления не потребовалось сложных преобразований — достаточно было:
- Явно выполнить разименование составного поля.
- Выбрать более подходящий источник данных (регистр сведений вместо справочника).
Именно такие, казалось бы, незначительные упущения и приводят к катастрофическому падению производительности в промышленной эксплуатации.
Это еще не все!
Ради интереса мы выполнили поиск в модуле "РасчетСебестоимостиПостатейныеЗатраты" по фразе ".АналитикаУчетаНоменклатуры.". Результат показал не менее двух десятков таких вхождений.
Если использование такого поля в списках выборки (ВЫБРАТЬ) относительно безопасно, то его применение в условиях фильтрации (ГДЕ) и соединениях (СОЕДИНЕНИЕ) — это потенциальные точки проблем с производительности.
Яркий пример — функция "ТекстВыпущеннаяПродукция". Её запрос содержит идентичную проблему, и предложенная нами оптимизация (явное разименование и замена источника данных) гарантированно даст аналогичный эффект — сокращение времени выполнения с часов до секунд.
Здесь напрашивается системный вопрос: можно ли исправить все подобные места в коде разом — эффективно и безболезненно? Технически — да, и такая работа принесет огромную пользу стабильности системы. Практически же её реализация зависит от приоритетов команды и понимания, что профилактика всегда дешевле и быстрее, чем экстренная оптимизация под давлением бизнеса.
Возможности и компромиссы: как исправить всё «здесь и сейчас»?
Резонный вопрос: можно ли массово исправить все подобные места быстро, не прибегая к масштабному рефакторингу?
Ключ к решению — новые возможности платформы. Поскольку используемая версия ERP требует платформы 8.3.27 и выше, у нас появляется мощный инструмент: возможность создания произвольных индексов для справочников и других прикладных объектов. Доступно только для КОРП лицензий.
Это позволяет реализовать элегантный «костыль»: мы можем превратить справочник «Ключи аналитики номенклатуры» в подобие регистра сведений, снабдив его составными индексами, точно соответствующими нашим частым запросам.
Инструкция: добавление составного индекса в справочник (см. рис. 4)
- Откройте конфигуратор и найдите справочник «КлючиАналитикиНоменклатуры».
- Перейдите в его свойства и откройте форму «Дополнительные индексы».
- Создайте новый индекс. В перечень индексируемых полей добавьте поля, участвующие в условиях соединения:
- Номенклатура
- Характеристика
- Назначение
- МестоХранения
- Рекомендация: Для максимальной эффективности расположите поля в порядке убывания селективности. В большинстве сценариев оптимальным будет порядок: «Номенклатура, Характеристика, ...», так как отборы по этим полям встречаются чаще всего.
После обновления конфигурации и переиндексации базы данных проблемные запросы автоматически начнут использовать новый индекс.

Рис. 4 Создание дополнительных индексов для справочника «Аналитика учета номенклатуры»
Более того, анализ показывает, что набор индексов в регистре сведений «Аналитика учета номенклатуры» немного избыточен. Мы можем создать точечные, более эффективные индексы прямо в справочнике, которые покроют все критические сценарии использования поля «АналитикаУчетаНоменклатуры» в условиях отбора.
Мы не поленились и выполнили тест расчета себестоимости с учетом дополнительных индексов и первоначальным запросом.
https://explain.tensor.ru/archive/explain/edb2d0e0dc8d67feff52a06279be5ed6:0:2025-12-21#visio
Время выполнения составило 3,9 с, что в сравнении с первоначальной ситуацией выглядит также великолепно.

Рис. 5 План запроса (используем дополнительные индексы)
Как видно на последнем плане выполнения, доступ к данным теперь осуществляется через индексную таблицу справочника «Ключи аналитики номенклатуры» и выполняется эффективно. Небольшое увеличение времени (по сравнению с вариантом, где был выполнен предыдущий оптимальный расчёт себестоимости) связано с изменением структуры плана запроса и состоянием данных (расчет себестоимости завершился успешно). Тем не менее, текущий вариант оптимизации полностью работоспособен и даёт приемлемый результат.
Плюсы индексации справочника:
- Быстро: Не нужно менять код запросов, эффект мгновенный.
- Локально: Все правки — только в структуре объекта.
Минус:
- Это костыль. Правильнее использовать регистры сведений (в рамках текущей архитектуры).
Но есть стратегическая перспектива. Возможность создания дополнительных индексов позволит убрать дублирующие регистры сведений, упростив конфигурацию и механизмы синхронизации. Костыль сегодня может стать основой архитектуры завтра!

P.S. Мы будем рады конструктивным комментариям и обсуждению технических деталей представленного подхода. Заранее благодарим за вклад в профессиональный диалог.
Вступайте в нашу телеграмм-группу Инфостарт