J'ai deux tableaux:
Tableau 1: 100 lignes
Tableau 2:10 millions de lignes
Exemple:
Tableau 1: tb100
;with splitdata as ( select f.item as data from tb100 t cross apply dbo.udf_split(t.name,' ') f ) select t2.name from tb10mil t2 inner join splitdata c on charindex(c.data,t2.name)>0 group by t2.name
Tableau 2: tb10mil
CREATE FUNCTION [dbo].[udf_Split] ( @InputString VARCHAR(8000), @Delimiter VARCHAR(50) ) RETURNS @Items TABLE (ID INTEGER IDENTITY(1,1), Item VARCHAR(8000)) AS BEGIN IF @Delimiter = ' ' BEGIN SET @Delimiter = ',' SET @InputString = REPLACE(@InputString, ' ', @Delimiter) END IF (@Delimiter IS NULL OR @Delimiter = '') SET @Delimiter = ',' DECLARE @Item VARCHAR(8000) DECLARE @ItemList VARCHAR(8000) DECLARE @DelimIndex INT SET @ItemList = @InputString SET @DelimIndex = CHARINDEX(@Delimiter, @ItemList, 0) WHILE (@DelimIndex != 0) BEGIN SET @Item = SUBSTRING(@ItemList, 0, @DelimIndex) INSERT INTO @Items VALUES (@Item) SET @ItemList = SUBSTRING(@ItemList, @DelimIndex+1, LEN(@ItemList)-@DelimIndex) SET @DelimIndex = CHARINDEX(@Delimiter, @ItemList, 0) END IF @Item IS NOT NULL BEGIN SET @Item = @ItemList INSERT INTO @Items VALUES (@Item) END ELSE INSERT INTO @Items VALUES (@InputString) RETURN END
Remarque: I voulez faire correspondre le nom entre deux tables, le cas échéant WORD (John, Smith, Will) présent dans une autre table. Par exemple John
présent dans John A Mark
.
Mon essai:
J'ai d'abord créé une fonction définie par l'utilisateur pour diviser le nom
de tb100
en lignes.
Fonction: udf_Split
create table tb10mil ( name varchar(50) ); insert into tb10mil values('John A Mak'),('K Smith Will'),('James Henry')......10 millions rows. create nonclustered index nci_tb10mil_name on tb10mil(name);
Et puis j'ai écrit la requête suivante:
create table tb100 ( name varchar(50) ); insert into tb100 values('Mak John'),('Will Smith'),('Luke W')......100 rows.
La requête ci-dessus prend plus de 20 minutes à s'exécuter.
3 Réponses :
Vous pouvez essayer comme suit.
;WITH splitdata AS (SELECT splitname FROM (SELECT *, Cast('<X>' + Replace(F.Name, ' ', '</X><X>') + '</X>' AS XML) AS xmlfilter FROM tb100 F)F1 CROSS apply (SELECT fdata.d.value('.', 'varchar(50)') AS splitName FROM f1.xmlfilter.nodes('X') AS fdata(d)) O) SELECT DISTINCT t2.NAME FROM tb10mil t2 INNER JOIN splitdata S ON T2.NAME LIKE '%' + S.splitname + '%'
Je doute vraiment qu'un JOIN par LIKE sur un motif avec un %
en tête soit rapide avec des millions de lignes ... Et juste pour mentionner ceci: Si les noms contiennent des caractères interdits (comme <,> ou &
et bien d'autres), cela peut casser sur CAST ()
. Et un autre indice sur les performances: GROUP BY
est beaucoup plus rapide que DISTINCT
.
LIKE
n'a rien à voir avec les performances dans cette réponse. C'est la méthode différente de fractionnement de chaînes qui a amélioré les performances. Ce qui sera toujours horrible par rapport à l'utilisation de recherches appropriées lors du chargement des données
Parlons de la performance
Le premier point est: essayez d'éviter scalaires et essayez d'éviter les multi-instructions-TVF si possible. Le seul l'approche rapide est une TVF en ligne (une déclaration en une ligne ).
Deuxième point: évitez les boucles si possible!
Le troisième point ( le tout premier en fait ): essayez de stocker vos données dans un format optimisé pour les requêtes rapides. Stockage de plus d'une valeur dans une cellule viole 1NF et un énorme tueur de vitesse.
Vous pouvez essayer ceci:
Simulez vos tables
DROP TABLE #t100; GO DROP TABLE #t1M; GO DROP TABLE #t100Splitted; GO DROP TABLE #t1MSplitted; GO
--Créez des tables pour stocker les fragments de nom de manière spear (c'est le format que vous devriez utiliser à la place)
SELECT t1.ID_t1M FROM #t1MSplitted t1 INNER JOIN #t100Splitted t2 ON t1.Fragment=t2.Fragment GROUP BY t1.ID_t1M GO
- Cette requête renverra toutes les lignes avec des fragments communs
- Vous pouvez rejoindre à nouveau vos tables source pour récupérer les valeurs
- Vous pouvez grouper par t2.ID_t100 afin d'obtenir les identifiants de la plus petite table (beaucoup plus rapide)
CREATE TABLE #t100Splitted(ID INT IDENTITY PRIMARY KEY,ID_t100 INT,Fragment NVARCHAR(200)); --Use an inline XML-splitter INSERT INTO #t100Splitted(ID_t100,Fragment) SELECT ID ,B.frg.value('text()[1]','nvarchar(200)') FROM #t100 CROSS APPLY(SELECT CAST('<x>' + REPLACE((SELECT SomeName AS [*] FOR XML PATH('')),' ','</x><x>') + '</x>' AS XML)) A(CastedToXml) CROSS APPLY A.CastedToXml.nodes('/x[text()]') B(frg); --add indexes CREATE INDEX IX_t100_ID_t100 ON #t100Splitted(ID_t100); CREATE INDEX IX_t100_Fragment ON #t100Splitted(Fragment); --The same for the second table CREATE TABLE #t1MSplitted(ID INT IDENTITY PRIMARY KEY,ID_t1M INT,Fragment NVARCHAR(200)); INSERT INTO #t1MSplitted(ID_t1M,Fragment) SELECT ID ,B.frg.value('text()[1]','nvarchar(200)') FROM #t1M CROSS APPLY(SELECT CAST('<x>' + REPLACE((SELECT SomeName AS [*] FOR XML PATH('')),' ','</x><x>') + '</x>' AS XML)) A(CastedToXml) CROSS APPLY A.CastedToXml.nodes('/x[text()]') B(frg); CREATE INDEX IX_tM_ID_t100 ON #t1MSplitted(ID_t1M); CREATE INDEX IX_tM_Fragment ON #t1MSplitted(Fragment); GO - Check the intermediate results SELECT * FROM #t100Splitted; SELECT * FROM #t1MSplitted; GO
--Nettoyer
CREATE TABLE #t100(ID INT IDENTITY,SomeName VARCHAR(200)); CREATE TABLE #t1M (ID INT IDENTITY,SomeName VARCHAR(200)); INSERT INTO #t100 VALUES('james smith'),('mak john'),('Luke W'); GO INSERT INTO #t1M values('John A Mak'),('K Smith Will'),('James Henry'),('James John'),('Some other'); GO
Sur mon système, 1 million de lignes ont été traitées en 2 minutes environ.
(avec de très nombreux appels)
Juste le fractionnement , pour transformer vos données en une meilleure forme: ~ 17 minutes
La sélection finale (juste la recherche):
La sélection finale mais recherche des ID de la petite table: quelques secondes
(une fois que vous aurez transformé vos données, ce sera la vitesse normale )
Approche du PSK (avec fractionnement en ligne): ~ 30 minutes
J'ai essayé de SAUVEGARDER LA MÉMOIRE et ainsi gagner du temps pour gérer la mémoire en évitant les jointures.
J'ai essayé de simuler votre problème sur des enregistrements ~ 420k
avec des valeurs 4
dans un tableau plus petit.
L'approche consiste à éviter les jointures et à limiter les problèmes de mémoire espace de mxn
à au moins plus grand de m & n.
select DISTINCT t2.name from tb10mil t2 where (SELECT TOP(1) 1 FROM #splitdata where CHARINDEX(data,t2.Problem)>0)=1
RÉSULTAT: la moitié du temps nécessaire à l'approche utilisé en question.
(réduit de ~ 28 s à ~ 14 s)
CON: L'approche n'est bénéfique que si l'un des tableaux est considérablement petit
Pourquoi utilisez-vous une boucle
WHILE
pour votre séparateur? Il existe de nombreuses solutions de jeux de données qui seront beaucoup plus rapides. Je suggérerais d'utiliser un séparateur XML ou delimitedsplit8k (si ce n'est pas unnvarchar (MAX)
). Ce séparateur est très probablement votre (premier) problème.Tout d'abord, SQL n'est pas un langage de traitement de texte, vous ne pouvez donc pas vous attendre à de bonnes performances de fractionnement de texte. Deuxièmement, sans index, vous ne pouvez pas améliorer les performances et cette fonction ne peut pas utiliser d'index. Enfin, SQL Server dispose déjà de capacités de recherche de texte intégral et de correspondance et de recherches floues dans SSIS. Si vous souhaitez faire correspondre les noms de manière floue lors du chargement de 10 millions de lignes, utilisez un opérateur de recherche floue dans SSIS
Si vous souhaitez faire correspondre par exemple une table de ventes avec un ensemble d'enregistrements clients par nom, faites-le lors du chargement des données et stockez uniquement les ID réels dans la table cible. N'essayez pas de répéter la recherche floue chaque fois que vous souhaitez rejoindre les tables. Quelle que soit la technique que vous utilisez, une correspondance floue sera toujours plus lente qu'une jointure sur deux colonnes indexées
Si cette requête est fréquemment utilisée, il est préférable de conserver une table de mappage à partir d'un déclencheur AFTER sur les tables principales et de conserver les données fractionnées en tant que valeurs de colonne de table. Les jointures peuvent alors être construites sur cette nouvelle table de mappage
Pour diviser une chaîne à l'aide de la méthode SQL FOR XML PATH, la fonction UDF suivante peut être une alternative kodyaz.com/articles/...