Filtraggio dei dati ordinati per rowversion


8

Ho una tabella di dati SQL con la seguente struttura:

CREATE TABLE Data(
    Id uniqueidentifier NOT NULL,
    Date datetime NOT NULL,
    Value decimal(20, 10) NULL,
    RV timestamp NOT NULL,
 CONSTRAINT PK_Data PRIMARY KEY CLUSTERED (Id, Date)
)

Il numero di ID distinti varia da 3000 a 50000.
La dimensione della tabella varia fino a oltre un miliardo di righe.
Un ID può coprire tra poche righe fino al 5% della tabella.

La singola query più eseguita su questa tabella è:

SELECT Id, Date, Value, RV
FROM Data
WHERE Id = @Id
AND Date Between @StartDate AND @StopDate

Ora devo implementare il recupero incrementale dei dati su un sottoinsieme di ID, inclusi gli aggiornamenti.
Ho quindi utilizzato uno schema di richiesta in cui il chiamante fornisce una specifica rowversion, recupera un blocco di dati e utilizza il valore massimo di rowversion dei dati restituiti per la chiamata successiva.

Ho scritto questa procedura:

CREATE TYPE guid_list_tbltype AS TABLE (Id uniqueidentifier not null primary key)
CREATE PROCEDURE GetData
    @Ids guid_list_tbltype READONLY,
    @Cursor rowversion,
    @MaxRows int
AS
BEGIN
    SELECT A.* 
    FROM (
        SELECT 
            Data.Id,
            Date,
            Value,
            RV,
            ROW_NUMBER() OVER (ORDER BY RV) AS RN
        FROM Data
             inner join (SELECT Id FROM @Ids) Ids ON Ids.Id = Data.Id
        WHERE RV > @Cursor
    ) A 
    WHERE RN <= @MaxRows
END

Dove @MaxRowsandrà da 500.000 a 2.000.000 a seconda di come il cliente vorrà i suoi dati.


Ho provato diversi approcci:

  1. Indicizzazione su (Id, RV):
    CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, RV) INCLUDE(Date, Value);

Utilizzando l'indice, la query cerca le righe in cui RV = @Cursorper ciascuna Idin @Ids, legge le seguenti righe quindi unisce il risultato e ordina.
L'efficienza dipende quindi dalla posizione relativa del @Cursorvalore.
Se è vicino alla fine dei dati (ordinata da RV) la query è istantanea e in caso contrario la query può richiedere fino a minuti (mai lasciarla funzionare fino alla fine).

il problema con questo approccio è che @Cursorè vicino alla fine dei dati e l'ordinamento non è doloroso (nemmeno necessario se la query restituisce meno righe di @MaxRows) o è più indietro e la query deve ordinare le @MaxRows * LEN(@Ids)righe.

  1. Indicizzazione su camper:
    CREATE NONCLUSTERED INDEX IDX_RV ON Data(RV) INCLUDE(Id, Date, Value);

Utilizzando l'indice, la query cerca la riga in cui RV = @Cursorquindi legge ogni riga scartando gli ID non richiesti fino a raggiungere @MaxRows.
L'efficienza dipende quindi dalla% degli ID richiesti ( LEN(@Ids) / COUNT(DISTINCT Id)) e dalla loro distribuzione.
L'ID% più richiesto significa meno righe scartate, il che significa letture più efficienti, l'ID% meno richiesto significa righe più scartate, il che significa più letture per lo stesso numero di righe risultanti.

Il problema con questo approccio è che se gli ID richiesti contengono solo pochi elementi, potrebbe essere necessario leggere l'intero indice per ottenere le righe desiderate.

  1. Utilizzo dell'indice filtrato o delle viste indicizzate
    CREATE NONCLUSTERED INDEX IDX_RVClient1 ON Data(Id, RV) INCLUDE(Date, Value)
    WHERE Id IN (/* list of Ids for specific client*/);

O

    CREATE VIEW vDataClient1 WITH SCHEMABINDING
    AS
    SELECT
        Id,
        Date,
        Value,
        RV
    FROM dbo.Data
    WHERE Id IN (/* list of Ids for specific client*/)
    CREATE UNIQUE CLUSTERED INDEX IDX_IDRV ON vDataClient1(Id, Rv);

Questo metodo consente piani di indicizzazione e di esecuzione delle query perfettamente efficienti, ma presenta degli svantaggi: 1. In pratica, dovrò implementare SQL dinamico per creare gli indici o le viste e modificare la procedura di richiesta per utilizzare l'indice o la vista corretti. 2. Dovrò mantenere un indice o una vista dal client esistente, incluso lo spazio di archiviazione. 3. Ogni volta che un cliente dovrà modificare il suo elenco di ID richiesti, dovrò eliminare l'indice o visualizzarlo e ricrearlo.


Non riesco a trovare un metodo adatto alle mie esigenze.
Sto cercando idee migliori per implementare il recupero incrementale dei dati. Quelle idee potrebbero implicare una rielaborazione dello schema richiedente o dello schema del database, anche se preferirei un approccio di indicizzazione migliore se ce n'è uno.


Crosspost con stackoverflow.com/questions/11586004/... . Per il momento ho rimosso la versione Oracle perché ho scoperto che ORA_ROWSCN non è indicizzabile (e difficilmente attraverso viste materializzate indicizzate).
Paciv

Come si inserisce il campo data? È possibile aggiornare una riga con un ID e una data particolari nella tabella? E se è così, anche la data è aggiornata (come un timestamp aggiuntivo?)
8kb

Sembra come per il tentativo di GetData (), l'ordine per dovrebbe includere l'ID (ordine per camper, ID). Puoi commentare usando un indice di (Rv, Id)? Anche l'utilizzo di ">" max rowversion della chiamata precedente sembra che mancherà i record tra blocchi se le righe hanno la stessa rowversion (non è possibile?).
crokusek,

@ 8kb: le istruzioni di aggiornamento eseguite nella tabella modificano solo la Valuecolonna. @crokusek: non ordinando per camper, ID anziché camper aumenta solo il carico di lavoro di ordinamento senza alcun vantaggio, non capisco il ragionamento alla base del tuo commento. Da quello che ho letto, RV dovrebbe essere unico a meno che non si inseriscano dati specifici in quella colonna, cosa che l'applicazione no.
Paciv

Il client può accettare i risultati nell'ordine (Id, Rv) e fornire un argomento LastId in aggiunta all'argomento LastRowVersion per eliminare l'ordinamento RV tra gli ID? I miei precedenti commenti erano tutti basati sul presupposto che RV avesse duplicati. L'indice filtrato per client sembrava interessante.
crokusek,

Risposte:


5

Una soluzione è che l'applicazione client ricordi il massimo rowversionper ID. Il tipo di tabella definito dall'utente cambierebbe in:

CREATE TYPE
    dbo.guid_list_tbltype
AS TABLE 
    (
    Id      uniqueidentifier PRIMARY KEY, 
    LastRV  rowversion NOT NULL
    );

La query nella procedura può quindi essere riscritta per utilizzare il APPLYmodello (vedere i miei articoli SQLServerCentral parte 1 e parte 2 - è richiesto l'accesso gratuito). La chiave per una buona prestazione qui è la ORDER BY- evita il pre-fetching non ordinato sull'unione di loop nidificati. Il RECOMPILEè necessario consentire l'ottimizzatore di vedere la cardinalità della variabile tavolo al momento della compilazione (probabilmente risultante in un piano parallelo desiderabile).

ALTER PROCEDURE dbo.GetData

    @IDs        guid_list_tbltype READONLY,
    @MaxRows    bigint

AS
BEGIN

    SELECT TOP (@MaxRows)
        d.Id,
        d.[Date],
        d.Value,
        d.RV
    FROM @Ids AS i
    CROSS APPLY
    (
        SELECT
            d.*
        FROM dbo.Data AS d
        WHERE
            d.Id = i.Id
            AND d.RV > i.LastRV
    ) AS d
    ORDER BY
        i.Id,
        d.RV
    OPTION (RECOMPILE);

END;

Dovresti ottenere un piano di query post-esecuzione come questo (il piano stimato sarà seriale):

piano di query


Bene, una delle soluzioni di modifica del progetto è di far ricordare al client il MAX(RV)per ID (o un sistema di abbonamento in cui l'applicazione interna ricorda tutte le coppie Id / RV) e io uso questo patern per un altro client. Un'altra soluzione consisteva nel forzare il client a recuperare sempre tutti gli ID (che rendono banale il problema dell'indicizzazione). Non copre ancora la particolare esigenza: recupero incrementale di un sottoinsieme di ID con un solo contatore globale fornito dal client.
Paciv,

2

Se possibile, ridisegnerei la tabella. Se possiamo avere VersionNumber come numero intero incrementale senza spazi vuoti, il compito di recuperare il blocco successivo è una scansione di intervallo totalmente banale. Tutto ciò di cui abbiamo bisogno è il seguente indice:

CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, VersionNumber) INCLUDE(Date, Value);

Naturalmente, dobbiamo assicurarci che VersionNumber inizi con uno e non abbia spazi vuoti. Questo è facile da fare con i vincoli.


Vuoi dire un globale o un ID locale VersionNumber? In entrambi i casi, non riesco a vedere come ciò possa aiutare con la domanda, potresti approfondire ulteriormente?
Paciv,

0

Cosa avrei fatto:

In questo caso, il tuo PK dovrebbe essere un campo di identità "Chiave surrogata" che si auto-incrementa.
Dato che sei già in miliardi, sarebbe meglio andare con un BigInt.
Chiamiamolo DataID .
Questo sarà:

  • Aggiungi 8 byte a ogni record nel tuo indice cluster.
  • Salva 16 byte su ogni record in ogni indice non cluster.
  • Quello che avevi era una "chiave naturale": un UniqueIdentifyer (16 byte) con un DateTime (8 byte).
  • Sono 24 byte in ogni record dell'indice per fare riferimento all'indice cluster!
  • Questo è il motivo per cui abbiamo chiavi surrogate come numeri interi incrementali più piccoli.


Imposta il tuo nuovo BigInt PK ( DataID ) per utilizzare un indice cluster
:

  • Assicurarsi che i record creati più di recente siano posizionati vicino alla fine.
  • Consentire l'indicizzazione più veloce con altri indici non cluster.
  • Consentire la futura espansione come FK in altre tabelle.


Crea un indice non cluster intorno (data, ID).
Questo sarà:

  • Accelera le tue query più comunemente utilizzate.
  • Potresti aggiungere "Valore", ma aumenterà la dimensione del tuo indice, il che rende più lento.
  • Suggerirei di provarlo dentro e fuori l'Indice per vedere se c'è una grande differenza nelle prestazioni.
  • Consiglio di non usare "Includi" se lo aggiungi.
  • Basta aggrapparsi in questo modo (data, ID, valore), ma solo se i test dimostrano che migliora le prestazioni.


Crea un indice non cluster su (RV, ID).
Questo sarà:

  • Tieni sempre i tuoi indici il più piccolo possibile.
  • A meno che tu non noti enormi guadagni di prestazioni con la data e il valore nei tuoi indici, ti suggerirei di lasciarli fuori per risparmiare spazio su disco. Prova prima senza di loro.
  • Se aggiungi Data o Valore, non utilizzare "Includi", invece aggiungili all'ordinamento dell'Indice.
  • Grazie all'incremento di DataID sui nuovi inserti nel tuo PK in cluster, i tuoi camper recenti appariranno di solito vicino alla fine (a meno che non si aggiornino continuamente fasce di dati del passato).
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.