Database
 sql >> Base de données >  >> RDS >> Database

Mec, à qui appartient cette table #temp ?

Vous avez probablement été dans un scénario où vous étiez curieux de savoir qui a créé une copie spécifique d'une table #temp. En juin 2007, j'ai demandé un DMV pour mapper les tables #temp aux sessions, mais cela a été rejeté pour la version 2008 (et a été balayé avec le retrait de Connect il y a quelques années).

Dans SQL Server 2005, 2008 et 2008 R2, vous devriez pouvoir extraire ces informations de la trace par défaut :

DECLARE @filename VARCHAR(MAX);
 
SELECT @filename = SUBSTRING([path], 0,
 LEN([path])-CHARINDEX('\', REVERSE([path]))+1) + '\Log.trc'  
FROM sys.traces   
WHERE is_default = 1;  
 
SELECT   
     o.name,   
     o.[object_id],  
     o.create_date, 
     gt.SPID,  
     NTUserName = gt.NTDomainName + '\' + gt.NTUserName,
     SQLLogin = gt.LoginName,  
     gt.HostName,  
     gt.ApplicationName,
     gt.TextData -- don't bother, always NULL 
  FROM sys.fn_trace_gettable(@filename, DEFAULT) AS gt  
  INNER JOIN tempdb.sys.objects AS o   
    ON gt.ObjectID = o.[object_id] 
  WHERE gt.DatabaseID = 2 
    AND gt.EventClass = 46 -- (Object:Created Event from sys.trace_events)  
    AND gt.EventSubClass = 1 -- Commit
    AND o.name LIKE N'#%'
    AND o.create_date >= DATEADD(MILLISECOND, -100, gt.StartTime)   
    AND o.create_date <= DATEADD(MILLISECOND,  100, gt.StartTime);

(Basé sur le code de Jonathan Kehayias.)

Pour déterminer l'utilisation de l'espace, vous pouvez encore améliorer cela pour joindre les données des DMV comme sys.dm_db_partition_stats – par exemple :

DECLARE @filename VARCHAR(MAX);
 
SELECT @filename = SUBSTRING([path], 0,
   LEN([path])-CHARINDEX('\', REVERSE([path]))+1) + '\Log.trc'  
FROM sys.traces   
WHERE is_default = 1;  
 
SELECT   
     o.name,   
     o.[object_id],  
     o.create_date, 
     gt.SPID,  
     NTUserName = gt.NTDomainName + '\' + gt.NTUserName,
     SQLLogin = gt.LoginName,  
     gt.HostName,  
     gt.ApplicationName,
     row_count = x.rc,
     reserved_page_count = x.rpc
  FROM sys.fn_trace_gettable(@filename, DEFAULT) AS gt  
  INNER JOIN tempdb.sys.objects AS o   
    ON gt.ObjectID = o.[object_id]
  INNER JOIN
  (
    SELECT 
      [object_id],
      rc  = SUM(CASE WHEN index_id IN (0,1) THEN row_count END), 
      rpc = SUM(reserved_page_count) 
    FROM tempdb.sys.dm_db_partition_stats
    GROUP BY [object_id]
  ) AS x 
    ON x.[object_id] = o.[object_id]
  WHERE gt.DatabaseID = 2 
    AND gt.EventClass = 46 -- (Object:Created Event from sys.trace_events)  
	AND gt.EventSubClass = 1 -- Commit
	AND gt.IndexID IN (0,1)
    AND o.name LIKE N'#%'
    AND o.create_date >= DATEADD(MILLISECOND, -100, gt.StartTime)   
    AND o.create_date <= DATEADD(MILLISECOND,  100, gt.StartTime);

Cependant, à partir de SQL Server 2012, cela a cessé de fonctionner si la table #temp était un tas. Bob Ward (@bobwardms) a fourni une explication détaillée de la raison pour laquelle cela s'est produit ; la réponse courte est qu'il y avait un bogue dans leur logique pour essayer de filtrer la création de table #temp à partir de la trace par défaut, et ce bogue a été partiellement corrigé lors du travail de SQL Server 2012 visant à mieux aligner la trace et les événements étendus. Notez que SQL Server 2012+ capturera toujours la création de table #temp avec des contraintes en ligne telles qu'une clé primaire, mais pas des tas.

[Cliquez ici pour afficher/masquer l'explication complète de Bob.]

L'événement Object:Created a en fait 3 sous-événements :Begin, Commit et Rollback. Ainsi, si vous réussissez à créer un objet, vous obtenez 2 événements :1 pour Begin et 1 pour Commit. Vous savez lequel en regardant EventSubClass.


Avant SQL Server 2012, seul Object:Created with subclass =Begin a le ObjectName rempli. Ainsi, la sous-classe =Commit ne contenait pas l'ObjectName rempli. C'était par conception pour éviter de répéter cela en pensant que vous pouviez rechercher le nom dans l'événement Begin.


Comme je l'ai dit, la trace par défaut a été conçue pour ignorer tous les événements de trace où dbid =2 et le nom de l'objet commençait par "#". Ainsi, ce qui peut apparaître dans la trace par défaut sont les événements Object:Created subclass =Commit (c'est pourquoi le nom de l'objet est vide).


Même si nous n'avons pas documenté nos "intentions" de ne pas tracer les objets tempdb, le comportement ne fonctionnait clairement pas comme prévu.


Passons maintenant à la construction de SQL Server 2012. Nous passons à un processus de portage des événements de SQLTrace vers XEvent. Nous avons décidé pendant ce laps de temps dans le cadre de ce travail XEvent que la sous-classe=Commit ou Rollback avait besoin de l'ObjectName rempli. Le code où nous faisons cela est le même code où nous produisons l'événement SQLTrace donc maintenant l'événement SQLTrace contient le ObjectName pour la sous-classe=Commit.


Et puisque notre logique de filtrage pour la trace par défaut n'a pas changé, vous ne voyez plus les événements Begin ou Commit.

Comment vous devriez le faire aujourd'hui

Dans SQL Server 2012 et versions ultérieures, les événements étendus vous permettront de capturer manuellement le object_created événement, et il est facile d'ajouter un filtre pour ne s'intéresser qu'aux noms commençant par # . La définition de session suivante capturera toutes les créations de table #temp, tas ou non, et inclura toutes les informations utiles qui seraient normalement extraites de la trace par défaut. De plus, il capture le batch SQL responsable de la création de la table (si vous le souhaitez), information non disponible dans la trace par défaut (TextData est toujours NULL ).

CREATE EVENT SESSION [TempTableCreation] ON SERVER 
ADD EVENT sqlserver.object_created
(
  ACTION 
  (
    -- you may not need all of these columns
    sqlserver.session_nt_username,
    sqlserver.server_principal_name,
    sqlserver.session_id,
    sqlserver.client_app_name,
    sqlserver.client_hostname,
    sqlserver.sql_text
  )
  WHERE 
  (
    sqlserver.like_i_sql_unicode_string([object_name], N'#%')
    AND ddl_phase = 1   -- just capture COMMIT, not BEGIN
  )
)
ADD TARGET package0.asynchronous_file_target
(
  SET FILENAME = 'c:\temp\TempTableCreation.xel',
  -- you may want to set different limits depending on
  -- temp table creation rate and available disk space
      MAX_FILE_SIZE = 32768,
      MAX_ROLLOVER_FILES = 10
)
WITH 
(
  -- if temp table creation rate is high, consider
  -- ALLOW_SINGLE/MULTIPLE_EVENT_LOSS instead
  EVENT_RETENTION_MODE = NO_EVENT_LOSS
);
GO
ALTER EVENT SESSION [TempTableCreation] ON SERVER STATE = START;

Vous pourrez peut-être faire quelque chose de similaire en 2008 et 2008 R2, mais je sais qu'il existe quelques différences subtiles par rapport à ce qui est disponible, et je ne l'ai pas testé après avoir reçu cette erreur dès le départ :

Msg 25623, Niveau 16, État 1, Ligne 1
Le nom de l'événement, "sqlserver.object_created", n'est pas valide ou l'objet est introuvable

Analyser les données

Extraire les informations du fichier cible est un peu plus fastidieux qu'avec la trace par défaut, principalement parce qu'elles sont toutes stockées au format XML (enfin, pour être pédant, c'est XML présenté comme NVARCHAR). Voici une requête que j'ai préparée pour renvoyer des informations similaires à la deuxième requête ci-dessus par rapport à la trace par défaut. Une chose importante à noter est que Extended Events stocke ses données en UTC, donc si votre serveur est défini sur un autre fuseau horaire, vous devrez vous ajuster pour que le create_date dans sys.objects est comparé comme s'il s'agissait de l'UTC. (Les horodatages sont définis pour correspondre car object_id les valeurs peuvent être recyclées. Je suppose ici qu'une fenêtre de deux secondes est suffisante pour filtrer toutes les valeurs recyclées.)

DECLARE @delta INT = DATEDIFF(MINUTE, SYSUTCDATETIME(), SYSDATETIME());
 
;WITH xe AS
(
  SELECT 
    [obj_name]  = xe.d.value(N'(event/data[@name="object_name"]/value)[1]',N'sysname'),
    [object_id] = xe.d.value(N'(event/data[@name="object_id"]/value)[1]',N'int'),
    [timestamp] = DATEADD(MINUTE, @delta, xe.d.value(N'(event/@timestamp)[1]',N'datetime2')),
    SPID        = xe.d.value(N'(event/action[@name="session_id"]/value)[1]',N'int'),
    NTUserName  = xe.d.value(N'(event/action[@name="session_nt_username"]/value)[1]',N'sysname'),
    SQLLogin    = xe.d.value(N'(event/action[@name="server_principal_name"]/value)[1]',N'sysname'),
    HostName    = xe.d.value(N'(event/action[@name="client_hostname"]/value)[1]',N'sysname'),
    AppName     = xe.d.value(N'(event/action[@name="client_app_name"]/value)[1]',N'nvarchar(max)'),
    SQLBatch    = xe.d.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar(max)')
 FROM 
    sys.fn_xe_file_target_read_file(N'C:\temp\TempTableCreation*.xel',NULL,NULL,NULL) AS ft
    CROSS APPLY (SELECT CONVERT(XML, ft.event_data)) AS xe(d)
) 
SELECT 
  DefinedName         = xe.obj_name,
  GeneratedName       = o.name,
  o.[object_id],
  xe.[timestamp],
  o.create_date,
  xe.SPID,
  xe.NTUserName,
  xe.SQLLogin, 
  xe.HostName,
  ApplicationName     = xe.AppName,
  TextData            = xe.SQLBatch,
  row_count           = x.rc,
  reserved_page_count = x.rpc
FROM xe
INNER JOIN tempdb.sys.objects AS o
ON o.[object_id] = xe.[object_id]
AND o.create_date >= DATEADD(SECOND, -2, xe.[timestamp])
AND o.create_date <= DATEADD(SECOND,  2, xe.[timestamp])
INNER JOIN
(
  SELECT 
    [object_id],
    rc  = SUM(CASE WHEN index_id IN (0,1) THEN row_count END), 
    rpc = SUM(reserved_page_count)
  FROM tempdb.sys.dm_db_partition_stats
  GROUP BY [object_id]
) AS x
ON o.[object_id] = x.[object_id];

Bien sûr, cela ne renverra que de l'espace et d'autres informations pour les tables #temp qui existent encore. Si vous voulez voir toutes les créations de table #temp toujours disponibles dans le fichier cible, même si elles n'existent pas maintenant, changez simplement les deux instances de INNER JOIN à LEFT OUTER JOIN .