sql >> Base de Datos >  >> RDS >> Database

Amigo, ¿quién es el dueño de esa mesa #temp?

Probablemente haya estado en un escenario en el que tenía curiosidad sobre quién creó una copia específica de una tabla #temp. En junio de 2007, pedí a un DMV que asignara tablas #temp a sesiones, pero esto fue rechazado para la versión de 2008 (y se eliminó con el retiro de Connect hace un par de años).

En SQL Server 2005, 2008 y 2008 R2, debería poder extraer esta información del seguimiento predeterminado:

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

(Basado en el código de Jonathan Kehayias.)

Para determinar el uso del espacio, puede mejorar aún más esto para unir datos de DMV como sys.dm_db_partition_stats – por ejemplo:

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

Sin embargo, a partir de SQL Server 2012, esto dejaba de funcionar si la tabla #temp era un montón. Bob Ward (@bobwardms) proporcionó una explicación detallada de por qué sucedió esto; la respuesta corta es que hubo un error en su lógica para tratar de filtrar la creación de la tabla #temp desde el seguimiento predeterminado, y este error se corrigió parcialmente durante el trabajo de SQL Server 2012 para alinear mejor el seguimiento y los eventos extendidos. Tenga en cuenta que SQL Server 2012+ seguirá capturando la creación de tablas #temp con restricciones en línea, como una clave principal, pero no montones.

[Haga clic aquí para mostrar/ocultar la explicación completa de Bob.]

El evento Object:Created en realidad tiene 3 subeventos:Begin, Commit y Rollback. Entonces, si crea un objeto con éxito, obtiene 2 eventos:1 para Begin y 1 para Commit. Sabes cuál mirando EventSubClass.


Antes de SQL Server 2012, solo Object:Created with subclass =Begin tiene el ObjectName poblado. Entonces, la subclase =Commit no contenía el ObjectName poblado. Esto fue por diseño para evitar repetir este pensamiento, podría buscar el nombre en el evento Begin.


Como he dicho, el seguimiento predeterminado se diseñó para omitir cualquier evento de seguimiento en el que dbid =2 y el nombre del objeto comenzaran con "#". Entonces, lo que puede aparecer en el seguimiento predeterminado es el objeto:subclase creada =eventos de confirmación (razón por la cual el nombre del objeto está en blanco).


Aunque no documentamos nuestras "intenciones" de no rastrear objetos tempdb, el comportamiento claramente no funcionaba según lo previsto.


Ahora avancemos a la creación de SQL Server 2012. Pasamos a un proceso de transferencia de eventos de SQLTrace a XEvent. Decidimos durante este período de tiempo como parte de este trabajo de XEvent que la subclase=Commit o Rollback necesitaba que se llenara el ObjectName. El código en el que hacemos esto es el mismo código en el que producimos el evento SQLTrace, por lo que ahora el evento SQLTrace tiene el nombre del objeto para la subclase =Confirmar.


Y dado que nuestra lógica de filtrado para el seguimiento predeterminado no ha cambiado, ahora no verá los eventos Begin o Commit.

Cómo debes hacerlo hoy

En SQL Server 2012 y versiones posteriores, Extended Events le permitirá capturar manualmente el object_created evento, y es fácil agregar un filtro para que solo se preocupe por los nombres que comienzan con # . La siguiente definición de sesión capturará toda la creación de la tabla #temp, montón o no, e incluirá toda la información útil que normalmente se recuperaría del seguimiento predeterminado. Además, captura el lote de SQL responsable de la creación de la tabla (si lo desea), información que no está disponible en el seguimiento predeterminado (TextData siempre es 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;

Es posible que pueda hacer algo similar en 2008 y 2008 R2, pero sé que hay algunas diferencias sutiles con respecto a lo que está disponible, y no lo probé después de recibir este error de inmediato:

Mensaje 25623, nivel 16, estado 1, línea 1
El nombre del evento, "sqlserver.object_created", no es válido o no se pudo encontrar el objeto

Análisis de los datos

Extraer la información del destino del archivo es un poco más engorroso que con el seguimiento predeterminado, principalmente porque todo está almacenado como XML (bueno, para ser pedante, es XML presentado como NVARCHAR). Aquí hay una consulta que preparé para devolver información similar a la segunda consulta anterior contra el seguimiento predeterminado. Una cosa importante a tener en cuenta es que Extended Events almacena sus datos en UTC, por lo que si su servidor está configurado en otra zona horaria, deberá ajustar para que create_date en sys.objects se compara como si fuera UTC. (Las marcas de tiempo están configuradas para coincidir porque object_id los valores se pueden reciclar. Supongo aquí que una ventana de dos segundos es suficiente para filtrar los valores reciclados).

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

Por supuesto, esto solo devolverá espacio y otra información para las tablas #temp que todavía existen. Si desea ver todas las creaciones de tablas #temp aún disponibles en el destino del archivo, incluso si no existen ahora, simplemente cambie ambas instancias de INNER JOIN a LEFT OUTER JOIN .