web-dev-qa-db-fra.com

Pourquoi existe-t-il des différences de plan d'exécution entre le décalage ... Fetch et le schéma de n ° de style de style?

Le nouveau OFFSET ... FETCH Le modèle introduit avec SQL Server 2012 propose une pagination simple et plus rapide. Pourquoi y a-t-il des différences qui envisagent de penser que les deux formes sont sémantiquement identiques et très courantes?

On supposerait que l'optimiseur reconnaît les deux et les optimise (trivialement) au maximum.

Voici un cas très simple où OFFSET ... FETCH est ~ 2 fois plus rapide selon l'estimation des coûts.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset-fetch.png

On peut varier ce boîtier de test en créant un CI sur object_id ou ajouter des filtres mais il est impossible de supprimer toutes les différences de plan. OFFSET ... FETCH est toujours plus rapide car il fonctionne moins à l'heure d'exécution.

15
usr

Les exemples de la question ne produisent pas tout à fait les mêmes résultats (l'exemple OFFSET a une erreur obsolète). Les formulaires mis à jour ci-dessous corrigent ce problème, supprimez le tri supplémentaire pour le ROW_NUMBER Case et utilisez des variables pour rendre la solution plus générale:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

Le ROW_NUMBER Le plan a un coût estimé de 0.0197935 :

Row Number Plan

Le plan OFFSET a un coût estimé de 0.0196955 :

Offset Plan

C'est une économie de 0.000098 unités de coût estimées (bien que le plan OFFSET nécessiterait des opérateurs supplémentaires si vous souhaitez retourner un numéro de ligne pour chaque rangée). Le plan OFFSET sera toujours légèrement moins cher, d'une manière générale, mais rappelez-vous que les coûts estimés sont exactement que: les tests réels sont toujours nécessaires. L'essentiel du coût dans les deux plans est le coût de la totalité du type d'entrée, des index utiles bénéficieraient des solutions.

Où des valeurs littérales constantes sont utilisées (par exemple, OFFSET 30 Dans l'exemple original), l'optimiseur peut utiliser une sorte de topn au lieu d'une sorte complète suivie d'un sommet. Lorsque les lignes nécessaires à partir du type TOPN sont un littéral constant et <= 100 (la somme de OFFSET et FETCH) Le moteur d'exécution peut utiliser n algorithme de tri différent qui peut fonctionner plus rapidement que le tri général généralisé. Les trois cas ont des caractéristiques de performance différentes dans l'ensemble.

Pourquoi l'optimiseur ne transforme pas automatiquement le ROW_NUMBER Motif de syntaxe à utiliser OFFSET, il existe un certain nombre de raisons:

  1. Il est presque impossible d'écrire une transformation qui correspondrait à toutes les utilisations existantes
  2. Avoir des requêtes de pagination automatiquement transformées et non les autres pourraient être déroutants
  3. Le plan OFFSET n'est pas garanti d'être meilleur dans tous les cas

Un exemple pour le troisième point ci-dessus se produit lorsque l'ensemble de radiomessagerie est assez large. Il peut être beaucoup plus efficace pour rechercher les clés nécessaires à l'aide d'un index non clusterisé et recherchez manuellement l'indice en cluster par rapport à la numérisation de l'index avec OFFSET ou ROW_NUMBER. Il y a questions supplémentaires à considérer Si l'application de pagination doit savoir combien de lignes ou de pages au total. Il y a une autre bonne discussion sur les mérites relatives des "méthodes de la recherche de la clé" et des "compensation" ici .

Dans l'ensemble, il est probablement préférable que les gens prennent une décision éclairée de modifier leurs requêtes de pagination pour utiliser OFFSET, le cas échéant, après des tests approfondis.

13
Paul White 9

Avec une légère violation de votre requête, je reçois un coût égal estimation (50/50) et égal IO Statistiques:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Cela évite le tri supplémentaire qui apparaît dans votre version en triant sur r au lieu de object_id.

5
Mark Storey-Smith