Як визначити, який запит заповнює журнал транзакцій tempdb?


65

Я хотів би знати, як визначити точний запит або збережений протокол, який фактично заповнює журнал транзакцій бази даних TEMPDB.



Я новачок на цьому сайті і не знаю, як редагувати публікацію. У мене немає доступу до PROD, щоб надати більше інформації. Все, що я чую від PROD DBA - ваш код заповнює tempdb! Чи є якісь найкращі практики кодування, яких слід дотримуватися, щоб переконатися, що наш код не заповнює журнал tempdb?

@prasanth Вам потрібно буде зареєструватися на цьому веб-сайті разом із вашим тим же відкритим, щоб внести зміни до свого питання тут. Це залежить від того, що робить ваш код, чому він використовує 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;

EDIT

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

Ви можете змінити значення " inner joinна" sys.dm_exec_requestsна "" 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. уникайте надмірного використання типів LOB (максимум типів, 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 Smith

@Martin: Помічено, що в cte є @@ SPID, який обмежує результати поточної сесії. Якщо ви хочете, щоб він охоплював усі сеанси, видаліть це.
Бен Тул

@BenThul - я провів запит в іншому з'єднанні. @@SPIDЦе <>НЕ =. dm_db_task_space_usageзвіти 0про spid з відкритою транзакцією для всіх стовпців для мене. Цікаво, чи потрібно запитувати його, коли запит справді виконується, а не простоюється з відкритою транзакцією.
Мартін Сміт

@MartinSmith запит знаходить лише активні запити, а не активні транзакції. Отже, якщо запит більше не працює, ви маєте рацію, ви можете відстежувати, використовуючи DMV транзакції. Але ви не обов'язково зможете з'ясувати запит, який його викликав, якщо він більше не працює - той самий шпід, можливо, видав кілька інших заяв у поточній транзакції.
Аарон Бертран

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

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 не може бути безпосередньо простежений до sessionID, переглядаючи запущені процеси.

Скоротіть файл журналу 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.