La règle WHERE-JOIN-ORDER- (SELECT) pour l'ordre des colonnes d'index est-elle incorrecte?

9

J'essaie d'améliorer cette (sous-) requête en faisant partie d'une requête plus large:

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

Sentry Plan Explorer a souligné quelques recherches de clés relativement coûteuses pour la table dbo. [I] effectuées par la requête ci-dessus.

Tableau 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);

Tableau 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);

Le tableau "I" compte environ 100 000 lignes, l'index cluster compte 9 386 pages.
La table IP est la table "enfant" de I et compte environ 175 000 lignes.

J'ai essayé d'ajouter un nouvel index en suivant la règle d'ordre des colonnes d'index: "WHERE-JOIN-ORDER- (SELECT)"

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

pour traiter les recherches de clés et créer une recherche d'index:

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

La requête extraite a immédiatement utilisé cet index. Mais la plus grande requête d'origine dont il fait partie ne l'a pas fait. Il ne l'a même pas utilisé lorsque je l'ai forcé à utiliser WITH (INDEX (IX_I_Status_1)).

Après un certain temps, j'ai décidé d'essayer un autre nouvel index et j'ai changé l'ordre des colonnes indexées:

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

WOHA! Cet index a été utilisé par la requête extraite et également par la requête plus grande!

J'ai ensuite comparé les statistiques d'E / S des requêtes extraites en l'obligeant à utiliser [IX_I_Status_1] et [IX_I_Status_2]:

Résultats [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

Résultats [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, je pouvais comprendre que la requête méga-grand-monstre était peut-être trop complexe pour que SQL Server intercepte le plan d'exécution idéal et pourrait manquer mon nouvel index. Mais je ne comprends pas pourquoi l'index [IX_I_Status_2] semble être plus adapté et plus efficace pour la requête.

Étant donné que la requête filtre tout d'abord la table I par colonne STATUS, puis la joint à la table IP, je ne comprends pas pourquoi [IX_I_Status_2] est meilleur et utilisé par Sql Server au lieu de [IX_I_Status_1]?

Magier
la source
Oui, il utilise cet index au cas où les critères de filtrage répondent. Il effectue un balayage d'index (comme pour IX_I_Status_2) et par rapport à cela, il enregistre 1 lecture physique. mais j'ai dû "inclure (état)" à cet index car l'état est dans la sortie et a été recherché à nouveau auparavant.
Magier
Note amusante: après avoir appliqué le meilleur index, je pouvais comprendre ([IX_I_Status_2]) et relancer la requête, maintenant j'obtiens une suggestion d'index manquant: CRÉER UN INDICE NON CLUSTRÉ [<Nom de l'index manquant, sysname,>] ON [ dbo]. [I] ([Status]) INCLUDE ([ID]) Ceci est une mauvaise suggestion et diminue les performances de la requête. Serveur TY Sql :)
Magier

Réponses:

19

La règle WHERE-JOIN-ORDER- (SELECT) pour l'ordre des colonnes d'index est-elle incorrecte?

À tout le moins, il s'agit de conseils incomplets et potentiellement trompeurs (je n'ai pas pris la peine de lire l'intégralité de l'article). Si vous allez lire des choses sur Internet (y compris cela), vous devez ajuster votre niveau de confiance en fonction de la façon dont vous connaissez et faites déjà confiance à l'auteur, mais vérifiez toujours par vous-même.

Il existe un certain nombre de «règles de base» pour créer des index, selon le scénario exact, mais aucune n'est vraiment un bon substitut pour comprendre les problèmes de base par vous-même. Renseignez-vous sur la mise en œuvre des index et des opérateurs de plan d'exécution dans SQL Server, passez par quelques exercices et apprenez bien comment les index peuvent être utilisés pour rendre les plans d'exécution plus efficaces. Il n'y a pas de raccourci efficace pour atteindre ces connaissances et cette expérience.

En général, je peux dire que vos index devraient avoir le plus souvent des colonnes utilisées pour les tests d'égalité en premier, avec toutes les inégalités en dernier, et / ou fournies par un filtre sur l'index. Il ne s'agit pas d'une instruction complète, car les index peuvent également fournir de l'ordre, ce qui peut être plus utile que de rechercher directement une ou plusieurs clés dans certaines situations. Par exemple, l'ordre peut être utilisé pour éviter un tri, pour réduire le coût d'une option de jointure physique comme fusionner la jointure, pour activer un agrégat de flux, trouver rapidement les premières lignes éligibles ... et ainsi de suite.

Je suis un peu vague ici, car la sélection du ou des index idéaux pour une requête dépend de nombreux facteurs - c'est un sujet très large.

Quoi qu'il en soit, il n'est pas rare de trouver des signaux contradictoires pour les «meilleurs» index dans une requête. Par exemple, votre prédicat de jointure souhaiterait que les lignes soient ordonnées dans un sens pour une jointure de fusion, le groupe par souhaiterait que les lignes soient triées d'une autre manière pour un agrégat de flux, et trouver les lignes qualifiées à l'aide de la clause where les prédicats suggéreraient d'autres index.

La raison pour laquelle l'indexation est à la fois un art et une science est qu'une combinaison idéale n'est pas toujours logiquement possible. Le choix des meilleurs index de compromis pour la charge de travail (et pas seulement une seule requête) nécessite des compétences analytiques, de l'expérience et des connaissances spécifiques au système. Si c'était facile , les outils automatisés seraient parfaits et les consultants en optimisation des performances seraient beaucoup moins demandés.

En ce qui concerne les suggestions d'index manquantes: elles sont opportunistes. L'optimiseur les porte à votre attention lorsqu'il essaie de faire correspondre les prédicats et l'ordre de tri requis à un index qui n'existe pas. Les suggestions sont donc basées sur des tentatives d'appariement particulières dans le contexte spécifique de la variation particulière du sous-plan qu'elle envisageait à l'époque.

Dans le contexte, les suggestions ont toujours un sens, en termes de réduction du coût estimé de l'accès aux données, selon le modèle de l'optimiseur. Il ne fait pas une analyse plus large de la requête dans son ensemble (et encore moins de la charge de travail plus large), vous devez donc considérer ces suggestions comme un indice léger qu'une personne qualifiée doit consulter les index disponibles, avec les suggestions comme point de départ. point (et généralement pas plus que cela).

Dans votre cas, la (Status) INCLUDE (ID)suggestion est probablement venue quand elle a examiné la possibilité d'un hachage ou d'une fusion (par exemple plus tard). Dans ce contexte étroit, la suggestion est logique. Pour la requête dans son ensemble, peut-être pas. L'index (ID, Status)permet une jointure de boucle imbriquée avec IDcomme référence externe: recherche d'égalité IDet inégalité à Statuschaque itération.

Une sélection possible d'index est:

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

... qui produit un plan comme:

Plan possible

Je ne dis pas que ces index sont optimaux pour vous; il m'arrive de travailler pour produire un plan raisonnable pour moi, sans pouvoir voir les statistiques des tables impliquées, ni les définitions complètes et l'indexation existante. De plus, je ne sais rien de la charge de travail plus large ou de la vraie requête.

Alternativement (juste pour montrer l'une des innombrables possibilités supplémentaires):

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

Donne:

Plan alternatif

Les plans d'exécution ont été générés à l'aide de SQL Sentry Plan Explorer .

Paul White 9
la source