Rejoining range search on nullable composite index?

14

Pour le schéma et les exemples de données suivants

CREATE TABLE T
  (
     A INT NULL,
     B INT NOT NULL IDENTITY,
     C CHAR(8000) NULL,
     UNIQUE CLUSTERED (A, B)
  )

INSERT INTO T
            (A)
SELECT NULLIF(( ( ROW_NUMBER() OVER (ORDER BY @@SPID) - 1 ) / 1003 ), 0)
FROM   master..spt_values 

Une application traite les lignes de cette table dans un ordre d'index cluster en 1 000 morceaux de ligne.

Les 1 000 premières lignes sont extraites de la requête suivante.

SELECT TOP 1000 *
FROM   T
ORDER  BY A, B 

La dernière ligne de cet ensemble est ci-dessous

+------+------+
|  A   |  B   |
+------+------+
| NULL | 1000 |
+------+------+

Existe-t-il un moyen d'écrire une requête qui cherche simplement dans cette clé d'index composite, puis la suit pour récupérer le prochain bloc de 1000 lignes?

/*Pseudo Syntax*/
SELECT TOP 1000 *
FROM   T
WHERE (A, B) is_ordered_after (@A, @B)
ORDER  BY A, B 

Le nombre de lectures le plus bas que j'ai réussi à obtenir jusqu'à présent est de 1020, mais la requête semble beaucoup trop compliquée. Existe-t-il un moyen plus simple d'efficacité égale ou meilleure? Peut-être que celui qui parvient à tout faire dans une gamme cherche?

DECLARE @A INT = NULL, @B INT = 1000

;WITH UnProcessed
     AS (SELECT *
         FROM   T
         WHERE  ( EXISTS(SELECT A
                         INTERSECT
                         SELECT @A)
                  AND B > @B )
         UNION ALL
         SELECT *
         FROM   T
         WHERE @A IS NULL AND A IS NOT NULL
         UNION ALL
         SELECT *
         FROM   T
         WHERE A > @A        
         )
SELECT TOP 1000 *
FROM   UnProcessed
ORDER  BY A,
          B 

entrez la description de l'image ici


FWIW: Si la colonne Aest créée NOT NULLet qu'une valeur sentinelle de -1est utilisée à la place, le plan d'exécution équivalent semble certainement plus simple

entrez la description de l'image ici

Mais l'opérateur de recherche unique dans le plan effectue toujours deux recherches plutôt que de le réduire en une seule plage contiguë et les lectures logiques sont à peu près les mêmes, donc je soupçonne que c'est peut-être à peu près aussi bon que possible?

Martin Smith
la source
Mon erreur. J'ai oublié que les NULLvaleurs sont toujours les premières. (supposé le contraire.) Condition corrigée à Fiddle
ypercubeᵀᴹ
Oui, Oracle est différent, je crois.
Martin Smith
SQL Fiddle
Martin Smith
@ypercube - SQL Server donne juste une analyse ordonnée pour cela, donc relit toutes les lignes déjà traitées par l'application (lectures logiques 2015). Il ne cherche pas dans la première clé de(NULL, 1000 )
Martin Smith
Avec 2 conditions différentes, qu'il @Asoit nul ou non, il semble qu'il ne fasse pas de scan. Mais je ne peux pas comprendre si les plans sont meilleurs que votre requête. Fiddle-2
ypercubeᵀᴹ

Réponses:

21

Existe-t-il un moyen d'écrire une requête qui cherche simplement dans cette clé d'index composite, puis la suit pour récupérer le prochain bloc de 1000 lignes?

Une de mes solutions préférées consiste à utiliser un APIcurseur:

SET NOCOUNT ON;
SET STATISTICS IO ON;

DECLARE 
    @cur integer,
    -- FAST_FORWARD, AUTO_FETCH, AUTO_CLOSE, CHECK_ACCEPTED_TYPES, FAST_FORWARD_ACCEPTABLE
    @scrollopt integer = 16 | 8192 | 16384 | 32768 | 1048576,
    -- READ_ONLY, CHECK_ACCEPTED_OPTS, READ_ONLY_ACCEPTABLE
    @ccopt integer = 1 | 32768 | 65536, 
    @rowcount integer = 1000,
    @rc integer;

-- Open the cursor and return (up to) the first 1000 rows
EXECUTE @rc = sys.sp_cursoropen
    @cur OUTPUT,
    N'
    SELECT A, B, C
    FROM T
    ORDER BY A, B;
    ',
    @scrollopt OUTPUT,
    @ccopt OUTPUT,
    @rowcount OUTPUT;

IF @rc <> 16 -- FastForward cursor automatically closed
BEGIN
    -- Name the cursor so we can use CURSOR_STATUS
    EXECUTE sys.sp_cursoroption
        @cur, 
        2, 
        'MyCursorName';

    -- Until the cursor auto-closes
    WHILE CURSOR_STATUS('global', 'MyCursorName') = 1
    BEGIN
        EXECUTE sys.sp_cursorfetch
            @cur,
            2,
            0,
            1000;
    END;
END;

SET STATISTICS IO OFF;

La stratégie globale est un balayage unique qui se souvient de sa position entre les appels. L'utilisation d'un APIcurseur signifie que nous pouvons retourner un bloc de lignes plutôt qu'une à la fois comme ce serait le cas avec un T-SQLcurseur:

Plans d'exécution

La STATISTICS IOsortie est:

Table 'T'. Scan count 1, logical reads 1011, physical reads 0, read-ahead reads 0
Table 'T'. Scan count 1, logical reads 1001, physical reads 0, read-ahead reads 0
Table 'T'. Scan count 1, logical reads 516, physical reads 0, read-ahead reads 0
Paul White 9
la source