Individua l'elemento mancante più piccolo in base a una formula specifica


8

Devo essere in grado di individuare un elemento mancante da una tabella con decine di milioni di righe e ha una chiave primaria di una BINARY(64)colonna (che è il valore di input da cui calcolare). Questi valori sono per lo più inseriti in ordine, ma a volte voglio riutilizzare un valore precedente che è stato eliminato. È impossibile modificare i record eliminati con una IsDeletedcolonna, poiché a volte viene inserita una riga che è molti milioni di valori prima delle righe attualmente esistenti. Ciò significa che i dati di esempio sarebbero simili:

KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF

Pertanto, l'inserimento di tutti i valori mancanti tra 0x000000000002ed 0xFFFFFFFFFFFFè impossibile, la quantità di tempo e spazio utilizzata sarebbe indesiderabile. In sostanza, quando eseguo l'algoritmo, mi aspetto che ritorni 0x000000000003, che è la prima apertura.

Ho escogitato un algoritmo di ricerca binaria in C #, che avrebbe interrogato il database per ciascun valore in posizione ie testato se quel valore era previsto. Per il contesto, il mio terribile algoritmo: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula

Questo algoritmo eseguirà, ad esempio, 26-27 query SQL su una tabella con 100.000.000 di elementi. (Non sembra molto, ma si verificherà molto frequentemente.) Attualmente, questa tabella contiene circa 50.000.000 di righe e le prestazioni stanno diventando evidenti .

Il mio primo pensiero alternativo è quello di tradurre questo in una procedura memorizzata, ma questo ha i suoi ostacoli. (Devo scrivere un BINARY(64) + BINARY(64)algoritmo, oltre a una serie di altre cose.) Sarebbe doloroso, ma non fattibile. Ho anche preso in considerazione l'implementazione dell'algoritmo di traduzione basato su ROW_NUMBER, ma ho una brutta sensazione al riguardo. (A BIGINTnon è abbastanza grande per questi valori.)

Sono pronto per altri suggerimenti, in quanto ho davvero bisogno che questo sia il più veloce possibile. Per quello che vale l' unica colonna selezionata dalla query C # è la KeyCol, le altre sono irrilevanti per questa porzione.


Inoltre, per quello che vale, la query corrente che recupera il record appropriato è sulla falsariga di:

SELECT [KeyCol]
  FROM [Table]
  ORDER BY [KeyCol] ASC
  OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY

Dov'è <VALUE>l'indice fornito dall'algoritmo. Inoltre non ho ancora avuto il BIGINTproblema OFFSET, ma lo farò. (Solo avere 50.000.000 di righe in questo momento significa che non richiede mai un indice sopra quel valore, ma a un certo punto supererà l' BIGINTintervallo.)

Alcuni dati aggiuntivi:

  • Dalle eliminazioni, il gap:sequentialrapporto è circa 1:20;
  • Le ultime 35.000 righe della tabella hanno valori> BIGINTmassimo;

Cerchi un po 'più di chiarimenti ... 1) perché hai bisogno del binario disponibile "più piccolo" rispetto a qualsiasi binario disponibile? 2) andando avanti, qualche possibilità di mettere un deletetrigger sul tavolo che scarichi il binario ora disponibile su una tabella separata (ad es. create table available_for_reuse(id binary64)), Specialmente alla luce del requisito di fare questa ricerca molto frequentemente ?
markp-fuso,

@markp Il valore più piccolo disponibile ha una "preferenza", pensalo come un accorciatore di URL, non vuoi il prossimo valore più lungo , perché qualcuno può specificare manualmente qualcosa del tipo mynameisebrownche significherebbe che otterrai mynameisebrowo, che tu non vorrei che fosse abcdisponibile.
Der Kommissar,

Cosa select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows onlyti dà una query ?
Lennart,

@Lennart Non è quello di cui ho bisogno. Ho dovuto usare SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol], che ritorna sempre1 .
Der Kommissar,

Mi chiedo se questo sia un qualche tipo di errore di casting, non dovrebbe restituire 1. Cosa seleziona t1.keycol da ... return?
Lennart,

Risposte:


6

Joe ha già colpito la maggior parte dei punti che ho appena passato un'ora a scrivere, in sintesi:

  • altamente dubbio che finirai tutti i KeyColvalori < bigintmax (9.2e18), quindi le conversioni (se necessario) da / verso bigintnon dovrebbero essere un problema fintanto che limiti le ricerche aKeyCol <= 0x00..007FFFFFFFFFFFFFFF
  • Non riesco a pensare a una query che "trova" in modo efficiente tutto il tempo; potresti essere fortunato e trovare un gap vicino all'inizio della tua ricerca, oppure potresti pagare caro per trovare il gap abbastanza nella tua ricerca
  • mentre ho brevemente pensato a come parallelizzare la query, ho scartato rapidamente quell'idea (come DBA non vorrei scoprire che il tuo processo annulla sistematicamente il mio dataserver con un utilizzo del 100% della cpu ... specialmente se tu potessi avere più copie di questo in esecuzione allo stesso tempo); noooo ... il parallelismo sarà fuori discussione

Quindi che si fa?

Mettiamo in pausa l'idea di ricerca (ripetuta, ad alta intensità di cpu, forza bruta) per un minuto e guardiamo l'immagine più grande.

  • in media un'istanza di questa ricerca dovrà scansionare milioni di chiavi di indice (e richiedere una buona quantità di CPU, blocco della cache db e un utente che guarda una clessidra) solo per individuare un singolo valore
  • moltiplica la cpu-use / cache-thrashing / spinning-hour-glass per ... quante ricerche ti aspetti in un giorno?
  • tieni presente che, in generale, ogni istanza di questa ricerca dovrà scansionare la stessa serie di (milioni di) chiavi di indice; questa è MOLTA attività ripetuta per un tale beneficio minimo

Quello che vorrei proporre sono alcune aggiunte al modello di dati ...

  • una nuova tabella che tiene traccia di una serie di KeyColvalori "disponibili per l'uso" , ad esempio:available_for_use(KeyCol binary(64) not null primary key)
  • quanti record conservi in ​​questa tabella spetta a te decidere, ad esempio, forse abbastanza per un mese di attività?
  • la tabella può essere periodicamente (settimanalmente?) "completata" con un nuovo batch di KeyColvalori (forse creare un proc memorizzato "top off"?) [es. aggiornare la select/top/row_number()query di Joe per fare un top 100000]
  • è possibile impostare un processo di monitoraggio per tenere traccia del numero di voci disponibili available_for_use nel caso in cui si inizi a esaurire i valori
  • un trigger DELETE nuovo (o modificato) su> main_table <che posiziona i KeyColvalori eliminati nella nostra nuova tabella available_for_useogni volta che una riga viene eliminata dalla tabella principale
  • se consenti gli aggiornamenti della KeyColcolonna, un trigger UPDATE nuovo / modificato su> main_table <per mantenere available_for_useaggiornata anche la nostra nuova tabella
  • quando arriva il momento di "cercare" un nuovo KeyColvalore per te select min(KeyCol) from available_for_use(ovviamente c'è un po 'di più in questo poiché a) dovrai codificare per problemi di concorrenza - non vuoi che 2 copie del tuo processo afferrino lo stesso min(KeyCol)eb) tu dovrai eliminare min(KeyCol)dalla tabella; questo dovrebbe essere relativamente facile da codificare, forse come un proc memorizzato e può essere affrontato in un altro Q&A se necessario)
  • nel peggiore dei casi, se il select min(KeyCol)processo non trova righe disponibili, è possibile dare il via al proc "top off" per generare un nuovo batch di righe

Con queste modifiche proposte al modello di dati:

  • elimini MOLTI cicli eccessivi di CPU [il tuo DBA ti ringrazierà]
  • elimini TUTTE quelle scansioni di indici ripetitivi e il thrashing della cache [il tuo DBA ti ringrazierà]
  • i tuoi utenti non devono più guardare la clessidra (anche se potrebbe non piacere la perdita di una scusa per allontanarsi dalla propria scrivania)
  • ci sono molti modi per monitorare le dimensioni della available_for_usetabella per assicurarsi di non rimanere mai senza nuovi valori

Sì, la available_for_usetabella proposta è solo una tabella di valori "chiave successiva" pre-generati; e sì, c'è un potenziale per qualche contesa quando si afferra il valore "successivo", ma qualsiasi contesa a) viene facilmente risolta attraverso la corretta progettazione di tabelle / indici / query eb) sarà minore / di breve durata rispetto al sovraccarico / ritardi con l'attuale idea di ripetute, forza bruta, ricerche di indice.


Questo è in realtà simile a quello che ho finito per pensare in chat, penso che probabilmente venga eseguito ogni 15-20 minuti, poiché la query di Joe viene eseguita relativamente rapidamente (sul server live con il caso peggiore di dati di test forzati era 4.5s, il migliore era 0.25s), riesco a inserire chiavi per un numero di giorni e non meno di nchiavi (probabilmente 10 o 20, per costringerlo a cercare valori che potrebbero essere inferiori e più desiderabili). Apprezzo molto la risposta qui, però, metti i pensieri per iscritto! :)
Der Kommissar,

ahhhh, se hai un server application / middleware in grado di fornire una cache intermedia di KeyColvalori disponibili ... sì, anche questo funzionerebbe :-) e ovviamente eliminerebbe la necessità di un cambio di modello di dati eh
markp-fuso

Precisamente, sto pensando di creare una cache statica sull'applicazione Web stessa, l'unico problema è che è distribuito (quindi ho bisogno di sincronizzare la cache tra i server), il che significa che un'implementazione SQL o middleware sarebbe molto preferito. :)
Der Kommissar,

hmmmm ... un KeyColgestore distribuito e la necessità di codificare per potenziali violazioni di PK se 2 (o più) istanze simultanee dell'app tentano di utilizzare lo stesso KeyColvalore ... yuck ... decisamente più semplice con un singolo server middleware o un soluzione db-centrica
markp-fuso

8

Ci sono alcune sfide con questa domanda. Gli indici in SQL Server possono eseguire le seguenti operazioni in modo molto efficiente con solo una lettura logica ciascuno:

  • controlla che esista una riga
  • controlla che non esista una riga
  • trova la riga successiva a partire da un certo punto
  • trova la riga precedente a partire da un certo punto

Tuttavia, non possono essere utilizzati per trovare l'ennesima riga in un indice. Per fare ciò è necessario ruotare il proprio indice memorizzato come tabella o scansionare le prime N righe nell'indice. Il tuo codice C # si basa fortemente sul fatto che puoi trovare in modo efficiente l'ennesimo elemento dell'array, ma non puoi farlo qui. Penso che l'algoritmo non sia utilizzabile per T-SQL senza una modifica del modello di dati.

La seconda sfida riguarda le restrizioni sui BINARYtipi di dati. Per quanto ne so, non è possibile eseguire addizioni, sottrazioni o divisioni nei normali modi. Puoi convertire il tuo BINARY(64)in a BIGINTe non genererà errori di conversione, ma il comportamento non è definito :

Le conversioni tra qualsiasi tipo di dati e tipi di dati binari non sono garantite uguali tra le versioni di SQL Server.

Inoltre, la mancanza di errori di conversione è un po 'un problema qui. Puoi convertire qualsiasi cosa più grande del BIGINTvalore più grande possibile ma ti darà risultati sbagliati.

È vero che al momento hai valori maggiori di 9223372036854775807. Tuttavia, se inizi sempre da 1 e cerchi il valore minimo più piccolo, quei valori grandi non possono essere rilevanti a meno che la tua tabella non contenga più di 9223372036854775807 righe. Ciò sembra improbabile perché la tua tabella a quel punto sarebbe di circa 2000 exabyte, quindi ai fini della risposta alla tua domanda suppongo che non è necessario cercare i valori molto grandi. Farò anche la conversione del tipo di dati perché sembrano inevitabili.

Per i dati del test, ho inserito l'equivalente di 50 milioni di numeri interi sequenziali in una tabella insieme a 50 milioni di numeri interi in più con un singolo divario di valore ogni 20 valori circa. Ho anche inserito un singolo valore che non si adatta correttamente in un segno BIGINT:

CREATE TABLE dbo.BINARY_PROBLEMS (
    KeyCol BINARY(64) NOT NULL
);

INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
    SELECT 1 + CASE WHEN t.RN > 50000000 THEN
        CASE WHEN ABS(CHECKSUM(NewId()) % 20)  = 10 THEN 1 ELSE 0 END
    ELSE 0 END OFFSET
    FROM
    (
        SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
        FROM master..spt_values t1
        CROSS JOIN master..spt_values t2
        CROSS JOIN master..spt_values t3
    ) t
) tt
OPTION (MAXDOP 1);

CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);

-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));

Il codice ha impiegato alcuni minuti per essere eseguito sulla mia macchina. Ho fatto in modo che la prima metà del tavolo non avesse spazi vuoti per rappresentare una specie di caso peggiore per le prestazioni. Il codice che ho usato per risolvere il problema analizza l'indice in modo che finisca molto rapidamente se il primo gap è all'inizio della tabella. Prima di arrivare a questo, verificiamo che i dati siano come dovrebbero essere:

SELECT TOP (2) KeyColBigInt
FROM
(
    SELECT KeyCol
    , CAST(KeyCol AS BIGINT) KeyColBigInt
    FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;

I risultati suggeriscono che il valore massimo che convertiamo in BIGINTè 102500672:

╔══════════════════════╗
     KeyColBigInt     
╠══════════════════════╣
 -9223372036854775808 
            102500672 
╚══════════════════════╝

Esistono 100 milioni di righe con valori che si adattano a BIGINT come previsto:

SELECT COUNT(*) 
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;

Un approccio a questo problema è la scansione dell'indice in ordine e la chiusura non appena il valore di una riga non corrisponde al ROW_NUMBER()valore previsto . Non è necessario eseguire la scansione dell'intera tabella per ottenere la prima riga: solo le righe fino al primo spazio. Ecco un modo per scrivere codice che probabilmente otterrà quel piano di query:

SELECT TOP (1) KeyCol
FROM
(
    SELECT KeyCol
    , CAST(KeyCol AS BIGINT) KeyColBigInt
    , ROW_NUMBER() OVER (ORDER BY KeyCol) RN
    FROM dbo.BINARY_PROBLEMS
    WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;

Per motivi che non rientrano in questa risposta, questa query verrà spesso eseguita in serie da SQL Server e SQL Server spesso sottostimerà il numero di righe che devono essere analizzate prima di trovare la prima corrispondenza. Sul mio computer, SQL Server esegue la scansione di 50000022 righe dall'indice prima di trovare la prima corrispondenza. L'esecuzione della query richiede 11 secondi. Si noti che questo restituisce il primo valore oltre il gap. Non è chiaro quale riga desideri esattamente, ma dovresti essere in grado di modificare la query in base alle tue esigenze senza troppi problemi. Ecco come appare il piano :

piano seriale

La mia unica altra idea era quella di costringere SQL Server a utilizzare il parallelismo per la query. Ho quattro CPU, quindi dividerò i dati in quattro intervalli e cercherò su tali intervalli. A ciascuna CPU verrà assegnato un intervallo. Per calcolare gli intervalli ho appena preso il valore massimo e ho ipotizzato che i dati fossero distribuiti uniformemente. Se vuoi essere più intelligente al riguardo, puoi guardare un istogramma delle statistiche campionato per i valori delle colonne e costruire i tuoi intervalli in quel modo. Il codice seguente si basa su molti trucchi non documentati che non sono sicuri per la produzione, incluso il flag di traccia 8649 :

SELECT TOP 1 ca.KeyCol
FROM (
    SELECT 1 bucket_min_value, 25625168 bucket_max_value
    UNION ALL
    SELECT 25625169, 51250336
    UNION ALL
    SELECT 51250337, 76875504
    UNION ALL
    SELECT 76875505, 102500672
) buckets
CROSS APPLY (
    SELECT TOP 1 t.KeyCol
    FROM
    (
        SELECT KeyCol
        , CAST(KeyCol AS BIGINT) KeyColBigInt
        , buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
        FROM dbo.BINARY_PROBLEMS
        WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <=  CAST(buckets.bucket_max_value AS BINARY(64))
    ) t
    WHERE t.KeyColBigInt <> t.RN
    ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 8649);

Ecco come appare il modello di ciclo nidificato parallelo:

piano parallelo

Nel complesso, la query funziona più di prima poiché esegue la scansione di più righe nella tabella. Tuttavia, ora funziona in 7 secondi sul mio desktop. Potrebbe parallelizzarsi meglio su un vero server. Ecco un link al piano reale .

Non riesco davvero a pensare a un buon modo per risolvere questo problema. Fare il calcolo al di fuori di SQL o cambiare il modello di dati può essere la tua migliore scommessa.


Anche se la risposta migliore è "questo non funzionerà bene in SQL", almeno mi dice dove spostarmi dopo. :)
Der Kommissar,

1

Ecco una risposta che probabilmente non funzionerà per te, ma la aggiungerò comunque.

Anche se BINARY (64) è enumerabile, il supporto per determinare il successore di un elemento è scarso. Poiché BIGINT sembra essere troppo piccolo per il tuo dominio, potresti prendere in considerazione l'utilizzo di un DECIMAL (38,0), che sembra essere il tipo NUMBER più grande nel server SQL.

CREATE TABLE SearchTestTableProper
( keycol decimal(38,0) not null primary key );

INSERT INTO SearchTestTableProper (keycol)
VALUES (1),(2),(3),(12);

Trovare il primo divario è facile poiché possiamo costruire il numero che stiamo cercando:

select top 1 t1.keycol+1 
from SearchTestTableProper t1 
where not exists (
    select 1 
    from SearchTestTableProper t2 
    where t2.keycol = t1.keycol + 1
)
order by t1.keycol;

Un join ad anello nidificato sull'indice pk dovrebbe essere sufficiente per trovare il primo elemento disponibile.

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.