Como DBAs SQL Server, sempre cuidamos de uma das coisas mais importantes para o negócio, os dados. Em alguns casos, os aplicativos podem ficar bastante complexos e você acaba com uma tonelada de tabelas de banco de dados espalhadas pelas instâncias do SQL Server. Isso pode levar a alguns inconvenientes, como:
- Saber como seus dados se comportam todos os dias, em termos de tendências de crescimento (espaço e/ou quantidade de linhas).
- Saber quais tabelas de banco de dados exigem (ou exigirão) uma estratégia específica/diferente para armazenar os dados porque estão crescendo muito rápido.
- Saber quais das suas tabelas de banco de dados ocupam muito espaço, possivelmente levando a restrições de armazenamento.
Devido à importância desses detalhes, criei alguns Stored Procedures que podem ser de grande ajuda para qualquer DBA SQL Server que queira acompanhar informações sobre tabelas de banco de dados em seu ambiente. Confie em mim, um deles é muito legal.
Considerações iniciais
- Certifique-se de que a conta que executa este procedimento armazenado tenha privilégios suficientes. Você provavelmente poderia começar com sysadmin e, em seguida, ir o mais granular possível para garantir que o usuário tenha o mínimo de privilégios necessários para que o SP funcione corretamente.
- Os objetos do banco de dados (tabela do banco de dados e procedimento armazenado) serão criados dentro do banco de dados selecionado no momento da execução do script, portanto, escolha com cuidado.
- O script é criado de forma que possa ser executado várias vezes sem que você receba um erro. Para o Stored Procedure, usei a instrução “CREATE OR ALTER PROCEDURE”, disponível desde o SQL Server 2016 SP1. É por isso que não se surpreenda se não funcionar sem problemas em uma versão anterior.
- Sinta-se à vontade para alterar os nomes dos objetos de banco de dados criados.
- Preste atenção aos parâmetros do procedimento armazenado que coleta os dados brutos. Eles podem ser cruciais em uma estratégia poderosa de coleta de dados para visualizar tendências.
Como usar os procedimentos armazenados?
- Copie e cole o código T-SQL (disponível neste artigo).
- O primeiro SP espera 2 parâmetros:
- @persistData:'Y' se um DBA quiser salvar a saída em uma tabela de destino e 'N' se o DBA quiser ver a saída diretamente.
- @truncateTable:'Y' para truncar a tabela antes de armazenar os dados capturados e 'N' se os dados atuais forem mantidos na tabela. Lembre-se de que o valor desse parâmetro é irrelevante se o valor do parâmetro @persistData for 'N'.
- O segundo SP espera 1 parâmetro:
- @targetParameter:o nome da coluna a ser usada para transpor as informações coletadas.
Campos apresentados e seu significado
- database_name: o nome do banco de dados onde a tabela reside.
- esquema: o nome do esquema onde a tabela reside.
- table_name: o espaço reservado para o nome da tabela.
- row_count: o número de linhas que a tabela tem atualmente.
- total_space_mb: o número de MegaBytes alocados para a tabela.
- used_space_mb: o número de MegaBytes realmente em uso pela tabela.
- unused_space_mb: o número de MegaBytes que a tabela não está usando.
- data_criada: a data/hora em que a tabela foi criada.
- data_collection_timestamp: visível apenas se ‘Y’ for passado para o parâmetro @persistData. Ele é usado para saber quando o SP foi executado e as informações foram salvas com sucesso na tabela DBA_Tables.
Testes de execução
Vou demonstrar algumas execuções dos Stored Procedures:
/* Exibe as informações das tabelas para todos os bancos de dados do usuário */
EXEC GetTablesData @persistData = 'N',@truncateTable = 'N'
/* Persiste as informações das tabelas do banco de dados e consulta a tabela de destino, truncando primeiro a tabela de destino */
EXEC GetTablesData @persistData = 'Y',@truncateTable = 'Y'
SELECT * FROM DBA_Tables
Consultas secundárias
*Consulta para visualizar as tabelas do banco de dados classificadas do maior número de linhas para o menor.
SELECT * FROM DBA_Tables ORDER BY row_count DESC;
*Consulta para visualizar as tabelas do banco de dados classificadas do maior espaço total para o menor.
SELECT * FROM DBA_Tables ORDER BY total_space_mb DESC;
*Consulta para visualizar as tabelas do banco de dados classificadas do maior espaço usado para o menor.
SELECT * FROM DBA_Tables ORDER BY used_space_mb DESC;
*Consulta para visualizar as tabelas do banco de dados classificadas do maior espaço não utilizado para o menor.
SELECT * FROM DBA_Tables ORDER BY unused_space_mb DESC;
*Consulta para visualizar as tabelas do banco de dados ordenadas por data de criação, da mais recente à mais antiga.
SELECT * FROM DBA_Tables ORDER BY created_date DESC;
Segue um código completo do Stored Procedure que captura as informações das tabelas do banco de dados:
*No início do script, você verá o valor padrão que o procedimento armazenado assume se nenhum valor for passado 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
Até este ponto, as informações parecem um pouco secas, mas deixe-me mudar essa percepção com a apresentação de um Stored Procedure complementar. Seu principal objetivo é transpor as informações coletadas na tabela de destino que serve de fonte para relatórios de tendências.
Veja como você pode executar o procedimento armazenado:
*Para fins de demonstração, inseri registros manuais na tabela de destino chamada t1 para simular minha execução usual de procedimento armazenado.
*O conjunto de resultados é um pouco amplo, então farei algumas capturas de tela para mostrar o resultado completo.
EXEC TransposeTablesInformation @targetParmeter = 'row_count'
Principais conclusões
- Se você automatizar a execução do script que preenche a tabela de destino, poderá perceber imediatamente se algo deu errado com ele ou com seus dados. Dê uma olhada nos dados para a tabela 't1' e a coluna '15'. Você pode ver NULL lá, que foi feito de propósito para mostrar algo que pode acontecer.
- Com esse tipo de visualização, você pode ver um comportamento peculiar para as tabelas de banco de dados mais importantes/críticas.
- No exemplo dado, escolhi o campo 'row_count' da tabela de destino, mas você pode escolher qualquer outro campo numérico como parâmetro e obter o mesmo formato de tabela, mas com dados diferentes.
- Não se preocupe, se você especificar um parâmetro inválido, o procedimento armazenado o avisará e interromperá sua execução.
Segue um código completo do Stored Procedure que transpõe as informações da tabela de destino:
*No início do script, você verá o valor padrão que o procedimento armazenado assume se nenhum valor for passado 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
Conclusão
- Você pode implantar o SP de coleta de dados em todas as instâncias do SQL Server sob seu suporte e implementar um mecanismo de alerta em toda a pilha de instâncias com suporte.
- Se você implementar um trabalho de agente que consulte essas informações com relativa frequência, poderá ficar por dentro do jogo em termos de saber como seus dados se comportam durante o mês. Claro, você pode ir ainda mais longe e armazenar os dados coletados mensalmente para ter uma visão ainda maior; você teria que fazer alguns ajustes no código, mas valeria a pena.
- Certifique-se de testar esse mecanismo corretamente em um ambiente de sandbox e, ao planejar uma implantação de produção, certifique-se de escolher períodos de baixa atividade.
- A coleta de informações desse tipo pode ajudar a diferenciar um DBA um do outro. Provavelmente existem ferramentas de terceiros que podem fazer a mesma coisa e até mais, mas nem todos têm orçamento para pagar. Espero que isso possa ajudar qualquer pessoa que decida usá-lo em seu ambiente.