Le moyen le plus rapide de diviser / stocker une longue chaîne pour la fonction charindex

8

J'ai une chaîne de chiffres de 1 To. Étant donné une séquence de 12 caractères de chiffres, je veux obtenir la position de départ de cette séquence dans la chaîne d'origine ( charindexfonction).

J'ai testé cela avec une chaîne de 1 Go et une sous-chaîne à 9 chiffres à l'aide de SQL Server, stockant la chaîne en tant que varchar(max). Charindexprend 10 secondes. Briser la chaîne de 1 Go en blocs de 900 octets qui se chevauchent et créer une table (StartPositionOfChunk, Chunkofstring) avec chunkofstring en classement binaire, l'indexation prend moins de 1 seconde. La dernière méthode pour 10 Go, sous-chaîne à 10 chiffres augmente le charindex à 1,5 min. Je voudrais trouver une méthode de stockage plus rapide.

Exemple

chaîne de chiffres: 0123456789 - sous-chaîne pour rechercher 345
charindex ('345', '0123456789') donne 4

Méthode 1 : je peux maintenant stocker cela dans une table de table SQL Server composée d'une colonne colstret effectuer:

select charindex('345',colstr) from strtable

Méthode 2 : ou je peux créer une table strtable2 (pos, colstr1) en divisant la chaîne d'origine: 1; 012 | 2, 123 | 3; 234 et puis nous pouvons avoir la requête

select pos from strtable2 where colstr1='345'

Méthode 3 : je peux créer une table strtable2 (pos2, colstr2) en divisant la chaîne d'origine en morceaux plus grands 1; 01234 | 4; 34567 | 7; 6789 puis

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

La première méthode est la plus lente.

La deuxième méthode fait exploser la taille de stockage de la base de données!

Méthode 3 : définir la longueur de colstr2 sur 900 octets dans le classement binaire, la création d'un index sur cette colonne prend 1 seconde pour une chaîne de 1 Go et une recherche de sous-chaîne à 9 chiffres. Pour une chaîne de 10 Go et une sous-chaîne à 10 chiffres, cela prend 90 secondes.

Une autre idée sur la façon de rendre cela plus rapide (peut-être en utilisant la chaîne composée de chiffres, avec des entiers longs, ....)?

La recherche consiste toujours à rechercher une sous-chaîne à 12 chiffres dans une chaîne de 1 To de chiffres SQL Server 2017 Developer Edition, 16 cœurs, 16 Go de RAM. L'objectif principal est la vitesse de recherche! 10 chiffres dans une chaîne de 10 Go (pour les tests de performances).

Werner Aumayr
la source

Réponses:

6

Je recommande d'utiliser une version de la méthode 2 et de diviser la plage de recherche en plusieurs tables cibles. 10000 tables est une bonne première tentative. Par exemple, si vous recherchez «012345678901», votre requête examinera la table associée aux données commençant par «0123». Vous auriez toujours environ un billion de lignes au total, mais la division des données en plusieurs tableaux présente les qualités positives suivantes:

  1. Toutes les chaînes de 12 chiffres possibles peuvent désormais tenir dans un INT.
  2. Construire une représentation plus efficace de la recherche de votre chaîne de 1 To va probablement coûter cher quoi qu'il arrive. Avec de nombreuses tables, vous pouvez facilement paralléliser le travail et même demander temporairement plus de CPU à allouer à votre VM.
  3. Vous pouvez créer une seule table comme preuve de concept pour déterminer le temps de requête et les exigences d'espace total pour la chaîne complète.
  4. Si jamais vous avez besoin de faire n'importe quel type de maintenance de base de données, vous serez heureux de ne pas avoir créé une énorme table.

À ce stade, la question principale pour moi est de savoir si vous utilisez un magasin de lignes compressé ou un magasin de colonnes. Le code ci-dessous crée une table de magasin de lignes pour l'espace de recherche "0123" et y insère 100 millions de lignes. Si votre chaîne est suffisamment aléatoire, vous pouvez également vous attendre à voir environ 100 millions de lignes par table.

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 mauvaise nouvelle concerne l'ensemble de données dont vous auriez probablement besoin d'environ 15,4 To. La bonne nouvelle est que les requêtes ne prennent que 1 ms pour moi même lorsqu'aucune donnée pertinente ne se trouve dans le cache de tampon, ce qui sera presque toujours le cas pour un ensemble de données aussi volumineux que le vôtre.

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Vous pouvez probablement jeter ces données sur le stockage le moins cher que vous avez et toujours voir de bons temps de réponse car la requête effectue si peu de lectures logiques.

Pour columnstore, vous ne pouvez pas rechercher les données dont vous avez besoin et vous avez encore très peu de chances de mettre toutes vos données en mémoire, il est donc important de lire le moins de données compressées possible avec vos requêtes. Je recommande fortement de partitionner vos tables. Une approche simple qui fonctionne bien consiste à utiliser les quatre premiers chiffres de votre chaîne de recherche pour trouver le nom de la table et les deux chiffres suivants comme partition. En utilisant à nouveau "012345678901", vous iriez à la partition 45 de la table qui contient les données pour "0123". 100 partitions est un bon nombre pour éviter les problèmes causés par trop de partitions et vous vous retrouverez avec environ 1 million de lignes pour chaque partition en moyenne. Le nombre maximum de lignes pouvant tenir dans un seul groupe de lignes est de 1048576, donc avec cette approche, vous ferez le moins d'E / S possible.

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

Avec cette approche, l'ensemble de données complet nécessiterait environ 10,9 To. Je ne sais pas comment réduire cela. La requête de recherche est un peu plus lente dans ce cas. Sur ma machine, cela prend environ 25 ms, mais cela dépendra principalement des IO:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

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

Une remarque importante sur l'approche columnstore est que le chiffre de 10,9 To correspond à des données 100% compressées. Il sera difficile de remplir efficacement une telle table tout en évitant les magasins delta. Il est probable que vous vous retrouviez avec des données non compressées dans les magasins delta à un moment donné du processus, ce qui pourrait facilement nécessiter plus que les 15,4 To utilisés pour l'approche Rowstore.

Joe Obbish
la source
6

Le stockage et le traitement de 1 To de données avec seulement 16 Go de RAM disponible peuvent s'avérer être un défi. 1 Go par cœur est plutôt déséquilibré, en particulier pour ce type de charge de travail. 8 Go par cœur serait un bien meilleur point de départ, plus souhaitable.

Cela dit, je serais toujours tenté d'essayer une variante de la méthode 2:

Stockez toutes les sous-chaînes possibles de 12 caractères comme bigintdans une table columnstore en cluster (avec compression d'archive si cela s'avère utile):

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

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

Vous devrez probablement implémenter un moyen de chargement incrémentiel des données source dans ce tableau. Assurez-vous de vous retrouver avec des groupes de lignes de taille maximale (1 048 576 lignes) dans la structure de magasin de colonnes terminée . Voir les instructions de chargement des données .

Vous pouvez organiser des lignes par multiples de 1048576 dans une table de stockage en ligne non indexée avant de créer un index columnstore en cluster sur celui-ci, puis basculer le résultat directement dans une table principale partitionnée. L'approche exacte dépend de la façon dont vous comptez charger les données, de leur ajout et de votre familiarité avec SQL Server en général.

De très bonnes performances sont possibles avec cette méthode, mais comme c'est souvent le cas avec columnstore, vous devez obtenir une élimination efficace des partitions et des segments. Le partitionnement sur la fragmentcolonne et la construction de l'index columnstore en série tout en remplaçant un index clusterisé rowstore à clé fragmentest le moyen d'y parvenir, comme indiqué dans la documentation liée ci-dessus. Cela minimisera également les besoins de stockage, car les fragmentvaleurs de la même plage seront stockées dans le même segment. Cela permet un rebasage de valeur efficace et un compactage des bits.

Lors du chargement, essayez de limiter les chaînes avec lesquelles vous travaillez dans SQL Server aux types non LOB (max). Si vous trouvez que travailler avec les LOB est le meilleur pour le débit, il y a souvent un point faible de longueur de données à trouver, au-dessus duquel les performances diminuent considérablement.

En fonction de la taille finale de la structure et de la vitesse de votre sous-système d'E / S, vous pouvez constater que cette approche offre constamment de bonnes performances. Augmenter la mémoire disponible améliorerait considérablement les choses.

Paul White 9
la source