Como DBA de SQL Server, siempre nos ocupamos de una de las cosas más importantes para el negocio, los datos. En algunos casos, las aplicaciones pueden volverse bastante complejas y termina con un montón de tablas de base de datos dispersas alrededor de su(s) instancia(s) de SQL Server. Esto podría ocasionar algunos inconvenientes, como:
- Saber cómo se comportan sus datos todos los días, en términos de tendencias de crecimiento (espacio y/o cantidad de filas).
- Saber qué tablas de base de datos requieren (o requerirán) una estrategia particular/diferente para almacenar los datos porque está creciendo demasiado rápido.
- Saber cuáles de las tablas de su base de datos ocupan demasiado espacio, lo que posiblemente genere restricciones de almacenamiento.
Debido a la importancia de estos detalles, he creado un par de procedimientos almacenados que pueden ser de gran ayuda para cualquier DBA de SQL Server que desee realizar un seguimiento de la información sobre las tablas de la base de datos en su entorno. Confía en mí, uno de ellos es genial.
Consideraciones iniciales
- Asegúrese de que la cuenta que ejecuta este procedimiento almacenado tenga suficientes privilegios. Probablemente podría comenzar con sysadmin y luego ir lo más granular posible para asegurarse de que el usuario tenga el mínimo de privilegios necesarios para que el SP funcione correctamente.
- Los objetos de la base de datos (tabla de la base de datos y procedimiento almacenado) se crearán dentro de la base de datos seleccionada en el momento en que se ejecute el script, así que elija con cuidado.
- La secuencia de comandos está diseñada de manera que se puede ejecutar varias veces sin que se produzca un error. Para el procedimiento almacenado, utilicé la declaración "CREATE OR ALTER PROCEDURE", disponible desde SQL Server 2016 SP1. Por eso, no se sorprenda si no funciona sin problemas en una versión anterior.
- Siéntase libre de cambiar los nombres de los objetos de base de datos creados.
- Preste atención a los parámetros del procedimiento almacenado que recopila los datos sin procesar. Pueden ser cruciales en una poderosa estrategia de recopilación de datos para visualizar tendencias.
¿Cómo utilizar los procedimientos almacenados?
- Copie y pegue el código T-SQL (disponible en este artículo).
- El primer SP espera 2 parámetros:
- @persistData:'Y' si un DBA quiere guardar el resultado en una tabla de destino y 'N' si el DBA quiere ver el resultado directamente.
- @truncateTable:'Y' para truncar la tabla primero antes de almacenar los datos capturados y 'N' si los datos actuales se mantienen en la tabla. Tenga en cuenta que el valor de este parámetro es irrelevante si el valor del parámetro @persistData es 'N'.
- El segundo SP espera 1 parámetro:
- @targetParameter:el nombre de la columna que se utilizará para transponer la información recopilada.
Campos presentados y su significado
- nombre_de_la_base_de_datos: el nombre de la base de datos donde reside la tabla.
- esquema: el nombre del esquema donde reside la tabla.
- nombre_tabla: el marcador de posición para el nombre de la tabla.
- recuento_de_filas: el número de filas que tiene actualmente la tabla.
- espacio_total_mb: el número de MegaBytes asignados para la tabla.
- used_space_mb: la cantidad de megabytes que la tabla usa actualmente.
- unused_space_mb: la cantidad de MegaBytes que la tabla no está usando.
- fecha_de_creación: la fecha/hora en que se creó la tabla.
- datos_coleccion_marca de tiempo: visible solo si se pasa 'Y' al parámetro @persistData. Se utiliza para saber cuándo se ejecutó el SP y se guardó correctamente la información en la tabla DBA_Tables.
Pruebas de ejecución
Demostraré algunas ejecuciones de los procedimientos almacenados:
/* Mostrar la información de las tablas para todas las bases de datos de usuarios */
EXEC GetTablesData @persistData = 'N',@truncateTable = 'N'

/* Conservar la información de las tablas de la base de datos y consultar la tabla de destino, truncando primero la tabla de destino */
EXEC GetTablesData @persistData = 'Y',@truncateTable = 'Y'
SELECT * FROM DBA_Tables

Consultas secundarias
*Consulta para ver las tablas de la base de datos ordenadas desde la mayor cantidad de filas hasta la menor.
SELECT * FROM DBA_Tables ORDER BY row_count DESC;
*Consulta para ver las tablas de la base de datos ordenadas desde el espacio total más grande hasta el más bajo.
SELECT * FROM DBA_Tables ORDER BY total_space_mb DESC;
* Consulta para ver las tablas de la base de datos ordenadas desde el espacio utilizado más grande hasta el más bajo.
SELECT * FROM DBA_Tables ORDER BY used_space_mb DESC;
* Consulta para ver las tablas de la base de datos ordenadas desde el espacio sin usar más grande hasta el más bajo.
SELECT * FROM DBA_Tables ORDER BY unused_space_mb DESC;
*Consulta para ver las tablas de la base de datos ordenadas por fecha de creación, de la más nueva a la más antigua.
SELECT * FROM DBA_Tables ORDER BY created_date DESC;
Aquí hay un código completo del Procedimiento almacenado que captura la información de las tablas de la base de datos:
*Al principio de la secuencia de comandos, verá el valor predeterminado que asume el procedimiento almacenado si no se pasa ningún valor para cada parámetro.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE OR ALTER PROCEDURE [dbo].[GetTablesData]
@persistData CHAR(1) = 'Y',
@truncateTable CHAR(1) = 'Y'
AS
BEGIN
SET NOCOUNT ON
DECLARE @command NVARCHAR(MAX)
DECLARE @Tmp_TablesInformation TABLE(
[database] [VARCHAR](255) NOT NULL,
[schema] [VARCHAR](64) NOT NULL,
[table] [VARCHAR](255) NOT NULL,
[row_count] [BIGINT]NOT NULL,
[total_space_mb] [DECIMAL](15,2) NOT NULL,
[used_space_mb] [DECIMAL](15,2) NOT NULL,
[unused_space_mb] [DECIMAL](15,2) NOT NULL,
[created_date] [DATETIME] NOT NULL
)
SELECT @command = '
USE [?]
IF DB_ID(''?'') > 4
BEGIN
SELECT
''?'',
s.Name AS [schema],
t.NAME AS [table],
p.rows AS row_count,
CAST(ROUND(((SUM(a.total_pages) * 8) / 1024.00), 2) AS DECIMAL(15, 2)) AS total_space_mb,
CAST(ROUND(((SUM(a.used_pages) * 8) / 1024.00), 2) AS DECIMAL(15, 2)) AS used_space_mb,
CAST(ROUND(((SUM(a.total_pages) - SUM(a.used_pages)) * 8) / 1024.00, 2) AS DECIMAL(15, 2)) AS unused_space_mb,
t.create_date as created_date
FROM sys.tables t
INNER JOIN sys.indexes i ON t.OBJECT_ID = i.object_id
INNER JOIN sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id
WHERE t.NAME NOT LIKE ''dt%''
AND t.is_ms_shipped = 0
AND i.OBJECT_ID > 255
GROUP BY t.Name, s.Name, p.Rows,t.create_date
ORDER BY total_space_mb DESC, t.Name
END'
INSERT INTO @Tmp_TablesInformation
EXEC sp_MSForEachDB @command
IF @persistData = 'N'
SELECT * FROM @Tmp_TablesInformation
ELSE
BEGIN
IF(@truncateTable = 'Y')
TRUNCATE TABLE DBA_Tables
INSERT INTO DBA_Tables
SELECT *,GETDATE() FROM @Tmp_TablesInformation ORDER BY [database],[schema],[table]
END
END
GO
Hasta este punto, la información parece un poco seca, pero permítanme cambiar esa percepción con la presentación de un procedimiento almacenado complementario. Su objetivo principal es transponer la información recopilada en la tabla de destino que sirve como fuente para los informes de tendencias.
Así es como puede ejecutar el procedimiento almacenado:
*Para fines de demostración, inserté registros manuales en la tabla de destino denominada t1 para simular mi ejecución habitual de procedimiento almacenado.
*El conjunto de resultados es un poco amplio, así que tomaré un par de capturas de pantalla para mostrar el resultado completo.
EXEC TransposeTablesInformation @targetParmeter = 'row_count'


Conclusiones clave
- Si automatiza la ejecución de la secuencia de comandos que completa la tabla de destino, puede notar inmediatamente si algo salió mal con ella o con sus datos. Eche un vistazo a los datos de la tabla 't1' y la columna '15'. Puede ver NULL allí, que se hizo a propósito para mostrarle algo que podría suceder.
- Con este tipo de vista, puede ver un comportamiento peculiar para las tablas de base de datos más importantes/críticas.
- En el ejemplo dado, elegí el campo 'row_count' de la tabla de destino, pero puede elegir cualquier otro campo numérico como parámetro y obtener el mismo formato de tabla, pero con datos diferentes.
- No se preocupe, si especifica un parámetro no válido, el procedimiento almacenado le avisará y detendrá su ejecución.

Aquí hay un código completo del procedimiento almacenado que transpone la información de la tabla de destino:
*Al principio de la secuencia de comandos, verá el valor predeterminado que asume el procedimiento almacenado si no se pasa ningún valor para cada parámetro.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE OR ALTER PROCEDURE [dbo].[TransposeTablesInformation]
@targetParameter NVARCHAR(15) = 'row_count'
AS
BEGIN
SET NOCOUNT ON;
IF (@targetParameter <> 'row_count' AND @targetParameter <> 'total_space_mb' AND @targetParameter <> 'used_space_mb' AND @targetParameter <> 'unused_space_mb')
BEGIN
PRINT 'Please specify a valid parameter!'
PRINT 'i.e. row_count | total_space_mb | used_space_mb | unused_space_mb'
RETURN
END
ELSE
BEGIN
CREATE TABLE #TablesInformation(
[database] [VARCHAR](255) NOT NULL,
[schema] [VARCHAR](64) NOT NULL,
[table] [VARCHAR](255) NOT NULL,
[1] [DECIMAL](10,2) NULL,
[2] [DECIMAL](10,2) NULL,
[3] [DECIMAL](10,2) NULL,
[4] [DECIMAL](10,2) NULL,
[5] [DECIMAL](10,2) NULL,
[6] [DECIMAL](10,2) NULL,
[7] [DECIMAL](10,2) NULL,
[8] [DECIMAL](10,2) NULL,
[9] [DECIMAL](10,2) NULL,
[10] [DECIMAL](10,2) NULL,
[11] [DECIMAL](10,2) NULL,
[12] [DECIMAL](10,2) NULL,
[13] [DECIMAL](10,2) NULL,
[14] [DECIMAL](10,2) NULL,
[15] [DECIMAL](10,2) NULL,
[16] [DECIMAL](10,2) NULL,
[17] [DECIMAL](10,2) NULL,
[18] [DECIMAL](10,2) NULL,
[19] [DECIMAL](10,2) NULL,
[20] [DECIMAL](10,2) NULL,
[21] [DECIMAL](10,2) NULL,
[22] [DECIMAL](10,2) NULL,
[23] [DECIMAL](10,2) NULL,
[24] [DECIMAL](10,2) NULL,
[25] [DECIMAL](10,2) NULL,
[26] [DECIMAL](10,2) NULL,
[27] [DECIMAL](10,2) NULL,
[28] [DECIMAL](10,2) NULL,
[29] [DECIMAL](10,2) NULL,
[30] [DECIMAL](10,2) NULL,
[31] [DECIMAL](10,2) NULL
)
INSERT INTO #TablesInformation([database],[schema],[table])
SELECT DISTINCT [database_name],[schema],[table_name]
FROM DBA_Tables
ORDER BY [database_name],[schema],table_name
DECLARE @databaseName NVARCHAR(255)
DECLARE @schemaName NVARCHAR(64)
DECLARE @tableName NVARCHAR(255)
DECLARE @value DECIMAL(10,2)
DECLARE @dataTimestamp DATETIME
DECLARE @sqlCommand NVARCHAR(MAX)
IF(@targetParameter = 'row_count')
BEGIN
DECLARE TablesCursor CURSOR FOR
SELECT
[database_name],
[schema],
[table_name],
[row_count],
[data_collection_timestamp]
FROM DBA_Tables
ORDER BY [database_name],[schema],table_name
END
IF(@targetParameter = 'total_space_mb')
BEGIN
DECLARE TablesCursor CURSOR FOR
SELECT
[database_name],
[schema],
[table_name],
[total_space_mb],
[data_collection_timestamp]
FROM DBA_Tables
ORDER BY [database_name],[schema],table_name
END
IF(@targetParameter = 'used_space_mb')
BEGIN
DECLARE TablesCursor CURSOR FOR
SELECT
[database_name],
[schema],
[table_name],
[used_space_mb],
[data_collection_timestamp]
FROM DBA_Tables
ORDER BY [database_name],[schema],table_name
END
IF(@targetParameter = 'unused_space_mb')
BEGIN
DECLARE TablesCursor CURSOR FOR
SELECT
[database_name],
[schema],
[table_name],
[unused_space_mb],
[data_collection_timestamp]
FROM DBA_Tables
ORDER BY [database_name],[schema],table_name
END
OPEN TablesCursor
FETCH NEXT FROM TablesCursor INTO @databaseName,@schemaName,@tableName,@value,@dataTimestamp
WHILE(@@FETCH_STATUS = 0)
BEGIN
SET @sqlCommand = CONCAT('
UPDATE #TablesInformation
SET [',DAY(@dataTimestamp),'] = ',@value,'
WHERE [database] = ',CHAR(39),@databaseName,CHAR(39),'
AND [schema] = ',CHAR(39),@schemaName+CHAR(39),'
AND [table] = ',CHAR(39),@tableName+CHAR(39),'
')
EXEC(@sqlCommand)
FETCH NEXT FROM TablesCursor INTO @databaseName,@schemaName,@tableName,@value,@dataTimestamp
END
CLOSE TablesCursor
DEALLOCATE TablesCursor
IF(@targetParameter = 'row_count')
SELECT [database],
[schema],
[table],
CONVERT(INT,[1]) AS [1],
CONVERT(INT,[2]) AS [2],
CONVERT(INT,[3]) AS [3],
CONVERT(INT,[4]) AS [4],
CONVERT(INT,[5]) AS [5],
CONVERT(INT,[6]) AS [6],
CONVERT(INT,[7]) AS [7],
CONVERT(INT,[8]) AS [8],
CONVERT(INT,[9]) AS [9],
CONVERT(INT,[10]) AS [10],
CONVERT(INT,[11]) AS [11],
CONVERT(INT,[12]) AS [12],
CONVERT(INT,[13]) AS [13],
CONVERT(INT,[14]) AS [14],
CONVERT(INT,[15]) AS [15],
CONVERT(INT,[16]) AS [16],
CONVERT(INT,[17]) AS [17],
CONVERT(INT,[18]) AS [18],
CONVERT(INT,[19]) AS [19],
CONVERT(INT,[20]) AS [20],
CONVERT(INT,[21]) AS [21],
CONVERT(INT,[22]) AS [22],
CONVERT(INT,[23]) AS [23],
CONVERT(INT,[24]) AS [24],
CONVERT(INT,[25]) AS [25],
CONVERT(INT,[26]) AS [26],
CONVERT(INT,[27]) AS [27],
CONVERT(INT,[28]) AS [28],
CONVERT(INT,[29]) AS [29],
CONVERT(INT,[30]) AS [30],
CONVERT(INT,[31]) AS [31]
FROM #TablesInformation
ELSE
SELECT * FROM #TablesInformation
END
END
GO
Conclusión
- Puede implementar el SP de recopilación de datos en cada instancia de SQL Server bajo su soporte e implementar un mecanismo de alerta en toda su pila de instancias compatibles.
- Si implementa un trabajo de agente que consulta esta información con relativa frecuencia, puede estar al tanto del juego en términos de saber cómo se comportan sus datos durante el mes. Por supuesto, puede ir aún más lejos y almacenar los datos recopilados mensualmente para tener una imagen aún más amplia; tendrías que hacer algunos ajustes al código, pero valdría la pena.
- Asegúrese de probar este mecanismo correctamente en un entorno de espacio aislado y, cuando planee una implementación de producción, asegúrese de elegir períodos de baja actividad.
- La recopilación de información de este tipo puede ayudar a diferenciar un DBA de otro. Probablemente existen herramientas de terceros que pueden hacer lo mismo, e incluso más, pero no todos tienen el presupuesto para permitírselo. Espero que esto pueda ayudar a cualquiera que decida usarlo en su entorno.