Мы ускорили запрос в 390 раз, не переписав его логику: он не был медленным — он стоял

04.08.26

База данных - HighLoad оптимизация

Мы ускорили запрос регламентной обработки в 390 раз — с 39 минут до 6 секунд, — не изменив его логику и получив построчно идентичный результат. Разгадка в том, что запрос не был медленным: из 2 353 секунд elapsed чистой работы (CPU) было лишь 124 — остальные 95 % он ждал блокировки, потому что срез последних без отбора читал весь регистр цен следом за массовой записью. Детектив по шагам с цифрами из статистики СУБД, чек-лист «проверь свою обработку за 15 минут» и один твист в финале.

В регламентной обработке ценообразования для маркетплейсов был шаг «Акции», который занимал около 50 минут. Внутри него жил один запрос, выполнявшийся 39 минут. Не «медленный запрос, который хочется ускорить» — а запрос, из-за которого регламент не влезал ни в одно окно.

Эта история — про то, как он превратился в 6 секунд. Спойлер дважды: во-первых, запрос эти 39 минут почти не работал — он стоял. Во-вторых, лечился он не там, где болело. Разбор по шагам, с цифрами из монитора СУБД — потому что именно они, а не интуиция, привели к разгадке.

 

Контекст: право на ошибку — ноль

Обработка живая: считает цены десятков тысяч SKU и публикует их в реальные кабинеты маркетплейсов работающего ритейлера. Ошибка оптимизации — это неверные цены на витрине. Поэтому весь проект шёл по жёсткому контуру: замерный стенд на копии боевой базы, одна правка — один прогон — сравнение результата с эталоном побайтово (полная таблица расчёта, ~100 колонок на позицию). Правка принималась только при идентичном результате.

Держите этот контур в голове: всё, что описано ниже, было бы азартной игрой без него.

 

Акт первый: подозреваемый очевиден. И невиновен

Запрос «удаление позиций по высокой цене» в блоке акций. Текст — обычный для таких обработок: срез последних по регистру цен, соединение с таблицей товаров акции, отбор по условию цены. Первая интуиция любого оптимизатора: «тяжёлый срез, много данных, перепишем повально на временные таблицы».

Прежде чем переписывать, мы посмотрели на факты. Штатная статистика СУБД по этому запросу (доступна любому администратору через консоль сервера — мы читаем её, ничего не меняя):

 

Показатель Значение
Полное время выполнения (elapsed) 2 353 с (~39 минут)
Процессорное время (CPU) 124 с (~2 минуты)
Доля ожиданий 95 % времени

 




Вчитайтесь: из 39 минут запрос работал две. Остальные 37 он ждал. Это переворачивает задачу: оптимизировать текст запроса, который занят работой 5 % времени, — значит оптимизировать 5 % проблемы. Сначала надо понять, чего он ждёт.

💡 Правило, которое дешевле выучить на чужом проекте: elapsed ≠ CPU. Пока вы не разложили время запроса на «работал» и «ждал», вы не знаете, что оптимизируете. Разница между этими числами — первое, на что стоит смотреть в любом «медленном запросе».

 

Акт второй: чего он ждал

Ответ нашёлся в расписании регламента. Запрос акций стартовал сразу после массовой записи цен — этапа, который в тот момент шёл 17 минут и активно писал в регистр цен тысячи записей, порциями, с проведением документов.

А теперь — что делал наш запрос. Его срез последних был написан так:

// срез по ВСЕМУ регистру цен: отбор только по типу цен, и тот — по строковому коду
РегистрСведений.Цены.СрезПоследних(, ТипЦен.Код = "000000164")

Срез виртуальной таблицы без отбора по номенклатуре означает: СУБД строит срез по всей таблице регистра — десятки миллионов записей, — чтобы потом соединением оставить несколько тысяч товаров акции. То есть запрос читает всё. А «читать всё» в момент, когда сосед «пишет много», — это очередь за блокировками по всей ширине таблицы. Отсюда 95 % ожиданий: широкое чтение застряло за массовой записью.

Заметьте, как связались две вещи, которые обычно разбирают порознь: «неоптимальный запрос» и «блокировки». Широкое чтение — это не только медленно, это ещё и конфликтно. Запрос, читающий лишние миллионы строк, конкурирует за них со всеми, кто в это время пишет. Сузив чтение, вы чините обе беды сразу.

 

Акт третий: лечение — одна правка

Раз запросу нужны только товары акции — отбор по ним должен стоять внутри параметров среза, а не снаружи соединением. Заодно строковый код типа цен заменяется ссылкой-параметром (условие .Код = "строка" — это скрытое соединение со справочником и фильтр по тексту без индекса):

// ПОСЛЕ: срез только по нужным товарам и типу цен по ссылке
РегистрСведений.Цены.СрезПоследних(
    ,
    ТипЦен = &ТипЦенКабинета
        И Номенклатура В (ВЫБРАТЬ Т.Номенклатура ИЗ ВТ_ТоварыАкции КАК Т))

Результат на боевом прогоне:

  До После
Запрос «удаление по высокой цене» 2 353 с 6 с
Шаг «Акции» целиком (оценка) ~50 мин ~10–12 мин
Результат запроса идентичен — сверен построчно

 




×390.
Не от гениальной правки — от того, что чинили причину, а не симптом: запрос перестал читать (и удерживать) миллионы чужих строк, и очередь за блокировками исчезла вместе с объёмом.

 

Акт четвёртый: рецидивы

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

И это вторая привычка, которая окупается: нашли паттерн — ищите его везде. Погрепать тексты запросов обработки по «СрезПоследних(,» и по «.Код = "» — минуты; каждая находка — потенциальные минуты регламента.

 

Частые возражения — и почему мы сделали иначе

Разбор на ретро вызвал ровно те вопросы, которые зададут и в комментариях. Отвечаю заранее.

«Почему просто не перенести шаг акций подальше от записи цен?» Это лечит симптом у одного пациента. Перенос шага убрал бы очередь в этом расписании — но запрос остался бы широким: он всё так же читал бы десятки миллионов строк, всё так же был бы уязвим к любому соседу-писателю (а на общем сервере они есть всегда) и всё так же грел бы кэш СУБД чужими страницами. Сужение чтения лечит болезнь, перенос — прячет её. Кстати, после правки вопрос переноса отпал сам: 6-секундному запросу всё равно, кто рядом пишет.

«Может, дело в отсутствии индекса?» Первая версия этой гипотезы у нас тоже была. Но посмотрите ещё раз на соотношение: CPU 124 с, ожидания — 95 %. Плохой план исполнения выглядит иначе: высокий CPU, высокие чтения, малые ожидания. Индекс ускоряет работу — а наш запрос не работал, он ждал. Это и есть главная ценность разложения времени: оно говорит, какого класса у вас проблема, до того как вы начали чинить не ту.

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

«А почему не читать регистр напрямую запросом с МАКСИМУМ(Период)?» Потому что виртуальная таблица с отбором внутри параметров делает ровно это, но силами платформы и с правильными планами. Ручная эмуляция среза — код, который придётся сопровождать, и место для новых ошибок. Приём проще: не отказываться от виртуальных таблиц, а кормить их отборами.

 

Как проверить свою обработку за 15 минут

Рецепт без установки чего-либо, только чтение:

  1. Возьмите фактические тайминги этапов из журнала регистрации или лога самой обработки (у большинства регламентных обработок он есть). Найдите самый жирный этап.
  2. Разложите его время на CPU и ожидания по статистике сервера СУБД (консоль администратора, представления статистики запросов — доступны на чтение любому DBA). Ищите запросы, где elapsed кратно больше CPU.
  3. Погрепайте тексты запросов обработки по трём паттернам: СрезПоследних(, (срез без отбора), .Код = " (строковые сравнения), СрезПервых(,. Каждое совпадение — кандидат.
  4. Сопоставьте расписание: какие шаги-писатели идут перед вашим тяжёлым читателем или параллельно ему. Широкое чтение после массовой записи — та самая конфигурация, что дала нам 39 минут.
  5. Прежде чем править — снимите эталон результата. Хотя бы выгрузку итоговой таблицы с сортировкой и контрольной суммой. Правка без эталона — это надежда, а не инженерия.

По нашему опыту, три паттерна из пункта 3 покрывают большую часть «необъяснимо медленных» регламентов на выросших базах. Не потому, что разработчики плохие — потому, что код писался, когда таблицы были в сто раз меньше, и тогда он был честно быстрым.

И бонус-наблюдение с того же проекта про устойчивость: оптимизированные версии не просто быстрее — они стабильнее под чужой нагрузкой. Старый вариант ключевого запроса на тихом сервере шёл 358 секунд, а в час пик — 1 179; новый — 130 и 256 соответственно. Широкие запросы деградируют под нагрузкой нелинейно, потому что конкурируют за всё; узкие — почти не замечают соседей. Если ваш регламент «то влезает в окно, то нет» — это, возможно, не мистика, а мера широты ваших чтений.

 

Цена дисциплины — и что она поймала

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

За проект контур поймал две вещи, которые не поймал бы глаз: опечатку в тексте запроса (зарезервированное слово), ломавшую расчёт ровно в одном из четырёх ценовых контуров — три остальных прогона были бы зелёными; и подтвердил, что застарелый чужой баг ведёт себя одинаково до и после наших правок — то есть мы его не разбудили. Обе находки — из разряда «узнали бы от пользователей через неделю», и обе стоили бы дороже всего проекта оптимизации.

 

Что из этого унести себе

  1. Сначала elapsed vs CPU, потом всё остальное. Если разница в разы — вы ищете не «медленный запрос», а то, чего он ждёт. Это меняет и метод, и место починки.
  2. Срез последних без отбора — конфликтная мина. Он не просто читает лишнее — он воюет за это лишнее со всеми пишущими. Отбор по номенклатуре (и любым измерениям, по которым он у вас есть) — внутрь параметров виртуальной таблицы.
  3. Строковые коды в запросах — скрытые соединения. НайтиПоКоду() один раз в коде, дальше — ссылка-параметр.
  4. Смотрите на соседей по расписанию. Запрос может быть «медленным» только потому, что стартует в тень самого пишущего этапа. Иногда лечится переносом шага, у нас — сужением чтения.
  5. Ни одной правки без сверки результата. Наш контур — эталон и побайтовое сравнение после каждой правки — поймал за проект две ошибки, невидимые глазами. «Стало быстрее» без «стало так же» — это не оптимизация, а лотерея.

Весь проект, из которого взят этот эпизод, дал ускорение процессов ценообразования в 2–6 раз без изменения единой итоговой цены — но это уже другая история, с другими паттернами (запрос в цикле 810 → 43 с, поиск O(N²) 113 → 21 с, десять соединений одного регистра). Если интересно — расскажу в следующей статье.

И последний твист, который часть читателей, подозреваю, угадала по почерку: черновую работу в этом проекте — чтение кода, разбор статистики СУБД, подготовку правок и прогоны замеров — выполнял ИИ-агент (Claude Code) под управлением инженера. Все решения о выпуске принимал человек, и именно поэтому контур побайтовой сверки был обязательным: он превращает скорость агента из риска в преимущество. Как мы вообще пришли к работе агентов с продуктивом — я разбирал в предыдущих статьях про бенчмарк нейросетей на боевой базе. А спорить и делиться своими «39 минутами» приглашаю в комментарии: самые дикие elapsed/CPU-соотношения, которые вы встречали в бою, — отдельная коллекционная ценность.

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

оптимизация запросов срез последних блокировки elapsed CPU виртуальные таблицы производительность регистр сведений ценообразование маркетплейсы ожидания статистика СУБД запрос в цикле побайтовая сверка регламентные задания HighLoad DMV Claude Code ИИ-агент оптимизация 1С

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

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

См. также

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

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

16500 руб.

02.09.2020    270076    1498    422    

1180

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

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

1 стартмани

16.05.2025    12239    155    zup_dev    32    

86

Запросы Программист 1С:Предприятие 8 1C:Бухгалтерия Бесплатно (free)

Столкнулся с интересной ситуацией, которую хотел бы разобрать, ввиду её неочевидности. Речь пойдёт про использование функции запроса АВТОНОМЕРЗАПИСИ() и проблемы, которые могут возникнуть.

11.10.2024    21693    XilDen    39    

113

HighLoad оптимизация Инструменты администратора БД Системный администратор Программист 1С 8.3 Абонемент ($m)

Обработка для простого и удобного анализа настроек, нагрузки и проблем с SQL сервером с упором на использование оного для 1С. Анализ текущих запросов на sql, ожиданий, конвертация запроса в 1С и рекомендации, где может тормозить.

10 стартмани

15.02.2024    23971    410    ZAOSTG    115    

132

HighLoad оптимизация Программист 1С:Предприятие 8 1C:Бухгалтерия Абонемент ($m)

Встал вопрос: как быстро удалить строки из ТЗ? Рассмотрел пять вариантов реализации этой задачи. Сравнил их друг с другом на разных объёмах данных с разным процентом удаляемых строк. Также сравнил с выгрузкой с отбором по структуре.

09.01.2024    45725    doom2good    50    

80

HighLoad оптимизация Системный администратор Программист Бесплатно (free)

При переводе типовой конфигурации 1C ERP/УТ/КА на PostgreSQL придется вложить ресурсы в доработку и оптимизацию запросов. Расскажем, на что обратить внимание при потерях производительности и какие инструменты/подходы помогут расследовать проблемы после перехода.

20.11.2023    24327    ivanov660    7    

84

HighLoad оптимизация Бесплатно (free)

Казалось бы, КОРП-системы должны быть устойчивы, быстры и надёжны. Но, работая в рамках РКЛ, мы видим немного другую картину. Об основных болевых точках КОРП-систем и подходах к их решению пойдет речь в статье.

15.11.2023    13808    a.doroshkevich    23    

78

HighLoad оптимизация Запросы

Очень немногие из тех, кто занимается поддержкой MS SQL, работают с хранилищем запросов. А ведь хранилище запросов – это очень удобный, мощный и, главное, бесплатный инструмент, позволяющий быстро найти и локализовать проблему производительности и потребления ресурсов запросами. В статье расскажем о том, как использовать хранилище запросов в MS SQL и какие плюсы и минусы у него есть.

11.10.2023    27611    skovpin_sa    15    

107
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. paulwist 04.08.26 16:34 Сейчас в теме
2. gzharkoj 596 04.08.26 16:44 Сейчас в теме
по дефолту 1с ждет блокировку 20 сек, у вас больше и таймаута нет, как-то подозрительно. Понимать бы какая версия 1с, субд, режим совместимости.
3. nedomolkov.ivan 41 04.08.26 17:14 Сейчас в теме
(1) Изоляция — READ COMMITTED, RCSI выключен. В этом и вся механика: при включённой снапшот-изоляции читатель за писателем не встал бы вовсе, и истории бы не было.

(2) Про 20 секунд вы правы, и формулировка в статье это смазывает. 20 секунд — потолок ОДНОГО ожидания, а не суммарного времени оператора. Здесь один SELECT сканирует срез по всему регистру и по ходу скана многократно встаёт на короткие ожидания на данных, которые прямо сейчас переписывает соседний этап. Ни одно ожидание потолка не достигло — поэтому в журнале нет ни одной ошибки блокировки, а elapsed вырос до 2 353 при CPU 124. Таймаут ловит одно долгое ожидание, а не длинную сумму коротких.

Граница честности: разбивки по wait_type в статье нет, 95 % — это (elapsed − CPU). Атрибуция к блокировкам держится на двух косвенных фактах: совпадении с этапом массовой записи по расписанию и на том, что сужение чтения убрало разрыв целиком (6 с вместо 2 353 при построчно идентичном результате). Полный wait-stats был бы аргументом сильнее, согласен.

Версии: платформа 8.3, СУБД MS SQL (статистику брали из DMV). Точный релиз и режим совместимости наизусть не назову — база заказчика, а врать в таком разборе хуже, чем промолчать.
4. gzharkoj 596 04.08.26 18:02 Сейчас в теме
(3)
Изоляция — READ COMMITTED, RCSI выключен
- ну вот это все объясняет, это какая-то конфа на автоматических блокировках и в режиме совместимости с 8.2. Там и не такое встретишь.
5. nedomolkov.ivan 41 04.08.26 18:12 Сейчас в теме
(4) Тут как раз наоборот. READ COMMITTED без RCSI — это признак управляемых блокировок, а не автоматических: в автоматическом режиме платформа поднимает изоляцию до REPEATABLE READ / SERIALIZABLE, и картина была бы другой — конфликтовали бы диапазоны, а в журнале лежали бы таймауты. Их там нет ни одного.

RCSI выключен не по замыслу, а потому что это состояние базы по умолчанию: его отдельно никто не включал.

В этом и смысл разбора: экзотики в конфигурации нет, связка «управляемые + RCSI off» стоит у большинства. Именно поэтому чтение может простоять 2 353 секунды на чужой записи и не оставить в журнале ни одной ошибки блокировки — искать причину идут в план запроса, а её там нет.
6. gzharkoj 596 04.08.26 18:21 Сейчас в теме
(5) RCSI без режима совместимости для 8.3 - это штатный функционал с какой-то очень ранней 8.3. Все-таки для полноты картины приведите все параметры информационной базы, так будет понятно.
7. nedomolkov.ivan 41 04.08.26 19:13 Сейчас в теме
(6) Согласен: RCSI на 8.3 с управляемыми блокировками — штатный поддерживаемый режим, включить его никто не мешал. В этом и смысл разбора: он поддерживается, но по умолчанию выключен, а отдельного решения включать его на этой базе никто не принимал. Дефолт и оказался частью механики.

Что могу привести без выдумки: режим блокировок — управляемый (не автоматический), СУБД MS SQL, is_read_committed_snapshot_on = 0 и snapshot_isolation_state = OFF по sys.databases, статистика снималась из DMV. Точный релиз платформы и режим совместимости наизусть не назову — база заказчика, доступа под рукой сейчас нет, а придумать цифру в таком разборе хуже, чем оставить пробел. Понадобится для сути — уточню и допишу в статью.

У себя это проверяется одной строкой:
SELECT is_read_committed_snapshot_on, snapshot_isolation_state_desc FROM sys.databases WHERE name = DB_NAME();

Встречный вопрос: какой из недостающих параметров, по-вашему, способен изменить картину? Режим совместимости на ожидание чтения за писателем в READ COMMITTED не влияет — если вы видите механизм, где влияет, это интереснее самой статьи.
Для отправки сообщения требуется регистрация/авторизация