web-dev-qa-db-fra.com

ROW_NUMBER () OVER (PARTITION BY B, A ORDER BY C) n'utilise pas d'index sur (A, B, C)

Considérez ces deux fonctions:

ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C)

ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C)

Autant que je sache, ils produisent exactement le même résultat. En d'autres termes, l'ordre dans lequel vous répertoriez les colonnes dans la clause PARTITION BY N'a pas d'importance.

S'il y a un index sur (A,B,C) Je m'attendais à ce que l'optimiseur utilise cet index dans les deux variantes.

Mais, étonnamment, l'optimiseur a décidé de faire un tri extra explicite dans la deuxième variante.

Je l'ai vu sur SQL Server 2008 Standard et SQL Server 2014 Express.

Voici un script complet que j'ai utilisé pour le reproduire.

Testé sur Microsoft SQL Server 2014 - 12.0.2000.8 (X64) 20 février 2014 20:04:26 Copyright (c) Microsoft Corporation Express Edition (64 bits) sur Windows NT 6.1 (Build 7601: Service Pack 1)

et Microsoft SQL Server 2014 (SP1-CU7) (KB3162659) - 12.0.4459.0 (X64) 27 mai 2016 15:33:17 Copyright (c) Microsoft Corporation Express Edition (64 bits) sur Windows NT 6.1 (Build 7601: Service Pack 1)

avec l'ancien et le nouveau Cardinality Estimator en utilisant OPTION (QUERYTRACEON 9481) et OPTION (QUERYTRACEON 2312).

Configuration de la table, de l'index et des exemples de données

CREATE TABLE [dbo].[T](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [A] [int] NOT NULL,
    [B] [int] NOT NULL,
    [C] [int] NOT NULL,
    CONSTRAINT [PK_T] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, 
STATISTICS_NORECOMPUTE = OFF, 
IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, 
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [IX_ABC] ON [dbo].[T]
(
    [A] ASC,
    [B] ASC,
    [C] ASC
)WITH (PAD_INDEX = OFF, 
STATISTICS_NORECOMPUTE = OFF, 
SORT_IN_TEMPDB = OFF, 
DROP_EXISTING = OFF, 
ONLINE = OFF, 
ALLOW_ROW_LOCKS = ON, 
ALLOW_PAGE_LOCKS = ON)
GO

INSERT INTO [dbo].[T] ([A],[B],[C]) VALUES
(10, 20, 30),
(10, 21, 31),
(10, 21, 32),
(10, 21, 33),
(11, 20, 34),
(11, 21, 35),
(11, 21, 36),
(12, 20, 37),
(12, 21, 38),
(13, 21, 39);

Requêtes

SELECT -- AB
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C) AS rnAB
FROM T
ORDER BY C
OPTION(RECOMPILE);

SELECT -- BA
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C) AS rnBA
FROM T
ORDER BY C
OPTION(RECOMPILE);

SELECT -- both
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C) AS rnAB
    ,ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C) AS rnBA
FROM T
ORDER BY C
OPTION(RECOMPILE);

Plans d'exécution

PARTITION PAR A, B

AB

PARTITION PAR B, A

BA

Les deux

both

Comme vous pouvez le voir, le deuxième plan a un tri supplémentaire. Il ordonne par B, A, C. L'optimiseur, apparemment, n'est pas assez intelligent pour se rendre compte que PARTITION BY B,A Est identique à PARTITION BY A,B Et reclasse les données.

Fait intéressant, la troisième requête contient les deux variantes de ROW_NUMBER Et il n'y a pas de tri supplémentaire! Le plan est le même que pour la première requête. (Le projet de séquence a une expression supplémentaire dans la liste de sortie pour la colonne supplémentaire, mais pas de tri supplémentaire). Ainsi, dans ce cas plus compliqué, l'optimiseur semblait être assez intelligent pour réaliser que PARTITION BY B,A Est identique à PARTITION BY A,B.

Dans les première et troisième requêtes, l'opérateur Index Scan a la propriété Ordered: True, dans la deuxième requête, il est False.

Encore plus intéressant, si je réécris la troisième requête comme ceci (permutez deux colonnes):

SELECT -- both
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C) AS rnBA
    ,ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C) AS rnAB
FROM T
ORDER BY C
OPTION(RECOMPILE);

alors le tri supplémentaire apparaît à nouveau!

Quelqu'un pourrait-il faire la lumière? Que se passe-t-il ici dans l'optimiseur?

12
Vladimir Baranov

Il semble qu'il n'y ait pas de bonne "réponse" définitive à la question "ce qui se passe dans l'optimiseur", à moins que vous ne soyez son développeur et que vous connaissiez ses internes.

Je vais rassembler les commentaires ici.

Dans l'ensemble, il semble qu'il serait trop difficile de l'appeler un bogue, car le résultat final de la requête est correct. Dans certains cas, le plan d'exécution n'est tout simplement pas optimal. ypercubeᵀᴹ , Martin Smith et Aaron Bertrand appelons cela "l'optimisation manquée".

  • Semble être GROUP BY a,b et GROUP BY b,a donne des plans identiques mais PARTITION BY ne peut pas utiliser la même transformation

  • Il y a aussi d'autres optimisations manquantes où les fonctions de fenêtre avec la même spécification de fenêtre peuvent avoir une opération de tri supplémentaire si elles sont séparées dans la liste de sélection par une avec une spécification différente.

  • Oui, cela semble être une autre optimisation manquée, et il y en a beaucoup. L'optimiseur est écrit par des humains et n'est pas parfait


Il y a un article quelque peu connexe Index décroissants. Ordre des index, parallélisme et calculs de classement par Itzik Ben-Gan. Itzik y traite des index descendants et donne également un exemple de la façon dont la direction de la définition d'index affecte les fonctions de la fenêtre avec les partitions. Il montre des exemples de requêtes et de plans générés avec ROW_NUMBER qui ont un opérateur de tri supplémentaire que l'optimiseur aurait pu éviter.


Pour moi, le résultat pratique serait de garder à l’esprit cette particularité d’optimiseur. Lors de l'utilisation de PARTITION BY dans les fonctions de fenêtre essaient toujours de faire correspondre l'ordre dans lequel vous listez les colonnes dans PARTITION BY avec l'ordre dans lequel ils sont répertoriés dans l'index. Même si cela n'a pas d'importance.

Un autre aspect de cette précaution est lorsque vous examinez vos index et décidez de permuter certaines colonnes dans la définition d'index. Sachez que vous pouvez par inadvertance affecter certaines requêtes existantes qui ne devraient apparemment pas être affectées. C'est en fait ainsi que j'ai remarqué cette particularité de l'optimiseur.

Si vous ne le faites pas, l'optimiseur peut ne pas être en mesure d'utiliser l'index à son plein potentiel. Même si l'optimiseur choisit un plan optimal, ce plan peut devenir moins optimal avec une moindre modification innocente de la requête, comme changer l'ordre des colonnes dans l'instruction SELECT.

2
Vladimir Baranov