A regra WHERE-JOIN-ORDER- (SELECT) para a ordem da coluna do índice está errada?

9

Estou tentando melhorar essa (sub) consulta fazendo parte de uma consulta maior:

select SUM(isnull(IP.Q, 0)) as Q, 
        IP.OPID 
    from IP
        inner join I
        on I.ID = IP.IID
    where 
        IP.Deleted=0 and
        (I.Status > 0 AND I.Status <= 19) 
    group by IP.OPID

O Sentry Plan Explorer apontou algumas pesquisas de chave relativamente caras para a tabela dbo. [I] realizadas pela consulta acima.

Tabela dbo.I

    CREATE TABLE [dbo].[I] (
  [ID]  UNIQUEIDENTIFIER NOT NULL,
  [OID]  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  CHAR (3) NOT NULL,
  []  CHAR (3)  DEFAULT ('EUR') NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  [] CHAR (10)  NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (35) NULL,
  [] NVARCHAR (100) NOT NULL,
  []  NVARCHAR (100) NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [Status]  INT DEFAULT ((0)) NOT NULL,
  []  DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DATETIME DEFAULT (getdate()) NULL,
  []  DATETIME NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [] TINYINT  DEFAULT ((0)) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (50) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  ROWVERSION NOT NULL,
  []  DATETIME NULL,
  []  INT  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  [] NVARCHAR (50)  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  []  DECIMAL (18, 2)  NULL,
  []  DECIMAL (18, 2)  NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  [] DATETIME NULL,
  [] DATETIME NULL,
  []  VARCHAR (35) NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  CONSTRAINT [PK_I] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
  CONSTRAINT [FK_I_O] FOREIGN KEY ([OID]) REFERENCES [dbo].[O] ([ID]),
  CONSTRAINT [FK_I_Status] FOREIGN KEY ([Status]) REFERENCES [dbo].[T_Status] ([Status])
);                  


GO
CREATE CLUSTERED INDEX [CIX_Invoice]
  ON [dbo].[I]([OID] ASC) WITH (FILLFACTOR = 90);

Tabela dbo.IP

CREATE TABLE [dbo].[IP] (
 [ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
 [IID] UNIQUEIDENTIFIER NOT NULL,
 [OID] UNIQUEIDENTIFIER NOT NULL,
 [Deleted] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] INT NOT NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (100) NOT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] NTEXT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (4, 2) NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME DEFAULT (getdate()) NOT NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 [] DATETIME NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 [] ROWVERSION NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] INT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 []NVARCHAR (35) NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] VARCHAR (12) NULL,
 [] VARCHAR (4) NULL,
 [] NVARCHAR (50) NULL,
 [] NVARCHAR (50) NULL,
 [] VARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] NVARCHAR (50) NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 2) NULL,
 []TINYINT DEFAULT ((1)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((1)) NOT NULL,
 CONSTRAINT [PK_IP] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
 CONSTRAINT [FK_IP_I] FOREIGN KEY ([IID]) REFERENCES [dbo].[I] ([ID]) ON DELETE CASCADE NOT FOR REPLICATION,
 CONSTRAINT [FK_IP_XType] FOREIGN KEY ([XType]) REFERENCES [dbo].[xTYPE] ([Value]) NOT FOR REPLICATION
);

GO
CREATE CLUSTERED INDEX [IX_IP_CLUST]
 ON [dbo].[IP]([IID] ASC) WITH (FILLFACTOR = 90);

A tabela "I" possui cerca de 100.000 linhas, o índice clusterizado possui 9.386 páginas.
O IP da tabela é a tabela "filho" de I e possui cerca de 175.000 linhas.

Tentei adicionar um novo índice seguindo a regra de ordem da coluna de índice: "WHERE-JOIN-ORDER- (SELECT)"

https://www.mssqltips.com/sqlservertutorial/3208/use-where-join-orderby-select-column-order-when-creating-indexes/

para abordar as principais pesquisas e criar uma busca de índice:

CREATE NONCLUSTERED INDEX [IX_I_Status_1]
    ON [dbo].[Invoice]([Status], [ID])

A consulta extraída imediatamente usou esse índice. Mas a consulta maior original da qual faz parte, não o fez. Ele nem o usou quando forcei a usar WITH (INDEX (IX_I_Status_1)).

Depois de um tempo, decidi tentar outro novo índice e mudei para a ordem das colunas indexadas:

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])

WOHA! Esse índice foi usado pela consulta extraída e também pela consulta maior!

Em seguida, comparei as estatísticas de E / S das consultas extraídas forçando-a a usar [IX_I_Status_1] e [IX_I_Status_2]:

Resultados [IX_I_Status_1]:

Table 'I'. Scan count 5, logical reads 636, physical reads 16, read-ahead reads 574
Table 'IP'. Scan count 5, logical reads 1134, physical reads 11, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0

Resultados [IX_I_Status_2]:

Table 'I'. Scan count 1, logical reads 615, physical reads 6, read-ahead reads 631
Table 'IP'. Scan count 1, logical reads 1024, physical reads 5, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,  read-ahead reads 0

OK, eu pude entender que a consulta de monstros mega grandes talvez seja muito complexa para fazer com que o SQL server pegue o plano de execução ideal e possa perder meu novo índice. Mas não entendo por que o índice [IX_I_Status_2] parece ser mais adequado e mais eficiente para a consulta.

Como a consulta primeiro filtra a tabela I pela coluna STATUS e depois se une à tabela IP, não entendo por que [IX_I_Status_2] é melhor e usado pelo Sql Server em vez de [IX_I_Status_1]?

Magier
fonte
Sim, ele usa esse índice caso os critérios de filtro atendam. Ele executa uma varredura de índice (igual a IX_I_Status_2) e, comparado a isso, salva 1 leitura física. mas tive que "incluir (status)" neste índice porque o status está na saída e foi novamente pesquisado antes.
Magier 25/05
Nota lateral engraçada: depois de aplicar o melhor índice que consegui descobrir ([IX_I_Status_2]) e executar a consulta novamente, agora recebo uma sugestão de índice ausente: CREATE NONCLUSTERED INDEX [<Nome do índice ausente, sysname,>] ON [ dbo]. [I] ([Status]) INCLUDE ([ID]) Essa é uma sugestão ruim e diminui o desempenho da consulta. TY Sql server :)
Magier 25/05

Respostas:

19

A regra WHERE-JOIN-ORDER- (SELECT) para a ordem da coluna do índice está errada?

No mínimo, é um conselho incompleto e potencialmente enganoso (não me preocupei em ler o artigo inteiro). Se você estiver lendo coisas na Internet (incluindo isso), ajuste sua quantidade de confiança de acordo com o quanto conhece e confia no autor, mas sempre verifique por si mesmo.

Existem várias "regras práticas" para a criação de índices, dependendo do cenário exato, mas nenhuma é realmente um bom substituto para a compreensão dos principais problemas. Leia sobre a implementação de índices e operadores de plano de execução no SQL Server, faça alguns exercícios e obtenha um bom entendimento sólido de como os índices podem ser usados ​​para tornar os planos de execução mais eficientes. Não existe um atalho eficaz para atingir esse conhecimento e experiência.

Em geral, posso dizer que seus índices costumam ter colunas usadas para testes de igualdade primeiro, com todas as desigualdades por último e / ou fornecidas por um filtro no índice. Esta não é uma declaração completa, porque os índices também podem fornecer ordem, o que pode ser mais útil do que procurar diretamente uma ou mais chaves em algumas situações. Por exemplo, a ordenação pode ser usada para evitar uma classificação, reduzir o custo de uma opção de junção física, como junção de mesclagem, habilitar um agregado de fluxo, encontrar as primeiras linhas qualificadas rapidamente ... e assim por diante.

Estou sendo um pouco vago aqui, porque a seleção do (s) índice (s) ideal (is) para uma consulta depende de muitos fatores - este é um tópico muito amplo.

De qualquer forma, não é incomum encontrar sinais conflitantes para os 'melhores' índices de uma consulta. Por exemplo, seu predicado de junção gostaria que as linhas ordenadas de uma maneira para uma junção de mesclagem, o grupo gostaria que as linhas fossem ordenadas de outra maneira para um agregado de fluxo, e encontrar as linhas qualificadas usando os predicados da cláusula where sugeriria outros índices.

A razão pela qual a indexação é uma arte e uma ciência é que uma combinação ideal nem sempre é logicamente possível. A escolha dos melhores índices de comprometimento para a carga de trabalho (não apenas uma consulta) requer habilidades analíticas, experiência e conhecimento específico do sistema. Se fosse fácil , as ferramentas automatizadas seriam perfeitas e os consultores de ajuste de desempenho teriam muito menos demanda.

No que diz respeito à falta de sugestões de índices, estas são oportunistas. O otimizador os chama a atenção quando tenta corresponder predicados e ordem de classificação necessária a um índice que não existe. As sugestões são, portanto, baseadas em tentativas de correspondência específicas no contexto específico da variação específica do subplano que estava considerando na época.

No contexto, as sugestões sempre fazem sentido, em termos de redução do custo estimado do acesso aos dados, de acordo com o modelo do otimizador. Ele não faz uma análise mais ampla da consulta como um todo (muito menos a carga de trabalho mais ampla); portanto, você deve pensar nessas sugestões como uma dica gentil de que uma pessoa qualificada precisa examinar os índices disponíveis, com as sugestões como início. ponto (e geralmente não mais do que isso).

No seu caso, a (Status) INCLUDE (ID)sugestão provavelmente surgiu quando estava analisando a possibilidade de uma junção de hash ou mesclagem (exemplo mais tarde). Nesse contexto restrito, a sugestão faz sentido. Para a consulta como um todo, talvez não. O índice (ID, Status)permite que um loop aninhado se junte IDcomo uma referência externa: busca de igualdade IDe desigualdade Statuspor iteração.

Uma seleção possível de índices é:

CREATE INDEX i1 ON dbo.I (ID, [Status]);
CREATE INDEX i1 ON dbo.IP (Deleted, OPID, IID) INCLUDE (Q);

... que produz um plano como:

Plano possível

Não estou dizendo que esses índices são ideais para você; eles trabalham para produzir um plano de aparência razoável para mim, sem poder ver estatísticas para as tabelas envolvidas ou as definições completas e a indexação existente. Além disso, não sei nada sobre a carga de trabalho mais ampla ou a consulta real.

Como alternativa (apenas para mostrar uma das inúmeras possibilidades adicionais):

CREATE INDEX i1 ON dbo.I ([Status]) INCLUDE (ID);
CREATE INDEX i1 ON dbo.IP (Deleted, IID, OPID) INCLUDE (Q);

Dá:

Plano alternativo

Os planos de execução foram gerados usando o SQL Sentry Plan Explorer .

Paul White 9
fonte