Comment identifier la requête qui remplit le journal de transactions tempdb?

65

Je voudrais savoir comment identifier la requête exacte ou la procédure stockée qui est en train de remplir le journal transactionnel de la base de données TEMPDB.

prasanthe
la source
Je suis nouveau sur ce site et je ne sais pas comment éditer le post. Je n'ai pas accès à PROD pour donner plus d'informations. Tout ce que j'entends de PROD DBA, c'est que votre code remplit la base de données! Existe-t-il des meilleures pratiques de codage à suivre afin de s’assurer que notre code ne remplit pas le journal de tempdb?
@prasanth Vous devrez vous inscrire sur ce site avec votre même identifiant pour apporter des modifications à votre question ici. Cela dépend de ce que votre code fait pour savoir pourquoi il utilise tempdb. Le plan d'exécution doit montrer ce qu'il fait et si vous postez le code réel, nous pouvons vous aider à l'améliorer.
Cade Roux
@CadeRoux Je pense qu'il essaie d'identifier la requête (ou les requêtes), sans essayer de comprendre pourquoi une requête spécifique et connue est à l'origine du problème.
Aaron Bertrand
@AaronBertrand oui, mais le commentaire semble indiquer qu'il souhaite de meilleures pratiques en matière de codage.
Cade Roux

Réponses:

73

De 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;

MODIFIER

Comme Martin l'a fait remarquer dans un commentaire, les transactions actives occupant de l'espace dans tempdb ne seraient pas identifiées , mais uniquement les requêtes actives utilisant actuellement cet espace (et probablement responsables de l'utilisation actuelle du journal). Il peut donc y avoir une transaction ouverte, mais la requête à l'origine du problème n'est plus en cours d'exécution.

Vous pouvez changer le inner joinsur sys.dm_exec_requestsen left outer join, puis vous retournerez des lignes pour les sessions qui n'exécutent pas activement de requêtes.

La requête posée par Martin ...

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;

... identifierait session_ids avec des transactions actives occupant de l'espace de journalisation, mais vous ne seriez pas nécessairement en mesure de déterminer la requête réelle à l'origine du problème, car s'il n'est pas en cours d'exécution, il ne sera pas capturé dans la requête ci-dessus pour demandes actives. Vous pourrez peut-être vérifier de manière réactive la requête la plus récente en utilisant, DBCC INPUTBUFFERmais cela ne vous dira peut-être pas ce que vous voulez entendre. Vous pouvez créer une jointure externe de manière similaire pour capturer ceux qui sont en cours d'exécution, par exemple:

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;

Vous pouvez également utiliser le logiciel DMV sys.dm_db_session_space_usagepour voir l'utilisation globale de l'espace par session (mais encore une fois, vous risquez de ne pas obtenir des résultats valides pour la requête; si la requête n'est pas active, ce que vous récupérez ne sera peut-être pas le véritable coupable).

;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;

Avec toutes ces questions à votre disposition, vous devriez être en mesure de déterminer qui utilise tempdb et comment, surtout si vous les surprenez en flagrant délit.

Quelques astuces pour minimiser l'utilisation de tempdb

  1. utiliser moins de tables #temp et de variables @table
  2. minimiser la maintenance simultanée des index et éviter l' SORT_IN_TEMPDBoption si elle n'est pas nécessaire
  3. éviter les curseurs inutiles; évitez les curseurs statiques si vous pensez que cela peut être un goulot d'étranglement, car les curseurs statiques utilisent des tables de travail dans tempdb - bien que ce soit le type de curseur que je recommande toujours si tempdb n'est pas un goulot d'étranglement
  4. essayez d'éviter les spools (par exemple, les grands CTE référencés plusieurs fois dans la requête)
  5. n'utilisez pas MARS
  6. testez minutieusement l'utilisation des niveaux d'isolement des instantanés / RCSI - ne l'activez pas uniquement pour toutes les bases de données, car on vous a dit que c'était mieux que NOLOCK (c'est vrai, mais ce n'est pas gratuit)
  7. dans certains cas, cela peut sembler peu intuitif, mais utilisez davantage de tables temporaires. Par exemple, diviser une requête volumineuse en plusieurs parties peut être légèrement moins efficace, mais si elle peut éviter un débordement important de la mémoire sur tempdb, car la requête simple et volumineuse nécessite une allocation mémoire trop importante ...
  8. éviter d'activer les déclencheurs pour les opérations en bloc
  9. éviter l'utilisation excessive de types LOB (types max, XML, etc.) en tant que variables locales
  10. garder les transactions courtes et faciles
  11. ne définissez pas tempdb comme base de données par défaut pour tout le monde -

Vous pouvez également considérer que l'utilisation de votre journal tempdb peut être provoquée par des processus internes sur lesquels vous n'avez que peu ou pas de contrôle - par exemple, le courrier de base de données, les notifications d'événement, les notifications de requête et le courtier de service utilisent tous tempdb d'une manière ou d'une autre. Vous pouvez cesser d'utiliser ces fonctionnalités, mais si vous les utilisez, vous ne pouvez pas dicter comment et quand ils utilisent tempdb.

Aaron Bertrand
la source
Merci pour le lien Aaron. En général, existe-t-il des meilleures pratiques de codage à suivre pour éviter de remplir les journaux transactionnels TEMPDB?
2
Hmm, j'ai juste testé cela et la session incriminée n'a pas été trouvée même si la session_idrequête suivante s'affiche 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. La requête que je m'attendais à trouver était après avoir exécuté ce qui suitBEGIN TRAN CREATE TABLE #T(X CHAR(8000)) INSERT INTO #T SELECT name FROM sys.objects
Martin Smith
@Martin: A noté qu'il y a un @@ SPID dans le cte, ce qui limiterait les résultats à la session en cours. Si vous souhaitez qu'il s'étende sur toutes les sessions, supprimez-le.
Ben Thul
@BenThul - J'ai exécuté la requête dans une autre connexion. Le @@SPIDn'est <>pas =. dm_db_task_space_usagerapports 0pour le spid avec la transaction ouverte pour toutes les colonnes pour moi. Je me demande si vous devez l'interroger lorsque la demande est en train de s'exécuter plutôt que de rester inactif avec une transaction ouverte.
Martin Smith
@MartinSmith la requête ne trouve que les requêtes actives, pas les transactions actives. Donc, si la requête n'est plus en cours d'exécution, vous avez raison, vous pouvez effectuer un suivi en utilisant les DMV de transaction. Mais vous ne seriez pas nécessairement en mesure de déterminer la requête qui l'a provoquée si elle n'est plus en cours d'exécution - ce même spid peut avoir émis plusieurs autres instructions dans la transaction en cours.
Aaron Bertrand
5

https://social.msdn.microsoft.com/Forums/sqlserver/en-US/17d9f862-b9ae-42de-ada0-4229f56712dc/tempdb-log-filling-cannot-find-how-or-what-forwhat=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
la source
4

Merci pour ce post, probablement le seul en son genre. Mon test était simple: créez une table temporaire et assurez-vous qu’elle s’affiche lorsque j’exécute l’une des requêtes de cet article ... Seules une ou deux ont vraiment réussi. Je l'ai corrigé pour rejoindre le T-SQL, optimisé pour de plus longues durées et rendu utile. Faites-moi savoir si j'ai manqué quelque chose, mais vous avez jusqu'à présent un script automatisé / en boucle. Il fournit un moyen d'évaluer la requête / le SPID du délinquant sur une période en utilisant la requête à écart type (STDEV) ci-dessous.

Cela fonctionne toutes les 3 minutes pendant 40 fois, soit 2 heures. Modifiez les paramètres comme bon vous semble.

Il existe un filtre WHERE> 50 pages ci-dessous que les utilisateurs peuvent vouloir effacer au cas où vous auriez beaucoup de petits tableaux. Sinon, vous ne pourrez pas saisir cette nuance avec le sous-titre tel quel ...

Prendre plaisir!

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
Joe Zee
la source
Combiner cela avec la réponse acceptée constitue un moyen pratique de suivre l'activité en cours d'élimination de tempdb. L'exécution de cette tâche via une tâche planifiée de l'agent SQL gardera cette exécution même si SSMS est fermé. Merci d'avoir partagé!
Lockszmith
1

Malheureusement, le journal tempDB ne peut pas être directement retracé vers les ID de session en affichant les processus en cours d'exécution.

Réduisez le fichier journal tempDB à un point où il connaîtra une nouvelle croissance importante. Créez ensuite un événement étendu pour capturer la croissance du journal. Une fois que cela a repris, vous pouvez développer l'événement étendu et afficher le fichier d'événements du package. Ouvrez le fichier, ajoutez un filtre de temps, un filtre de type de fichier (vous ne voulez pas que les résultats du fichier de données soient inclus), puis regroupez-le par ID de session dans SSMS. Cela vous aidera à trouver le ou les coupables pendant que vous recherchez les identifiants de session avec le plus grand nombre de groupes. Bien sûr, vous devez collecter ce qui est en cours d'exécution dans les identifiants de session via un autre processus ou un autre outil. Peut-être que quelqu'un sait comment obtenir la requête à partir de la colonne query_hash et aura la gentillesse de poster la solution.

Résultats de l'événement étendu:

entrez la description de l'image ici

Script pour créer l'événement étendu:

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)
Tequila
la source