Estou trabalhando em uma solução de manutenção personalizada usando o modo de sys.dm_db_index_physical_stats
exibição. Atualmente, ele está sendo referenciado a partir de um procedimento armazenado. Agora, quando esse procedimento armazenado é executado em um dos meus bancos de dados, ele faz o que eu quero e obtém uma lista de todos os registros referentes a qualquer banco de dados. Quando eu o coloco em um banco de dados diferente, ele puxa uma lista de todos os registros relacionados apenas a esse banco de dados.
Por exemplo (código na parte inferior):
- A consulta executada no banco de dados 6 mostra informações [solicitadas] para os bancos de dados 1-10.
- A consulta executada no banco de dados 3 mostra informações [solicitadas] apenas para o banco de dados 3.
A razão pela qual quero este procedimento especificamente no banco de dados três é porque prefiro manter todos os objetos de manutenção no mesmo banco de dados. Eu gostaria que esse trabalho ficasse no banco de dados de manutenção e funcionasse como se estivesse no banco de dados do aplicativo.
Código:
ALTER PROCEDURE [dbo].[GetFragStats]
@databaseName NVARCHAR(64) = NULL
,@tableName NVARCHAR(64) = NULL
,@indexID INT = NULL
,@partNumber INT = NULL
,@Mode NVARCHAR(64) = 'DETAILED'
AS
BEGIN
SET NOCOUNT ON;
DECLARE @databaseID INT, @tableID INT
IF @databaseName IS NOT NULL
AND @databaseName NOT IN ('tempdb','ReportServerTempDB')
BEGIN
SET @databaseID = DB_ID(@databaseName)
END
IF @tableName IS NOT NULL
BEGIN
SET @tableID = OBJECT_ID(@tableName)
END
SELECT D.name AS DatabaseName,
T.name AS TableName,
I.name AS IndexName,
S.index_id AS IndexID,
S.avg_fragmentation_in_percent AS PercentFragment,
S.fragment_count AS TotalFrags,
S.avg_fragment_size_in_pages AS PagesPerFrag,
S.page_count AS NumPages,
S.index_type_desc AS IndexType
FROM sys.dm_db_index_physical_stats(@databaseID, @tableID,
@indexID, @partNumber, @Mode) AS S
JOIN
sys.databases AS D ON S.database_id = D.database_id
JOIN
sys.tables AS T ON S.object_id = T.object_id
JOIN
sys.indexes AS I ON S.object_id = I.object_id
AND S.index_id = I.index_id
WHERE
S.avg_fragmentation_in_percent > 10
ORDER BY
DatabaseName, TableName, IndexName, PercentFragment DESC
END
GO
Uma maneira seria criar um procedimento do sistema
master
e, em seguida, criar um wrapper em seu banco de dados de manutenção. Observe que isso funcionará apenas para um banco de dados por vez.Primeiro, no mestre:
Agora, em seu banco de dados de manutenção, crie um wrapper que use SQL dinâmico para definir o contexto corretamente:
(A razão pela qual o nome do banco de dados não pode realmente ser
NULL
é porque você não pode se juntar a coisas comosys.objects
esys.indexes
desde que existam independentemente em cada banco de dados. Portanto, talvez tenha um procedimento diferente se desejar informações em toda a instância.)Agora você pode chamar isso para qualquer outro banco de dados, por exemplo
E você sempre pode criar um
synonym
em cada banco de dados para nem precisar referenciar o nome do banco de dados de manutenção:Outra maneira seria usar SQL dinâmico, porém isso também funcionará apenas para um banco de dados por vez:
Outra maneira seria criar uma exibição (ou função com valor de tabela) para unir os nomes de tabela e índice de todos os seus bancos de dados; no entanto, você teria que codificar os nomes dos bancos de dados na exibição e mantê-los à medida que adiciona /remove bancos de dados que você deseja permitir que sejam incluídos nesta consulta. Isso permitiria, ao contrário dos outros, recuperar estatísticas para vários bancos de dados de uma só vez.
Primeiro, a visão:
Então o procedimento:
Bem, há más notícias, boas notícias com um problema e algumas notícias realmente boas.
As más notícias
Os objetos T-SQL são executados no banco de dados onde residem. Existem duas exceções (não muito úteis):
sp_
e que existem no banco de[master]
dados (não é uma ótima opção: um banco de dados por vez, adicionando algo a[master]
, possivelmente adicionando sinônimos a cada banco de dados, o que deve ser feito para cada novo banco de dados)sp_
procedimento armazenado em[master]
.A boa notícia (com uma pegadinha)
Muitos (talvez a maioria?) As pessoas estão cientes das funções internas para obter alguns metadados realmente comuns:
O uso dessas funções pode eliminar a necessidade de JOINs
sys.databases
(embora este não seja realmente um problema),sys.objects
(preferido sobre osys.tables
que exclui exibições indexadas) esys.schemas
(você estava perdendo esse e nem tudo está nodbo
esquema ;-). Mas mesmo removendo três dos quatro JOINs, ainda estamos funcionalmente no mesmo lugar, certo? Errado!Um dos recursos interessantes das funções
OBJECT_NAME()
eOBJECT_SCHEMA_NAME()
é que elas têm um segundo parâmetro opcional para@database_id
. Ou seja, enquanto o JOINing para essas tabelas (exceto parasys.databases
) é específico do banco de dados, o uso dessas funções fornece informações de todo o servidor. Mesmo OBJECT_ID() permite informações de todo o servidor, fornecendo a ele um nome de objeto totalmente qualificado.Ao incorporar essas funções de metadados na consulta principal, podemos simplificar e, ao mesmo tempo, expandir além do banco de dados atual. A primeira passagem da refatoração da consulta nos dá:
E agora, para o "pedaço": não há função de metadados para obter nomes de índice, muito menos uma para todo o servidor. Então é isso? Estamos em 90% completos e ainda precisando estar em um banco de dados específico para obter
sys.indexes
dados? Nós realmente precisamos criar um procedimento armazenado para usar o SQL dinâmico para preencher, cada vez que nosso proc principal é executado, uma tabela temporária de todas assys.indexes
entradas em todos os bancos de dados para que possamos ingressar nela? NÃO!A notícia realmente boa
Então vem um pequeno recurso que algumas pessoas adoram odiar, mas quando usado corretamente, pode fazer coisas incríveis. Sim: SQLCLR. Por quê? Como as funções SQLCLR podem obviamente enviar instruções SQL, mas pela própria natureza do envio do código do aplicativo, é um SQL dinâmico. Portanto, ao contrário das funções T-SQL, as funções SQLCLR podem injetar um nome de banco de dados na consulta antes de executá-la. Ou seja, podemos criar nossa própria função para espelhar a capacidade de pegar
OBJECT_NAME()
e obter as informações desse banco de dados.OBJECT_SCHEMA_NAME()
database_id
O código a seguir é essa função. Mas é preciso um nome de banco de dados em vez de ID para que não precise fazer a etapa extra de procurá-lo (o que o torna um pouco menos complicado e um pouco mais rápido).
If you will notice, we are using the Context Connection, which is not only fast, but also works in
SAFE
Assemblies. Yep, this works in an Assembly marked asSAFE
, so it (or variations of it) should even work on Azure SQL Database V12(support for SQLCLR was removed, rather abruptly, from Azure SQL Database in April, 2016).So our second-pass refactoring of the main query gives us the following:
That's it! Both this SQLCLR Scalar UDF and your maintenance T-SQL Stored Procedure can live in the same centralized
[maintenance]
database. AND, you don't have to process one database at a time; now you have meta-data functions for all dependent info that is server-wide.P.S. There is no
.IsNull
checking of input parameters in the C# code since the T-SQL wrapper object should be created with theWITH RETURNS NULL ON NULL INPUT
option:Additional notes:
The method described here can also be used to solve other, very similar problems of missing cross-database meta-data functions. The following Microsoft Connect suggestion is an example of one such case. And, seeing that Microsoft has closed it as "Won't Fix", it is clear that they are not interested providing built-in functions like
OBJECT_NAME()
to meet this need (hence the Workaround that is posted on that Suggestion :-).Add metadata function to get object name from hobt_id
To learn more about using SQLCLR, please take a look at the Stairway to SQLCLR series I am writing on SQL Server Central (free registration is required; sorry, I don't control the policies of that site).
The
IndexName()
SQLCLR function shown above is available, pre-compiled, in an easy-to-install script on Pastebin. The script enables the "CLR Integration" feature if it is not already enabled, and the Assembly is marked asSAFE
. It is compiled against .NET Framework version 2.0 so that it will work in SQL Server 2005 and newer (i.e. all versions that support SQLCLR).SQLCLR Meta-data function for cross-database IndexName()
If anyone is interested in the
IndexName()
SQLCLR function and over 320 other functions and stored procedures, it is available in the SQL# library (which I am the author of). Please note that while there is a Free version, the Sys_IndexName function is only available in the Full version (along with a similar Sys_AssemblyName function).