АйТи Фреш
Главная / Статьи / 1С и базы данных
1С и базы данных

Включили Optimized Locking — и параллельные UPDATE стали давать другой результат

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~23 мин чтения
Включили Optimized Locking — и параллельные UPDATE стали давать другой результат
Иллюстрация к статье «Включили Optimized Locking — и параллельные UPDATE стали давать другой результат».

Вы мигрировали базу на SQL Server 2025, включили Accelerated Database Recovery и Optimized Locking, порадовались упавшим ожиданиям на блокировках — а через неделю бухгалтерия говорит, что часть документов «не провелась», и в логах приложения при этом ни одной ошибки. Никаких дедлоков, никаких таймаутов, просто другой набор обработанных строк. Виноват не баг, а компонент Optimized Locking под названием Lock After Qualification: он проверяет условие UPDATE по последней зафиксированной версии строки ещё до взятия блокировки и молча проходит мимо той строки, которую прямо сейчас меняет соседняя транзакция. Ниже — что именно ломается, как это выглядит на живом стенде, как доказать вину LAQ по расширенным событиям и какими четырьмя способами это лечится, не выключая всю оптимизацию целиком.

Тихая потеря заданий: как это выглядит из окопа

Самый неприятный класс проблем — тот, где нет ошибки. Приложение отработало, транзакция закоммитилась, метрики зелёные, а данные не те. Именно так выглядит переезд очереди заданий или конечного автомата статусов на базу с включённым Optimized Locking. Разработчик писал код в предположении, что второй UPDATE подождёт первый, увидит уже изменённое значение и отработает по нему. Пятнадцать лет это предположение исполнялось само собой — не потому что так гарантировано стандартом, а потому что движок брал U-блокировку на строке при проверке предиката, и вторая сессия честно висела в очереди.

В SQL Server 2025 (17.x) появилась возможность включить оптимизированные блокировки, и вот эта неявная зависимость отваливается. Условие WHERE проверяется без блокировки, по последней зафиксированной версии строки. Если строка под условие не подходит — сессия идёт дальше по скану и даже не узнает, что рядом кто-то её прямо сейчас доводит до нужного состояния. Никакого ожидания, никакой ошибки, ноль затронутых строк.

Дальше срабатывает вторая мина, уже в коде приложения. Практически в каждой самописной очереди есть строчка вида «если @@ROWCOUNT = 0, значит задание уже забрал другой воркер — снимаем его с обработки». Раньше нулевой rowcount действительно означал «кто-то опередил и довёл до конца». Теперь он означает ещё и «кто-то в этот момент только начал, и я решил не ждать». Задание выкидывается из обработки в состоянии, из которого оно уже никогда не выйдет.

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

Если ваш код где-то трактует @@ROWCOUNT = 0 как «работу уже сделал кто-то другой» — это первое место, куда надо идти после включения Optimized Locking. Второе — переходы статусов вида UPDATE ... SET status = 3 WHERE status = 2.
Памятка: Тихая потеря заданий: как это выглядит из окопа — схема
Памятка: Тихая потеря заданий: как это выглядит из окопа. Открыть схему в полном размере

Что вы включаете на самом деле: TID-блокировки, LAQ и три обязательных условия

Optimized locking состоит из двух разных механизмов, и путать их нельзя, потому что ломает семантику только один из них. Первый — TID locking, блокировка по идентификатору транзакции. Каждая строка внутри штампуется идентификатором последней изменившей её транзакции; вместо тысячи X-блокировок на ключах до конца транзакции держится одна X-блокировка на ресурсе XACT, а построчные и постраничные блокировки снимаются сразу после изменения строки. Это чистая экономия: меньше памяти под блокировки, практически нет эскалации, меньше дедлоков. Семантику TID-блокировки не меняют.

Второй механизм — Lock After Qualification, LAQ. Он надстроен над TID и меняет порядок действий при DML. Без него движок сначала берёт U-блокировку на строке, потом проверяет предикат, и если тот выполнился — конвертирует блокировку в X и держит её до конца транзакции. С LAQ предикат проверяется оптимистично, по последней зафиксированной версии строки, вообще без блокировок; X-блокировка берётся только на подошедшую строку и снимается сразу после изменения, не дожидаясь коммита. Отсюда и рост параллельности — и отсюда же изменение поведения.

Жёстких требований два, плюс одна особенность, о которой забывают. Optimized locking включается только на базе, где уже включён Accelerated Database Recovery. Компонент LAQ работает только при включённом READ_COMMITTED_SNAPSHOT и только на уровне изоляции READ COMMITTED — без RCSI вы получите одни TID-блокировки. А особенность в том, что в SQL Server 2025 всё это по умолчанию выключено и включается на уровне отдельной базы, в отличие от Azure SQL Database, SQL database в Microsoft Fabric и Azure SQL Managed Instance с политиками обновления Always-up-to-date и SQL Server 2025, где оптимизированные блокировки включены всегда. Это отдельный повод для внимания при переносе базы из Azure на локальный сервер и обратно: поведение конкурентных UPDATE может отличаться между средами.

Порядок включения такой (все три команды требуют, чтобы к базе не было других активных подключений, кроме вашего — отсюда ROLLBACK IMMEDIATE; переводить базу в single-user режим при этом не нужно):

ALTER DATABASE optika_orders SET ACCELERATED_DATABASE_RECOVERY = ON WITH ROLLBACK IMMEDIATE;
ALTER DATABASE optika_orders SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
ALTER DATABASE optika_orders SET OPTIMIZED_LOCKING = ON WITH ROLLBACK IMMEDIATE;

Проверка текущего состояния — одним запросом, я держу его в закладках SSMS и прогоняю на каждой базе перед разбором любого «странного» инцидента:

SELECT database_id, name,
       is_accelerated_database_recovery_on,
       is_read_committed_snapshot_on,
       is_optimized_locking_on
FROM sys.databases
WHERE name = DB_NAME();

И короткий вариант для скриптов: SELECT DATABASEPROPERTYEX(DB_NAME(), 'IsOptimizedLockingOn'); — вернёт 1, 0 или NULL, если на этой платформе фича вообще недоступна.

Практический вывод, который экономит нервы: если нужна только экономия памяти и уход от эскалации, а рисковать семантикой не хочется — Optimized Locking на базе без RCSI даст TID-блокировки без LAQ. Но не переносите это правило на базы 1С вслепую: у баз 1С 8.3 на MS SQL READ_COMMITTED_SNAPSHOT нередко оказывается включён, поэтому сначала посмотрите is_read_committed_snapshot_on в sys.databases, а уже потом решайте.
Включили Optimized Locking — и параллельные UPDATE стали давать другой результат — схема
Схема к статье. Открыть схему в полном размере

Пример на четыре строки, который объясняет всё

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

CREATE TABLE t4 (a int NOT NULL, b int NULL);
INSERT INTO t4 VALUES (1, 1);

Дальше две сессии. Первая: BEGIN TRANSACTION T1; UPDATE t4 SET b = 2 WHERE a = 1; — и транзакция остаётся открытой. Вторая: BEGIN TRANSACTION T2; UPDATE t4 SET b = 3 WHERE b = 2;. Потом коммитим первую, потом вторую.

Без LAQ вторая транзакция блокируется и ждёт первую. Как только T1 закоммитилась, T2 видит b = 2, предикат выполняется, строка обновляется. В таблице остаётся a = 1, b = 3. Именно на это поведение опирается 90 % самописных конечных автоматов: «дождись предыдущего шага и продолжи с того места».

С LAQ вторая транзакция берёт последнюю зафиксированную версию строки, а там ещё b = 1. Предикат b = 2 не выполняется, строка пропускается, оператор завершается мгновенно и без единой блокировки. В таблице остаётся a = 1, b = 2. Разный итог данных при одном и том же тексте запросов и одном и том же порядке команд — вот вся суть проблемы на четырёх строках кода.

Отдельно про требалификацию, потому что её часто понимают наоборот. Движок действительно перепроверяет предикат — но только для строки, которая уже прошла квалификацию, и если строку успела изменить другая транзакция. То есть повторная проверка спасает от записи по устаревшим данным в подошедшей строке, но она принципиально не возвращает строку, которая под условие не подошла с первого раза. Пропущенная строка пропущена окончательно. Более того, если план запроса использует оператор, который требалификацию не поддерживает, движок внутренне прерывает выполнение оператора и перезапускает его уже без LAQ — при этом срабатывает расширенное событие lock_after_qual_stmt_abort.

Цитата из документации, которую стоит распечатать и повесить над столом разработчика: даже без LAQ приложение не должно предполагать, что движок гарантирует строгий порядок без locking-хинтов при изоляции с версионированием строк. Рекомендация Microsoft для нагрузок, завязанных на порядок, — переходить на REPEATABLE READ или SERIALIZABLE.

Стенд салона оптики «Зоркий глаз»: очередь заказов в мастерскую и 137 потерянных заданий

Разбор из практики. Салон оптики «Зоркий глаз», 11 рабочих мест: консультанты в зале, оптометрист, мастер по сборке очков и бухгалтер. Помимо 1С у салона есть самописный сервис заказов поверх SQL Server — заказ на линзы и сборку очков проходит путь от приёма в зале до выдачи клиенту, параллельно синхронизируется с сайтом и с поставщиком линз. Переезжали с SQL Server 2019 Standard на SQL Server 2025 (17.x) Standard, Windows Server 2022, виртуалка 4 vCPU / 16 ГБ. База optika_orders — около 38 ГБ вместе с историей заказов и сканами рецептов, RCSI на ней был включён ещё разработчиком сервиса, поэтому вопрос «включать ли ADR и Optimized Locking» решался легко: ночная пересборка остатков оправ и линз упиралась в эскалацию блокировок, и утренние синхронизации с сайтом падали по таймауту.

Включили ADR (PVS оставили на PRIMARY — отдельная файловая группа под версии на таком объёме не нужна), затем OPTIMIZED_LOCKING = ON. Эффект по метрикам был ровно тот, за которым шли: пиковое потребление памяти под блокировки на ночной пересборке упало примерно с 260 МБ до 20 МБ, ожидания LCK_M_U в этом окне ушли практически в ноль, лок-эскалаций на таблице остатков за неделю — ноль вместо 10–15 за ночь. Ночной регламент стал укладываться в окно примерно на 18 % быстрее. Пока всё прекрасно.

Через шесть дней администратор салона заметила, что несколько готовых заказов не ушли клиентам в SMS «очки готовы», а бухгалтер — что они не выгрузились в 1С. Таблица dbo.job_queue, около 90 тысяч строк за три года, статусы 0 (новое) → 1 (взято) → 2 (готово к выгрузке) → 3 (выгружено и отправлено уведомление), три фоновых воркера на .NET в бесконечном цикле. Воркер, который переводит 2 → 3, делал так:

UPDATE dbo.job_queue
   SET status = 3, done_at = SYSUTCDATETIME()
 WHERE job_id = @id AND status = 2;
-- в коде: if (rowsAffected == 0) { MarkHandledByPeer(id); return; }

Строка MarkHandledByPeer и есть та самая мина. Раньше при нулевом rowcount задание действительно было уже проведено другим воркером. Теперь ноль возвращался и в ситуации, когда воркер, переводящий 1 → 2, ещё держал транзакцию открытой: LAQ проверял status = 2 по последней зафиксированной версии, видел там 1, строку пропускал.

Замерили честно. Синтетический прогон на копии базы: 20 000 заданий (это больше, чем салон набирает за полгода), три воркера, средняя длительность транзакции этапа 1 → 2 около 40 мс. С LAQ 137 заданий (0,69 %) уехали в «обработано соседом», не будучи обработанными вообще. После того как в этот один запрос добавили хинт READCOMMITTEDLOCK, повторный прогон дал ровно ноль потерь при том же профиле нагрузки и практически той же длительности прогона — 6 минут 12 секунд против 6 минут 05 секунд. То есть цена локальной защиты — семь секунд на двадцати тысячах заданий, а не откат всей оптимизации на всей базе.

Не откатывайте Optimized Locking на всей базе из-за одного сломавшегося сценария. Ломается, как правило, две-три конкретные инструкции DML, где предикат стоит на колонке, которую параллельно меняет другая транзакция. Их и лечите точечно.
Цифры и версии: Стенд салона оптики «Зоркий глаз»: очередь заказов в мастерскую и 137 потерянных заданий — схема
Цифры и версии: Стенд салона оптики «Зоркий глаз»: очередь заказов в мастерскую и 137 потерянных заданий. Открыть схему в полном размере

Диагностика: как доказать, что виноват именно LAQ

Первое, что нужно понять: обычными средствами вы этого не увидите. Нет ошибки, нет дедлока, нет длинного ожидания — наоборот, ожидания исчезли. Поэтому диагностика строится не на поиске проблем, а на подтверждении гипотезы: «в момент X оператор отработал без блокировки и вернул ноль строк».

Штатный инструмент — расширенные события. Событие locking_stats срабатывает по каждой базе раз в несколько минут и отдаёт агрегированную статистику: сколько было эскалаций, включены ли компоненты TID locking и LAQ, и сколько запросов не использовали LAQ и по каким причинам. Именно последняя часть отвечает на вопрос «а вообще работает ли у меня LAQ на этой инструкции». Событие locking_stats2 (есть в SQL Server и Azure SQL Managed Instance) добавляет статистику по skip index locks и по эвристикам LAQ. Событие lock_after_qual_stmt_abort срабатывает при внутреннем перезапуске оператора из-за конфликта с другой транзакцией — на нашем стенде за десять минут нагрузки оно отработало 4 812 раз, и это было первым внятным сигналом, что LAQ действительно активен именно на этом коде.

CREATE EVENT SESSION [laq_watch] ON SERVER
ADD EVENT sqlserver.lock_after_qual_stmt_abort,
ADD EVENT sqlserver.locking_stats,
ADD EVENT sqlserver.locking_stats2
ADD TARGET package0.event_file (SET filename = N'C:\xe\laq_watch.xel', max_file_size = 64)
WITH (MAX_MEMORY = 8MB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, STARTUP_STATE = ON);
GO
ALTER EVENT SESSION [laq_watch] ON SERVER STATE = START;

Второе место — блокировки в динамических представлениях. При включённых оптимизированных блокировках в sys.dm_tran_locks вы увидите ресурс типа XACT вместо привычной россыпи KEY и PAGE: на пишущей транзакции ровно одна X-блокировка на XACT. Ожидания тоже переименовались — вместо LCK_M_U и LCK_M_X появляются LCK_M_S_XACT_READ (ждём с намерением прочитать) и LCK_M_S_XACT_MODIFY (ждём с намерением изменить). Если вы поддерживаете свой мониторинг блокировок с фильтром по типам ожиданий, его надо править: старые правила при OPTIMIZED_LOCKING = ON просто перестанут срабатывать, и вам покажется, что блокировок в базе не осталось. В графе дедлоков появился элемент <xactlock>, раскрывающий, какие реальные ресурсы стоят за блокировкой TID, — без него разбор дедлока на 2025 превращается в гадание.

Третье — Query Store. Движок ведёт обратную связь по LAQ на уровне плана запроса, и найти планы, где LAQ был выключен эвристикой, можно в sys.query_store_plan_feedback: у таких записей feature_id = 4 и feature_desc = 'LAQ Feedback'. Эта же обратная связь переживает перезапуск базы, если запрос попал в Query Store. Отсюда следует практический вывод, который сбивает с толку половину тех, кто пытается воспроизвести проблему на тесте: на одном и том же сервере один и тот же запрос может выполниться и с LAQ, и без него.

SELECT plan_id, feature_id, feature_desc, state_desc, create_time
FROM sys.query_store_plan_feedback
WHERE feature_desc = 'LAQ Feedback'
ORDER BY create_time DESC;
Отдельно проверьте свой мониторинг блокировок и алерты Zabbix/SCOM. После включения оптимизированных блокировок правила, завязанные на LCK_M_U и LCK_M_X, замолкают — и вы теряете видимость ровно тогда, когда она нужнее всего.

Как чинить: четыре рабочих способа и один вредный

Способ первый и основной — хинт READCOMMITTEDLOCK на той таблице, где нужен старый блокирующий порядок. Это официально рекомендованный Microsoft ответ на вопрос «как заставить запросы блокироваться, несмотря на оптимизированные блокировки», и работает он именно при включённом RCSI. LAQ на такой инструкции не применяется, поведение возвращается к дособытийному: движок берёт U-блокировку, ждёт соседнюю транзакцию, перепроверяет предикат уже по её результату.

UPDATE q
   SET status = 3, done_at = SYSUTCDATETIME()
  FROM dbo.job_queue AS q WITH (READCOMMITTEDLOCK)
 WHERE q.job_id = @id AND q.status = 2;

Важный нюанс: хинт на одной таблице не отключает оптимизированные блокировки для остальных таблиц того же запроса, и не влияет на другие запросы к этой же таблице. Это не рубильник, это скальпель — за что я его и люблю.

Способ второй — переписать предикат так, чтобы он не зависел от колонки, которую параллельно меняет другая транзакция. Если условие стоит на первичном ключе, а изменяемая колонка в WHERE не участвует, LAQ ничего не ломает: строка квалифицируется по стабильному значению. В сервисе заказов салона часть переходов мы так и переделали: вместо WHERE status = 2 берём пачку job_id отдельным запросом, а UPDATE выполняем по ключам с проверкой rowversion. Дольше по коду, зато поведение перестаёт зависеть от настроек базы.

Способ третий — классический паттерн очереди с WITH (UPDLOCK, READPAST). Он и раньше был правильным способом разбирать очередь несколькими воркерами, а теперь получил бонус: UPDLOCK входит в список конфликтующих хинтов, при которых LAQ не применяется, а READPAST заставляет пропускать занятые строки явно и предсказуемо, а не по стечению обстоятельств. Способ четвёртый — более строгий уровень изоляции, REPEATABLE READ или SERIALIZABLE на конкретной транзакции; это официальная рекомендация для нагрузок, завязанных на строгий порядок, но цена в параллельности выше, чем у точечного хинта, поэтому я его держу как запасной.

А теперь вредный способ, который встречается чаще всех перечисленных: навесить READCOMMITTEDLOCK или UPDLOCK на все запросы через шаблон в ORM или через план-гайд. Смысл включения оптимизированных блокировок при этом теряется полностью — locking-хинты заставляют движок брать построчные и постраничные блокировки и держать их до конца транзакции, ровно как раньше. Вы получите старую производительность, но уже с накладными расходами ADR и версионного хранилища. Если после аудита выяснилось, что хинты нужны почти везде, — честнее выключить OPTIMIZED_LOCKING на базе и вернуться к вопросу после рефакторинга приложения.

Перед правкой убедитесь, что проблема вообще ваша. Если предикат UPDATE стоит только на неизменяемых колонках (ключ, дата создания), LAQ на результат не влияет — вы просто получите меньше блокировок и ничего больше.
Памятка: Как чинить: четыре рабочих способа и один вредный — схема
Памятка: Как чинить: четыре рабочих способа и один вредный. Открыть схему в полном размере

Где LAQ не работает вовсе, почему он не воспроизводится и как внедрять

Половина писем в духе «мы прогнали ваш сценарий, у нас всё как раньше» объясняется тем, что LAQ в этом сценарии просто не применялся. Список случаев, когда он выключается, довольно длинный, и его стоит знать наизусть — он же служит подсказкой при разборе инцидента: если ваш проблемный запрос попадает в список, ищите причину в другом месте. Отдельно отмечу самый частый пункт для очередей: если у инструкции есть предложение OUTPUT, вставляющее данные в табличную переменную или возвращающее результирующий набор, LAQ не используется. То есть классический паттерн UPDATE TOP (1) ... SET status = 1 OUTPUT inserted.job_id ... WHERE status = 0 от этой проблемы защищён по построению. Хорошая новость для тех, кто писал очередь по канону.

Второй источник «невоспроизводимости» — эвристики. Движок ведёт обратную связь: измеряет в логических чтениях объём потенциально впустую сделанной работы и долю перезапущенных операторов, и если пороги превышены — LAQ отключается автоматически, для конкретного плана запроса или для всей базы. Когда показатели возвращаются в норму, LAQ включается обратно. Обратная связь по плану живёт в Query Store и переживает перезапуск базы, а обратная связь уровня базы пересобирается после каждого старта. Практический вывод: не пытайтесь воспроизвести эффект на «прогретой» базе, где нагрузка уже успела отключить LAQ. Тест делайте на свежем экземпляре и обязательно проверяйте через locking_stats, что LAQ реально включён.

Теперь про порядок внедрения, раз уж мы дошли до конца. Я не отговариваю от Optimized Locking — на массовых UPDATE выигрыш реальный и измеримый. Я отговариваю включать его в пятницу вечером одной командой на боевой базе. Мой чек-лист: сначала аудит кода — грепом по репозиторию ищем UPDATE и DELETE, у которых в WHERE стоит колонка, изменяемая другими транзакциями (статусы, флаги, признаки занятости, остатки), и отдельно места, где нулевой rowcount трактуется как бизнес-решение; в сервисе заказов «Зоркого глаза» таких нашлось четыре инструкции на примерно шесть тысяч строк кода. Потом стенд с копией прода, сессия расширенных событий и нагрузка реальным профилем, а не синтетикой на трёх строках, с проверкой бизнес-инвариантов, а не только времени выполнения. Потом точечные хинты и повторный прогон. И только потом прод — с включённым Query Store и обновлёнными правилами мониторинга под ожидания XACT. Про откат помните главное: сначала OPTIMIZED_LOCKING = OFF, только потом ADR, обратный порядок движок не пропустит.

Отдельно про 1С, потому что в небольших компаниях на том же SQL Server почти всегда крутится ещё и бухгалтерия. Запросы платформы 1С вы не правите, хинты в них не расставите, а управление блокировками платформа частично берёт на себя. Поэтому для баз 1С порядок у меня простой: проверить, какие версии SQL Server заявлены вендором как поддерживаемые для вашей версии платформы, посмотреть в sys.databases фактическое состояние READ_COMMITTED_SNAPSHOT и ADR, и не включать OPTIMIZED_LOCKING на рабочей базе 1С без прогона типовых регламентов (закрытие месяца, проведение документов пакетом, обмены) на копии. Базу самописного сервиса и базу 1С на одном экземпляре можно и нужно настраивать по-разному — опция задаётся на уровне базы, а не сервера.

И про честность формулировок. Единого мнения в сообществе по поводу «включать ли по умолчанию» пока нет: в Azure SQL Database выбора вообще не дают, и весь мир там живёт с LAQ давно и без катастроф, а на локальных серверах Microsoft осознанно оставила фичу выключенной именно из-за таких изменений поведения. Моя позиция: включать стоит, но только после аудита кода и только на базах, где вы контролируете приложение. Для покупных систем, где вы не можете поправить ни одного запроса, я оставляю OPTIMIZED_LOCKING выключенным, пока вендор явно не подтвердит поддержку.

Пункт про «более одного оператора поиска или сканирования» — главная причина плавающего поведения: смена плана после обновления статистики включает или выключает LAQ на одном и том же тексте запроса. И отдельно: если приложение покупное и править запросы вы не можете — не включайте оптимизированные блокировки на этой базе вообще.

Частые вопросы

Нужно ли включать Optimized Locking сразу после переезда на SQL Server 2025?

Нет, и торопиться не надо. В SQL Server 2025 (17.x) оптимизированные блокировки по умолчанию выключены, и это осознанное решение Microsoft именно из-за изменения поведения конкурентных UPDATE. Сначала аудит кода на предикаты по изменяемым колонкам и на трактовку нулевого @@ROWCOUNT, потом стенд с нагрузкой, и только потом прод.

Как быстро проверить, включён ли LAQ на моей базе?

Одним запросом: SELECT database_id, name, is_accelerated_database_recovery_on, is_read_committed_snapshot_on, is_optimized_locking_on FROM sys.databases WHERE name = DB_NAME(). LAQ активен только если включены все три: ADR, RCSI и OPTIMIZED_LOCKING, и при этом транзакция выполняется на уровне изоляции READ COMMITTED. Дополнительно фактическое состояние компонентов показывает расширенное событие locking_stats.

Затронет ли это базы 1С?

Зависит от состояния READ_COMMITTED_SNAPSHOT на конкретной базе, и гадать тут нельзя: у баз 1С 8.3 на MS SQL RCSI нередко включён. Если RCSI выключен, OPTIMIZED_LOCKING даст только TID-блокировки — меньше памяти под блокировки и меньше эскалаций без изменения семантики. Если включён — активируется и LAQ, а запросы платформы вы править не можете. Поэтому сначала запрос к sys.databases, проверка поддержки SQL Server 2025 вашей версией платформы у вендора и прогон регламентов на копии базы, и только потом решение по боевой.

Спасёт ли переход на SERIALIZABLE?

Да, но это тяжёлое лекарство. LAQ не применяется на любом уровне изоляции, отличном от READ COMMITTED, поэтому REPEATABLE READ и SERIALIZABLE возвращают блокирующее поведение — это официальная рекомендация Microsoft для нагрузок, зависящих от строгого порядка транзакций. Только цена в параллельности выше, чем у точечного хинта READCOMMITTEDLOCK на одной инструкции.

А MERGE и OUTPUT затронуты?

Нет. LAQ не используется в инструкциях MERGE, а также если в DML есть предложение OUTPUT, вставляющее данные в табличную переменную или возвращающее результирующий набор, и если в инструкции есть присваивание переменной. Классический паттерн разбора очереди через UPDATE ... OUTPUT inserted.id от этой проблемы защищён по построению.

Можно ли просто выключить обратно, если что-то пошло не так?

Можно, но помните порядок: сначала ALTER DATABASE ... SET OPTIMIZED_LOCKING = OFF, и только затем при необходимости выключается ADR — обратный порядок движок не пропустит. Обе команды требуют, чтобы к базе не было других активных подключений, кроме выполняющего, поэтому планируйте окно и добавляйте WITH ROLLBACK IMMEDIATE.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи