Comment écrire une requête dans SQL Server pour trouver les valeurs les plus proches


16

Disons que j'ai les valeurs entières suivantes dans une table

32
11
15
123
55
54
23
43
44
44
56
23

OK, la liste peut continuer; ça n'a pas d'importance. Maintenant, je veux interroger cette table et je veux retourner un certain nombre de closest records. Disons que je veux renvoyer les 10 correspondances d'enregistrement les plus proches au chiffre 32. Puis-je y parvenir efficacement?

C'est dans SQL Server 2014.

Réponses:


21

En supposant que la colonne est indexée, les éléments suivants devraient être raisonnablement efficaces.

Avec deux recherches de 10 lignes, puis une sorte de (jusqu'à) 20 retournés.

WITH CTE
     AS ((SELECT TOP 10 *
          FROM   YourTable
          WHERE  YourCol > 32
          ORDER  BY YourCol ASC)
         UNION ALL
         (SELECT TOP 10 *
          FROM   YourTable
          WHERE  YourCol <= 32
          ORDER  BY YourCol DESC))
SELECT TOP 10 *
FROM   CTE
ORDER  BY ABS(YourCol - 32) ASC 

(c'est-à-dire potentiellement quelque chose comme ci-dessous)

entrez la description de l'image ici

Ou une autre possibilité (qui réduit le nombre de lignes triées à 10 au maximum)

WITH A
     AS (SELECT TOP 10 *,
                       YourCol - 32 AS Diff
         FROM   YourTable
         WHERE  YourCol > 32
         ORDER  BY Diff ASC, YourCol ASC),
     B
     AS (SELECT TOP 10 *,
                       32 - YourCol AS Diff
         FROM   YourTable
         WHERE  YourCol <= 32
         ORDER  BY YourCol DESC),
     AB
     AS (SELECT *
         FROM   A
         UNION ALL
         SELECT *
         FROM   B)
SELECT TOP 10 *
FROM   AB
ORDER  BY Diff ASC

entrez la description de l'image ici

NB: Le plan d'exécution ci-dessus était pour la définition de table simple

CREATE TABLE [dbo].[YourTable](
    [YourCol] [int] NOT NULL CONSTRAINT [SomeIndex] PRIMARY KEY CLUSTERED 
)

Techniquement, le tri sur la branche inférieure ne devrait pas non plus être nécessaire car cela est également ordonné par Diff, et il serait possible de fusionner les deux résultats ordonnés. Mais je n'ai pas pu obtenir ce plan.

La requête a ORDER BY Diff ASC, YourCol ASCet pas seulement ORDER BY YourCol ASC, car c'est ce qui a fini par fonctionner pour se débarrasser du tri dans la branche supérieure du plan. J'avais besoin d'ajouter la colonne secondaire (même si elle ne changera jamais le résultat car ce YourColsera le même pour toutes les valeurs avec le même Diff) afin qu'elle passe par la jointure de fusion (concaténation) sans ajouter de tri.

SQL Server semble capable de déduire qu'un index sur X recherché dans l'ordre croissant fournira des lignes ordonnées par X + Y et aucun tri n'est nécessaire. Mais il n'est pas en mesure de déduire que le déplacement de l'index dans l'ordre décroissant fournira des lignes dans le même ordre que YX (ou même simplement unaire moins X). Les deux branches du plan utilisent un index pour éviter un tri, mais les TOP 10dans la branche inférieure sont ensuite triées par Diff(même si elles sont déjà dans cet ordre) pour les obtenir dans l'ordre souhaité pour la fusion.

Pour d'autres requêtes / définitions de table, il peut être plus difficile ou impossible d'obtenir le plan de fusion avec juste une sorte de branche - car il repose sur la recherche d'une expression de classement que SQL Server:

  1. Accepte que la recherche d'index fournira l'ordre spécifié afin qu'aucun tri ne soit nécessaire avant le sommet.
  2. Est heureux d'utiliser dans l'opération de fusion et ne nécessite donc aucun tri après la TOP

1

Je suis un peu perplexe et surpris que nous devions faire Union dans ce cas. Le suivi est simple et plus efficace

SELECT TOP (@top) *
FROM @YourTable
ORDER BY ABS(YourCol-@x)

Voici le code complet et le plan d'exécution comparant les deux requêtes

DECLARE @YourTable TABLE (YourCol INT)
INSERT @YourTable (YourCol)
VALUES  (32),(11),(15),(123),(55),(54),(23),(43),(44),(44),(56),(23)

DECLARE @x INT = 100, @top INT = 5

--SELECT TOP 100 * FROM @YourTable
SELECT TOP (@top) *
FROM @YourTable
ORDER BY ABS(YourCol-@x)

;WITH CTE
     AS ((SELECT TOP 10 *
          FROM   @YourTable
          WHERE  YourCol > 32
          ORDER  BY YourCol ASC)
         UNION ALL
         (SELECT TOP 10 *
          FROM   @YourTable
          WHERE  YourCol <= 32
          ORDER  BY YourCol DESC))
SELECT TOP 10 *
FROM   CTE
ORDER  BY ABS(YourCol - 32) ASC 

Comparaison du plan d'exécution


-3

Affinement de la deuxième suggestion de Martin:

WITH AB
     AS (SELECT *, ABS(32 - YourCol) AS Offset
         FROM   YourTable),
SELECT TOP 10 *
FROM   AB
ORDER  BY Offset ASC

2
C'est peut-être un code un peu plus simple mais ça va être beaucoup moins efficace. Nous pourrions même utiliser SELECT TOP 10 * FROM YourTable ORDER BY ABS(YourCol - 32) ;Encore plus simple. Pas efficace non plus.
ypercubeᵀᴹ
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.