Редкие чужие данные и AV-дампы в SQL Server 2025: как понять, что виноват SESSION_CONTEXT в параллельном плане, а не ваш пул соединений
Если неверное значение SESSION_CONTEXT или AV-дамп появляются только в параллельных планах и пропадают при последовательном выполнении, это известный дефект движка SQL Server, а не ваш пул. Если воспроизводится и в последовательном плане, виноват код, не выставляющий контекст после Open. Ниже: признаки, проверка за час и обход.
Что именно сломано в SQL Server 2025 и при чём тут SESSION_CONTEXT
Мы в АйТи-Фреш сопровождаем базы на MS SQL и PostgreSQL в офисах до 50 рабочих мест, и самые неприятные заявки там не про упавший сервер. Упавший сервер виден сразу. Неприятно, когда раз в несколько дней у одного сотрудника открывается чужой список документов или пустой экран, а при повторе всё нормально. Такое списывают на «глюк» или на прикладной код, и разработчик месяц ищет у себя то, чего у него нет. Поэтому сразу к фактам: Microsoft перечисляет проблему в разделе известных проблем SQL Server 2025 (17.x), он называется «Incorrect behavior of SESSION_CONTEXT in parallel plans». Запросы со встроенной функцией SESSION_CONTEXT могут вернуть неверные результаты или вызвать access violation (AV) и дампы, если выполняются в параллельном плане. Причина названа прямо: взаимодействие функции с параллельными потоками, особенно когда сессия сбрасывается для повторного использования. Официальный обход: не выполнять SESSION_CONTEXT параллельно.
Второй факт, который меняет отношение к теме: это не новинка 2025 года. На странице самой функции SESSION_CONTEXT в таблице Known issues запись датирована январём 2022 года, статус «Has workaround», столбец «дата исправления» пуст. Там же объяснена история. Исправление неверных результатов в параллельных планах, появившееся в SQL Server 2019 (15.x) CU 14, позже вызвало AV-исключения при определённых условиях. Запись относится к SQL Server 2019 CU 14 и более поздним версиям, то есть наследуется и в 2022, и в 2025. На момент моей проверки 17.10.2026 обе страницы документации (последние правки в них датированы августом и сентябрём 2026 года) по-прежнему показывают обход, а не исправление. Если вы услышите «поставьте последнюю сборку, там починено», проверьте это по заметкам к выпуску своей сборки. Я такой записи в документации не нашёл, и придумывать номер обновления не стану.
Почему тема вообще болезненная. SESSION_CONTEXT нужен не ради красоты. Это штатный способ передать в базу идентификатор пользователя приложения, когда все работают под одной учётной записью подключения. Документация по row-level security показывает ровно этот сценарий: приложение после открытия соединения кладёт идентификатор в контекст сессии, а политика безопасности фильтрует строки по нему. Теперь представьте, что функция в параллельном потоке отдаёт не то значение. Фильтр безопасности тихо работает по чужому идентификатору. Красивая архитектура, в которой безопасность сидит в базе, а не в приложении, и одна редкая гонка в движке, которая эту безопасность обнуляет. Ирония бесплатная, последствия нет.
Как отличить дефект движка от ошибки приложения: признаки и таблица
Принцип такой: у двух версий разные отпечатки, и их можно развести без чтения исходников. Дефект движка привязан к плану выполнения. Он проявляется только там, где запрос идёт параллельно, исчезает, когда тот же запрос выполняется последовательно, и зависит от того, что сессия до этого была сброшена пулом. Ошибка приложения привязана к пути в коде. Она воспроизводится на конкретном сценарии, не зависит от степени параллелизма и не интересуется, сколько ядер у сервера. Отдельный признак дефекта движка: AV-дамп. Обычный запрос не должен ронять поток внутри sqlservr.exe, и если рядом с неверным значением в журнале ошибок SQL Server появляются сообщения об исключении и файлы дампов (искать стоит в каталоге журналов экземпляра, но проверьте путь у себя), прикладной код тут не главный подозреваемый.
Сведу признаки в таблицу, чтобы её можно было повесить рядом с монитором администратора. | Признак | Дефект движка (параллельный план) | Ошибка приложения или пула | |---|---|---| | Зависимость от плана | Проявляется при dop больше 1, пропадает при последовательном плане | Не зависит от параллелизма | | Повторяемость | Редкая, плавающая, на тех же данных то есть, то нет | Стабильная для одного сценария в коде | | Вид сбоя | Неверное значение или AV-дамп | NULL, значение прошлого запроса, ошибка при повторной установке | | Размер запроса | Только запросы, достаточно дорогие для параллелизма | Любые, в том числе мелкие | | Тестовая база | Обычно не воспроизводится (мало данных, план последовательный) | Воспроизводится и на тесте | | Что помогает | Флаг трассировки 11042 или запрос в один поток | Правка кода: установка контекста после каждого Open | У таблицы есть оговорка, о которой честно скажу сразу. Обе причины могут жить в одной системе одновременно, и в моих проектах так бывает чаще, чем чистый случай. Поэтому цель диагностики не выбрать одну виновницу, а разложить симптомы по двум корзинам и закрыть обе.
Теперь о том, что доказательством не является. «У разработчика на тестовой базе не воспроизводится» не говорит ничего: на малом объёме данных оптимизатор выбирает последовательный план, и дефект ему негде проявиться. «Раньше работало» тоже не аргумент, если за это время выросла таблица, поменялась статистика или кто-то правил порог стоимости параллелизма. Параллельный план появляется, когда стоимость запроса переходит заданный порог, и на небольших серверах этот порог часто никто не пересматривал. Если хотите разобраться в настройках параллелизма для 1С и других нагрузок, у нас есть отдельный разбор про MAXDOP и порог параллелизма. Здесь важно одно: меняется ли план, а не как красиво он называется.
Что пул соединений делает с контекстом сессии и где его ломает приложение
Теперь про вторую корзину. Пул соединений в ADO.NET (Microsoft.Data.SqlClient) переиспользует физические соединения. Open берёт соединение из пула, Close или Dispose сбрасывает его и возвращает обратно. Документация прямо говорит, что пул сбрасывает переиспользуемое состояние сессии перед выдачей соединения другому логическому подключению, но тут же добавляет важную строчку: приложение всё равно должно выставлять нужное состояние внутри своей единицы работы. В документации по row-level security то же самое сказано про read_only: значение нельзя изменить, пока соединение не закрыто, то есть не возвращено в пул. Правильный шаблон один: открыли соединение, сразу выставили контекст через sp_set_session_context, поработали, освободили. Пул тут не враг, он как раз заботливо чистит за вами. Беда в том, что чистит он перед вами, а вот выставить значение после очистки забывает уже код.
Вот список вещей, которые я чаще всего нахожу в коде, когда меня зовут разобраться с «магией»: - контекст установлен один раз при старте запроса, а затем в том же запросе открывается второе соединение (отчёт, фоновая задача, вложенный вызов), и на нём контекста нет; - вызов sp_set_session_context стоит до Open на другом объекте соединения, а выполняется на третьем, потому что соединение берётся из пула заново; - контекст устанавливается через @read_only = 1, а затем тот же путь кода выставляет его второй раз на том же логическом соединении: повторная установка read-only-ключа не пройдёт; - несколько пакетов MARS на одном соединении: значение, установленное одним пакетом, не видно пакетам, которые начались до его завершения, а при нескольких активных пакетах read_only вообще нельзя использовать; - функция возвращает sql_variant, а код сравнивает её с числом без явного приведения типа: тип значения на входе и тип в сравнении не совпали, результат неожиданный; - значение или ключ вылезли за ограничения: ключ до 128 байт, значение до 8000 байт, весь контекст сессии не больше 1 МБ. Каждый из этих пунктов дублирует симптом дефекта (неверная или пустая строка), и именно поэтому их так часто путают.
Ещё один инструмент, о котором почему-то вспоминают редко. В sys.dm_exec_sessions есть статус Dormant: по документации это сессия, которая была сброшена из-за пулинга и теперь находится в состоянии prelogin. То есть на сервере вы видите не «зависшие» соединения, а нормально отдыхающие после сброса. Поэтому набор Dormant-сессий от приложения не признак утечки, а признак работающего пула. Тревожные цифры в другом месте: если у приложения заметно больше пулов, чем должно быть, их плодит разница в строках подключения (порядок ключевых слов, отличающиеся Application Name, разные идентификаторы Windows). Документация называет это фрагментацией пулов. Размер одного пула по умолчанию 100 физических соединений (Max Pool Size), и поднимать его, не проверив, освобождаются ли соединения во всех ветках кода, не стоит. Лечится это на стороне приложения и к дефекту движка отношения не имеет.
Как проверить за час: пошаговая диагностика без остановки продакшена
Порядок работ такой, и я его не меняю. Шаг первый: зафиксировать версию и состояние флагов. Шаг второй: посмотреть, где у вас вообще бывают параллельные запросы, использующие контекст. Шаг третий: сравнить поведение в параллельном и в последовательном режимах на одном и том же запросе. Шаг четвёртый: проверить приложение логом, а не вдохновением. Начинаем с версии и флагов, это три строки, которые окупаются при любом обращении к вендору:
SELECT @@VERSION;
DBCC TRACESTATUS (-1);
DBCC TRACESTATUS (11042, -1);Команда DBCC TRACESTATUS по документации выполняется с правами роли public, а с ключом -1 показывает флаги, включённые глобально и в текущей сессии. В столбцах результата вы увидите Status, Global и Session, нам нужны единицы в нужных местах. Если флаг 11042 уже включён, а проблемы остаются, смотрите в сторону приложения.
Теперь ищем параллельные запросы в работе. Для этого достаточно двух динамических представлений. Столбцы dop и parallel_worker_count в sys.dm_exec_requests документированы начиная с SQL Server 2016, а статус Dormant, о котором мы говорили, находится в sys.dm_exec_sessions. Для просмотра чужих сессий в SQL Server 2022 и новее нужно право VIEW SERVER PERFORMANCE STATE, в более ранних версиях VIEW SERVER STATE. Запрос такой:
SELECT r.session_id, r.dop, r.parallel_worker_count,
s.program_name, s.status, r.start_time
FROM sys.dm_exec_requests AS r
JOIN sys.dm_exec_sessions AS s ON s.session_id = r.session_id
WHERE r.dop > 1;Выполняйте его несколько раз в часы пиковой нагрузки и записывайте program_name. Это быстро показывает, какие именно приложения гоняют параллельные запросы, в которых есть проверка прав через контекст. Если ни одного такого запроса нет, версия «дефект движка» резко теряет вес, потому что ему негде проявиться.
Третий шаг: контрольная проба. Мы берём запрос, на котором наблюдали сбой, и добавляем к нему сравнение: значение контекста, считанное один раз в переменную в начале пакета, и значение, которое функция отдаёт внутри запроса. Затем выполняем запрос дважды: с подсказкой на один поток и с разрешением параллелизма. Вот шаблон (имя таблицы и ключ замените на свои):
EXEC sys.sp_set_session_context @key = N'UserId', @value = 42;
DECLARE @expected int = CAST(SESSION_CONTEXT(N'UserId') AS int);
SELECT COUNT_BIG(*) AS bad_rows
FROM dbo.BigTable AS t
WHERE CAST(SESSION_CONTEXT(N'UserId') AS int) <> @expected
OPTION (MAXDOP 1);
-- то же самое, но с разрешённым параллелизмом
SELECT COUNT_BIG(*) AS bad_rows
FROM dbo.BigTable AS t
WHERE CAST(SESSION_CONTEXT(N'UserId') AS int) <> @expected
OPTION (MAXDOP 4);Ограничение честное: Microsoft не публикует воспроизводящий сценарий, а дефект связан со сбросом сессии для повторного использования, поэтому проба из одного окна SSMS его может и не поймать. Её смысл другой. Её нужно запускать через ту же прослойку подключения и тот же пул, что использует приложение, на большой таблице, где план действительно будет параллельным (проверяйте по плану выполнения и по столбцу dop). Ненулевой bad_rows при параллельном плане и ноль при MAXDOP 1 это почти приговор движку. Ненулевой результат в обоих режимах отправляет вас в код.
Четвёртый шаг самый скучный и самый результативный: журнал приложения. В точке, где код выставляет контекст, пишем в журнал идентификатор логического соединения, значение ключа и время. В точке, где выполняется бизнес-запрос, читаем SESSION_CONTEXT в переменную и пишем её тоже. Расхождение между «поставили» и «прочитали» в одном запросе это либо дефект, либо чужое соединение в середине цепочки, и по идентификатору соединения вы это разделите. Журнал включайте на ограниченный срок, он содержит идентификаторы пользователей. Бэкапы делают все, а вот проверку «положили то, что хотели, а прочитали то, что положили» почему-то считают излишней. Зря.
Если вам не хочется собирать эту диагностику самим ночью в субботу, это нормально. Мы делаем такую проверку как разовую работу или в рамках обслуживания сервера: смотрим версию, планы, пулы и код подключения, а на выходе вы получаете понятный вывод «движок или код» с доказательствами.
Как обойти дефект: флаг трассировки 11042 и чего он стоит
Официальный обход из документации: флаг трассировки 11042. Его можно включить как параметр запуска, как глобальный или как сессионный флаг, и он заставляет SESSION_CONTEXT выполняться последовательно, то есть функция больше не участвует в параллельных планах. Есть тонкость, которую я хочу отметить: в общем списке флагов на странице DBCC TRACEON и trace flags флага 11042 я не нашёл (список идёт от 11024 сразу к 11047). Единственное официальное описание лежит в записи Known issues на странице SESSION_CONTEXT, и ссылаться нужно на неё. Включить глобально, до перезапуска, можно так, а сессионный вариант получается без ключа -1:
DBCC TRACEON (11042, -1);
DBCC TRACESTATUS (11042, -1);Глобальный флаг, включённый через DBCC TRACEON, после перезапуска службы пропадает, и это любимая причина «вчера починили, сегодня опять». Проверку состояния флагов добавьте в мониторинг вместе с остальными.
Чтобы флаг пережил перезапуск, его задают параметром запуска. По документации: для флага в параметре -T используется заглавная T и без пробела между ключом и номером, то есть -T11042. Строчная t принимается сервером, но включает другие внутренние флаги, нужные только инженерам поддержки Microsoft, поэтому регистр здесь не мелочь. На Windows параметры запуска задаются в SQL Server Configuration Manager на вкладке Startup Parameters свойств службы, и после добавления службу нужно перезапустить. На Linux флаг запуска включается через mssql-conf: sudo /opt/mssql/bin/mssql-conf traceflag 11042 on, затем sudo systemctl restart mssql-server. Без перезапуска службы флаг из конфигурации не применится, это тоже прописано в документации к mssql-conf. Перезапуск экземпляра планируйте в окно, а не «на минутку, пока никого нет».
Теперь цена. Если функция выполняется последовательно, то запросы, где она участвует, перестают использовать параллельный план. Для небольших офисных нагрузок это обычно незаметно. Для тяжёлых отчётов с row-level security по большим таблицам цена может быть ощутимой, и это нужно измерять, а не угадывать: сравните время выполнения ваших 5 самых дорогих запросов до и после, на одинаковых данных. Не путайте флаг с радикальным лекарством в виде MAXDOP 1 на весь сервер. Это та самая ситуация «вылечить насморк гильотиной», и последствия для остальных нагрузок, включая 1С, вы потом будете разгребать долго. Про настройки параллелизма говорить мы любим в другом месте, а тут нужен точечный флаг. Заодно полезно знать, что это не единственный флаг запуска в известных проблемах SQL Server 2025: например, для проблемы с рабочими потоками при большом числе баз Microsoft рекомендует другой флаг, и там тоже важно, что включать его надо именно при запуске.
Есть и обходные приёмы на уровне запросов, которые я применяю там, где флаг по какой-то причине ставить нельзя. Первый: читать значение контекста в переменную в начале пакета и использовать в запросе уже переменную. Функция тогда вычисляется один раз и последовательно, а не внутри параллельного плана. Второй: добавлять к конкретному запросу OPTION (MAXDOP 1). Оба приёма мои инженерные решения, документация их как обход напрямую не называет (там сказано лишь не выполнять SESSION_CONTEXT параллельно), поэтому их нужно проверять на своих данных. И они не помогут там, где функция спрятана внутри предиката row-level security: в предикат переменную не передашь. Для такого случая остаётся флаг. И последнее: после включения запустите ту же контрольную пробу и наблюдайте несколько недель. Исчезновение редкого сбоя за три дня ничего не доказывает, редкое оно потому и редкое.
Как это выглядело на деле: кейс архитектурного бюро «ЧертёжБюро»
Условный клиент для этого разбора: архитектурное проектирование «ЧертёжБюро», 30 рабочих мест. Внутренний портал согласования чертежей и ведомостей работает на одном экземпляре SQL Server 2025 на виртуальной машине (16 vCPU, 64 ГБ памяти), база портала около 90 ГБ. Все пользователи ходят в базу под одной учётной записью подключения, а видимость проектов ограничена политикой row-level security, которая берёт идентификатор пользователя из SESSION_CONTEXT, ровно по сценарию из документации Microsoft. Жалоба звучала так: за месяц трое сотрудников один раз увидели чужие проекты или пустой список, при перезагрузке страницы всё исчезало. Разработчики подрядчика полтора месяца искали ошибку в коде, ничего не нашли и честно написали, что «это, наверное, сеть». Меня сеть в этом сценарии не убедила.
Работали мы в четыре шага за три рабочих дня. В первый день сняли версию и состояние флагов (флаг 11042 не был включён) и выяснили через sys.dm_exec_requests, что запросы списков проектов действительно идут с dop больше 1 в часы пик. Во второй день прошли по коду подключения: нашли две ветки, где контекст выставляется до открытия второго соединения для формирования отчёта. Это ровно из списка выше, и это настоящая ошибка приложения, которую подрядчик исправил за день. В третий день включили 11042 глобально, затем добавили -T11042 в параметры запуска службы и перезапустили экземпляр в вечернее окно. Контрольную пробу гоняли через ту же прослойку подключения, что и портал. Мы не пытались выбрать одного виновника, закрыли обе корзины. Это, на мой взгляд, и есть правильный подход к «редким» сбоям.
Итог через шесть недель наблюдения: повторных жалоб на чужие данные не было, лишних дампов в журнале экземпляра не появилось, время открытия самого тяжёлого списка проектов выросло незначительно (мы замеряли до и после на одинаковых данных, для 30 пользователей разница осталась в пределах нормы). Параллельный план в других запросах не пострадал, потому что флаг затрагивает только функцию. В регламент добавили две строки: проверка состояния флага в мониторинге после каждого перезапуска и пересмотр флага при установке очередного накопительного обновления, пока Microsoft не отметит исправление. Остальное обслуживание экземпляра ведётся по общему регламенту обслуживания SQL Server. Мораль простая: когда сбой редкий, не спорьте, чья вина, и разведите гипотезы по признакам из таблицы.
Частые вопросы
Это дефект именно SQL Server 2025?
Нет. В документации запись датирована январём 2022 года и относится к SQL Server 2019 CU 14 и более поздним версиям, включая 2022 и 2025. Статус на момент проверки (17.10.2026): есть обход, исправление не заявлено.
Какой флаг трассировки заставляет SESSION_CONTEXT выполняться последовательно?
Флаг 11042. Его можно задать как параметр запуска (-T11042 с заглавной T, без пробела), как глобальный (DBCC TRACEON (11042, -1)) или как сессионный флаг. Проверка состояния: DBCC TRACESTATUS (11042, -1).
Как быстро понять, что виноват движок, а не пул соединений?
Выполните запрос с проверкой контекста через прослойку приложения дважды: с OPTION (MAXDOP 1) и с разрешённым параллелизмом. Расхождение только в параллельном плане и AV-дампы указывают на движок, расхождение в обоих режимах указывает на код подключения.
Пул соединений сам очищает SESSION_CONTEXT?
Пул сбрасывает переиспользуемое состояние сессии перед выдачей соединения, но документация требует выставлять нужное состояние внутри единицы работы. Выставляйте контекст сразу после каждого Open, при необходимости с @read_only = 1.
Замедлит ли флаг 11042 сервер?
Только запросы, где используется SESSION_CONTEXT: они перестанут быть параллельными. Для небольших нагрузок это обычно незаметно, для тяжёлых отчётов с row-level security измерьте время до и после.
Источники
- Microsoft Learn: SQL Server 2025 Known Issues — Проверена запись «Incorrect behavior of SESSION_CONTEXT in parallel plans», симптомы и обход. https://learn.microsoft.com/en-us/sql/sql-server/sql-server-2025-known-issues?view=sql-server-ver17
- Microsoft Learn: SESSION_CONTEXT (Transact-SQL) — Проверены синтаксис, тип sql_variant, замечание про MARS и таблица Known issues с флагом 11042. https://learn.microsoft.com/en-us/sql/t-sql/functions/session-context-transact-sql?view=sql-server-ver17
- Microsoft Learn: sp_set_session_context — Проверены параметры @key, @value, @read_only и лимиты (128 байт, 8000 байт, 1 МБ). https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-set-session-context-transact-sql?view=sql-server-ver17
- Microsoft Learn: DBCC TRACESTATUS и параметры запуска службы — Проверены синтаксис DBCC TRACESTATUS и правило записи параметра -T. https://learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-tracestatus-transact-sql?view=sql-server-ver17 и https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/database-engine-service-startup-options?view=sql-server-ver17, а для Linux команда mssql-conf traceflag: https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-configure-mssql-conf?view=sql-server-ver17
- Microsoft Learn: пулы соединений SqlClient — Проверены сброс состояния сессии, Max Pool Size, фрагментация пулов. https://learn.microsoft.com/en-us/sql/connect/ado-net/sql-server-connection-pooling
- Microsoft Learn: Row-Level Security — Проверен сценарий с SESSION_CONTEXT и @read_only для приложения среднего звена. https://learn.microsoft.com/en-us/sql/relational-databases/security/row-level-security?view=sql-server-ver17



