Как определить, какой запрос заполняет журнал транзакций tempdb?

65

Я хотел бы знать, как определить точный запрос или сохраненный процесс, который фактически заполняет журнал транзакций базы данных TEMPDB.

Prasanth
источник
4
См. Раздел « Устранение неполадок, связанных с нехваткой места на диске» в базе данных tempdb на MSDN.
Дэвид Брабант
Я новичок в этом сайте и не уверен, как редактировать сообщение. У меня нет доступа к PROD, чтобы дать больше информации. Все, что я слышу от PROD DBA - твой код заполняет базу данных! Есть ли какие-либо рекомендации по кодированию, которым нужно следовать, чтобы убедиться, что наш код не заполняет журнал tempdb?
@prasanth Вам нужно будет зарегистрироваться на этом сайте с тем же openid, чтобы внести изменения в ваш вопрос здесь. Это зависит от того, что делает ваш код, и почему он использует базу данных tempdb. План выполнения должен показывать, что он делает, и если вы публикуете реальный код, мы можем помочь улучшить его.
Каде Ру
@CadeRoux Я думаю, что он пытается идентифицировать запрос (или запросы), не пытаясь выяснить, почему конкретный, известный запрос вызывает проблему.
Аарон Бертран
@AaronBertrand, да, но комментарий, кажется, указывает на то, что он хочет лучшие практики для кодирования.
Каде Ру

Ответы:

73

С http://www.sqlservercentral.com/scripts/tempdb/72007/

;WITH task_space_usage AS (
    -- SUM alloc/delloc pages
    SELECT session_id,
           request_id,
           SUM(internal_objects_alloc_page_count) AS alloc_pages,
           SUM(internal_objects_dealloc_page_count) AS dealloc_pages
    FROM sys.dm_db_task_space_usage WITH (NOLOCK)
    WHERE session_id <> @@SPID
    GROUP BY session_id, request_id
)
SELECT TSU.session_id,
       TSU.alloc_pages * 1.0 / 128 AS [internal object MB space],
       TSU.dealloc_pages * 1.0 / 128 AS [internal object dealloc MB space],
       EST.text,
       -- Extract statement from sql text
       ISNULL(
           NULLIF(
               SUBSTRING(
                 EST.text, 
                 ERQ.statement_start_offset / 2, 
                 CASE WHEN ERQ.statement_end_offset < ERQ.statement_start_offset 
                  THEN 0 
                 ELSE( ERQ.statement_end_offset - ERQ.statement_start_offset ) / 2 END
               ), ''
           ), EST.text
       ) AS [statement text],
       EQP.query_plan
FROM task_space_usage AS TSU
INNER JOIN sys.dm_exec_requests ERQ WITH (NOLOCK)
    ON  TSU.session_id = ERQ.session_id
    AND TSU.request_id = ERQ.request_id
OUTER APPLY sys.dm_exec_sql_text(ERQ.sql_handle) AS EST
OUTER APPLY sys.dm_exec_query_plan(ERQ.plan_handle) AS EQP
WHERE EST.text IS NOT NULL OR EQP.query_plan IS NOT NULL
ORDER BY 3 DESC;

РЕДАКТИРОВАТЬ

Как отметил Мартин в комментарии, он не найдет активные транзакции , которые занимают место в базе данных tempdb, он найдет только активные запросы , которые в настоящее время используют место (и, вероятно, виновники для текущего использования журнала). Таким образом, может быть открытая транзакция, но фактический запрос, вызывающий проблему, больше не выполняется.

Вы можете изменить inner joinon sys.dm_exec_requestsна a left outer join, тогда вы будете возвращать строки для сеансов, которые в данный момент не выполняют активно запросы.

В запросе Мартина размещены ...

SELECT database_transaction_log_bytes_reserved,session_id 
  FROM sys.dm_tran_database_transactions AS tdt 
  INNER JOIN sys.dm_tran_session_transactions AS tst 
  ON tdt.transaction_id = tst.transaction_id 
  WHERE database_id = 2;

... идентифицирует session_ids с активными транзакциями, которые занимают пространство журнала, но вы не обязательно сможете определить фактический запрос, вызвавший проблему, поскольку, если он не выполняется сейчас, он не будет зафиксирован в приведенном выше запросе для активные запросы. Вы можете быть в состоянии реактивно проверить самый последний запрос, используя, DBCC INPUTBUFFERно он может не сказать вам, что вы хотите услышать. Вы можете выполнить внешнее объединение аналогичным образом, чтобы захватить активно работающих пользователей, например

SELECT tdt.database_transaction_log_bytes_reserved,tst.session_id,
       t.[text], [statement] = COALESCE(NULLIF(
         SUBSTRING(
           t.[text],
           r.statement_start_offset / 2,
           CASE WHEN r.statement_end_offset < r.statement_start_offset
             THEN 0
             ELSE( r.statement_end_offset - r.statement_start_offset ) / 2 END
         ), ''
       ), t.[text])
     FROM sys.dm_tran_database_transactions AS tdt
     INNER JOIN sys.dm_tran_session_transactions AS tst
     ON tdt.transaction_id = tst.transaction_id
         LEFT OUTER JOIN sys.dm_exec_requests AS r
         ON tst.session_id = r.session_id
         OUTER APPLY sys.dm_exec_sql_text(r.plan_handle) AS t
     WHERE tdt.database_id = 2;

Вы также можете использовать DMV sys.dm_db_session_space_usageдля просмотра общего использования пространства по сеансам (но, опять же, вы не сможете получить действительные результаты для запроса; если запрос неактивен, то, что вы получите, может не быть фактическим виновником).

;WITH s AS
(
    SELECT 
        s.session_id,
        [pages] = SUM(s.user_objects_alloc_page_count 
          + s.internal_objects_alloc_page_count) 
    FROM sys.dm_db_session_space_usage AS s
    GROUP BY s.session_id
    HAVING SUM(s.user_objects_alloc_page_count 
      + s.internal_objects_alloc_page_count) > 0
)
SELECT s.session_id, s.[pages], t.[text], 
  [statement] = COALESCE(NULLIF(
    SUBSTRING(
        t.[text], 
        r.statement_start_offset / 2, 
        CASE WHEN r.statement_end_offset < r.statement_start_offset 
        THEN 0 
        ELSE( r.statement_end_offset - r.statement_start_offset ) / 2 END
      ), ''
    ), t.[text])
FROM s
LEFT OUTER JOIN 
sys.dm_exec_requests AS r
ON s.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.plan_handle) AS t
ORDER BY s.[pages] DESC;

Имея все эти запросы в вашем распоряжении, вы сможете определить, кто использует tempdb и как, особенно если вы поймали их в действии.

несколько советов по минимизации использования базы данных tempdb

  1. используйте меньше #temp таблиц и переменных @table
  2. свести к минимуму одновременное обслуживание индекса и исключить SORT_IN_TEMPDBвозможность, если она не нужна
  3. избегать ненужных курсоров; избегайте статических курсоров, если вы думаете, что это может быть узким местом, поскольку статические курсоры используют рабочие таблицы в базе данных tempdb - хотя это тип курсора, который я всегда рекомендую, если tempdb не является узким местом
  4. старайтесь избегать катушек (например, большие CTE, на которые ссылаются несколько раз в запросе)
  5. не используйте MARS
  6. тщательно протестируйте использование уровней изоляции моментального снимка / RCSI - не просто включите его для всех баз данных, поскольку вам сказали, что он лучше, чем NOLOCK (да, но он не бесплатный)
  7. в некоторых случаях это может звучать не интуитивно, но использовать больше временных таблиц. например, разбиение огромного запроса на части может быть немного менее эффективным, но если это поможет избежать огромного разлива памяти для базы данных tempdb, потому что для одного более крупного запроса требуется слишком большой грант памяти ...
  8. избегать включения триггеров для массовых операций
  9. избегайте чрезмерного использования типов больших объектов (макс. типов, XML и т. д.) в качестве локальных переменных
  10. сделайте транзакции короткими и приятными
  11. не устанавливайте базу данных tempdb как базу данных по умолчанию для всех -

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

Аарон Бертран
источник
Спасибо за ссылку, Аарон. В целом, существуют ли какие-либо рекомендации по кодированию, которые необходимо соблюдать, чтобы избежать заполнения журналов транзакций TEMPDB?
2
Хм, только что проверил это, и он не нашел мой оскорбительный сеанс, хотя session_idобнаруживается со следующим запросом SELECT database_transaction_log_bytes_reserved,session_id FROM sys.dm_tran_database_transactions tdt JOIN sys.dm_tran_session_transactions tst ON tdt.transaction_id = tst.transaction_id WHERE database_id = 2. Запрос я ожидал найти было после запуска следующейBEGIN TRAN CREATE TABLE #T(X CHAR(8000)) INSERT INTO #T SELECT name FROM sys.objects
Мартин Смит
@Martin: заметил, что в cte есть @@ SPID, который ограничит результаты до текущего сеанса. Если вы хотите, чтобы он охватывал все сеансы, удалите это.
Бен Тхул
@BenThul - я выполнил запрос в другом соединении. @@SPIDЭто <>не =. dm_db_task_space_usageотчеты 0для спида с открытой транзакцией для всех столбцов для меня. Интересно, если вам нужно запросить его, когда запрос фактически выполняется, а не простаивает с открытой транзакцией.
Мартин Смит
@MartinSmith запрос находит только активные запросы, а не активные транзакции. Так что, если запрос больше не выполняется, вы правы, вы можете отследить, используя транзакции DMV. Но вы не обязательно сможете выяснить запрос, который вызвал его, если он больше не выполняется - этот же spid мог выдать несколько других операторов в текущей транзакции.
Аарон Бертран
5

https://social.msdn.microsoft.com/Forums/sqlserver/en-US/17d9f862-b9ae-42de-ada0-4229f56712dc/tempdb-log-filling-cannot-find-how-or-what?forum=sqldatabaseengine

 SELECT tst.[session_id],
            s.[login_name] AS [Login Name],
            DB_NAME (tdt.database_id) AS [Database],
            tdt.[database_transaction_begin_time] AS [Begin Time],
            tdt.[database_transaction_log_record_count] AS [Log Records],
            tdt.[database_transaction_log_bytes_used] AS [Log Bytes Used],
            tdt.[database_transaction_log_bytes_reserved] AS [Log Bytes Rsvd],
            SUBSTRING(st.text, (r.statement_start_offset/2)+1,
            ((CASE r.statement_end_offset
                    WHEN -1 THEN DATALENGTH(st.text)
                    ELSE r.statement_end_offset
            END - r.statement_start_offset)/2) + 1) AS statement_text,
            st.[text] AS [Last T-SQL Text],
            qp.[query_plan] AS [Last Plan]
    FROM    sys.dm_tran_database_transactions tdt
            JOIN sys.dm_tran_session_transactions tst
                ON tst.[transaction_id] = tdt.[transaction_id]
            JOIN sys.[dm_exec_sessions] s
                ON s.[session_id] = tst.[session_id]
            JOIN sys.dm_exec_connections c
                ON c.[session_id] = tst.[session_id]
            LEFT OUTER JOIN sys.dm_exec_requests r
                ON r.[session_id] = tst.[session_id]
            CROSS APPLY sys.dm_exec_sql_text (c.[most_recent_sql_handle]) AS st
            OUTER APPLY sys.dm_exec_query_plan (r.[plan_handle]) AS qp
    WHERE   DB_NAME (tdt.database_id) = 'tempdb'
    ORDER BY [Log Bytes Used] DESC
GO
Saurabh Sinha
источник
4

Спасибо за этот пост, вероятно, единственный в своем роде. Мой тест был прост: создайте временную таблицу и убедитесь, что она появляется, когда я запускаю любой из запросов из этого поста ... только один или два действительно успешны. Я исправил его, чтобы присоединиться к T-SQL, оптимизировал его для более длительных запусков и сделал его довольно полезным. Дайте мне знать, если я что-то пропустил, но пока у вас есть автоматический / зацикленный скрипт. Он предоставляет способ оценить, какой запрос / SPID является нарушителем за определенный период времени, используя запрос стандартного отклонения (STDEV), приведенный ниже.

Это выполняется каждые 3 минуты в течение 40 раз, так что 2 часа. Измените параметры по своему усмотрению.

Ниже есть фильтр WHERE> 50 страниц, который люди могут захотеть очистить на случай, если у вас много небольших таблиц. В противном случае вы не поймете этот нюанс с ниже, как это ...

Наслаждайтесь!

DECLARE @minutes_apart INT; SET @minutes_apart = 3
DECLARE @how_many_times INT; SET @how_many_times = 40
--DROP TABLE tempdb..TempDBUsage
--SELECT * FROM tempdb..TempDBUsage
--SELECT session_id, STDEV(pages) stdev_pages FROM tempdb..TempDBUsage GROUP BY session_id HAVING STDEV(pages) > 0 ORDER BY stdev_pages DESC

DECLARE @delay_string NVARCHAR(8); SET @delay_string = '00:' + RIGHT('0'+ISNULL(CAST(@minutes_apart AS NVARCHAR(2)), ''),2) + ':00'
DECLARE @counter INT; SET @counter = 1

SET NOCOUNT ON
if object_id('tempdb..TempDBUsage') is null
    begin
    CREATE TABLE tempdb..TempDBUsage (
        session_id INT, pages INT, num_reads INT, num_writes INT, login_time DATETIME, last_batch DATETIME,
        cpu INT, physical_io INT, hostname NVARCHAR(64), program_name NVARCHAR(128), text NVARCHAR (MAX)
    )
    end
else
    begin
        PRINT 'To view the results run this:'
        PRINT 'SELECT * FROM tempdb..TempDBUsage'
        PRINT 'OR'
        PRINT 'SELECT session_id, STDEV(pages) stdev_pages FROM tempdb..TempDBUsage GROUP BY session_id HAVING STDEV(pages) > 0 ORDER BY stdev_pages DESC'
        PRINT ''
        PRINT ''
        PRINT 'Otherwise manually drop the table by running the following, then re-run the script:'
        PRINT 'DROP TABLE tempdb..TempDBUsage'
        RETURN
    end
--GO
TRUNCATE TABLE tempdb..TempDBUsage
PRINT 'To view the results run this:'; PRINT 'SELECT * FROM tempdb..TempDBUsage'
PRINT 'OR'; PRINT 'SELECT session_id, STDEV(pages) stdev_pages FROM tempdb..TempDBUsage GROUP BY session_id HAVING STDEV(pages) > 0 ORDER BY stdev_pages DESC'
PRINT ''; PRINT ''

while @counter <= @how_many_times
begin
INSERT INTO tempdb..TempDBUsage (session_id,pages,num_reads,num_writes,login_time,last_batch,cpu,physical_io,hostname,program_name,text)
    SELECT PAGES.session_id, PAGES.pages, r.num_reads, r.num_writes, sp.login_time, sp.last_batch, sp.cpu, sp.physical_io, sp.hostname, sp.program_name, t.text
    FROM sys.dm_exec_connections AS r
    LEFT OUTER JOIN master.sys.sysprocesses AS sp on sp.spid=r.session_id
    OUTER APPLY sys.dm_exec_sql_text(r.most_recent_sql_handle) AS t
    LEFT OUTER JOIN (
        SELECT s.session_id, [pages] = SUM(s.user_objects_alloc_page_count + s.internal_objects_alloc_page_count) 
        FROM sys.dm_db_session_space_usage AS s
        GROUP BY s.session_id
        HAVING SUM(s.user_objects_alloc_page_count + s.internal_objects_alloc_page_count) > 0
    ) PAGES ON PAGES.session_id = r.session_id
    WHERE PAGES.session_id IS NOT NULL AND PAGES.pages > 50
    ORDER BY PAGES.pages DESC;
PRINT CONVERT(char(10), @counter) + ': Ran at: ' + CONVERT(char(30), GETDATE())
SET @counter = @counter + 1
waitfor delay @delay_string
end
Джо Зи
источник
Объединение этого с принятым ответом - удобный способ отследить активность tempdb. Выполнение этого с помощью запланированной задачи агента SQL продолжит работу, даже если SSMS закрыт. Спасибо, что поделился!
Слесарь
1

К сожалению, журнал tempDB нельзя напрямую отследить до идентификаторов сеансов, просматривая запущенные процессы.

Сожмите файл журнала tempDB до такой степени, что он снова значительно вырастет. Затем создайте расширенное событие, чтобы зафиксировать рост журнала. Когда он снова вырастет, вы можете развернуть расширенное событие и просмотреть файл событий пакета. Откройте файл, добавьте фильтр времени, фильтр типа файла (вам не нужны результаты файла данных), а затем сгруппируйте его по идентификатору сеанса в SSMS. Это поможет вам найти виновника (ей), так как вы ищете идентификаторы сессий с наибольшим количеством групп. Конечно, вам нужно собрать то, что выполняется в идентификаторах сессий через другой процесс или инструмент. Может быть, кто-то знает, как получить запрос из столбца query_hash, и будет достаточно любезен, чтобы опубликовать решение.

Расширенные результаты мероприятия:

введите описание изображения здесь

Скрипт для создания расширенного события:

CREATE EVENT SESSION [tempdb_file_size_changed] ON SERVER ADD EVENT 
sqlserver.database_file_size_change(SET collect_database_name=(1)ACTION(sqlserver.client_app_name,sqlserver.client_hostname,sqlserver.is_system,sqlserver.query_hash,sqlserver.session_id,sqlserver.session_nt_username,sqlserver.sql_text,sqlserver.username) WHERE ([database_id]=(2))) ADD TARGETpackage0.event_file(SET filename=N'C:\ExtendedEvents\TempDBGrowth.xel',max_file_size=(100),max_rollover_files=(25)) WITH (MAX_MEMORY=4096 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=1 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=OFF,STARTUP_STATE=ON)
текила
источник