Der schnellste Weg, eine lange Zeichenfolge für die Charindex-Funktion zu teilen / zu speichern


8

Ich habe eine 1-TB-Ziffernfolge. Bei einer 12-stelligen Ziffernfolge möchte ich die Startposition dieser Folge in der ursprünglichen Zeichenfolge ( charindexFunktion) erhalten.

Ich habe dies mit einer 1-GB-Zeichenfolge und einer 9-stelligen Teilzeichenfolge unter Verwendung von SQL Server getestet und die Zeichenfolge als gespeichert varchar(max). Charindexdauert 10 Sekunden. Das Aufteilen der 1-GB-Zeichenfolge in überlappende 900-Byte-Blöcke und das Erstellen einer Tabelle (StartPositionOfChunk, Chunkofstring) mit Chunkofstring in binärer Sortierung dauert weniger als 1 Sekunde. Die letzte Methode für 10 GB, 10-stellige Teilzeichenfolge erhöht den Zeichenindex auf 1,5 Minuten. Ich möchte eine schnellere Speichermethode finden.

Beispiel

Ziffernfolge: 0123456789 - Teilzeichenfolge für die Suche nach 345
Zeichenindex ('345', '0123456789') ergibt 4

Methode 1 : Ich kann dies jetzt in einer SQL Server-Tabelle speichern, die aus einer Spalte besteht, colstrund Folgendes ausführen:

select charindex('345',colstr) from strtable

Methode 2 : oder ich kann eine Tabelle strtable2 (pos, colstr1) erstellen, indem ich die ursprüngliche Zeichenfolge aufteile : 1; 012 | 2; 123 | 3; 234 aso und dann können wir die Abfrage haben

select pos from strtable2 where colstr1='345'

Methode 3 : Ich kann eine Tabelle strtable2 (pos2, colstr2) erstellen, indem ich die ursprüngliche Zeichenfolge in größere Teile 1; 01234 | aufteile 4; 34567 | 7; 6789 und dann

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

Die erste Methode ist die langsamste.

Die zweite Methode vergrößert die Größe des Datenbankspeichers!

Methode 3 : Festlegen der Länge von colstr2 auf 900 Byte bei der binären Sortierung. Das Erstellen eines Index für diese Spalte dauert 1 Sekunde für die Suche nach 1 GB Zeichenfolge und 9-stelliger Teilzeichenfolge. Für 10 GB Zeichenfolge und 10-stellige Teilzeichenfolge dauert es 90 Sekunden.

Irgendeine andere Idee, wie man dies schneller macht (vielleicht besteht die Verwendung der Zeichenfolge aus Ziffern mit langen ganzen Zahlen, ....)?

Die Suche erfolgt immer nach einer 12-stelligen Teilzeichenfolge in einer 1-TB-Ziffernfolge SQL Server 2017 Developer Edition, 16 Kerne, 16 GB RAM. Hauptziel ist die Suchgeschwindigkeit! 10 Ziffern in einer 10-GB-Zeichenfolge (für Leistungstests).

Antworten:


6

Ich empfehle die Verwendung einer Variante von Methode 2 und die Aufteilung des Suchbereichs in viele Zieltabellen. 10000 Tische sind ein guter erster Versuch. Wenn Sie beispielsweise nach "012345678901" suchen, wird in Ihrer Abfrage die Tabelle angezeigt, die Daten zugeordnet ist, die mit "0123" beginnen. Sie hätten immer noch insgesamt etwa eine Billion Zeilen, aber die Aufteilung der Daten in viele Tabellen hat die folgenden positiven Eigenschaften:

  1. Alle möglichen 12-stelligen Zeichenfolgen können jetzt in eine INT passen.
  2. Das Erstellen einer sucheffizienteren Darstellung Ihrer 1-TB-Zeichenfolge wird wahrscheinlich teuer, egal was passiert. Mit vielen Tabellen können Sie den Job problemlos parallelisieren und sogar vorübergehend nach mehr CPUs fragen, die Ihrer VM zugewiesen werden sollen.
  3. Sie können eine einzelne Tabelle als Proof-of-Concept erstellen, um die Abfragezeit und den Gesamtplatzbedarf für die vollständige Zeichenfolge zu bestimmen.
  4. Wenn Sie jemals eine Datenbankwartung durchführen müssen, sind Sie froh, dass Sie keine einzige große Tabelle erstellt haben.

An dieser Stelle ist die Hauptfrage für mich, ob Sie einen komprimierten Zeilen- oder Spaltenspeicher verwenden. Der folgende Code erstellt eine Rowstore-Tabelle für den Suchbereich "0123" und fügt 100 Millionen Zeilen ein. Wenn Ihre Zeichenfolge zufällig genug ist, können Sie auch mit etwa 100 Millionen Zeilen pro Tabelle rechnen.

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;

Die schlechte Nachricht ist, dass Sie für den gesamten Datensatz wahrscheinlich etwa 15,4 TB benötigen. Die gute Nachricht ist, dass Abfragen für mich nur 1 ms dauern, selbst wenn sich keine relevanten Daten im Puffercache befinden, was bei einem so großen Datensatz wie Ihrem fast immer der Fall ist.

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Sie können diese Daten wahrscheinlich auf den billigsten Speicher werfen, den Sie haben, und trotzdem gute Antwortzeiten sehen, da die Abfrage so wenige logische Lesevorgänge ausführt.

Beim Spaltenspeicher können Sie nicht nach den Daten suchen, die Sie benötigen, und es ist äußerst unwahrscheinlich, dass alle Ihre Daten in den Speicher passen. Daher ist es wichtig, bei Ihren Abfragen so wenig komprimierte Daten wie möglich zu lesen. Ich empfehle dringend, Ihre Tabellen zu partitionieren. Ein einfacher Ansatz, der gut funktioniert, besteht darin, die ersten vier Ziffern Ihrer Suchzeichenfolge zu verwenden, um den Tabellennamen und die nächsten zwei Ziffern als Partition zu finden. Wenn Sie erneut "012345678901" verwenden, gelangen Sie zur Partition 45 der Tabelle, die Daten für "0123" enthält. 100 Partitionen sind eine gute Zahl, um Probleme zu vermeiden, die durch zu viele Partitionen verursacht werden, und Sie erhalten durchschnittlich 1 Million Zeilen für jede Partition. Die maximale Anzahl von Zeilen, die in eine einzelne Zeilengruppe passen, beträgt 1048576, sodass Sie mit diesem Ansatz so wenig E / A wie möglich ausführen.

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

Bei diesem Ansatz würde der vollständige Datensatz etwa 10,9 TB erfordern. Mir ist nicht klar, wie ich das kleiner machen soll. Die Suchabfrage ist in diesem Fall etwas langsamer. Auf meinem Computer dauert es ungefähr 25 ms, dies hängt jedoch hauptsächlich von der E / A ab:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Ein wichtiger Hinweis zum Columnstore-Ansatz ist, dass die 10,9-TB-Zahl für 100% komprimierte Daten gilt. Es wird schwierig sein, eine solche Tabelle effizient zu füllen und gleichzeitig die Delta-Speicher zu meiden. Es ist wahrscheinlich, dass Sie zu einem bestimmten Zeitpunkt unkomprimierte Daten in Delta-Speichern haben, für die möglicherweise mehr als 15,4 TB für den Rowstore-Ansatz erforderlich sind.


6

Das Speichern und Verarbeiten von 1 TB Daten mit nur 16 GB RAM kann sich als Herausforderung erweisen. 1 GB pro Kern ist ziemlich unausgeglichen, insbesondere für diese Art von Arbeitslast. 8 GB pro Kern wären ein viel besserer und wünschenswerter Ausgangspunkt.

Trotzdem wäre ich immer noch versucht, eine Variante von Methode 2 auszuprobieren:

Speichern Sie alle möglichen bigintTeilzeichenfolgen mit 12 Zeichen wie in einer Clustered-Columnstore-Tabelle (mit Archivkomprimierung, falls sich dies als nützlich herausstellt):

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

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

Sie müssen wahrscheinlich eine Methode implementieren, um die Quelldaten schrittweise in diese Tabelle zu laden. Stellen Sie sicher, dass Sie in der fertigen Spaltenspeicherstruktur Zeilengruppen mit maximaler Größe (1.048.576 Zeilen) haben . Siehe die Anleitung zum Laden von Daten .

Sie können Zeilen in Vielfachen von 1048576 in einer nicht indizierten Rowstore-Tabelle bereitstellen, bevor Sie einen Clustered Columnstore-Index dafür erstellen und das Ergebnis dann direkt in eine partitionierte Haupttabelle umwandeln. Der genaue Ansatz hängt davon ab, wie Sie die Daten laden möchten, ob sie hinzugefügt werden und wie gut Sie mit SQL Server im Allgemeinen vertraut sind.

Mit dieser Methode ist eine sehr gute Leistung möglich, aber wie so oft mit Columnstore müssten Sie eine effektive Partitions- und Segmenteliminierung erreichen. Das Partitionieren in der fragmentSpalte und das serielle Erstellen des Columnstore-Index beim Ersetzen eines Rowedore-Clustered-Index mit Schlüssel fragmentist der Weg, um dies zu erreichen, wie in der oben verlinkten Dokumentation angegeben. Dies minimiert auch den Speicherbedarf, da fragmentWerte im gleichen Bereich im gleichen Segment gespeichert werden. Dies ermöglicht eine effektive Wertumbasierung und Bitpackung.

Versuchen Sie beim Laden, die Zeichenfolgen, mit denen Sie in SQL Server arbeiten, auf Nicht-LOB-Typen (max) zu beschränken. Wenn Sie die Arbeit mit LOBs für den Durchsatz am besten finden, ist häufig ein Sweet Spot der Datenlänge zu finden, oberhalb dessen die Leistung erheblich abnimmt.

Abhängig von der endgültigen Größe der Struktur und der Geschwindigkeit Ihres E / A-Subsystems stellen Sie möglicherweise fest, dass dieser Ansatz eine konstant gute Leistung bietet. Das Erhöhen des verfügbaren Speichers würde die Dinge deutlich verbessern.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.