Nous effectuons de nombreuses recherches de produits sur un vaste catalogue de codes à barres partiellement appariés.
Nous avons commencé avec une requête simple comme
select * from products where barcode like '%2345%'
Mais cela prend beaucoup trop de temps, car cela nécessite un balayage complet de la table. Nous pensions qu'une recherche en texte intégral pourrait nous aider à utiliser ici le contenu.
select * from products where contains(barcode, '2345')
Mais il semble que contient ne prend pas en charge la recherche de mots contenant partiellement un texte, mais uniquement une correspondance complète Word ou un préfixe. (Mais dans cet exemple, nous recherchons '123456').
Ma réponse est: @DenisReznik avait raison :)
ok, jetons un coup d'oeil.
Je travaille avec des codes à barres et des catalogues volumineux depuis de nombreuses années et cette question me passionnait.
J'ai donc fait quelques tests moi-même.
J'ai créé une table pour stocker les données de test:
CREATE TABLE [like_test](
[N] [int] NOT NULL PRIMARY KEY,
[barcode] [varchar](40) NULL
)
Je sais qu'il existe de nombreux types de codes à barres, certains contenant uniquement des chiffres, d'autres contenant également des lettres et d'autres pouvant même être beaucoup plus complexes.
Supposons que notre code à barres est une chaîne aléatoire.
Je l’ai rempli avec 10 millions d’enregistrements de données alfanumériques aléatoires:
insert into like_test
select (select count(*) from like_test)+n, REPLACE(convert(varchar(40), NEWID()), '-', '') barcode
from FN_NUMBERS(10000000)
FN_NUMBERS () est juste une fonction que j'utilise dans mes bases de données (une sorte de tally_table) Pour obtenir rapidement des enregistrements.
J'ai 10 millions de disques comme ça:
N barcode
1 1C333262C2D74E11B688281636FAF0FB
2 3680E11436FC4CBA826E684C0E96E365
3 7763D29BD09F48C58232C7D33551E6C9
Déclarons une variable à rechercher:
declare @s varchar(20) = 'D34F15' -- random alfanumeric string
Essayons avecCOMMEpour comparer les résultats avec:
select * from like_test where barcode like '%'+@s+'%'
Sur mon poste de travail, il faut 24,4 secondes pour une analyse d'index en cluster complète. Très lent.
SSMS suggère d'ajouter un index sur la colonne de code à barres:
CREATE NONCLUSTERED INDEX [ix_barcode] ON [like_test] ([barcode]) INCLUDE ([N])
500Mo d'index, je réessaie la sélection, cette fois-ci 24 secondes pour la recherche d'index non-cluster .. moins de 2% de mieux, presque le même résultat. Très loin des 75% supposés par SSMS. Il me semble que cet indice n'en vaut vraiment pas la peine. Peut-être que mon SSD Samsung 840 fait la différence ..
Pour le moment, je laisse l’index actif.
Essayons la solutionCHARINDEX:
select * from like_test where charindex(@s, barcode) > 0
Cette fois, il a fallu 23,5 secondes pour terminer, pas tellement mieux que LIKE.
Examinons maintenant la suggestion de @DenisReznik selon laquelle utiliser le Binary Collation devrait accélérer les choses.
select * from like_test
where barcode collate Latin1_General_BIN like '%'+@s+'%' collate Latin1_General_BIN
WOW, ça a l'air de marcher! Seulement 4,5 secondes c'est impressionnant! 5 fois mieux ..
Alors, qu'en est-il de CHARINDEX et de Collation? Essayons:
select * from like_test
where charindex(@s collate Latin1_General_BIN, barcode collate Latin1_General_BIN)>0
Incroyable! 2,4 secondes, 10 fois mieux ..
Ok, jusqu’à présent, j’ai réalisé que CHARINDEX est meilleur que LIKE, et que le classement binaire est meilleur que le classement normal des chaînes, donc je ne continuerai plus que avec CHARINDEX et Collation.
Pouvons-nous faire autre chose pour obtenir des résultats encore meilleurs? Peut-être que nous pouvons essayer de réduire nos très longues chaînes .. un scan est toujours un scan ..
Tout d’abord, une chaîne logique utilisant SUBSTRING permet de travailler virtuellement sur des codes à barres de 8 caractères:
select * from like_test
where charindex(
@s collate Latin1_General_BIN,
SUBSTRING(barcode, 12, 8) collate Latin1_General_BIN
)>0
Fantastique! 1,8 secondes .. J'ai essayé à la fois SUBSTRING(barcode, 1, 8)
(tête de la chaîne) et SUBSTRING(barcode, 12, 8)
(milieu de la chaîne) avec les mêmes résultats.
Ensuite, j'ai essayé de réduire physiquement la taille de la colonne de codes à barres, presque pas de différence que d'utiliser SUBSTRING ()
Enfin, j'ai essayé de supprimer l'index de la colonne de code à barres et de répéter TOUS les tests ci-dessus ... J'ai été très surpris d'obtenir des résultats presque identiques, avec très peu de différences.
Index réalise une performance de 3 à 5% supérieure, mais au coût de 500 Mo d’espace disque et de maintenance si le catalogue est mis à jour.
Naturellement, pour une recherche de clé directe comme where barcode = @s
avec l'index, il faut 20-50 millisecs, sans index, nous ne pouvons pas obtenir moins de 1,1 seconde en utilisant la syntaxe de collation where barcode collate Latin1_General_BIN = @s collate Latin1_General_BIN
C'était intéressant.
J'espère que ça aide
J'utilise souvent charindex et ai tout aussi souvent ce débat.
Il se peut que votre performance augmente considérablement en fonction de votre structure.
http://cc.davelozinski.com/sql/like-vs-substring-vs-leftright-vs-charindex
La bonne option ici pour votre cas - créer votre index FTS. Voici comment cela pourrait être implémenté:
1) Créer des termes de table:
CREATE TABLE Terms
(
Id int IDENTITY NOT NULL,
Term varchar(21) NOT NULL,
CONSTRAINT PK_TERMS PRIMARY KEY (Term),
CONSTRAINT UK_TERMS_ID UNIQUE (Id)
)
Remarque: la déclaration d'index dans la définition de table est une fonctionnalité de 2014. Si vous avez une version antérieure, extrayez-la simplement de l'instruction CREATE TABLE et créez-la séparément.
2) Découpez les codes-barres en grammes et sauvegardez-les dans un tableau Termes. Par exemple: code à barres = "123456", votre tableau doit comporter 6 lignes: "123456", "23456", "3456", "456", "56", "6".
3) Créer la table BarcodeIndex:
CREATE TABLE BarcodesIndex
(
TermId int NOT NULL,
BarcodeId int NOT NULL,
CONSTRAINT PK_BARCODESINDEX PRIMARY KEY (TermId, BarcodeId),
CONSTRAINT FK_BARCODESINDEX_TERMID FOREIGN KEY (TermId) REFERENCES Terms (Id),
CONSTRAINT FK_BARCODESINDEX_BARCODEID FOREIGN KEY (BarcodeId) REFERENCES Barcodes (Id)
)
4) Enregistrez une paire (TermId, BarcodeId) pour le code à barres dans la table BarcodeIndex. TermId a été généré à la deuxième étape ou existe dans la table Terms. BarcodeId - est un identifiant du code à barres, stocké dans la table des codes à barres (ou le nom de votre choix). Pour chaque code à barres, il doit y avoir 6 lignes dans la table BarcodeIndex.
5) Sélectionnez les codes à barres par leurs parties en utilisant la requête suivante:
SELECT b.* FROM Terms t
INNER JOIN BarcodesIndex bi
ON t.Id = bi.TermId
INNER JOIN Barcodes b
ON bi.BarcodeId = b.Id
WHERE t.Term LIKE 'SomeBarcodePart%'
Cette solution force le stockage à proximité de toutes les parties similaires des codes à barres. SQL Server utilisera donc la stratégie Analyse par plage d'index pour extraire les données de la table Terms. Les termes du tableau des termes doivent être uniques pour que ce tableau soit aussi petit que possible. Cela pourrait être fait dans la logique d'application: vérifier l'existence -> insérer un nouveau si un terme n'existe pas. Ou en définissant l'option IGNORE_DUP_KEY pour l'index clusterisé de la table Terms. La table BarcodesIndex est utilisée pour référencer les termes et les codes à barres.
Veuillez noter que les clés étrangères et les contraintes de cette solution sont les points à prendre en compte. Personnellement, je préfère avoir des clés étrangères, jusqu'à ce qu'elles me fassent mal.
Après des tests supplémentaires, après avoir lu et discuté avec @DenisReznik, je pense que la meilleure option pourrait être d’ajouter des colonnes virtuelles à la table de codes à barres pour scinder le code à barres.
Nous n’avons besoin que de colonnes pour les positions de départ du 2e au 4e car nous utiliserons la colonne de code à barres originale et la dernière, ce qui n’est pas du tout utile (quel type de correspondance partielle correspond à 1 caractère sur 6 lorsque 60% des enregistrements correspondent ?):
CREATE TABLE [like_test](
[N] [int] NOT NULL PRIMARY KEY,
[barcode] [varchar](6) NOT NULL,
[BC2] AS (substring([BARCODE],(2),(5))),
[BC3] AS (substring([BARCODE],(3),(4))),
[BC4] AS (substring([BARCODE],(4),(3))),
[BC5] AS (substring([BARCODE],(5),(2)))
)
puis d'ajouter des index sur ces colonnes virtuelles:
CREATE NONCLUSTERED INDEX [IX_BC2] ON [like_test2] ([BC2]);
CREATE NONCLUSTERED INDEX [IX_BC3] ON [like_test2] ([BC3]);
CREATE NONCLUSTERED INDEX [IX_BC4] ON [like_test2] ([BC4]);
CREATE NONCLUSTERED INDEX [IX_BC5] ON [like_test2] ([BC5]);
CREATE NONCLUSTERED INDEX [IX_BC6] ON [like_test2] ([barcode]);
maintenant, nous pouvons simplement trouver des correspondances partielles avec cette requête
declare @s varchar(40)
declare @l int
set @s = '654'
set @l = LEN(@s)
select N from like_test
where 1=0
OR ((barcode = @s) and (@l=6)) -- to match full code (rem if not needed)
OR ((barcode like @s+'%') and (@l<6)) -- to match strings up to 5 chars from beginning
or ((BC2 like @s+'%') and (@l<6)) -- to match strings up to 5 chars from 2nd position
or ((BC3 like @s+'%') and (@l<5)) -- to match strings up to 4 chars from 3rd position
or ((BC4 like @s+'%') and (@l<4)) -- to match strings up to 3 chars from 4th position
or ((BC5 like @s+'%') and (@l<3)) -- to match strings up to 2 chars from 5th position
c'estL'ENFERvite!
Il n'y aura pas d'espace supplémentaire utilisé pour la table, mais chaque index prendra jusqu'à 200 Mo (pour 1 million de codes à barres)
FAITES ATTENTION
Testé sur Microsoft SQL Server Express (64 bits) et Microsoft SQL Server Enterprise (64 bits), l'optimiseur de ce dernier est légèrement meilleur, mais la différence principale est que:
dans l'édition Express, vous devez extraireUNIQUEMENTla clé primaire lors de la recherche de votre chaîne. Si vous ajoutez d'autres colonnes dans la commande SELECT, l'optimiseur n'utilisera plus d'index, mais utilisera l'analyse complète des index en cluster pour besoin de quelque chose comme
;with
k as (-- extract only primary key
select N from like_test
where 1=0
OR ((barcode = @s) and (@l=6))
OR ((barcode like @s+'%') and (@l<6))
or ((BC2 like @s+'%') and (@l<6))
or ((BC3 like @s+'%') and (@l<5))
or ((BC4 like @s+'%') and (@l<4))
or ((BC5 like @s+'%') and (@l<3))
)
select N
from like_test t
where exists (select 1 from k where k.n = t.n)
sur l'édition standard (entreprise), vousAVEZà choisir
select * from like_test -- take a look at the star
where 1=0
OR ((barcode = @s) and (@l=6))
OR ((barcode like @s+'%') and (@l<6))
or ((BC2 like @s+'%') and (@l<6))
or ((BC3 like @s+'%') and (@l<5))
or ((BC4 like @s+'%') and (@l<4))
or ((BC5 like @s+'%') and (@l<3))
MIS À JOUR:
Nous savons que les recherches FULL-TEXT peuvent être utilisées pour:
Recherche en texte intégral - MSDN
- Un ou plusieurs mots ou expressions spécifiques (terme simple)
- Un mot ou une phrase dont les mots commencent par le texte spécifié (préfixe terme)
- Formes flexionnelles d'un mot spécifique (terme de génération)
- Un mot ou une expression proche d'un autre mot ou expression (terme de proximité)
- Formes synonymes d'un mot spécifique (thésaurus)
- Mots ou expressions utilisant des valeurs pondérées (terme pondéré)
Est-ce que certains de ceux-ci sont satisfaits par vos exigences de requête? Si vous devez rechercher des modèles comme vous l'avez décrit, sans modèle cohérent (tel que '1%'), il se peut que SQL ne puisse pas utiliser une variable SARG
.
Boolean
Venant d'une perspective C++
, B-Trees
sont accessibles à partir de traversals
en précommande, en commande et après commande et utilisent des instructions Boolean
pour rechercher le B-Tree
. Traités beaucoup plus rapidement que les comparaisons de chaînes, les booléens offrent au moins une performance améliorée.
Nous pouvons le voir dans les deux options suivantes:
PATINDEX
CHARINDEX est une solution
CHARINDEX('200', barcode) > 1
.Preuve de ce que je dis, revenons à l’ancien [AdventureWorks2012].[Production].[TransactionHistory]
. Nous avons TransactionID qui contient le nombre d’éléments que nous voulons et laisse supposer que vous voulez que chaque transactionID compte 200 à la fin.
-- WITH LIKE
SELECT TOP 1000 [TransactionID]
,[ProductID]
,[ReferenceOrderID]
,[ReferenceOrderLineID]
,[TransactionDate]
,[TransactionType]
,[Quantity]
,[ActualCost]
,[ModifiedDate]
FROM [AdventureWorks2012].[Production].[TransactionHistory]
WHERE TransactionID LIKE '%200'
-- WITH CHARINDEX(<delimiter>, <column>) > 3
SELECT TOP 1000 [TransactionID]
,[ProductID]
,[ReferenceOrderID]
,[ReferenceOrderLineID]
,[TransactionDate]
,[TransactionType]
,[Quantity]
,[ActualCost]
,[ModifiedDate]
FROM [AdventureWorks2012].[Production].[TransactionHistory]
WHERE CHARINDEX('200', TransactionID) > 3
Remarque CHARINDEX supprime la valeur 200200 dans la recherche, vous devrez donc peut-être ajuster votre code correctement. Mais regardez les résultats:
J'ai été un peu surpris de l'ampleur de la différence, mais les fondamentaux sont les mêmes. Les instructions Integers
et Boolean
sont toujours plus rapides à traiter que les comparaisons de chaînes.
Vous n'incluez pas beaucoup de contraintes, ce qui signifie que vous souhaitez rechercher une chaîne dans une chaîne - et s'il existait un moyen d'optimiser un index pour rechercher une chaîne dans une chaîne, il serait simplement intégré!
D'autres choses qui rendent difficile de donner une réponse spécifique:
Ce n'est pas clair ce que "énorme" et "trop long" veulent dire.
On ne sait pas comment votre application fonctionne. Recherchez-vous par lot lorsque vous ajoutez 1 000 nouveaux produits? Autorisez-vous un utilisateur à entrer un code à barres partiel dans une zone de recherche?
Je peux faire quelques suggestions qui peuvent ou peuvent ne pas être utiles dans votre cas.
Accélérer certaines des requêtes
J'ai une base de données avec beaucoup de plaques d'immatriculation; Parfois, un officier veut chercher parmi les 3 derniers caractères de la plaque. Pour supporter cela, je range la plaque d'immatriculation à l'envers, puis utilise LIKE ('ZYX%')
pour faire correspondre ABCXYZ
. Lorsqu'ils font la recherche, ils ont l'option d'une recherche "contient" (comme vous) qui est lente, ou l'option de faire "Commence/termine par" qui est super à cause de l'index. Cela résoudrait parfois votre problème (ce qui peut être suffisant), surtout s'il s'agit d'un besoin courant.
Requêtes parallèles
Un index fonctionne parce qu'il organise les données, un index ne peut pas aider avec une chaîne dans une chaîne car il n'y a pas d'organisation. La vitesse semble être votre objectif d'optimisation, vous pouvez donc stocker/interroger vos données de manière à effectuer des recherches en parallèle. Exemple: s'il faut 10 secondes pour rechercher séquentiellement 10 millions de lignes, le fait d'avoir 10 processus parallèles (la recherche de processus 1 million) vous prendra de 10 secondes à 1 seconde (kind'a-sort'a) . Pensez-y comme une mise à l'échelle. Il existe différentes options pour cela, dans votre seule instance SQL (essayez le partitionnement des données) ou sur plusieurs serveurs SQL (si c'est une option).
BONUS: Si vous n'êtes pas sur une configuration RAID, cela peut aider avec les lectures puisqu'il s'agit d'une lecture en parallèle.
Réduire un goulot d'étranglement
Une des raisons pour lesquelles chercher "d'énormes" ensembles de données prend "trop longtemps" est parce que toutes ces données doivent être lues à partir du disque, ce qui est toujours lent. Vous pouvez sauter le disque et utiliser InMemory Tables. Puisque "énorme" n'est pas défini, cela peut ne pas fonctionner.