Партицированная дисциплина в 1С: явная граница Index Seek при join больших регистров сведений на MS SQL

06.10.26

Разработка - Запросы

Соединение ~20 млн идентификаторов во временной таблице с регистром сделок ~210 млн строк на MS SQL 2019 даёт Merge Join и Index Seek по разделителю без сужения по ключу. В статье разбор плана, версионного хранения, цепочки ПОМЕСТИТЬ/ИНДЕКСИРОВАТЬ и приёма явной границы по Период как у partition column.

Партицированная дисциплина программиста в 1С нужна там, где регистры сведений с версионным хранением и разделителями БСП упираются в MS SQL: даже при корректном join к временной таблице идентификаторов оптимизатор выбирает Merge Join и Index Seek только по разделителю и прокачивает почти весь индекс. Ниже разбор кейса на SQL Server 2019: как читать план, зачем явная нижняя граница по следующему ключу индекса и почему это аналог partitioning column без физических партиций.

 

Когда партиций нет, а дисциплина «как у партиций» обязательна

В типовых конфигурациях 1С:Предприятие 8.3 партицирование таблиц на стороне СУБД упирается в общие реквизиты и разделители. Платформа ставит разделитель первым полем в индексы, и для большинства типовых решений это оправдано. Часть объектов метаданных разнесена по нескольким таблицам без одного осмысленного partition column: регистры бухгалтерии, документы с табличными частями. Классификацию таких случаев подробно разбирают в материале «Методика похудения для 1С - 100%»; для нашей темы достаточно помнить, что «просто включить партиции» на всю базу не получится.

Долго казалось, что поможет железо и штатное партицирование SQL Server. Узкое место сместилось в текст запросов: без явного сужения по ключу индекса seek по разделителю формально корректен и по факту слишком широк. Отсюда партицированная дисциплина программиста: для каждого тяжёлого обращения к большой таблице 1С задавать границу, с которой оптимизатор отсекает хвост индекса, как partition elimination при работе с партициями.

 

Цепочка ПОМЕСТИТЬ и join регистра транзакций к сделкам

 

Цепочка ПОМЕСТИТЬ и join регистра транзакций к сделкам

На рисунке выше зафиксирована цепочка, с которой начинали замеры: отбор идентификаторов во временную таблицу и join к регистру сделок. Речь не об инциденте у пользователя, а о порядке шагов в одном пакете запросов платформы.

 

Версионные регистры сведений и join через временную таблицу

Операции лежат в двух регистрах сведений: агрегированные денежные трансферы со ссылкой на сделку и агрегированные сделки. Документы для потоковой загрузки не подошли: нужна максимальная скорость импорта пакетами по тысяче записей без обновлений по измерениям. Версионное хранение завязано на поле Период как дату и время загрузки версии; пишутся только новые наборы записей. Пакетная запись без обновлений по измерениям подробнее описана в материале «Язык мой враг мой». Минус переносится на выборку: позже где-то придётся брать последние версии по измерениям, но это отдельная тема и не цель текущего разбора.

 

Контекст: Версионные регистры сведений и join через временную таблицу

 

Контекст: Версионные регистры сведений и join через временную таблицу

Масштабы на MS SQL Server 2019 (15.0.2000.5): порядка 210 миллионов строк в регистре сделок и около 474 миллионов в регистре транзакций. Задача сопровождения: отобрать трансферы по критериям (в том числе Период не раньше параметра), собрать DISTINCT идентификаторов связанной операции во временную таблицу, проиндексировать её и соединить с регистром сделок по полю идентификатора в исходной системе.

Прямой join «последних срезов» трансферов и сделок сознательно не рассматривали: у объектов разные наборы версий, а выборка актуальных версий в одном запросе с join на сотни миллионов строк оказалась бы ещё тяжелее. Временная таблица стала компромиссом между скоростью загрузки и сложностью последующей аналитики.

 

Базовый запрос и что показывает план на MS SQL

Первый шаг на языке запросов 1С: собрать уникальные идентификаторы операций из транзакций за интервал и положить их во временную таблицу с индексом по ключу соединения.

 

Контекст: Базовый запрос и что показывает план на MS SQL

 

Контекст: Базовый запрос и что показывает план на MS SQL

ВЫБРАТЬ РАЗЛИЧНЫЕ
    Транзакции.СвязаннаяОпИдИсхСистемы КАК ИдСделкиИзТранзакции
ПОМЕСТИТЬ ВремИдИзТранзакций
ИЗ
    РегистрСведений.СУУ_АгрегированныеДенежныеТранзакции КАК Транзакции
ГДЕ
    Транзакции.Период >= &ДатаНачала

ИНДЕКСИРОВАТЬ ПО
    ИдСделкиИзТранзакции;

ВЫБРАТЬ
    Сделки.Период,
    Сделки.ИсходнаяСистема,
    Сделки.ИдИсхСистемы,
    Сделки.ОсновнойСчет,
    Сделки.НогаСделки
ПОМЕСТИТЬ РезультатСделокПоИд
ИЗ
    РегистрСведений.СУУ_АгрегированнаяСделкаКП КАК Сделки
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВремИдИзТранзакций КАК ИдИзТранзакций
        ПО Сделки.ИдИсхСистемы = ИдИзТранзакций.ИдСделкиИзТранзакции

На стороне SQL платформа разворачивает это во временные таблицы tempdb (#tt1, #tt2) и INSERT…SELECT с фильтром по разделителю области данных. Фактический текст последнего шага join выглядит так:

INSERT INTO #tt2 WITH (TABLOCK)
    (_Q_001_F_000, _Q_001_F_001RRef, _Q_001_F_002, _Q_001_F_003RRef, _Q_001_F_004RRef)
SELECT
    СделкиРег._Period,
    СделкиРег._Fld18861RRef,
    СделкиРег._Fld18865,
    СделкиРег._Fld18863RRef,
    СделкиРег._Fld19363RRef
FROM dbo._InfoRg18860 AS СделкиРег WITH (NOLOCK)
    INNER JOIN #tt1 AS ВремИд WITH (NOLOCK)
        ON СделкиРег._Fld18865 = ВремИд._Q_000_F_000
WHERE СделкиРег._Fld628 = @P1;

Чтобы сравнивать планы, а не кэш прошлых прогонов, перед серией замеров сбрасывали wait stats, кэш планов и чистые буферы (DBCC SQLPERF для sys.dm_os_wait_stats, FREEPROCCACHE, DROPCLEANBUFFERS). Это лабораторная методика: на продуктиве так делать нельзя, но для сопоставления Nested Loop и Merge Join на одном тексте запроса она даёт повторяемую картину.

Фактический план с MS SQL снимаю так: в SSMS включаю «Include Actual Execution Plan» и выполняю тот же SQL, что сгенерировала платформа; либо собираю Extended Events с событием sqlserver.query_post_execution_showplan; либо трассировку с showplan. Обычно сверяю estimated и actual rows на join и на Index Seek, а не одну цифру времени из клиента 1С.

 

Batch 1С и ответ SQL Server с планом выполнения

 

Batch 1С и ответ SQL Server с планом выполнения

Сеанс 1С и SQL Server обмениваются параметризованным batch: сервер возвращает план и статистику операторов. Если смотреть только факт «запрос выполнился», граница seek в плане остаётся незаметной.

 

Nested Loop, Merge Join и ловушка «правильного» плана

Пока во #tt1 лежало порядка 100–200 тысяч идентификаторов, SQL Server брал Nested Loop: соединение помещалось в память, Merge Join не включался. На копии базы с урезанным периодом такой план легко ввести в заблуждение: он не переносится на месячную выборку с миллионами ключей.

 

Контекст: Nested Loop, Merge Join и ловушка «правильного» плана

 

Контекст: Nested Loop, Merge Join и ловушка «правильного» плана

Когда период по транзакциям расширили до типичного месяца, во #tt1 оказалось около 20 миллионов строк. План сменился: Clustered Index Scan по #tt1 ORDERED FORWARD и Index Seek по индексу сделок _InfoR18860_ByDims18897_STRRRR с полем идентификатора _Fld18865, Merge Join Inner, MANY-TO-MANY. Сортировки по ключам join совпадают, картина выглядит здоровой.

На этом этапе соблазно решить, что раз Merge Join, осталось подкрутить MAXDOP или память. Фактический план показал иное. Seek по сделкам шёл только по разделителю _Fld628 = @P1 (общий реквизит области данных), без предиката на следующие ключи индекса. В поток Merge Join ушло порядка 210 649 142 строк, то есть фактически весь индекс по области, хотя #tt1 содержала около 20 миллионов ключей.

|--Merge Join(Inner Join, MANY-TO-MANY MERGE:([T2].[_Q_000_F_000])=([T1].[_Fld18865]),
|              RESIDUAL:(...))
|   |--Clustered Index Scan(OBJECT:([tempdb].[dbo].[#tt1] AS [T2]), ORDERED FORWARD)
|   |--Index Seek(OBJECT:([dbo].[_InfoRg18860].[_InfoR18860_ByDims18897_STRRRR] AS [T1]),
|                 SEEK:([T1].[_Fld628]=[@P1]) ORDERED FORWARD)

Оптимизатор MS SQL не выводит автоматически min/max идентификатора из #tt1 как верхнюю и нижнюю границу seek по _Fld18865, если в тексте запроса нет явного условия. Разделитель в WHERE обязателен для мультитенантности, но как единственный предикат seek он закрывает лишь первый столбец индекса. Время растёт с размером таблицы сделок при фиксированном объёме #tt1.

Отдельно смотрели оценку cardinality по #tt1. После ПОМЕСТИТЬ большого набора статистика на временной таблице на стороне SQL часто неточна; в actual plan сверяем estimated и actual rows на scan #tt1. При прямом T-SQL иногда помогает UPDATE STATISTICS на #tt1. Неверная оценка чаще меняет выбор между Nested Loop и Merge Join, но симптом сохранялся и при «ожидаемом» Merge Join: широкий ordered seek по сделкам.

 

Scan #tt1 и широкий Index Seek по _Fld628

 

Scan #tt1 и широкий Index Seek по _Fld628

На иллюстрации параллельно идут два потока: компактный scan временной таблицы и широкий seek по регистру сделок. Пока второй поток не сужен следующим ключом индекса, стоимость join привязана к полному объёму области, а не к двадцати миллионам идентификаторов.

 

Явная граница seek: Период и порядок ключей индекса

Имя индекса регистра сведений _InfoR18860_ByDims18897_STRRRR читается по правилам платформы: префикс _InfoRg, блок ByDims с перечислением измерений, суффиксы типов полей. Суффиксы намекают на типы столбцов в ключе; точный порядок проще сверить с sys.index_columns на копии базы, чем угадывать по одному _Fld*. Поле для «границы seek» выбирают по этому порядку и по предикатам в запросе. Фильтр по реквизиту, который в jоин-индексе стоит дальше по ключу, чем вы думали, seek не сузит так же, как предикат по Период сразу после разделителя.

 

Контекст: Явная граница seek: Период и порядок ключей индекса

 

Контекст: Явная граница seek: Период и порядок ключей индекса

В рассматриваемом индексе после разделителя участвует идентификатор _Fld18865; следующий ключ, которым можно осмысленно сузить версионный регистр, это _Period (в метаданных 1С поле Период версии). К запросу ко второму регистру добавили условие по периоду загрузки, согласованное с отбором транзакций, со сдвигом на три месяца назад от &ДатаНачала:

ВЫБРАТЬ
    Сделки.Период,
    Сделки.ИсходнаяСистема,
    Сделки.ИдИсхСистемы,
    Сделки.ОсновнойСчет,
    Сделки.НогаСделки
ПОМЕСТИТЬ РезультатСделокПоИд
ИЗ
    РегистрСведений.СУУ_АгрегированнаяСделкаКП КАК Сделки
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВремИдИзТранзакций КАК ИдИзТранзакций
        ПО Сделки.ИдИсхСистемы = ИдИзТранзакций.ИдСделкиИзТранзакции
ГДЕ
    Сделки.Период >= ДОБАВИТЬКДАТЕ(&ДатаНачала, МЕСЯЦ, -3);

В плане на T-SQL появился дополнительный предикат seek по _Period. Оценочная стоимость фрагмента в тесте автора снизилась с 11424 до 7801 (без обещания тех же секунд на проде). Для сопровождения важнее другое: в seek появилась нижняя граница по следующему ключу после разделителя, а не только @P1.

Если по смыслу данных граница по Период недоступна, можно сужать по бизнес-дате операции (в исходном кейсе поле «Дата»), но только когда она входит в тот же индекс или есть другой подходящий индекс. Иначе filter или scan вынесутся из мерге-потока и план снова станет неожиданным.

Краткая альтернатива из того же опыта: ограничение по минимальному значению идентификатора из #tt1 (подзапрос или второй проход). Для строкового типа идентификатора сравнение min/max и collations проверяют отдельно; lexicographic min не всегда совпадает с бизнес-минимумом.

Параметр @P1 на разделитель напоминает про parameter sniffing: план с широким seek при одной области данных может отличаться от плана при другой. Query Store в MS SQL 2019 помогает поймать регрессию плана при изменении &ДатаНачала и объёма #tt1, но явные границы в тексте запроса всё равно прописывают вручную.

 

Партицированная дисциплина: чек-лист для больших запросов

Правило, которое мы зафиксировали в регламенте отчётов и регламентных выгрузок: для каждого тяжёлого обращения к регистру сведений с разделителем в индексе явно задавать нижнюю и при необходимости верхнюю границу по следующему ключу seek, по аналогии с partitioning column. Одного попадания в индекс по _Fld628 мало.

  • Сопоставить имя индекса _InfoRg*_ByDims* с полями в WHERE и ON.
  • После отбора во #tt1 проверить в actual plan actual rows на Merge Join и Index Seek по регистру.
  • Не доверять плану на 100 тысячах строк, если на проде ожидаются миллионы идентификаторов.
  • При смене &ДатаНачала сверить план в Query Store, не полагаясь на один успешный прогон.
  • Если добавили фильтр по Период или Дате, убедиться, что поле стоит в нужном месте ключа индекса.

Смежные темы за рамками статьи: выбор последних версий в версионном регистре, тонкая настройка партиций SQL под 1С, хинты как единственный способ лечения. Их не смешивают с базовой дисциплиной границы seek.

Три вывода из кейса

  1. Index Seek по разделителю на таблице в сотни миллионов строк может отдавать в Merge Join весь индекс области; join здесь ни при чём, не хватает явной границы по следующему ключу.
  2. Версионное хранение в регистрах сведений и join через временную таблицу меняют постановку задачи: оптимизация join к «сырой» таблице версий не сводится к индексу только по идентификатору.
  3. Партицированная дисциплина программиста в 1С - осознанный предикат по полю индекса после разделителя (Период, идентификатор или согласованная бизнес-дата), как partition elimination без физических партиций.

 

От широкого seek к границе по Период

 

От широкого seek к границе по Период

Итоговая диаграмма собирает цепочку: temp table с индексом, join по идентификатору, проверка плана, добавление границы seek, повторная сверка стоимости и строк.

Вступайте в нашу телеграмм-группу Инфостарт

1С:Предприятие 8.3 MS SQL Server регистры сведений производительность язык запросов оптимизация запросов Index Seek партицированная дисциплина Merge Join разделители версионное хранение MS SQL 2019 временная таблица 1С

Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

См. также

Инструментарий разработчика Роли и права Запросы СКД Разработчик Руководитель проекта 1С:Предприятие 8 Платные (руб)

Инструменты для разработчиков 1С 8.3 и 8.5: Infostart Toolkit. Автоматизация и ускорение разработки на управляемых формах. Легкость работы с 1С.

16500 руб.

02.09.2020    285445    1606    423    

1219

Инструментарий разработчика Запросы Разработчик 1С 8.3 1С:Бухгалтерия 3.0 1С:Зарплата и Управление Персоналом 3.x Абонемент ($m)

Структура запроса. Обработка визуально показывает, из какого количества подзапросов состоит запрос.

2 стартмани

06.07.2026    4920    65    XSlava    20    

47

Запросы Разработчик 1С 8.3 Россия Бесплатно (free)

Визуальный «Конструктор запроса» 1С, встроенный прямо в VS Code, — аналог конструктора из Конфигуратора и EDT, но без запуска платформы. Расширение читает конфигурацию, выгруженную в файлы, позволяет собрать запрос мышью и генерирует корректный текст на языке запросов 1С (SDBL). Открывает уже существующий текст запроса из кода для правки и — что особенно ценно — сохраняет комментарии при цикле «открыть - править - сохранить». Подходит тем, кто пишет код 1С в VS Code, держит под рукой множество проектов и не хочет ради одного запроса запускать тяжёлый Конфигуратор или EDT.

26.06.2026    10640    Alex_YAM    7    

34

Нейросети Инструментарий разработчика Запросы Разработчик 1С:Предприятие 8 Абонемент ($m)

Консоль запросов 1.0: добавлен ИИ-помощник (запрос в DeepSeek), который помогает быстрее получать каркас Запроса 1С Сформулируйте простое описание; нажмите кнопку – получите результат прямо в консоли. Где дальше его можно дорабатывать и тестировать. 28.5.2026 Добавил "Консоль запросов 2.0" (запрос в DeepSeek + Claude) 4.10.2026 Обновил "Консоль запросов 2.0"

3 стартмани

20.05.2026    9543    46    German4739    72    

28

WEB-интеграция Запросы Разработчик 1С 8.3 Абонемент ($m)

Post1C - это внешняя обработка, которая превращает 1С в полноценный инструмент для тестирования REST API. Всё управление сосредоточено в одном окне: настройка запроса, выполнение, просмотр ответа и генерация кода - без переключения между формами. Аналог Postman, но работающий в привычной среде 1С.

1 стартмани

02.04.2026    4630    105    priem_nv    27    

70

Инструментарий разработчика Запросы Разработчик 1С:Предприятие 8 1С:Зарплата и кадры государственного учреждения 3 1С:Зарплата и Управление Персоналом 3.x Абонемент ($m)

QueryConsole1C — расширение, включающее консоль запросов с поддержкой исполняемых представлений — аналогов виртуальных таблиц, основанных на методах программного интерфейса ЗУП. Оно позволяет выполнять запросы с учётом встроенной бизнес-логики, отлаживать алгоритмы получения данных и автоматически генерировать код на встроенном языке 1С.

1 стартмани

16.05.2025    13641    168    zup_dev    32    

87

Инструментарий разработчика Запросы Разработчик 1С:Предприятие 8 1С:ERP Управление предприятием 2 Абонемент ($m)

В данной публикации рассказывается о решении, которое позволяет находить сразу все ошибки в тексте запроса за раз, а не только самую первую.

2 стартмани

05.03.2025    7705    25    XilDen    12    

29
Для отправки сообщения требуется регистрация/авторизация