Come impostare la vista indicizzata quando si seleziona TOP 1 con ORDER BY da tabelle diverse


11

Faccio fatica a configurare una vista indicizzata nel seguente scenario in modo che la query seguente venga eseguita senza due scansioni dell'indice in cluster. Ogni volta che creo una vista indice per questa query e quindi la utilizzo, sembra ignorare qualsiasi indice che ho inserito su di essa:

    -- +++ THE QUERY THAT I WANT TO IMPROVE PERFORMANCE-WISE +++

    SELECT TOP 1 *
    FROM    dbo.TB_test1 t1
            INNER JOIN dbo.TB_test2 t2 ON t1.PK_ID1 = t2.FK_ID1
    ORDER BY t1.somethingelse1
           ,t2.somethingelse2;


    GO

L'impostazione della tabella è la seguente:

  • due tavoli
  • sono uniti da un join interno dalla query sopra
  • e ordinato da una colonna dalla prima e quindi da una colonna dalla seconda tabella dalla query sopra; è selezionato solo TOP 1
  • (nello script qui sotto ci sono anche alcune righe per generare dati di test, nel caso in cui aiuti a riprodurre il problema)

    -- +++ TABLE SETUP +++
    
    CREATE TABLE [dbo].[TB_test1]
        (
         [PK_ID1] [INT] IDENTITY(1, 1)  NOT NULL
        ,[something1] VARCHAR(40) NOT NULL
        ,[somethingelse1] BIGINT NOT NULL
            CONSTRAINT [PK_TB_test1] PRIMARY KEY CLUSTERED ( [PK_ID1] ASC )
        );
    
    GO
    
    create TABLE [dbo].[TB_test2]
        (
         [PK_ID2] [INT] IDENTITY(1, 1)  NOT NULL
        ,[FK_ID1] [INT] NOT NULL
        ,[something2] VARCHAR(40) NOT NULL
        ,[somethingelse2] BIGINT NOT NULL
            CONSTRAINT [PK_TB_test2] PRIMARY KEY CLUSTERED ( [PK_ID2] ASC )
        );
    
    GO
    
    ALTER TABLE [dbo].[TB_test2]  WITH CHECK ADD  CONSTRAINT [FK_TB_Test1] FOREIGN KEY([FK_ID1])
    REFERENCES [dbo].[TB_test1] ([PK_ID1])
    GO
    
    ALTER TABLE [dbo].[TB_test2] CHECK CONSTRAINT [FK_TB_Test1]
    
    GO
    
    
    -- +++ TABLE DATA GENERATION +++
    
    -- this might not be the quickest way, but it's only to set up test data
    
    INSERT INTO dbo.TB_test1
            ( something1, somethingelse1 )
    VALUES  ( CONVERT(VARCHAR(40), NEWID())  -- something1 - varchar(40)
              ,ISNULL(ABS(CHECKSUM(NewId())) % 92233720368547758078, 1)   -- somethingelse1 - bigint
              )
    
    GO 100000
    
    RAISERROR( 'Finished setting up dbo.TB_test1', 0, 1) WITH NOWAIT    
    
    GO    
    
    INSERT INTO dbo.TB_test2
            ( FK_ID1, something2, somethingelse2 )
    VALUES  ( ISNULL(ABS(CHECKSUM(NewId())) % ((SELECT MAX(PK_ID1) FROM dbo.TB_test1) - 1), 0) + 1 -- FK_ID1 - int
              ,CONVERT(VARCHAR(40), NEWID())  -- something2 - varchar(40)
              ,ISNULL(ABS(CHECKSUM(NewId())) % 92233720368547758078, 1)   -- somethingelse2 - bigint
              )
    
    GO 100000
    
    RAISERROR( 'Finished setting up dbo.TB_test2', 0, 1) WITH NOWAIT          
    
    GO
    

La vista indicizzata dovrebbe probabilmente essere definita come segue e la query TOP 1 risultante è inferiore. Ma quali indici sono necessari affinché questa query funzioni meglio che senza la vista indicizzata?

    CREATE VIEW VI_test
    WITH SCHEMABINDING
    AS
        SELECT  t1.PK_ID1
               ,t1.something1
               ,t1.somethingelse1
               ,t2.PK_ID2
               ,t2.FK_ID1
               ,t2.something2
               ,t2.somethingelse2
        FROM    dbo.TB_test1 t1
                INNER JOIN dbo.TB_test2 t2 ON t1.PK_ID1 = t2.FK_ID1


    GO


    SELECT TOP 1 * FROM dbo.VI_test ORDER BY somethingelse1,somethingelse2


    GO

Risposte:


12

Sembra ignorare qualsiasi indice che ho messo su di esso

A meno che non si utilizzi SQL Server Enterprise Edition (o equivalentemente, versione di prova e sviluppatore), WITH (NOEXPAND)per poterlo utilizzare sarà necessario utilizzare il riferimento della vista. In effetti, anche se stai usando Enterprise, ci sono buoni motivi per usare quel suggerimento .

Senza il suggerimento, Query Optimizer (in Enterprise Edition) può fare una scelta basata sui costi tra l'utilizzo della vista materializzata o l'accesso alle tabelle di base. Se la vista è grande come le tabelle di base, questo calcolo può favorire le tabelle di base.

Un altro punto di interesse è che senza un NOEXPANDsuggerimento, i riferimenti alla vista vengono sempre espansi nella query di base prima dell'inizio dell'ottimizzazione. Man mano che l'ottimizzazione avanza, l'ottimizzatore può o meno essere in grado di far corrispondere la definizione espansa alla vista materializzata, a seconda dell'attività di ottimizzazione precedente. Questo non è quasi certamente il caso della tua semplice query, ma lo cito per completezza.

Pertanto, l'utilizzo del NOEXPANDsuggerimento tabella è l'opzione principale, ma potresti anche pensare a materializzare solo le chiavi della tabella di base e le colonne necessarie per ordinare nella vista. Creare un indice cluster univoco sulle colonne chiave combinate, quindi un indice separato non cluster sulle colonne di ordinamento.

Ciò ridurrà la dimensione della vista materializzata e limiterà il numero di aggiornamenti automatici che devono essere effettuati per mantenere la vista sincronizzata con le tabelle di base. La query può quindi essere scritta per recuperare le prime 1 chiavi nell'ordine richiesto dalla vista (idealmente con NOEXPAND), quindi unire nuovamente le tabelle di base per recuperare tutte le colonne rimanenti utilizzando le chiavi dalla vista.

Un'altra variante è quella di raggruppare la vista sulle colonne di ordinamento e le chiavi della tabella, quindi scrivere la query per recuperare manualmente le colonne senza vista dalla tabella di base utilizzando le chiavi. L'opzione migliore per te dipende dal contesto più ampio. Un buon modo per decidere è testarlo con i dati reali e il carico di lavoro.

Soluzione di base

CREATE VIEW VI_test
WITH SCHEMABINDING
AS
    SELECT
        t1.PK_ID1,
        t1.something1,
        t1.somethingelse1,
        t2.PK_ID2,
        t2.FK_ID1,
        t2.something2,
        t2.somethingelse2
    FROM dbo.TB_test1 t1
    INNER JOIN dbo.TB_test2 t2 
        ON t1.PK_ID1 = t2.FK_ID1;
GO
-- Brute force unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (somethingelse1, somethingelse2, PK_ID1, PK_ID2);
GO
SELECT TOP (1) * 
FROM dbo.VI_test WITH (NOEXPAND)
ORDER BY somethingelse1,somethingelse2;

Progetto esecutivo:

Indice di forza bruta

Utilizzando un indice non cluster

-- Minimal unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (PK_ID1, PK_ID2)
WITH (DROP_EXISTING = ON);
GO
-- Nonclustered index for ordering
CREATE NONCLUSTERED INDEX ix 
ON dbo.VI_test (somethingelse1, somethingelse2);

Progetto esecutivo:

Indice non cluster per l'ordinazione

C'è una ricerca in questo piano, ma viene utilizzato solo per recuperare una singola riga.

Vista indicizzata minima

ALTER VIEW VI_test
WITH SCHEMABINDING
AS
    SELECT
        t1.PK_ID1,
        t2.PK_ID2,
        t1.somethingelse1,
        t2.somethingelse2
    FROM dbo.TB_test1 t1
    INNER JOIN dbo.TB_test2 t2 
        ON t1.PK_ID1 = t2.FK_ID1;
GO
-- Unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (somethingelse1, somethingelse2, PK_ID1, PK_ID2);

Query:

SELECT TOP (1)
    V.PK_ID1,
    TT1.something1,
    V.somethingelse1,
    V.PK_ID2,
    TT2.FK_ID1,
    TT2.something2,
    V.somethingelse2
FROM dbo.VI_test AS V WITH (NOEXPAND)
JOIN dbo.TB_test1 AS TT1 ON TT1.PK_ID1 = V.PK_ID1
JOIN dbo.TB_test2 AS TT2 ON TT2.PK_ID2 = V.PK_ID2
ORDER BY somethingelse1,somethingelse2;

Progetto esecutivo:

Piano di query finale

Ciò mostra le chiavi della tabella da recuperare (un singolo recupero di riga dall'indice cluster della vista in ordine) seguito da due ricerche a riga singola sulle tabelle di base per recuperare le colonne rimanenti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.