Il modo più veloce per dividere / memorizzare una stringa lunga per la funzione charindex


8

Ho una stringa di cifre da 1 TB. Data una sequenza di 12 caratteri di cifre, voglio ottenere la posizione iniziale di questa sequenza nella stringa ( charindexfunzione) originale.

Ho provato questo con una stringa da 1 GB e una sottostringa di 9 cifre utilizzando SQL Server, memorizzando la stringa come varchar(max). Charindexrichiede 10 secondi. Spezzare la stringa da 1 GB in blocchi sovrapposti di 900 byte e creare una tabella (StartPositionOfChunk, Chunkofstring) con chunkofstring in fascicolazione binaria, l'indicizzazione richiede meno di 1 secondo. Il metodo Latter per 10 GB, sottostringa a 10 cifre aumenta il charindex a 1,5 min. Vorrei trovare un metodo di archiviazione più veloce.

Esempio

stringa di cifre: 0123456789 - sottostringa da cercare 345
charindex ('345', '0123456789') restituisce 4

Metodo 1 : ora posso archiviarlo in una tabella di SQL Server con tabella composta da una colonna colstred eseguire:

select charindex('345',colstr) from strtable

Metodo 2 : oppure posso creare una tabella strtable2 (pos, colstr1) suddividendo la stringa originale: 1; 012 | 2; 123 | 3; 234 aso e quindi possiamo avere la query

select pos from strtable2 where colstr1='345'

Metodo 3 : posso creare una tabella strtable2 (pos2, colstr2) suddividendo la stringa originale in blocchi più grandi 1; 01234 | 4; 34567 | 7; 6789 e quindi

select pos2+charindex('345',colstr2) from strtable2 where colstr2 like '%345%'

Il primo metodo è il più lento.

Il secondo metodo fa esplodere le dimensioni della memoria del database!

Metodo 3 : impostare la lunghezza di colstr2 su 900 byte nelle regole di confronto binarie, la creazione di un indice su questa colonna richiede 1 secondo per la stringa da 1 GB e la ricerca di sottostringa a 9 cifre. Per una stringa da 10 GB e una sottostringa di 10 cifre, occorrono 90 secondi.

Qualche altra idea di come renderlo più veloce (forse utilizzando la stringa è composta da cifre, con numeri interi lunghi, ...)?

Cerca sempre una sottostringa di 12 cifre in una stringa di cifre da 1 TB SQL Server 2017 Developer Edition, 16 core, 16 GB di RAM. L'obiettivo principale è la velocità di ricerca! 10 cifre in una stringa da 10 GB (per test delle prestazioni).

Risposte:


6

Raccomando di utilizzare una variante del metodo 2 e suddividere l'intervallo di ricerca in molte tabelle di destinazione. 10000 tavoli è un buon primo tentativo. Ad esempio, se si cerca "012345678901", la query esaminerebbe la tabella associata ai dati che iniziano con "0123". Avresti ancora circa un trilione di righe in totale, ma la suddivisione dei dati in molte tabelle ha le seguenti qualità positive:

  1. Tutte le possibili stringhe a 12 cifre ora possono adattarsi a un INT.
  2. Costruire una rappresentazione più efficiente in termini di ricerca della stringa da 1 TB sarà probabilmente costoso, qualunque cosa accada. Con molte tabelle, è possibile parallelizzare facilmente il lavoro e persino richiedere temporaneamente l'assegnazione di più CPU alla VM.
  3. È possibile creare una singola tabella come prova di concetto per determinare i tempi di query e i requisiti di spazio totale per l'intera stringa.
  4. Se hai mai bisogno di eseguire qualsiasi tipo di manutenzione del database, sarai felice di non aver creato una tabella enorme.

A questo punto, la domanda principale per me è se usi un archivio righe o un archivio colonne compressi. Il codice seguente crea una tabella di archivio righe per lo spazio di ricerca "0123" e vi inserisce 100 milioni di righe. Se la tua stringa è abbastanza casuale, potresti anche aspettarti di vedere circa 100 milioni di righe per tabella.

DROP TABLE IF EXISTS #t;

SELECT TOP (10000) 0 ID INTO #t
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.Q229892_RAW_100M_RANGE;

CREATE TABLE dbo.Q229892_RAW_100M_RANGE (
STRING_PIECE INT NOT NULL,
STR_POS BIGINT NOT NULL
);

INSERT INTO dbo.Q229892_RAW_100M_RANGE WITH (TABLOCK)
SELECT ABS(CHECKSUM(NEWID()) % 100000000),
TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT) * TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT)
FROM #t t1
CROSS JOIN #t t2
OPTION (MAXDOP 4);


DROP TABLE IF EXISTS dbo.T0123_Q229892_PAGE_COMPRESSION;

CREATE TABLE dbo.T0123_Q229892_PAGE_COMPRESSION (
    STRING_PIECE INT NOT NULL,
    STR_POS BIGINT NOT NULL,
    PRIMARY KEY (STRING_PIECE, STR_POS)
) WITH (DATA_COMPRESSION = PAGE);

INSERT INTO dbo.T0123_Q229892_PAGE_COMPRESSION WITH (TABLOCK)
SELECT STRING_PIECE, STR_POS
FROM dbo.Q229892_RAW_100M_RANGE;

La cattiva notizia è per il set completo di dati di cui probabilmente avresti bisogno intorno a 15,4 TB. La buona notizia è che le query richiedono solo 1 ms per me anche quando non ci sono dati rilevanti nella cache del buffer, il che sarà quasi sempre il caso di un set di dati grande come il tuo.

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

SELECT MIN(STR_POS)
FROM dbo.T0123_Q229892_PAGE_COMPRESSION
WHERE STRING_PIECE = 45678901; -- searching for '012345678901'

Probabilmente puoi lanciare questi dati sullo spazio di archiviazione più economico che hai e vedere ancora buoni tempi di risposta poiché la query esegue così poche letture logiche.

Per il columnstore, non puoi cercare i dati di cui hai bisogno e sei ancora estremamente improbabile di adattare tutti i tuoi dati in memoria, quindi è importante leggere il minor numero possibile di dati compressi con le tue query. Consiglio vivamente di partizionare i tuoi tavoli. Un approccio semplice che funziona bene consiste nell'utilizzare le prime quattro cifre della stringa di ricerca per trovare il nome della tabella e le due cifre successive come partizione. Utilizzando di nuovo "012345678901", si passerà alla partizione 45 della tabella che contiene i dati per "0123". 100 partizioni sono un buon numero per evitare problemi causati da troppe partizioni e in media finirai con circa 1 milione di righe per ogni partizione. Il numero massimo di righe che possono rientrare in un singolo rowgroup è 1048576, quindi con questo approccio eseguirai il minor numero di IO possibile.

DROP TABLE IF EXISTS dbo.Q229892_RAW_1M_RANGE;

CREATE TABLE dbo.Q229892_RAW_1M_RANGE (
STRING_PIECE INT NOT NULL,
STR_POS BIGINT NOT NULL
);

INSERT INTO dbo.Q229892_RAW_1M_RANGE WITH (TABLOCK)
SELECT TOP (1000000) ABS(CHECKSUM(NEWID()) % 1000000),
TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT) * TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);



DECLARE @IntegerPartitionFunction nvarchar(max) = 
    N'CREATE PARTITION FUNCTION partition100 (tinyint) 
    AS RANGE LEFT FOR VALUES (';  
DECLARE @i int = 0;  
WHILE @i < 100
BEGIN  
SET @IntegerPartitionFunction += CAST(@i as nvarchar(10)) + N', ';  
SET @i += 1;  
END  
SET @IntegerPartitionFunction += CAST(@i as nvarchar(10)) + N');';  
EXEC sp_executesql @IntegerPartitionFunction;  
GO  

CREATE PARTITION SCHEME partition100_scheme
AS PARTITION partition100  
ALL TO ([DEFAULT]);

DROP TABLE IF EXISTS dbo.T0123_Q229892_COLUMNSTORE;

-- this table must be partitioned by PART_ID!
CREATE TABLE dbo.T0123_Q229892_COLUMNSTORE (
    PART_ID TINYINT NOT NULL,
    STRING_PIECE INT NOT NULL,
    STR_POS BIGINT NOT NULL,
    INDEX CCS CLUSTERED COLUMNSTORE
) ON partition100_scheme (PART_ID);


GO

DECLARE @part_id TINYINT = 0;
SET NOCOUNT ON;
WHILE @part_id < 100
BEGIN
    INSERT INTO dbo.T0123_Q229892_COLUMNSTORE WITH (TABLOCK)
    SELECT @part_id, STRING_PIECE, STR_POS
    FROM dbo.Q229892_RAW_1M_RANGE
    OPTION (MAXDOP 1);

    SET @part_id = @part_id + 1;
END;

GO

Con questo approccio l'intero set di dati richiederebbe circa 10,9 TB. Non mi è chiaro come ridurlo. La query di ricerca è un po 'più lenta in questo caso. Sulla mia macchina ci vogliono circa 25 ms, ma questo dipenderà principalmente da IO:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

SELECT MIN(STR_POS)
FROM dbo.T0123_Q229892_COLUMNSTORE
WHERE PART_ID = 45
AND STRING_PIECE = 678901; -- searching for '012345678901'

Una nota importante sull'approccio columnstore è che la cifra di 10,9 TB è per dati compressi al 100%. Sarà difficile popolare una tabella del genere in modo efficiente evitando i negozi delta. È probabile che finirai con dati non compressi negli archivi delta a un certo punto del processo che potrebbero facilmente richiedere più di 15,4 TB utilizzati per l'approccio del negozio di file.


6

La memorizzazione e l'elaborazione di 1 TB di dati con solo 16 GB di RAM disponibili può rivelarsi una sfida. 1 GB per core è piuttosto sbilanciato, soprattutto per questo tipo di carico di lavoro. 8 GB per core sarebbe un punto di partenza molto migliore, con più auspicabile.

Detto questo, sarei ancora tentato di provare una variante del metodo 2:

Memorizza tutte le possibili sottostringhe di 12 caratteri come bigintin una tabella columnstore cluster (con compressione dell'archivio se risulta utile):

CREATE TABLE dbo.Search
(
    pos bigint NOT NULL,
    fragment bigint NOT NULL,

    INDEX CCS CLUSTERED COLUMNSTORE 
        WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE) -- optional
);

Probabilmente dovrai implementare un modo per caricare in modo incrementale i dati di origine in questa tabella. Assicurati di finire con gruppi di righe di dimensioni massime (1.048.576 righe) nella struttura di archivio colonne finito . Consulta la guida al caricamento dei dati .

È possibile posizionare le righe in multipli di 1048576 in una tabella di archivio righe non indicizzata prima di creare un indice di archivio di colonne cluster su quello, quindi cambiare il risultato direttamente in una tabella principale partizionata. L'approccio esatto dipende da come si intende caricare i dati, se verranno aggiunti e dalla familiarità con SQL Server in generale.

Con questo metodo sono possibili ottime prestazioni, ma come spesso accade con il columnstore, è necessario ottenere un'efficace eliminazione delle partizioni e dei segmenti. Partizionare sulla fragmentcolonna e costruire l'indice columnstore in serie mentre si sostituisce un indice cluster di rowstore su cui fragmentsi digita è il modo per raggiungere questo obiettivo, come notato nella documentazione collegata sopra. Ciò ridurrà anche al minimo le esigenze di archiviazione, da allorafragment valori nello stesso intervallo verranno memorizzati nello stesso segmento. Ciò consente un efficace rebasing del valore e imballaggio dei bit.

Durante il caricamento, prova a limitare le stringhe con cui stai lavorando all'interno di SQL Server a tipi non LOB (max). Se ritieni che lavorare con i LOB sia il migliore per la velocità effettiva, è spesso possibile trovare un punto debole della lunghezza dei dati, al di sopra del quale le prestazioni diminuiscono in modo significativo.

A seconda delle dimensioni finali della struttura e della velocità del sottosistema I / O, è possibile che questo approccio offra prestazioni sufficientemente buone. Aumentare la memoria disponibile migliorerebbe notevolmente le cose.

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.