web-dev-qa-db-fra.com

Utiliser SQLServer contient pour des mots partiels

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').

15
Guy Korland

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

12
MtwStark

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

5
John Cappelletti

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. 

2
Denis Reznik

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! 

  • pour les chaînes de recherche de 6 caractères 15-20 millisecondes (code complet)
  • pour les chaînes de recherche de 5 caractères 25 millisecondes (20-80)
  • pour les chaînes de recherche de 4 caractères 50 millisecondes (40-130)
  • pour les chaînes de recherche de 3 caractères 65 millisecondes (50-150)
  • pour les chaînes de recherche de 2 caractères 200 millisecondes (190-260)

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))
1
MtwStark

MIS À JOUR:

Nous savons que les recherches FULL-TEXT peuvent être utilisées pour:

Recherche en texte intégral - MSDN

  1. Un ou plusieurs mots ou expressions spécifiques (terme simple)
  2. Un mot ou une phrase dont les mots commencent par le texte spécifié (préfixe terme)
  3. Formes flexionnelles d'un mot spécifique (terme de génération)
  4. Un mot ou une expression proche d'un autre mot ou expression (terme de proximité)
  5. Formes synonymes d'un mot spécifique (thésaurus)
  6. 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.

  • Vous pouvez utiliser des instructions 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

  • Seulement si votre colonne n'est pas numérique, car PATINDEX est conçu pour les chaînes.
  • Retourne un entier (comme CHARINDEX) qui est plus facile à traiter que les chaînes.

CHARINDEX est une solution

  • CHARINDEX n'a ​​aucun problème à rechercher les INT et, encore une fois, retourne un nombre.
  • Peut nécessiter quelques cas supplémentaires intégrés (le premier numéro est toujours ignoré), mais vous pouvez les ajouter comme suit: 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:

 Amazing Awesomeness

  • Clairement, les booléens et les nombres sont des comparaisons plus rapides.
  • LIKE utilise des comparaisons de chaînes, qui sont encore beaucoup plus lentes à traiter.

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.

0
clifton_h

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.

0
Robert Paulsen