Tally Tables – Trabalhando com Datas I (SQL Server)

Olá a todos! Fiquei um tempo sem blogar porque mudei de estado (SC) e ainda estou me adaptando ao novo ambiente (lindo, por sinal).

Pois bem! Muito de nós, em algum ponto de nossas vidas, tivemos a necessidade de criar relatórios ou consultas totalizadoras que mostrassem somatórios mensais, anuais, semanais, etc. Na maioria das vezes, nos pediam para fazer essas atividades muito rapidamente (o famoso “para ontem”) e nos deparávamos com algum problema para montar algumas estruturas.

É aqui que entram as famosas “tally tables”. Em somatórios, elas normalmente são usadas para retornar uma quantidade fixa de linhas quando a tabela original que estamos usando para os cálculos não possui informações suficientes para ser usada sozinha. Então, usamos essa tabela auxiliar junto com a tabela original. A intenção deste artigo é mostrar um exemplo prático de como isso pode ser útil.

Suponhamos que seu chefe pediu um relatório simples de vendas mês a mês de determinado produto ou vendedor. A tabela de vendas da sua empresa (bem simplificada, para o exemplo) encontra-se abaixo:

OBS: Estou usando o SQL Server 2008 aqui. Caso esteja usando uma versão mais antiga, talvez seja necessário editar os scripts.

USE tempdb;
GO
CREATE TABLE dbo.Venda
(
	IdVendedor INT NOT NULL,
	ValorTotal DECIMAL(15,2) NOT NULL DEFAULT 0,
	DataCadastro DATETIME NOT NULL DEFAULT SYSDATETIME()
);

Vamos inserir alguns registros na tabela para o ano de 2012:

INSERT INTO dbo.Venda
	(IdVendedor, ValorTotal, DataCadastro)
VALUES
	(1, 150, '20120101'),
	(1, 761.17, '20120412'),
	(2, 780.00, '20120510'),
	(3, 233.75, '20120510');

Certo! Agora, tudo o que precisamos é de um SELECT simples usando a função agregadora SUM() e um GROUP BY, correto?
Seguindo essa lógica, nossa consulta ficaria assim:

SELECT MONTH(DataCadastro) AS Mes,
       SUM(ValorTotal) AS Total
  FROM dbo.Venda
 GROUP BY MONTH(DataCadastro);

E os dados retornados, assim:

Ao mostrar para o seu chefe, todo contente, ele lhe faz a seguinte pergunta:
- Muito legal, mas e quanto aos outros meses?
Você responde:
- Mas não houve venda nos outros meses!
E ele rebate:
- Tudo bem. Então mostre um valor zerado para os que não tiveram venda nenhuma!

É aqui onde uma tally table pode facilitar as coisas para a gente!

A primeira ideia será criar uma tabela contendo uma coluna numeral “Mes”, onde a gente incluiria doze registros (números de 1 a 12). Irei usar uma CTE para mostrar rapidamente isso, mas você pode criar uma tabela no banco com os doze meses, se preferir (até mesmo se for usar em mais de uma consulta). O segredo aqui é unir essa tabela auxiliar com doze linhas, cada uma contendo um mês, com a nossa tabela de vendas. Vamos ao código:

WITH tbMes AS
(
	SELECT 1 AS Mes UNION ALL
	SELECT 2 AS Mes UNION ALL
	SELECT 3 AS Mes UNION ALL
	SELECT 4 AS Mes UNION ALL
	SELECT 5 AS Mes UNION ALL
	SELECT 6 AS Mes UNION ALL
	SELECT 7 AS Mes UNION ALL
	SELECT 8 AS Mes UNION ALL
	SELECT 9 AS Mes UNION ALL
	SELECT 10 AS Mes UNION ALL
	SELECT 11 AS Mes UNION ALL
	SELECT 12 AS Mes
)
SELECT TD.Mes AS Mes,
       ISNULL(SUM(V.ValorTotal), 0) AS Total
  FROM tbMes TD
  LEFT JOIN dbo.Venda V ON MONTH(V.DataCadastro) = TD.Mes
 GROUP BY TD.Mes;

E o resultado ficou mais apresentável, com “cara” de relatório:

Apesar de ter atendido à consulta mensal daquela atividade, ainda assim, a nossa consulta está incompleta, pois eu estava contando que minha tabela de vendas teria somente vendas de 2012. E quando outro ano aparecer na equação? Vamos dar uma dificultada no nosso cenário? Vamos incluir uma venda do ano de 2011 e ver o que acontece:

INSERT INTO dbo.Venda
	(IdVendedor, ValorTotal, DataCadastro)
VALUES
	(3, 505.67, '20110406');

Agora, precisaríamos agrupar usando o ano também. Já que tenho registros de 2011 e 2012, precisaria mostrar os doze meses de 2011 e os doze meses de 2012, isto é, 24 linhas. Primeiramente, preciso saber quais são os anos envolvidos, pois eles podem mudar dinamicamente. Para isso, pensei em algo simples assim:

SELECT YEAR(DataCadastro) AS Ano
  FROM dbo.Venda
 GROUP BY YEAR(DataCadastro)
-- 2011
-- 2012

Posso aqui, usar o CROSS JOIN para ligar cada mês com cada ano, usando a rotina acima em uma tabela derivada:

Opção 1: Trazer o ano a partir da tabela de vendas diretamente

WITH tbMes AS
(
	SELECT 1 AS Mes UNION ALL
	SELECT 2 AS Mes UNION ALL
	SELECT 3 AS Mes UNION ALL
	SELECT 4 AS Mes UNION ALL
	SELECT 5 AS Mes UNION ALL
	SELECT 6 AS Mes UNION ALL
	SELECT 7 AS Mes UNION ALL
	SELECT 8 AS Mes UNION ALL
	SELECT 9 AS Mes UNION ALL
	SELECT 10 AS Mes UNION ALL
	SELECT 11 AS Mes UNION ALL
	SELECT 12 AS Mes
)
SELECT A.Ano,
       M.Mes AS Mes
  FROM tbMes M
 CROSS JOIN (SELECT YEAR(DataCadastro) AS Ano
               FROM dbo.Venda
              GROUP BY YEAR(DataCadastro)) A;

Note que eu usei o agrupamento da tabela de vendas para me trazer o ano. Mas eu poderia perfeitamente criar uma tabelinha auxiliar de anos ou usar uma CTE, como eu fiz com os meses. Segue abaixo o exemplo:

Opção 2: trazer o ano a partir de uma tabela auxiliar ou CTE (tbAno)

WITH tbMes AS
(
	SELECT 1 AS Mes UNION ALL
	SELECT 2 AS Mes UNION ALL
	SELECT 3 AS Mes UNION ALL
	SELECT 4 AS Mes UNION ALL
	SELECT 5 AS Mes UNION ALL
	SELECT 6 AS Mes UNION ALL
	SELECT 7 AS Mes UNION ALL
	SELECT 8 AS Mes UNION ALL
	SELECT 9 AS Mes UNION ALL
	SELECT 10 AS Mes UNION ALL
	SELECT 11 AS Mes UNION ALL
	SELECT 12 AS Mes
),
tbAno AS
(
	SELECT 2011 AS Ano UNION ALL
	SELECT 2012 AS Ano
)
SELECT A.Ano,
       M.Mes AS Mes
  FROM tbMes M
 CROSS JOIN tbAno A

E o resultado é o mesmo para as duas opções:

Normalmente, é interessante criar tabelas persistidas no banco em vez de CTEs por causa da reusabilidade, indexação, melhor leitura, etc. Vamos analisar algumas das vantagens e desvantagens das duas opções acima.


Opção 1: Trazer o ano a partir da tabela de vendas diretamente

Vantagens:

  • Não é necessário informar os anos na mão (hardcoded)
  • Sem necessidade de criar uma tabela para controlar os anos
  • Desvantagens:

  • Desempenho comprometido, pois é necessário vasculhar a tabela/índice inteira(o) para trazer os anos agrupados

  • Opção 2: trazer o ano a partir de uma tabela auxiliar ou CTE (tbAno)

    Vantagens:

  • Ótimo desempenho pois a tabela será pequena e lida sem agrupamento
  • Por ter tamanho reduzido, há chances de ela caber totalmente na memória, podendo ser reutilizada rapidamente
  • Desvantagens:

  • Temos que incluir os anos manualmente na consulta, aumentando a necessidade de manutenção no código

  • Apesar de ter mostrado as duas opções, uma tally table contendo os anos seria o ideal. Jeff Moden, um dos escritores de artigos do site SQLServerCentral.com e grande nome do universo SQL Server, sempre ressalta que uma tally table é mais útil quando ela é compacta e pode caber totalmente na memória. A nossa tabela de meses seria ideal, pois é sempre estática (vai ter 12 registros eternamente). No caso de uma tabela de anos, quanto menos registros, melhor. Poderíamos já criar previamente os registros desde 1950 até 2050 (apenas 100 linhas com uma coluna SMALLINT) e teríamos a solução mais otimizada possível.

    Mas continuando no nosso exemplo, estamos usando CTEs em vez de tabelas, somente para simplificar.

    Ainda ficou faltando juntarmos as duas CTEs de ano e mês com nossa tabela de vendas e chegarmos ao resultado esperado. Um LEFT JOIN simples poderá resolver nosso problema:

    WITH tbMes AS
    (
    	SELECT 1 AS Mes UNION ALL
    	SELECT 2 AS Mes UNION ALL
    	SELECT 3 AS Mes UNION ALL
    	SELECT 4 AS Mes UNION ALL
    	SELECT 5 AS Mes UNION ALL
    	SELECT 6 AS Mes UNION ALL
    	SELECT 7 AS Mes UNION ALL
    	SELECT 8 AS Mes UNION ALL
    	SELECT 9 AS Mes UNION ALL
    	SELECT 10 AS Mes UNION ALL
    	SELECT 11 AS Mes UNION ALL
    	SELECT 12 AS Mes
    ),
    tbAno AS
    (
    	SELECT 2011 AS Ano UNION ALL
    	SELECT 2012 AS Ano
    )
    SELECT A.Ano,
           M.Mes AS Mes,
           ISNULL(SUM(V.ValorTotal), 0) AS Total
      FROM tbMes M
     CROSS JOIN tbAno A
      LEFT JOIN dbo.Venda V
        ON YEAR(V.DataCadastro) = A.Ano AND MONTH(V.DataCadastro) = M.Mes
     GROUP BY A.Ano, M.Mes;
    

    Observação sobre a consulta: usar as funções YEAR() e MONTH() na cláusula ON, como eu usei acima, pode não ser a melhor maneira para uma tabela muito grande, pois o otimizador de consultas do SQL Server pode não usar o índice correto ou da melhor maneira possível. De qualquer forma, é um método interessante para se chegar em um resultado rapidamente. Se a tabela (de vendas, no nosso exemplo) for muito grande, você pode tentar um range de datas.
    As possibilidades são muitas e as tally tables podem ser usadas de diversas maneiras criativas!

    O resultado final ficou assim:

    Qualquer sugestão para melhoria do código é bem-vinda!
    Até a parte 2! Obrigado pela atenção e leitura.

    Publicado em Passo-a-Passo, SQL Server, T-SQL, Tally Table | Com a tag , , , , , , , , , , | Deixar um comentário

    Script para reconstruir índices no SQL Server

    Olá a todos.

    Gostaria de publicar uma tentativa de script que fiz para detectar índices com alto percentual de fragmentação no SQL Server. O script está longe de ser o ideal ainda; muitas melhorias poderiam ser adicionadas (como a de reconstruir um índice enquanto ele está ONLINE), mas acredito que é um começo.

    Sabemos que, quando os índices alcançam uma alta fragmentação, o Query Optimizer pode não usá-los, mesmo quando parece que o mesmo foi feito para a query em questão. Isso ocorre porque o otimizador percebe que pode gastar mais tempo do que necessário tentando achar os registros no índice, já que estão fragmentados. O Query Optimizer acaba, então, escolhendo um índice que dê menos trabalho a ele, mesmo que não seja o ideal. É por isso que, em alguns casos, percebemos, ao analisar um plano de execução, que o SQL Server não está usando um índice que criamos especialmente para uma consulta, em favor de outro como a chave primário da tabela.

    Quantas vezes você viu lá que o SQL Server prefere usar um índice PK, em vez de ter trabalho de usar um índice que você criou? Logicamente que não é só por esse motivo que isso acontece. Tudo em banco de dados depende da situação. Não existe uma regra fixa.

    Abaixo, mostro uma maneira de procurar índices fragmentados e corrigí-los de uma só vez. Tenha em mente que uma operação dessa em um banco de dados com muitas tabelas contendo muitos registros pode levar um tempo considerável. Muita cautela nunca é demais. Mas ele deve funcionar na maioria dos casos e eu estou ansioso por usá-lo na empresa em que trabalho para acabar com um problema que vem assombrando nossos clientes desde sempre. Outro ponto a ser considerado é que esse script usa a opção REBUILD em ALTER INDEX e isso faz com que os índices sejam recriados, em vez de desfragmentados (poderíamos usar a opção REORGANIZE para isso). Ao reconstruir um índice dessa forma, o SQL Server precisa travá-lo no banco de dados, impedindo seu uso até que a operação seja concluída. Por isso, aconselho que esse tipo de coisa seja feita fora do horário de trabalho, se for usado em um ambiente de produção. Existe uma opção do comando ALTER INDEX que permite reconstruir índices enquanto eles estão sendo usados (opção ONLINE = ON) que funciona em conjunto com a opção REBUILD. No entanto, essa opção só está disponível nas edições Enterprise e Developer. Mais informações podem ser encontradas no Books On Line aqui: http://technet.microsoft.com/en-us/library/ms188388.aspx

    Inicio do script:

    IF OBJECT_ID('dbo.PD_ReconstruirIndicesFragmentados', 'P') IS NOT NULL
      DROP PROCEDURE dbo.PD_ReconstruirIndicesFragmentados;
    GO
    
    CREATE PROCEDURE dbo.PD_ReconstruirIndicesFragmentados
    (
      @Tabela SYSNAME = NULL,
      @FragmentacaoMinima TINYINT
    )
    AS
    BEGIN
      DECLARE @Comando VARCHAR(800),
              @NomeIndice SYSNAME,
              @NomeTabela SYSNAME,
              @TotalIndices INT,
              @LinhasAfetadas INT;
    
      SET NOCOUNT ON;
    
      -- A tabela informada existe?
      IF @Tabela IS NOT NULL
      BEGIN
        IF (SELECT COUNT(*) FROM sys.tables WHERE name = @Tabela) = 0
        BEGIN
    	  RAISERROR('A tabela ''%s'' não existe no banco de dados.',
    	            16, 1, @Tabela);
    	  RETURN(-1);
        END;
      END;
    
      -- O percentual de fragmentação deve estar entre 1 e 100
      IF NOT @FragmentacaoMinima BETWEEN 1 AND 100
      BEGIN
        RAISERROR('O percentual de fragmentação deve ser entre 1 e 100.',
                  16, 1);
        RETURN(-1);
      END;
    
      SELECT OBJECT_NAME(STA.object_id) AS objeto_nome,
             OBJECT_SCHEMA_NAME(STA.object_id) AS schema_nome,
             IDX.name AS indice_nome,
             IDX.type AS indice_tipo,
             STA.avg_fragmentation_in_percent AS percentual_fragmentado,
             STA.page_count,
             IDX.is_unique,
             IDX.is_primary_key
        INTO dbo.#InfoIndices
        FROM sys.dm_db_index_physical_stats(DB_ID(),
                                            NULL,
                                            NULL,
                                            NULL,
                                            NULL) AS STA
       INNER JOIN sys.indexes AS IDX
          ON IDX.index_id = STA.index_id AND IDX.object_id = STA.object_id
       WHERE STA.avg_fragmentation_in_percent >= @FragmentacaoMinima
         AND STA.index_type_desc <> 'HEAP'
         AND STA.page_count > 80
         AND (@Tabela IS NULL OR OBJECT_NAME(STA.object_id) = @Tabela);
    
      SELECT indice_nome,
             indice_tipo,
             objeto_nome,
             'ALTER INDEX ' +
             CASE
               WHEN indice_nome IS NULL THEN
                 'ALL'
               ELSE
                 QUOTENAME(indice_nome)
             END + ' ON ' + QUOTENAME(schema_nome) + '.' +
             QUOTENAME(objeto_nome) +
             ' REBUILD WITH (MAXDOP = 1, SORT_IN_TEMPDB = ON);' AS Comando
       INTO dbo.#AlterIndex
       FROM dbo.#InfoIndices;
    
      SELECT @LinhasAfetadas = @@ROWCOUNT,
             @TotalIndices = 0;	
    
      WHILE @LinhasAfetadas > 0
      BEGIN
        SELECT TOP(1)
               @Comando = Comando,
               @NomeIndice = indice_nome,
               @NomeTabela = objeto_nome,
               @TotalIndices += 1
          FROM dbo.#AlterIndex
         ORDER BY indice_tipo, indice_nome;
    
        EXECUTE(@Comando);
    
        RAISERROR('O índice ''%s'' do objeto ''%s'' foi desfragmentado com sucesso.',
                  0, 1, @NomeIndice, @NomeTabela) WITH NOWAIT; 
    
        DELETE Tabela
          FROM (SELECT TOP(1) *
                  FROM dbo.#AlterIndex
                 ORDER BY indice_tipo, indice_nome) AS Tabela;
    
        SET @LinhasAfetadas = @@ROWCOUNT;
      END;
    
      RAISERROR('Total de índices desfragmentados: %d', 0, 1, @TotalIndices);
    END;
    GO
    
    Publicado em Administração de Bancos de Dados, SQL Server, T-SQL | Com a tag , , , , , , , , , , | 2 comentários

    SQL Saturday #100 – Brasil

    Simplesmente fenomenal! Um dos maiores eventos sobre SQL Server da América Latina teve lugar na sede da Microsoft, em São Paulo, capital e reuniu mentes brilhantes e entusiastas de todo o Brasil. Nunca um sábado foi tão bem aproveitado como o do dia 26/11/2011.

    Tive a sorte de poder participar do evento e assisti treinamentos diversos em várias áreas relacionadas à SQL Server. Tudo foi perfeito, café, acomodações, almoço (lanche natureba para quem pagou adiantado, pela internet) e palestras inspiradas!

    Agradeço, de coração, a todo o pessoal que organizou o evento: Felipe Ferreira, Viviane Ribeiro, Andressa Martins, Laerte Junior e muitos outros que tornaram esse evento gratuito em uma fonte de conhecimento para todos nós.

    Fiquei meio tímido lá, já que cheguei sozinho, mas tirei algumas fotos, que selecionei abaixo:

    Fila para receber o crachá na entrada do prédio

    Outro ângulo da fila de entrada

    Fabiano Amorim, antes de sua palestra e Viviane Ribeiro

    Aguardando ansiosamente tudo começar…

    Aguardando a chegada de todos, assisitindo vídeos de introdução…

    Introdução com os organizadores do evento…

    Marcelo Fernandes e sua palestra sobre TDE

    Vista do prédio da Microsoft

    Hora do café

    Mais hora do café…

    Eu, contemplando a vista.

    Laerte Junior – Usando WMI no Powershell

    Daniel Camillo mostrando o System Center

    Sponsor Time! Pessoal da Confio

    Loco y logo.

    Vítor Fava – Alta Disponibilidade

    Vitor Fava – Clustering

    Nilton Pinheiro – AlwaysOn no SQL Server 2012

    Nilton Pinheiro – Resumo Final

    Enfim, foi muito bom ter estado ali…

     

    Até o próximo evento. Prometo levar mais gente comigo. :-D

    Uma ótima semana a todos!

    Publicado em Cursos Gratuitos, SQL Saturday 100, SQL Server | Com a tag , , , , , , , , , , | 2 comentários

    Introdução ao Change Tracking (e o que ele pode fazer por você)

    O Change Tracking é uma nova funcionalidade do SQL Server 2008 e tem conquistado adeptos em muitos fóruns por aí. Vamos rever como ele funciona? A seguir, publico um mini-tutorial de como habilitar e usar o dito cujo e como armazenar mais informação do que ele já nos armazena de fábrica.

    Primeiramente, vamos criar um banco de dados teste:

    USE master;
    GO
    
    IF DB_ID('BancoTesteChangeTracking') IS NOT NULL
    	DROP DATABASE BancoTesteChangeTracking;
    GO
    CREATE DATABASE BancoTesteChangeTracking;
    GO
    
    USE BancoTesteChangeTracking;
    GO

    Criaremos, também, uma tabela que será usada para INSERTs de dados, que serão monitorados pelo Change Tracking, mais para a frente:

    IF OBJECT_ID('dbo.TabelaChangeTracking', 'U') IS NOT NULL
    	DROP TABLE dbo.TabelaChangeTracking;
    GO
    CREATE TABLE dbo.TabelaChangeTracking
    (
    	Id INT IDENTITY,
    	Informacao VARCHAR(200)
    );

    Temos que habilitar a funcionalidade de CHANGE TRACKING no banco de dados antes de habilitá-la para qualquer tabela dentro do banco. A opção AUTO_CLEANUP nos proporciona um método automático de limpar o histórico que não nos interessa mais. Podemos definir, através da opção CHANGE_RETENTION, quantos minutos, horas ou dias queremos que a informação fique persistida na tabela interna do CT, antes que a limpeza seja feita.

    ALTER DATABASE BancoTesteChangeTracking SET CHANGE_TRACKING = ON
    (
    	AUTO_CLEANUP = ON,
    	CHANGE_RETENTION = 2 DAYS
    );
    GO

    Apesar de o CHANGE TRACKING rodar tranquilamente sem maiores configurações, é recomendado habilitar o isolamento do banco de dados em SNAPSHOT ISOLATION, o que transformará nosso isolamento de READ_COMMITTED em READ_COMMITTED_SNAPSHOT, garantindo uma melhor segurança de que os dados gravados pelo CHANGE TRACKING serão consistentes, sem bloqueios e com a versão correta.

    Do BOL:Quando a opção banco de dados ALLOW_SNAPSHOT_ISOLATION está definida como ON, a instância do mecanismo de banco de dados do SQL Server não gera versões de linhas para os dados modificados até que todas as transações ativas que modificaram os dados no banco de dados estejam concluídas.

    Então, se você tiver diversas transações modificando a mesma coisa, o SQL Server irá esperar e, quando todas as transações tiverem finalizado, ele irá versionar corretamente as alterações, causando maior segurança no Change Tracking e sem bloqueios, já que é para isso que o isolamento SNAPSHOT serve.

    Mais informações: http://msdn.microsoft.com/pt-br/library/ms175095.aspx

     

    ALTER DATABASE BancoTesteChangeTracking
    	SET ALLOW_SNAPSHOT_ISOLATION ON;
    GO

    O próximo passo é habilitar o CHANGE TRACKING para uma tabela do banco de dados – no exemplo, a tabela “dbo.TabelaChangeTracking”. Tente fazer isso com o comando abaixo e você verá um erro sendo mostrado.

    ALTER TABLE dbo.TabelaChangeTracking ENABLE CHANGE_TRACKING
    	WITH (TRACK_COLUMNS_UPDATED = ON);
    GO


    Msg 4997, Level 16, State 1, Line 1

    Cannot enable change tracking on table ‘TabelaChangeTracking’. Change tracking requires a primary key on the table. Create a primary key on the table before enabling change tracking.

    Se você tiver o SQL Server configurado em inglês para o login atual, essa é a mensagem que irá aparecer. Como regra geral, é necessária uma chave primária na tabela em que você quiser habilitar o CHANGE TRACKING. Só deixei isso acontecer para mostrar que há uma coisa importante a ser levada em consideração.

    Então, vamos fazê-lo:

    ALTER TABLE dbo.TabelaChangeTracking
    	ADD CONSTRAINT PK_Id PRIMARY KEY(Id);
    GO

    Agora, sim, podemos habilitar o CHANGE TRACKING. Segue o comando abaixo. Repare que ele difere do comando do ALTER DATABASE com a cláusula ENABLE em vez de usar o SET. A opção TRACK_COLUMNS_UPDATED serve para identificarmos quais colunas foram alteradas.

    ALTER TABLE dbo.TabelaChangeTracking ENABLE CHANGE_TRACKING
    	WITH (TRACK_COLUMNS_UPDATED = ON);
    GO

    Vamos, então, testar nosso primeiro INSERT na tabela:

    INSERT INTO dbo.TabelaChangeTracking(Informacao)
    	VALUES('We are the champions, my friend');
    GO

    Quando habilitamos o Change Tracking para a tabela, o SQL Server criou uma tabela interna chamada “sys.change_tracking_[object_id]“. Isto é, se sua tabela possuir o object_id de 5575058, por exemplo, o nome da tabela interna criada será “sys.change_tracking_5575058″. Como essa tabela é interna, você só conseguirá acessá-la usando uma conexão dedicada de administrador (DAC). Mesmo assim, você pode verificar que ela foi criada consultando a view de sistema “sys.internal_tables”.

    O acesso normal à tabela interna vem através da função CHANGETABLE, como mostrada a seguir.

    SELECT * FROM CHANGETABLE(CHANGES dbo.TabelaChangeTracking, 0) AS Tabela;

    Uma linha foi criada na tabela interna, contendo a primeira versão de alteração na tabela, com a operação de INSERT representada pela letra “I” na coluna SYS_CHANGE_COLUMNS e a chave primária (Id) identificando o registro alterado.

    Como via de regra, o Change Tracking não irá nos servir muito para fins de auditoria, pois é um mecanismo simplificado (peso-leve) de rastreamento de mudanças, sendo compatível com todas as edições do SQL Server. A disponibilidade em diversas edições, inclusive na edição Express, é um dos motivos de ser uma nova funcionalidade recomendada em vários blogs e artigos por aí.

    O Change Tracking não nos mostrará QUEM alterou os dados e COMO os dados estavam antes de serem alterados. Ele é mais recomendado na sincronização entre bancos de dados remotos ou em dispositivos móveis que ficam desconectados do banco de dados principal. Através dele, é possível saber QUAIS linhas foram alteradas ou incluídas para uma sincronização mais rápida ou importação através de algum método ETL (Extract-Transform-Load), por exemplo.

    Apesar de tudo, ainda existe uma maneira de fazer com que o Change Tracking sirva para auditorias leves ou, pelo menos, armazenar QUEM alterou os dados. Para isso, podemos usar uma funcionalidade importante (e muitas vezes desconhecida) do Change Tracking que é o CHANGE_TRACKING_CONTEXT.

    DECLARE @contexto VARBINARY(128);
    
    SET @contexto = CAST(USER_NAME() AS VARBINARY(128));
    
    WITH CHANGE_TRACKING_CONTEXT(@contexto)
    INSERT INTO dbo.TabelaChangeTracking(Informacao)
    	VALUES('I love rock''n roll!');
    GO

    Execute novamente a função CHANGETABLE (usando, no parâmetro de versão, o número zero de novo; isto é, desde o início) e vamos ver o que apareceu lá:

    SELECT * FROM CHANGETABLE(CHANGES dbo.TabelaChangeTracking, 0) AS Tabela;

    Vamos visualizar os resultados?

    Repare que, nos resultados acima, há uma nova inclusão e a versão dada a ela é 2. Mas perceba como a coluna SYS_CHANGE_CONTEXT está preenchida, desta vez, com o valor que informamos na variável @contexto. Isso significa que armazenamos o usuário que fez a inclusão desse registro.

    Quer ver só?

    SELECT CAST(SYS_CHANGE_CONTEXT AS SYSNAME)
      FROM CHANGETABLE(CHANGES dbo.TabelaChangeTracking, 1) as tab;

    Ao convertermos de VARBINARY(128) para SYSNAME, temos o nome de usuário que guardamos. Você pode estar se perguntando o motivo de eu ter informado valor 1 para o parâmetro “last_sync_version”. Este parâmetro mostra todas as alterações desde a última versão (last_sync_version) e, por causa disso, somente a versão 2 será mostrada. Se houvesse mais versões depois da versão 2, todas elas seriam mostradas.

    CONCLUSÃO:

    O fato de podermos armazenar algum tipo de informação na coluna SYS_CHANGE_CONTEXT, apesar do tamanho limitado, torna o Change Tracking atraente para um registro de log de operações simples. É uma boa opção, também, para quem possui a edição Express do SQL Server, pois o CT é compatível com qualquer edição. Fora isso, o processo de seleção de dados para sincronizar dois bancos de dados é minimizado, pois você pode trazer somente o que foi alterado desde a última sincronização. Espero que a Microsoft continue apoiando e melhorando essa funcionalidade.

    Em um próximo artigo, falarei mais sobre o Change Tracking, demonstrando algumas coisas que não abordei aqui, como a identificação de tabelas e um exemplo de sincronização.

    Um bom artigo mostrando um exemplo de sincronização usando o Change Tracking (em inglês), encontra-se no link a seguir: http://www.sqlservercentral.com/articles/Change+Tracking/74397/

    Espero que tenham gostado e até a próxima!


    REFERÊNCIA:

    - Livro “SQL Server Internals” – Kalen Delaney: http://www.amazon.com/Microsoft%C2%AE-SQL-Server%C2%AE-2008-Internals/dp/0735626243/ref=sr_1_1?ie=UTF8&qid=1322694216&sr=8-1

    Publicado em Change Tracking, T-SQL, Tutorial | Com a tag , , , | Deixar um comentário

    Dica Rápida: Transformar valores de uma coluna em CSV

    Uma dica bem interessante que uso no dia-a-dia e pensei em compartilhar é criar uma string CSV (de “comma separated values” ou “valores separados por vírgula”) a partir dos dados resultantes de um SELECT.

    Isto é, transformar o resultado de um SELECT em uma string como “valor1, valor2, valor3, valor4, valor5″.

    DECLARE @NomeBancos VARCHAR(MAX);
    
    SELECT @NomeBancos = ISNULL(@NomeBancos + ', ', '') + name
      FROM sys.databases;
    
    SELECT @NomeBancos;

    Coisa bem rápida, mas uma boa dica, especialmente se você está construindo uma lista de parâmetros para passar em uma procedure.

    Boa semana!

    Publicado em Dica Rápida, T-SQL | Com a tag , , | Deixar um comentário

    SQL Server + Inglês = :-)

    Ontem, eu e minha esposa assistimos a uma palestra ótima do Fabiano Amorim na faculdade FACCAT de Tupã, que parece ter motivado bastante os que compareceram. Será que vamos ver uma nova geração de DBAs juniores ou desenvolvedores daqui da região? Eu torço para que sim e um evento desse só serve para reforçar o que o Fabiano falou lá: que existe muita coisa de graça na internet para estudar, conteúdo imenso (principalmente em inglês, ponto alto da noite) e muita gente querendo ajudar em comunidades do SQL Server espalhadas por fóruns, grupos de discussão, sites especializados, etc, etc.

    Para você que quer aprender inglês e outras línguas, recomendo demais o site do Livemocha, que é uma comunidade ou rede social de gente que está interessada em aprender e também ensinar outras línguas. Tem curso para tudo que é nível e todos têm vídeos, lições escritas e outras em áudio, onde você participa de um diálogo em outra língua, usando um microfone de computador. O mais interessante é que suas lições são enviadas para toda a comunidade e aparece um monte de gente (que fala nativamente a língua que você quer aprender) para corrigir as suas lições, te dar dicas de pronúncias, etc.

    Eu faço parte da comunidade e contribuo como professor de inglês e português lá. Também estou aprendendo espanhol e japonês. Se alguém quiser entrar, me avise que eu adiciono como amigo e ajudo a corrigir os exercícios. Vamos usar as redes sociais para uma coisa útil, pessoal! :-D

    E como todos sabem, o inglês domina a internet, existem livros excelentes por aí que nunca vão ser traduzidos para o português, artigos a rodo e comunidades como o SQL Server Central e o Simple-Talk que contém um universo de informação sobre SQL Server.

    Mudando um pouco de assunto, alguns dias atrás, recebi presentes muito legais da Microsoft por ter enviado feedback e relatório de bugs sobre o SQL Server Denali, mais especificamente sobre o SQL Server Management Studio (vulgo SSMS). Obrigado à equipe do SQL Server 2012 pela dedicação.

    Sempre fui crítico quando se tratava de Microsoft, mas tenho que tirar o chapéu para a equipe inteira do SQL2012. Com certeza, vai ser um lançamento excelente.

    Eles só têm que lapidar um pouco mais o Management Studio e alguns outro problemas encontrados por outras pessoas, mas, pelo pouco que testei, o negócio realmente é monstruoso. :)

    Publicado em Cursos Gratuitos, Inglês, SQL Server | Com a tag , , , , , | Deixar um comentário

    Tabelas Temporárias – Contexto

    Um assunto interessante que gostaria de compartilhar é o uso de tabelas temporárias dentro de procedures. Muita gente coloca um comando no fim de uma procedure como o seguinte:

    DROP TABLE dbo.#TabelaTemporaria;
    

    Lógico que isso é uma medida genérica que é adotada em qualquer caso para não deixar objetos criados e esquecidos no banco TempDB. Mas não é necessária. Uma tabela temporária local é eliminada logo que o contexto dela termina. O contexto normalmente é o processo, “batch” de comando SQL ou, no caso apresentado, uma procedure. Resumindo: acabou a procedure, não existe mais a tabela temporária local.

    Isso ocorre porque a procedure ocorre em um contexto próprio de execução. E assim como a criação de uma nova tabela temporária, tudo o que acontece dentro de uma procedure faz parte do contexto dela. Até mesmo outras procedures que possam ser chamadas dentro dela. Isto é, você pode acessar uma tabela temporária local de dentro de uma procedure que está dentro do contexto da procedure que criou a dita cuja.

    Um exemplo:

    IF OBJECT_ID('dbo.Procedure2', 'P') IS NOT NULL
    	DROP PROCEDURE dbo.Procedure2;
    GO
    CREATE PROCEDURE dbo.Procedure2
    AS
    BEGIN
    	-- Incluindo um registro em uma tabela temporária
    	-- que não foi criada dentro desta procedure
    	INSERT INTO dbo.#Temp VALUES('Andre', 'Guerreiro');
    END;
    GO
    
    IF OBJECT_ID('dbo.Procedure1', 'P') IS NOT NULL
    	DROP PROCEDURE dbo.Procedure1;
    GO
    CREATE PROCEDURE dbo.Procedure1
    AS
    BEGIN
    	-- Criando a tabela temporária...
    	CREATE TABLE dbo.#Temp
    	(
    		Nome VARCHAR(50),
    		SobreNome VARCHAR(50)
    	);
    
    	-- Executando a Procedure2 que irá acessar #Temp
    	EXEC dbo.Procedure2;
    
    	-- Verificando que o registro foi incluído na
    	-- tabela temporária...
    	SELECT * FROM dbo.#Temp;
    END;
    GO
    
    EXEC dbo.Procedure1;
    GO
    

    Em poucas palavras, o que aconteceu foi que a Procedure2 conseguiu acesso à tabela temporária que foi criada dentro da Procedure1 simplesmente porque ela foi chamada pela Procedure1. Isso significa que a Procedure2 está dentro do código da procedure e executada dentro do contexto dela e, nesse caso a tabela temporária fica disponível a ela. Simples, não é?

    Se você fosse tentar acessar a tabela #Temp após a execução da Procedure1 e não dentro dela, seria mostrado um erro dizendo que a tabela #Temp não foi encontrada, bla, bla, bla…

    Teste o código abaixo:

    IF OBJECT_ID('dbo.Procedure2', 'P') IS NOT NULL
    	DROP PROCEDURE dbo.Procedure2;
    GO
    CREATE PROCEDURE dbo.Procedure2
    AS
    BEGIN
    	-- Incluindo um registro em uma tabela temporária
    	-- que não foi criada dentro desta procedure
    	INSERT INTO dbo.#Temp VALUES('Andre', 'Guerreiro');
    END;
    GO
    
    IF OBJECT_ID('dbo.Procedure1', 'P') IS NOT NULL
    	DROP PROCEDURE dbo.Procedure1;
    GO
    CREATE PROCEDURE dbo.Procedure1
    AS
    BEGIN
    	-- Criando a tabela temporária...
    	CREATE TABLE dbo.#Temp
    	(
    		Nome VARCHAR(50),
    		SobreNome VARCHAR(50)
    	);
    END;
    GO
    
    EXEC dbo.Procedure1;
    GO
    EXEC dbo.Procedure2;
    GO
    

    O erro será mostrado.

    Isso pode parecer uma assunto trivial mas é interessante saber esses tipos de hierarquia de contexto. Temos o mesmo em qualquer linguagem de programação onde os objetos ou variáveis que foram criados dentro de um bloco de código somente estarão disponíveis para aquele bloco de código e outros que são criados dentro dele (a não ser que a variável declarada seja global).

    Se precisarmos que a tabela fique persistida por mais tempo, teremos que criar uma tabela temporária global que tem o prefixo “##”. Esse tipo de tabela continua armazenada no banco de dados TempDB (e, às vezes, na memória) mas, em vez de ser excluída quando o processo que a criou termina (após um “GO” no Management Studio, por exemplo), ela só é eliminada quando a sessão que a criou é encerrada.

    Outra coisa: para verificarmos se uma tabela temporária existe, usando OBJECT_ID, devemos também informar à função que se trata do banco de dados TempDB:

    IF OBJECT_ID('tempdb.dbo.#Temp', 'U') IS NOT NULL
    	DROP TABLE dbo.#Temp;
    GO
    

    Mas como disse antes, se você estiver criando a tabela de dentro de uma procedure, o código acima não é necessário.
    Tabelas temporárias, assim como as variáveis TABLE são ótimos recursos para resultsets intermediários, cálculos e outros usos. Cada um tem suas vantagens e desvantagens, mas isso é assunto para outra conversa. :D

    Bom domingo!

    Publicado em Sem categoria | Com a tag , , , | Deixar um comentário

    Curiosidade: NOT IN + NULLs

    Feriado está acabando!
    Para quem não ficou sabendo, está acontecendo nos dias de hoje e amanhã o 24 Horas do PASS, um evento que oferece 24 horas de palestras online gratuitas (em inglês, diga-se de passagem) sobre SQL Server. Muitos dos assuntos são bem avançados mas como são muitas as palestras, vale a pena conferir.

    Uma das coisas que me chamaram a atenção essa semana foi a maneira como o SQL Server compara valores dentro de uma expressão NOT IN quando se tem qualquer valor nulo sendo comparado. Isso ocorre também no Oracle, pelo que li em um artigo, então, acredito ser padrão no SQL. Corrijam-me se estiver falando besteira.

    Vamos a um exemplo básico:

    -- Primeiro Exemplo: USANDO "IN"
    SELECT name
      FROM sys.databases
     WHERE name IN (NULL, 'master');
    

    Deve retornar somente um registro, correto?

    Agora vamos negar a comparação, isto é, usar NOT IN:

    -- Segundo Exemplo: USANDO "NOT IN"
    SELECT name
      FROM sys.databases
     WHERE name NOT IN (NULL, 'master');
    

    O que aconteceu com o meu result set? Estou ficando louco?
    Pode ser, mas a query acima não vai retornar nenhuma linha mesmo assim.

    Por que isso acontece?

    Li alguns artigos e fóruns afins para tentar entender esse tipo de resultado e muitos deles, apesar de explicar de forma teórica e bem detalhada, não me mostraram uma forma prática de aceitar esse comportamento. Então, segue abaixo minha própria dedução, baseado em operações dos planos de execução.

    Nós sabemos que NULL representa a ausência de valor e, como tal, não pode ser comparado a nenhum valor. Nem a ele mesmo, como na consulta abaixo:

    SELECT name
      FROM sys.databases
     WHERE NULL = NULL;
    

    Qualquer valor no lugar de NULL traria todos os registros da view mas, como NULL não pode ser comparado a nada, nada é retornado. O que ocorre nos exemplos acima em que um traz registros e o outro não?

    Vamos lá! Se analisarmos o plano de execução, poderemos ver que as cláusulas IN e NOT IN são substituídas por comparações diretas (= ou <>). Confira o código abaixo:

    -- Primeiro Exemplo: USANDO "IN"
    SELECT name
      FROM sys.databases
     WHERE name IN (NULL, 'master');
    
    -- O SQL é convertido para:
    SELECT name
      FROM sys.databases
     WHERE name = NULL OR name = 'master';
    
    -- O resultado lógico seria:
    SELECT name
      FROM sys.databases
     WHERE UNKNOWN OR TRUE;
    

    Sabemos que em SQL existem três valores lógicos possíveis para resultados: VERDADEIRO, FALSO e DESCONHECIDO, o que torna tudo meio confuso, ainda mais quando usamos tipos de dados BIT (que pode conter 0, 1 e NULL).

    A primeira comparação (name = NULL) retorna DESCONHECIDO porque NULL não pode ser comparado a nada e a segunda (name = ‘master’) retorna VERDADEIRO, mas como temos um operador OR unindo-as, qualquer uma das duas sendo verdadeira satisfará nossa consulta. Por isso, o motivo dessa query ter nos retornado registros.

    Agora, quanto ao segundo exemplo:

    -- Primeiro Exemplo: USANDO "IN"
    SELECT name
      FROM sys.databases
     WHERE name NOT IN (NULL, 'master');
    
    -- O SQL é convertido para:
    SELECT name
      FROM sys.databases
     WHERE name <> NULL AND name <> 'master';
    
    -- O resultado lógico seria:
    SELECT name
      FROM sys.databases
     WHERE UNKNOWN AND TRUE;
    

    A cláusula NOT IN nos diz algo como “encontre todos os valores que sejam diferentes de NULL E também diferentes de ‘master’“. Como temos um operador AND dessa vez, a expressão “name <> NULL” já retornou DESCONHECIDO e inutilizou nossa consulta toda, pois as duas expressões têm que retornar verdadeiro, correto?

    A mesma situação ocorre quando usamos EXISTS e NOT EXISTS. Dessa forma, o SQL Server (e outros SGBDs) espera uma comparação “IS NULL” ou “IS NOT NULL” e não “= NULL” ou “<> NULL“.

    Os exemplos que dei acima são bem simples, mas em vez de constantes separadas por vírgula dentro da cláusula NOT IN, poderíamos ter o resultado de uma tabela derivada. Um ou mais registros dessa tabela derivada poderia ser nulo e isso complicaria a visualização do problema em uma SELECT mais complexo.

    Se quisermos que o SQL Server trate NULL como um valor, devemos desabilitar a opção ANSI_NULLS para a sessão:

    SET ANSI_NULLS OFF;
    

    No entanto, os livros online recomendam deixar essa opção sempre habilitada e que, em uma futura versão, isso não poderá ser modificado. Parece ser uma forma do SQL Server se aproximar cada vez mais do padrão ANSI SQL, o que é bom para todos. :)

    Até a próxima e bom fim de feriado!

    Publicado em T-SQL | Deixar um comentário

    Bem-vindo ao meu blog. :)

    Olá a todos que estão visitando meu blog!

    Já faz algum tempo que eu queria criar vergonha na cara e abrir um blog para registrar os assuntos que vinha estudando sobre SQL Server e outras descobertas que faço, de vez em quando, no meu ambiente profissional.

    Logicamente que muitos posts terão a ajuda de algum colega de trabalho, amigo ou serão influenciados por sites como o brilhante SQLServerCentral, administrado por ninguém menos que Steve Jones, lenda no mundo do SQL Server. Outro site importante que reúne grandes cabeças é o Simple-Talk, que possui uma seção especial para ambientes SQL.

    Não poderia deixar de citar o blog do Fabiano Amorim (que muito me ensinou através de palestras e webcasts) e o SQL GO!!! da Andressa Martins e da Socorro Vieira.

    Existem vários outros que me fogem à memória agora, mas conforme for postando, vou colocando novos links!

    Já que este post é de introdução e não tem muita coisa, compartilho abaixo dois vídeos ótimos sobre planos de execução do Fabiano Amorim, disponibilizados publicamente:

    Chegou até aqui? Obrigado pela atenção. :)

    Publicado em Sem categoria | Com a tag , , | 3 comentários