RTP DesportoLiga das Nações. Jesus anuncia primeira lista de convocados à frente da seleçãoESPNFirst-month grades for all 20 Premier League teams: From an A+ to FThe Jerusalem PostUS government website used AI search tool from China that FBI said copied AnthropicDaily MaverickProtests erupt in Nigeria after 37 miners die in custody of paramilitary agencyInquirerCPD: 14.1% of Filipinos still use traditional family planningPunchAlexander-Arnold, Palmer return to England squad for Nations Leagueוואלהשלושה תלמידים נהרגו באירוע ירי בבית ספר בפיליפינים, בהם החשוד ביריEl Comercio“Estados Unidos tiene la tecnología, pero la guerra con Irán revela el límite de sus arsenales”VilaWebEl govern demana explicacions a l’empresa de traducció pel desgavell al judici de Roger EspañolIl Fatto QuotidianoOperaio 24enne colpito da un macchinario in una fonderia nel Bolognese: è grave. Sindacati proclamano uno sciopero di 4 ore7sur7L’Unesco peut être un “modérateur” dans le “débat global” sur l’IA, affirme son chefCBS NewsF-16 from Texas crashes in Michigan, evacuation order lifted
The Daily Newsstand · Free, Always
Friday, September 18, 2026

Диагностика медленных запросов в 1С: от технологического журнала до плана в SSMS

Translate

Разрабатываю отчёт, проверяю на тестовых данных — всё быстро. Передаю заказчику — три минуты ожидания. Знакомая ситуация. Первый инстинкт — переписать запрос, добавить индекс, сузить период. Это угадывание, и оно редко помогает с первой попытки. За несколько лет я выработал маршрут, который по моему опыту даёт конкретный ответ за 10–15 минут без привлечения DBA: читать не 1С-код, а план выполнения SQL-запроса, в который платформа его превращает.

Почему отчёт тормозит — и при чём здесь SQL

Тяжёлый расчёт при выполнении запроса происходит в СУБД, а не в коде 1С. Платформа транслирует 1С-запрос в SQL и передаёт его MS SQL Server или PostgreSQL — весь расчёт выполняется там. Тормозит, как правило, тоже там.

Есть три основных сценария деградации. Первый — неоптимальный текст запроса: оптимизатор строит плохой план из-за структуры самого запроса. Второй — устаревшая статистика СУБД: оптимизатор оценивает количество строк неверно и выбирает неподходящий алгоритм соединения. Третий — нагрузка на дисковую подсистему: запрос написан нормально, но сервер физически не успевает читать данные. Статья про первый сценарий — он целиком в руках разработчика.

По симптомам можно примерно понять, куда смотреть в первую очередь. Если тормозит только при конкретных параметрах — например, для одного склада — похоже на parameter sniffing, плохой план под конкретное значение; смотреть plan cache в SSMS или событие DBMSSQL в ТЖ. Если тормозит всегда, вне зависимости от параметров — вероятнее неоптимальная структура запроса или отсутствующий предикат в виртуальной таблице, тут поможет связка ТЖ и плана в SSMS или pgAdmin. Если тормозит только под нагрузкой или по ночам — это уже похоже на конкуренцию за блокировки или I/O, смотреть sys.dm_os_wait_stats и счётчики производительности. А если было быстро и стало медленно после обновления базы — скорее всего устарела статистика или поменялся план, решается через UPDATE STATISTICS и sp_recompile.

Шаг 1. Найти виновный запрос через технологический журнал

Технологический журнал — штатный инструмент платформы, описанный в документации ИТС в разделе «Технологический журнал». Он фиксирует события платформы вместе с временными характеристиками. Нас интересуют два типа событий: SDBL — запрос на языке 1С до трансляции, и DBMSSQL / DBPOSTGRS — уже в SQL, с реальным временем выполнения на стороне СУБД.

Минимальный конфиг, который не разрастётся до нескольких десятков гигабайт за ночь: история хранится один час (history="1"), фильтр — только запросы длиннее 10 секунд. В платформе 8.3 начиная примерно с версии 8.3.8 значение Duration указывается в микросекундах, поэтому 10 секунд = 10 000 000 (точное поведение для вашей версии стоит свериться с документацией ИТС к конкретному релизу).

⚠️ ЗДЕСЬ СПОЙЛЕР (оформить кнопкой «Спойлер» в редакторе Хабра, заголовок: «logcfg.xml — минимальный конфиг технологического журнала»):

<?xml version="1.0" encoding="UTF-8"?>

<config xmlns="http://v8.1c.ru/v8/tech-log">

  <log location="C:\logs\tl" history="1">

    <event>

      <eq property="Name" value="SDBL"/>

      <gt property="Duration" value="10000000"/>

    </event>

    <event>

      <eq property="Name" value="DBMSSQL"/>

      <gt property="Duration" value="10000000"/>

    </event>

  </log>

</config>

⚠️ КОНЕЦ СПОЙЛЕРА

Для PostgreSQL — заменить DBMSSQL на DBPOSTGRS, синтаксис тот же. Файл кладётся в каталог conf рабочей директории сервера 1С. В типовой установке 1С:Предприятие 8.3 на Windows это обычно C:\Program Files\1cv8\conf\logcfg.xml; точный путь проверяйте по параметрам запуска службы ragent или в документации ИТС к конкретной версии. Платформа перечитывает файл примерно раз в минуту — перезапускать сервис не нужно.

После воспроизведения медленного отчёта в папке C:\logs\tl появятся файлы вида YYYYMMDDHHMMSS.log. Пример строки из журнала:

00:05:43.241011-11234567,DBMSSQL,5,process=rphost,p:processName=1cv8s,

SessionID=1234,Usr=Иванов,

sql=SELECT T1._Fld157RRef,T1._Fld158,SUM(T1._Fld223) FROM _AccumRg223 T1

    GROUP BY T1._Fld157RRef,T1._Fld158,

Rows=48291,RowsAffected=48291

Ключевые поля: первое число после временной метки — Duration в микросекундах; sql — сырой SQL-запрос; SessionID — для поиска смежных событий; Rows — количество возвращённых строк. Порог в 10 секунд ориентировочный: если SLA отчёта — 5 секунд, имеет смысл снизить до 5000000. На нагруженном сервере слишком низкий порог быстро заполнит диск.

Шаг 2. Вытащить SQL и получить план в SSMS / pgAdmin

SQL из поля sql в журнале — фрагмент реального батча, который платформа отправила в СУБД. Запустить его напрямую не всегда получится: 1С активно использует временные таблицы (#tt1, #tt2 и подобные), которые создаются в рамках сессии и к моменту чтения журнала уже уничтожены.

-- Так выглядит фрагмент из ТЖ с временной таблицей

INSERT INTO #tt1 (_f1, _f2)

SELECT Reference157.IDRRef, Reference157.Description

FROM _Reference157

WHERE Reference157.Fld3456 = @P1

Два рабочих способа получить исполняемый план. Первый: в консоли запросов 1С:Предприятие нажать кнопку «Показать запрос» — платформа отобразит SQL-текст с контекстом, его можно скопировать в SSMS. Второй: в SSMS (версия 18+) найти полный батч через sys.dm_exec_sessions по SessionID прямо в момент выполнения тяжёлого отчёта.

В SSMS перед запуском добавить:

SET STATISTICS IO ON;

SET STATISTICS TIME ON;

И включить «Include Actual Execution Plan» (Ctrl+M). Запрос выполнится и вернёт план с метриками — в том числе Logical reads: число страниц, прочитанных из буферного пула.

В pgAdmin:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)

SELECT ... -- ваш запрос

BUFFERS показывает количество блоков из shared buffers и с диска — аналог Logical/Physical reads в MS SQL.

Как 1С транслирует объекты метаданных в SQL

Физические имена таблиц SQL зависят от внутреннего номера объекта метаданных — именно поэтому имена вроде _AccumRg223 непонятны без дополнительного контекста. Посмотреть соответствие можно через SSMS Object Explorer или утилитой v8unpack.

Справочник разворачивается в таблицу Reference<N> прямым SELECT, без подзапроса. С регистрами сложнее: СрезПоследних() на регистре сведений (таблица InfoRg<N>) превращается в подзапрос с ROW_NUMBER() OVER (PARTITION BY <измерения> ORDER BY Period DESC) на MS SQL — на PostgreSQL похожая логика через оконную функцию, конкретный вид плана зависит от версии платформы. Остатки() на регистре накопления (AccumRg<N>) — это подзапрос с GROUP BY и агрегатами по всей таблице или за указанный период, а Обороты() — SELECT SUM(...) GROUP BY по диапазону дат.

Ключевой момент: SQL для виртуальных таблиц содержит вложенный подзапрос. Условия, приписанные снаружи через ГДЕ, применяются к результату этого подзапроса — внутрь они не проникают. Это и есть источник главного антипаттерна, который разберём дальше на конкретном примере.

Поведение СрезПоследних() на MS SQL и PostgreSQL различается в деталях реализации. Это стоит проверять на реальной тестовой базе: выполнить запрос с виртуальной таблицей и сравнить планы в обоих окружениях, если они доступны.

Читаем план: три плохих паттерна

Открытый план выполнения — дерево операторов. Каждый узел — шаг обработки данных. Смотреть нужно на ширину стрелок (объём данных между узлами) и на жёлтые предупреждения в свойствах узла.

Первый паттерн — Table Scan или Index Scan вместо Index Seek: в плане видна иконка «Clustered Index Scan» с высоким estimated cost. Типичная 1С-причина — условие наложено снаружи виртуальной таблицы, и оптимизатор не видит предикат внутри подзапроса. Лечится передачей условия параметром виртуальной таблицы в скобках. Второй — Nested Loop на больших наборах: в плане тысячи выполнений внешней ветки, потому что один из источников не отфильтрован до соединения; помогает добавить отбор до соединения или перенести условие в параметры виртуальной таблицы. Третий, самый неприятный — Hash Match Spill: узел Hash Match с предупреждением, в свойствах SSMS Spill Level больше нуля. Значит промежуточный результат не помещается в память и сбрасывается на tempdb или диск — если проблема системная, а не разовая, это уже задача DBA по настройке памяти, а не запроса.

Про Hash Match Spill: MS SQL фиксирует это через Extended Events (события hash_warning и sort_warning) — подробная спецификация в разделе «Extended Events» на Microsoft Learn; в SSMS спилл виден в свойствах узла плана как «Spill Level > 0». Одиночный спилл — не катастрофа, но если он происходит при каждом запуске отчёта — время выполнения кратно вырастает из-за дисковых операций. Ориентир по Logical Reads на запрос к регистру с 1 млн строк: до 10 000 страниц — приемлемо, 50 000+ — повод изучить план. Общепринятого норматива нет, цифра из практики.

Главная ловушка — условие снаружи виртуальной таблицы

Условие ГДЕ, наложенное снаружи виртуальной таблицы, не попадает внутрь подзапроса СУБД — это главная причина Table Scan на регистре. Разберу на конкретном примере: запрос к остаткам товаров на складе, который кажется логичным.

// Плохой вариант

ВЫБРАТЬ

    Ост.Склад,

    Ост.Номенклатура,

    Ост.КоличествоОстаток

ИЗ

    РегистрНакопления.ТоварыНаСкладах.Остатки КАК Ост

ГДЕ

    Ост.Склад = &Склад

Что происходит в СУБД: платформа разворачивает Остатки() в подзапрос, который суммирует всю таблицу _AccumRg<N> с GROUP BY. Предикат Склад = &Склад применяется к результату подзапроса. Сканируется весь регистр — независимо от того, что нужен один склад.

Исправленный вариант:

// Хороший вариант

ВЫБРАТЬ

    Ост.Склад,

    Ост.Номенклатура,

    Ост.КоличествоОстаток

ИЗ

    РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Склад = &Склад) КАК Ост

Условие Склад = &Склад передаётся параметром виртуальной таблицы. Платформа включает его внутрь подзапроса с GROUP BY — оптимизатор получает предикат на нужном уровне и может использовать индекс.

Фрагмент плана до исправления (pgAdmin, текстовый формат):

->  Hash Aggregate  (cost=18432.00..18500.00 rows=68 ...)

      ->  Seq Scan on "_AccumRg223" t1  (cost=0.00..16800.00 rows=832000 ...)

Фрагмент плана после:

->  Hash Aggregate  (cost=312.00..315.00 rows=12 ...)

      ->  Index Scan using "_AccumRg223_ByDim_RRR" on "_AccumRg223" t1

            (cost=0.10..290.00 rows=240 ...)

            Index Cond: ("_Fld157RRef" = $1)

832 000 строк против 240 строк. Разница в три минуты ожидания объясняется именно этим. То же правило работает для СрезПоследних() и Обороты() — условия нужно передавать в скобках, а не накладывать снаружи через ГДЕ. В СКД это задаётся через параметры виртуальной таблицы в свойствах набора данных.

Чеклист — диагностика за 10 минут

Весь маршрут укладывается в шесть шагов. Сначала включить технологический журнал через logcfg.xml на сервере 1С — файлы должны появиться в папке за 1-2 минуты, если их нет, проверить путь и права. Дальше воспроизвести отчёт и найти запрос: искать по *.log слово DBMSSQL, смотреть на поле Duration — кандидат на разбор это то, что больше 5 000 000 мкс. Затем получить SQL через консоль запросов 1С, кнопку «Показать запрос» — важно проверить, нет ли среди виртуальных таблиц таких, что используются без параметров. Открыть план в SSMS (Ctrl+M) или через pgAdmin EXPLAIN ANALYZE и смотреть на операторы Scan, Nested Loop, Hash Match — Scan на большой таблице сразу красный флаг. Проверить паттерны через Plan Explorer или pgAdmin: Logical Reads, Spill, широкие стрелки между узлами — если Logical Reads выше 50 000, стоит смотреть дальше. И последний шаг — замерить ещё раз через ТЖ после правки и сравнить Duration до и после; по опыту, улучшение в 3-10 раз достижимо без привлечения DBA.

Когда без DBA не обойтись

Разработчик упирается в стену в трёх ситуациях — и во всех трёх дальше без прав на СУБД не продвинуться.

Устаревшая статистика: оптимизатор оценивает 100 строк вместо 1 000 000, выбирает Nested Loop там, где нужен Hash Join. Нужен UPDATE STATISTICS AccumRg<N> (MS SQL) или ANALYZE (PostgreSQL). Требуются права dbowner на уровне СУБД.

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

PostgreSQL не пробрасывает предикат во вложенный подзапрос СрезПоследних(). Это известное поведение, связанное с тем, как PostgreSQL обрабатывает оконные функции внутри подзапросов — предикат снаружи не «протекает» вниз. Решение — материализовать промежуточный результат во временную таблицу или использовать QProcessing (настройка уровня сервера, задача DBA).

Что передать DBA в виде задачи:

  • SQL-запрос из ТЖ — поле sql.

  • Скриншот или XML плана с отмеченным узлом-аномалией.

  • Метрику: Duration и Logical Reads до и после попытки исправления.

  • Версию платформы (p:processName из ТЖ) и версию СУБД (SELECT @@VERSION в MS SQL).

  • Время воспроизведения и имя пользователя из ТЖ — для поиска смежных событий.

Отдельно стоит проговорить пару моментов, которые обычно всплывают на этом этапе. ЕстьNull(Поле, ЗначениеПоУмолчанию) транслируется в ISNULL() (MS SQL) или COALESCE() (PostgreSQL) и почти не влияет на план, если не применяется к индексируемому столбцу в условии ГДЕ — для отбора лучше явная проверка ГДЕ Таблица.Поле ЕСТЬ NULL, оптимизатор обрабатывает её предсказуемее. А МЕЖДУ транслируется в BETWEEN и используется оптимизатором для диапазонного сканирования по индексу, если поле в него входит — для дат в регистрах накопления это стандартный способ ограничить период, и точно так же, как с условием на склад, МЕЖДУ &НачалоПериода И &КонецПериода нужно передавать параметром самой виртуальной таблицы, а не через ГДЕ снаружи.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.