Fabiano Neves's profileFabiano Neves Amorim - S...PhotosBlogListsMore Tools Help

Blog


    June 27

    10 pontos que devem ser observados quanto a performance de uma consulta Parte 2

    3.       Sempre que possível substituir condições com OR por UNION ALL, por ex:

    SET NOCOUNT ON

    GO

    IF OBJECT_ID('Teste ') IS NOT NULL

      DROP TABLE Teste

    GO

    CREATE TABLE Teste (ID        Int Identity(1,1),

                        CPF       Char(11),

                        Nome      VarChar(200),

                        Sobrenome VarChar(200),

                        Endereco  VarChar(200),

                        Bairro    VarChar(200),

                        Cidade    VarChar(200))

    GO

    -- Inclui 1000 mil de linhas na tabela

    INSERT INTO Teste(CPF, Nome, SobreNome, Endereco, Bairro, Cidade)

                VALUES('11111111111', NEWID(), 'Neves Amorim', NEWID(), NEWID(), NEWID())

    GO 1000

     

    CREATE CLUSTERED INDEX ix_ID ON Teste(ID)

    GO

    CREATE INDEX ix_Nome ON Teste(Nome)

    GO

     

    /*

      Seleciona todos os registros onde ID = 10 ou então o Nome inicia com 38.

      Esta consulta irá gerar um Scan na tabela pois o OR impede que o SQL use

      o ix_ID ou o ix_Nome.

    */

    SELECT *

      FROM Teste

     WHERE ID = 10

        OR Nome Like '38%'

    GO

     

    /*

      A instrução acima deve ser trocada por a consulta abaixo que utiliza o UNION ALL

    */

    SELECT Tab.*

      FROM (SELECT *

              FROM Teste

             WHERE ID = 10

             UNION ALL

            SELECT *

              FROM Teste

             WHERE Nome Like '38%') AS Tab

     

    Obs.: Sempre que possível utilize “UNION ALL” ao invés de “UNION” pois o “UNION” gera um distinct que geralmente gera um order by o que irá gerar um custo desnecessário comparado a concatenação do “UNION ALL”.

     

    Continua...

    10 pontos que devem ser observados quanto a performance de uma consulta Parte 1

    Performance de querys é sem dúvida uma das maiores causadoras de dor de cabeça em DBAs e afins(J), se vocês já leram algum post neste blog devem ter percebido que gosto muito deste assunto, trato diariamente com problemas deste tipo e tem alguns pontos que acho importantes de serem analisados quando falamos em análise de consultas, vou tentar explicar melhor abaixo.

     

    Primeiro vou falar um pouco da empresa onde trabalho a, CNP-M, graças a Deus somos uma empresa certificada MPS.BR pois os processos que foram implantados nos ajudam a diminuir e MUITO possíveis problemas de performance que teríamos e que não deixamos chegar nos clientes pois param no processo de validação. Os pontos que vou mencionar abaixo servem como base para procurar possíveis problemas de performance, bom chega de conversa mole e vamos para a melhor parte(você já sabe, TSQL).

     

    Todas as consultas abaixo foram executadas no banco AdventureWorks / SQL Server 2005.

     

    1.       Sempre verificar o plano de execução de cada select existente no código SQL, e analisar o uso ou não uso dos índices de cada tabela pertencente a query.

    2.       Verificar o uso de Functions. Caso exista alguma function envolvida no SQL analise bem a consulta e verifique se é possível alterar a consulta para fazer um join com a própria tabela ou então até mesmo tabelas temporárias, vamos ver alguns exemplos para ficar mais fácil de entender o que estou querendo dizer.

    IF OBJECT_ID('VendaPorCliente') IS NOT NULL

      DROP FUNCTION dbo.VendaPorCliente

     

    GO

    CREATE FUNCTION dbo.VendaPorCliente(@CustomerID Int)

    RETURNS Decimal(18,2)

    AS

    BEGIN

      DECLARE @Total Decimal(18,2)

     

      SELECT @Total = SUM(OrderQty * UnitPrice)

        FROM AdventureWorks.Sales.SalesOrderHeader a

       INNER JOIN AdventureWorks.Sales.SalesOrderDetail b

          ON a.SalesOrderID = b.SalesOrderID

       WHERE a.CustomerID = @CustomerID

     

      RETURN @Total

    END

    GO

    /*

                    Seleciona o total de venda por Customer

       Aqui temos um problema, pois para cada linha na tabela Customer o SQL Server irá

       executar a Function VendaPorCliente, ou seja se minha tabela Customer tiver

       50000 linhas o SQL irá acessar as tabelas de header e Detail 50000 vezes.

    */

    SELECT AccountNumber, dbo.VendaPorCliente(CustomerID) as Total

      FROM AdventureWorks.Sales.Customer

     

    /*

       Para resolver o problema da consuta acima poderiamos fazer o seguinte

       Criar uma nova function do tipo "multi-statement table-valued"

    */

     

    IF OBJECT_ID('VendaTotalClientes') IS NOT NULL

      DROP FUNCTION dbo.VendaTotalClientes

     

    GO

    CREATE FUNCTION dbo.VendaTotalClientes()

    RETURNS @tb_result TABLE

    (

      CustomerID Int,

      Total      Decimal(18,2),

      PRIMARY KEY(CustomerID)

    )

    AS

    BEGIN

      INSERT INTO @tb_result 

      SELECT a.CustomerID, SUM(OrderQty * UnitPrice) Total

        FROM AdventureWorks.Sales.SalesOrderHeader a

       INNER JOIN AdventureWorks.Sales.SalesOrderDetail b

          ON a.SalesOrderID = b.SalesOrderID

       GROUP BY a.CustomerID

     

      RETURN

    END

    GO

    /*

                   Seleciona o total de venda por Customer

       Desta vez ao invés de acessar a function para cada linha da tabela Customer

       o SQL Server irá ler os dados das tabelas Header e Detail apenas 1 vez pois a function

       irá retornar todos os dados em uma tabela.

    */

    SELECT AccountNumber, b.Total

      FROM AdventureWorks.Sales.Customer a

     INNER JOIN dbo.VendaTotalClientes() b

        ON a.CustomerID = b.CustomerID

    GO

    /*

       Agora chegamos onde eu queria!, imagine que eu queria retornar o total de venda apenas do

       Customer 'AW00000001', eu iriá escrever o seguinte select.

       O que acontece abaixo é que o SQL irá primeiro retornar todos os dados da function, ou seja,

       todas as vendas por customer e depois aplicar o filtro de AccountNumber = 'AW00000001'

    */

    SELECT AccountNumber, b.Total

      FROM AdventureWorks.Sales.Customer a

     INNER JOIN dbo.VendaTotalClientes() b

        ON a.CustomerID = b.CustomerID

     WHERE a.AccountNumber = 'AW00000001'

    GO

    /*

      O ideal neste caso seria usar:

    */

    SELECT AccountNumber, dbo.VendaPorCliente(CustomerID) as Total

      FROM AdventureWorks.Sales.Customer

     WHERE AccountNumber = 'AW00000001'

    GO

    /*

      Ou então simplesmente não utilizar a function e fazer o select direto nas tabelas

    */

    SELECT a.AccountNumber, SUM(OrderQty * UnitPrice) AS Total

      FROM AdventureWorks.Sales.Customer a

     INNER JOIN  AdventureWorks.Sales.SalesOrderHeader b

        ON a.CustomerID = b.CustomerID

     INNER JOIN AdventureWorks.Sales.SalesOrderDetail c

        ON b.SalesOrderID = c.SalesOrderID

     WHERE a.AccountNumber = 'AW00000001'

     GROUP BY a.AccountNumber

    GO

     

    Obs.: Atenção, o comando SET STATISTICS IO ON não leva em consideração as leituras efetuadas nas suas functions, o que certas vezes acaba gerando uma má interpretação do comando, portanto fique ligado nisso.

     

    Continua...

    June 18

    Why triggers are Bad - Part II

    Segue o post do Conor falando o que ele acha sobre Triggers.

     

    The Trouble with Triggers

     

    Excelente artigo.

     

    É bom saber que não sou só eu que penso assim, não gosto de triggers e sempre que puder tentarei evita-las, não tenho isso como regra mas como base, o que é diferente.

     

     

    June 17

    ALT + Left Click

    Puts, essa foi show, eu sabia a do ALT+SHIFT + setas do teclado, mas essa do ALT + Botão Esquerdo do mouse foi sacanagem.

     

    http://blogs.msdn.com/psssql/archive/2008/06/17/helpful-hint-making-review-of-a-query-plan-easier.aspx

     

    É o tipo da coisa que quando você descobre logo pensa, puts e ninguém me conta?

     

    Update:

      Aaa no Dephi e Visual Studio também funciona :-)

    Why Triggers are Bad

    I’m talking with Conor about triggers, I try explain why I don’t like triggers, and how they can be bad for performance. He ask-me one sample about bad performance then I create this script on database AdventureWorks, I think he will write in your blog about triggers soon, then keep eye.

     

    If you don’t know him, he blog on SQLSkills, do you need more? J

     

    http://www.sqlskills.com/blogs/conor/default.aspx

     

     

     

    IF OBJECT_ID('Production.uProduct') IS NOT NULL

      DROP TRIGGER Production.uProduct

    GO

     

    CREATE TRIGGER [Production].[uProduct] ON [Production].[Product]

    AFTER UPDATE NOT FOR REPLICATION AS

    BEGIN

        SET NOCOUNT ON;

     

        UPDATE [Production].[Product]

        SET [Production].[Product].[ModifiedDate] = GETDATE()

        FROM inserted

        WHERE inserted.[ProductID] = [Production].[Product].[ProductID];

    END;

    GO

     

    -- Here we have one column called ModifiedDate, and one trigger to update this column.

    SELECT ModifiedDate, *

      FROM Production.Product

     WHERE ProductID = 316

     

    -- Now if I update the column Name of the product, the trigger will update the column

    -- ModifiedDate with the GetDate()

    SET STATISTICS PROFILE ON

    SET STATISTICS IO ON

     

    UPDATE Production.Product

       SET Name = 'New Blade'

     WHERE ProductID = 316

    --Table 'Product'. Scan count 0, logical reads 6, physical reads 4, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    --Table 'Product'. Scan count 0, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

     

    SET STATISTICS IO OFF

    SET STATISTICS PROFILE OFF

    -- Whats happened here? First the Update will search for the clustered Index,

    -- then the Trigger will do the same thing again. On the other hands,

    -- 2 scans on table Product

     

     

    IF OBJECT_ID('Supper_UpdateProduct') IS NOT NULL

      DROP PROC dbo.Supper_UpdateProduct

    GO

    -- Now lets go create one Stored Procedure to Update the Name and ModifiedDate of the Product

    CREATE PROC dbo.Supper_UpdateProduct (@NewName nVarChar(50) = '', @ProductID Int = 0)

    AS

    BEGIN

      IF @ProductID = 0

      BEGIN

        UPDATE Production.Product

           SET Name = NewID(),

               ModifiedDate = GetDate()

      END

      ELSE

      BEGIN

        UPDATE Production.Product

           SET Name = @NewName,

               ModifiedDate = GetDate()

         WHERE ProductID = @ProductID

      END

    END

    GO

     

    DISABLE TRIGGER Production.uProduct ON Production.Product;

    GO

     

    SET STATISTICS PROFILE ON

    SET STATISTICS IO ON

     

    EXEC dbo.Supper_UpdateProduct @NewName = N'My New Blade', @ProductID = 316

    --Table 'Product'. Scan count 0, logical reads 6, physical reads 4, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

     

    SET STATISTICS IO OFF

    SET STATISTICS PROFILE OFF

    GO

    -- And now whats happened? The update search for the clustered index only one time.

     

    ENABLE TRIGGER Production.uProduct ON Production.Product;

    GO

     

     

     

    -- Now imagine one worst case,

    BEGIN TRAN

    GO

    SET STATISTICS IO ON

    GO

     

    ENABLE TRIGGER Production.uProduct ON Production.Product;

    GO

    UPDATE Production.Product

       SET Name = NEWID()

    GO

    /*

    Table 'Product'. Scan count 1, logical reads 2075, physical reads 4, read-ahead reads 26, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    Table 'Product'. Scan count 1, logical reads 1052, physical reads 1, read-ahead reads 6, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    */

     

    DISABLE TRIGGER Production.uProduct ON Production.Product;

    GO

    EXEC dbo.Supper_UpdateProduct

    GO

    /*

    Table 'Product'. Scan count 1, logical reads 2078, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    */

    ROLLBACK TRAN

    June 13

    Logo SQL Server 2008

    Uma vez li um livro chamado “O livro do Icone” do Willian Horton, no livro Willian falava sobre os ícones e telas do Windows 3.11 que ele havia ajudado a criar e é claro os ícones do Windows, valeu a leitura ele falou de alguns conceitos bem interessantes.

     

    Falando nisso, só agora a Microsoft finalmente resolveu criar um Logo descente para o SQL Server, fala sério, da para comparar o Logo do SQL 2005 com o do 2008?

     
    SQL 2005
     
     
    SQL 2008
    June 10

    Boas práticas na definição de Primary Keys

    Depois de ler um post no fórum do MSDN, resolvi escrever um pouco sobre índice, especialmente sobre o Índice Cluster que na maioria das vezes acaba sendo nossa primary key.

     

    Vou começar com os 3 pontos que sempre são vistos em ppts de WebCasts e apresentações de boas práticas em criação de índices.

     

    Índices cluster devem ser:

     

    ·         Únicos

    o   Quando uma tabela possui um índice cluster ele servirá como referência(lookup) para todos os índices non-cluster, isso significa que quando você faz uma consulta que utiliza seu índice non-cluster e ele necessita de alguma informação que não está no próprio índice(colunas informadas na criação do índice) ele irá para o índice cluster ler esta informação, por isso ele grava o valor do seu índice cluster no índice non-cluster para conseguir fazer este lookup. É importante dizer que o SQL Server não obriga que seu índice cluster seja único, porém caso ele não seja único o SQL irá “unificar” sua chave incluindo uma informação integer de 4 bytes afim de que ele se torne único. Portanto se o SQL tiver que incluir esta informação de 4 bytes alem de consumir recurso para gerar esta informação(para cada insert ou update ele terá que verificar se esta informação já existe para saber se ele tem que incluir os 4 bytes ou não) complementar ao seu índice, ele irá ocupar mais espaço por conta dos 4 bytes para cada linha duplicada. Seus updates e inserts irão sofrer as conseqüências de uma má escolha do índice cluster.

    ·         Pequenos

    o   Sabendo que a chave do índice cluster é salvo em todos índices non-cluster significa que quanto menor ele for menos espaço você irá utilizar para guardar esta informação no índice non-clustered. Por exemplo imagine que você possui uma tabela com uma chave primária com as colunas ID, Ano, Mes, Dia e possúi vários índices non-cluster em outras colunas, o SQL irá gravar os dados de ID, Ano, Mês, Dia em cada índice non-clustered de sua tabela. Então se ele for muito grande você terá uma grande perda de espaço e custo para seus selects(pois ele terá que ler mais páginas de dados para retornar sua informação), inserts e updates...

    o   Quanto maior for seu índice cluster mais espaço seus índices non-clusters irão ocupar.

    ·         Estáticos

    o   Deve ser estático porque se você alterar seu valor ele terá que alem de alterar o valor na tabela alterar todos os índices non-clustered lembra que ele também fica gravado nos índices non-clustered?. Outra coisa importante é que como o índice está ordenado pela chave caso o valor mude ele irá causar fragmentação na sua tabela.

     

    Bom com base nestas informações eu tento sempre utilizar o seguinte padrão, colunas Identity para minha chave primária, elas definitivamente são Unicas, Pequenas(Integer = 4 bytes), e estáticas. Algumas vezes o máximo que tenho que fazer é mudar meu índice cluster para outra coluna que também será um identity por exemplo uma foreign key, e então definir minha chave primária como non-clustered.

    Algumas pessoas podem dizer que colunas identity como primary key podem causar hot-spot, um hot-spot acontece quando existe um GRANDE número de inserts no final de uma tabela, isso pode causar Page Level Lock porque existem vários usuários tentando acessar a mesma página(final da tabela) para inserir seus dados. Sinceramente eu nunca vi isso acontecer e imagino que você vai ter que efetuar MUITOS inserts ao mesmo tempo para ver isso acontecer, portanto esse contra muitas vezes pode ser ignorado visto os benefícios do identity.

     

    Como sempre, gosto de ver na prática(código) o que escrevo portanto vamos a parte legal da coisa, scripts.

     

    USE Master

    -- Caso exista um banco chamado Teste, apaga ele.

    IF

    (SELECT DB_ID('Teste')) IS NOT NULL

    BEGIN

    USE Master

    ALTER DATABASE Teste SET SINGLE_USER WITH ROLLBACK IMMEDIATE

    DROP DATABASE Teste

    END

    GO

    -- Criar um banco de dados chamado Teste no C:\

    IF

    (SELECT DB_ID('Teste')) IS NULL

    BEGIN

    CREATE DATABASE Teste ON PRIMARY

    (NAME = N'teste', FILENAME = N'C:\teste.mdf' , SIZE = 51200KB , FILEGROWTH = 1024KB)

    LOG ON

    (NAME = N'teste_log', FILENAME = N'C:\teste_log.ldf' , SIZE = 51200KB , FILEGROWTH = 10%)

    END

    GO

    USE

    Teste

    -- Cria uma tabela de teste com uma chave composta colunas ID Int, CPF Char(11) Primary Key

    CREATE

    TABLE Teste (ID Int Identity(1,1),

    CPF Char(11),

    Nome VarChar(200),

    Sobrenome VarChar(200),

    Endereco VarChar(200),

    Bairro VarChar(200),

    Cidade VarChar(200),

    Primary Key(ID, CPF, Nome))

    -- Cria uma tabela de teste com uma coluna ID Identity e Primary Key

    CREATE

    TABLE TesteIdentity (ID Int Identity(1,1) Primary Key,

    CPF Char(11),

    Nome VarChar(200),

    Sobrenome VarChar(200),

    Endereco VarChar(200),

    Bairro VarChar(200),

    Cidade VarChar(200))

    SET

    NOCOUNT ON

    -- Inclui 50000 mil de linhas nas tabelas

    INSERT

    INTO Teste(CPF, Nome, SobreNome, Endereco, Bairro, Cidade)

    VALUES('11111111111', NEWID(), 'Neves Amorim', NEWID(), NEWID(), NEWID())

    GO

    50000

    INSERT

    INTO TesteIdentity(CPF, Nome, SobreNome, Endereco, Bairro, Cidade)

    SELECT

    CPF, Nome, SobreNome, Endereco, Bairro, Cidade

    FROM Teste

    -- Vamos criar alguns indices nonclustered para cada tabela

    CREATE

    NONCLUSTERED INDEX ix_NomeSobrenome ON Teste(Nome, SobreNome)

    CREATE

    NONCLUSTERED INDEX ix_Sobrenome ON Teste(SobreNome)

    CREATE

    NONCLUSTERED INDEX ix_Endereco ON Teste(Endereco)

    CREATE

    NONCLUSTERED INDEX ix_NomeSobrenome ON TesteIdentity(Nome, SobreNome)

    CREATE

    NONCLUSTERED INDEX ix_Sobrenome ON TesteIdentity(SobreNome)

    CREATE

    NONCLUSTERED INDEX ix_Endereco ON TesteIdentity(Endereco)

    -- PEQUENO

    -- Ao comparar o tamanho das tabelas já podemos observar que a tabela Teste

    -- é maior que a tabela TesteIdentity justamente por causa do index_size.

    sp_spaceUsed

    Teste

    GO

    sp_spaceUsed

    TesteIdentity

    -- A tabela teste é maior porque nos indices non-cluster é incluido os dados do indice cluster

    -- para comprovar isso podemos utilizar o comando abaixo.

    -- Repare que é exibida a informação das colunas Endereco, ID, CPF e Nome

    DBCC

    SHOW_STATISTICS('Teste', ix_Endereco)

    -- ESTÁTICOS

    -- Agora vamos ver quantas leituras de páginas são necessárias para atualizar

    -- 2000 linhas das tabelas, Vamos ligar as estatiscitas de IO para ver o resultado

    -- se você exibir o Plano de execução repare que o update na tabela Teste

    -- irá atualizar os indices non-cluster da tabela.

    SET

    STATISTICS IO ON

    update

    Teste set Nome = 'Fabio'

    where

    ID < 2000

    /*

    Table 'Teste'. Scan count 1, logical reads 50377, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    Table 'Worktable'. Scan count 4, logical reads 12544, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    */

    GO

    update TesteIdentity set Nome = 'Fabio'

    where

    ID < 2000

    /*

    Table 'TesteIdentity'. Scan count 1, logical reads 51, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    */

    SET

    STATISTICS IO OFF

    /*

    Para vizualizar a funcionalidade de incluir mais um valor de 4 bytes nos registros duplicados afim

    de torná-los únicos vamos alterar a primary key da tabela teste.

    */

    -- Pega o nome da primary key

    exec

    sp_pkeys Teste

    -- Apaga a primary key para recria-la como não cluster.

    ALTER

    TABLE Teste DROP CONSTRAINT PK__Teste__3E52440B

    -- Recria a primary key como não cluster

    ALTER

    TABLE Teste ADD CONSTRAINT PK__Teste PRIMARY KEY NONCLUSTERED(ID, CPF, Nome)

    -- Cria um indice cluster com base na coluna CPF

    CREATE

    CLUSTERED INDEX ix_CPF ON Teste(CPF)

    /*

    Para vizualizar o valor que o SQL incluiu em cada valor duplicado vamos utilizar o comando

    DBCC PAGE

    */

    -- Pega o endereço físico do Nivel raiz do indice coluna Root da tabela SysIndexes

    SELECT

    *

    FROM SysIndexes

    WHERE ID = Object_id('Teste')

    AND Name = 'ix_CPF'

    -- Resultado 0x300D00000100

    -- 0x0D30

    -- Transforma o HexaDecimal em Inteiro

    SELECT

    CAST(0x0D30 AS INT)

    -- Pega o ID do banco

    SELECT

    DB_ID(DB_Name())

    DBCC

    TRACEON(3604)

    GO

    DBCC

    PAGE(8,1,3376,3)

    /* Resultado

    FileId |PageId |Row |Level |ChildFileId |ChildPageId |CPF (key) |UNIQUIFIER (key) |KeyHashValue

    1 |2738 |0 |2 |1 |2736 |NULL |NULL |(1d0151a9cf2f)

    1 |2738 |1 |2 |1 |2737 |11111111111 |11894 |(930152642c7b)

    1 |2738 |2 |2 |1 |2739 |11111111111 |23454 |(bd0117b642ad)

    1 |2738 |3 |2 |1 |2740 |11111111111 |35014 |(e601bd2493e9)

    1 |2738 |4 |2 |1 |2741 |11111111111 |46574 |(0f021bded106)

    */-- Podemos ver que foi gerada uma coluna "UNIQUIFIER (key)", Bunito esse nome né? Uniquifier :-) /*

     
    June 05

    Vídeo WebCast SQL 2008

    Já está disponível para download o Vídeo da WebCast.

     

    Basta clicar no link e seguir os passos de inscrição.

     

    https://msevents.microsoft.com/cui/r.aspx?t=5&c=pt-br&r=1297386063

    June 04

    WebCast SQL 2008

     

    Hoje fiz a WebCast de Caminhos para SQL Serve 2008, apesar de ter ficado nervoso no inicio foi um prazer e espero que seja a primeira de muitas.

    Obrigado ao Luciano Moreira pelo apoio, Daniel pelo Convite e Leandro pelo suporte.

     

    Segue o link para download do PPT que usei na WebCast de hoje. Assim que o vídeo estiver disponível coloco o link aqui no Blog.

     

     

     

    God bless!