使用SQLServer包含部分单词

时间:2016-06-07 10:45:48

标签: sql-server azure-sql-database contains sql-like

我们在一个包含部分匹配条形码的大型目录上运行许多产品。

我们从简单的查询开始

select * from products where barcode like '%2345%'

但这需要太长时间,因为它需要全表扫描。 我们认为全文搜索将能够使用contains来帮助我们。

select * from products where contains(barcode, '2345')

但是,似乎包含不支持查找部分包含文本的单词,但只支持单词匹配或前缀。 (但在这个例子中我们正在寻找'123456')。

9 个答案:

答案 0 :(得分:13)

我的回答是:@DenisReznik是对的:)

好的,我们来看看吧 多年来我一直使用条形码和大型目录,我很好奇这个问题。

所以我自己做了一些测试。

我创建了一个用于存储测试数据的表:

CREATE TABLE [like_test](
    [N] [int] NOT NULL PRIMARY KEY,
    [barcode] [varchar](40) NULL
) 

我知道有很多类型的条形码,有些只包含数字,其他包含字母,其他甚至可能更复杂。

我们假设条形码是随机字符串 我已经填写了1000万条随机数字数据记录:

insert into like_test
select (select count(*) from like_test)+n, REPLACE(convert(varchar(40), NEWID()), '-', '') barcode 
from FN_NUMBERS(10000000)
  

FN_NUMBERS()只是我在DB中使用的一个函数(一种tally_table)   快速获取记录。

我有1000万条记录:

N   barcode
1   1C333262C2D74E11B688281636FAF0FB
2   3680E11436FC4CBA826E684C0E96E365
3   7763D29BD09F48C58232C7D33551E6C9

让我们声明一个var来搜索:

declare @s varchar(20) = 'D34F15' -- random alfanumeric string 

让我们用 LIKE 进行基础尝试,将结果与以下内容进行比较:

select * from like_test where barcode like '%'+@s+'%'

在我的工作站上,完整的聚簇索引扫描需要24.4秒。非常慢。

SSMS建议在条形码列上添加索引:

CREATE NONCLUSTERED INDEX [ix_barcode] ON [like_test] ([barcode]) INCLUDE ([N])

500Mb的索引,我重试选择,这个时间为24.0秒,非聚集索引寻求...不到2%更好,几乎相同的结果。离SSMS假设的75%差距很远。在我看来,这个指数真的不值得。也许我的SSD三星840正在发挥作用..
目前我让索引处于活动状态。

让我们试试 CHARINDEX 解决方案:

select * from like_test where charindex(@s, barcode) > 0

这次花了23.5秒完成,并没有比LIKE好多了。

现在让我们检查@DenisReznik的建议,即使用二进制整理可以加快速度。

select * from like_test
where barcode collate Latin1_General_BIN like '%'+@s+'%' collate Latin1_General_BIN 
哇,似乎工作了!只有4.5秒这令人印象深刻!好5倍..
那么,CHARINDEX和Collat​​ion怎么样呢?我们来试试吧:

select * from like_test
where charindex(@s collate Latin1_General_BIN, barcode collate Latin1_General_BIN)>0

Unbelivable! 2.4秒,10倍更好..

好的,到目前为止我已经意识到CHARINDEX比LIKE好,并且Binary Collat​​ion比普通的字符串排序更好,所以从现在起我将继续使用CHARINDEX和Collat​​ion。

现在,我们可以做任何其他事情来获得更好的结果吗?也许我们可以尝试减少我们很长的字符串..扫描总是扫描..

首先尝试使用SUBSTRING切割逻辑字符串以实际处理8个字符的条形码:

select * from like_test
where charindex(
        @s collate Latin1_General_BIN, 
        SUBSTRING(barcode, 12, 8) collate Latin1_General_BIN
      )>0

优秀! 1.8秒..我已经尝试了两个SUBSTRING(barcode, 1, 8)(字符串的头部)和SUBSTRING(barcode, 12, 8)(字符串的中间部分),结果相同。

然后我试图减少条形码列的大小,几乎没有使用SUBSTRING()

的差异

最后,我试图删除条形码列上的索引并重复上述所有测试... 得到几乎相同的结果,我感到非常惊讶。差异很小 索引的性能提高3-5%,但是如果目录更新则需要500Mb的磁盘空间和维护成本。

当然,对于像where barcode = @s这样的直接键查找,索引需要20-50毫秒,没有索引,我们使用排序规则语法where barcode collate Latin1_General_BIN = @s collate Latin1_General_BIN

不能少于1.1秒

这很有趣。
我希望这有帮助

答案 1 :(得分:5)

我经常使用charindex,就像经常讨论的那样。

事实证明,根据您的结构,您实际上可能会大幅提升性能。

http://cc.davelozinski.com/sql/like-vs-substring-vs-leftright-vs-charindex

答案 2 :(得分:2)

这里的好选择适用于您的情况 - 创建您的FTS索引。以下是它的实现方式:

1)创建表条款:

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

注意:表定义中的索引声明是2014年的一个特性。如果您的版本较低,只需将其从CREATE TABLE语句中取出并单独创建。

2)将条形码剪切为克,并将每个条形码保存到表格中。例如:条形码=' 123456',您的表格应该有6行:' 123456',' 23456',' 3456', ' 456',' 56',' 6'。

3)创建表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)将条形码的一对(TermId,BarcodeId)保存到表BarcodeIndex中。 TermId在第二步生成或存在于条款表中。 BarcodeId - 条形码的标识符,存储在条形码(或您使用的任何名称)表中。对于每个条形码,BarcodeIndex表中应该有6行。

5)使用以下查询按部件选择条形码:

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%'

此解决方案强制将条形码的所有类似部分存储在附近,因此SQL Server将使用“索引范围扫描”策略从“条件”表中获取数据。条款表中的术语应该是唯一的,以使此表尽可能小。这可以在应用程序逻辑中完成:检查存在 - >如果术语不存在则插入新的。或者通过为Terms表的聚集索引设置选项IGNORE_DUP_KEY。 BarcodesIndex表用于引用术语和条形码。

请注意,此解决方案中的外键和约束是考虑因素。就个人而言,我更喜欢有外键,直到他们伤到我。

答案 3 :(得分:1)

经过进一步测试,阅读和与@DenisReznik交谈后,我认为最好的选择是将虚拟列添加到条形码表以分割条形码。

我们只需要从第2到第4的起始位置的列,因为对于第1个我们将使用原始条形码列,最后我认为它根本没用(当6%时,什么样的部分匹配是6个字符6记录会匹配?):

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

然后在此虚拟列上添加索引:

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]);

现在我们可以简单地找到与此查询的部分匹配

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

这是 HELL 快!

  • 搜索字符串为6个字符15-20毫秒(完整代码)
  • 搜索字符串为5个字符25毫秒(20-80)
  • 搜索字符串为4个字符50毫秒(40-130)
  • 搜索字符串为3个字符65毫秒(50-150)
  • 搜索字符串为2个字符200毫秒(190-260)

表格没有额外的空间,但每个索引最多需要200Mb(100万条码)

请注意
在Microsoft SQL Server Express(64位)和Microsoft SQL Server Enterprise(64位)上进行测试,后者的优化器稍微好一点,但主要区别在于:

在快速版上,你必须在搜索字符串时提取 ONLY 主键,如果在SELECT中添加其他列,优化器将不再使用索引,但它将用于完整的聚簇索引扫描所以你需要像

这样的东西
;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)

在标准(企业)版本上

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

答案 4 :(得分:0)

更新:

我们知道FULL-TEXT搜索可以用于以下内容:

  

Full-Text Search - MSDN

     
      
  1. 一个或多个特定单词或短语(简单术语)
  2.   
  3. 单词或短语,其中单词以指定文本(前缀术语)开头
  4.   
  5. 特定单词的屈折形式(代词)
  6.   
  7. 靠近另一个单词或短语(近距离术语)的单词或短语
  8.   
  9. 特定单词(同义词库)的同义形式
  10.   
  11. 使用加权值的字或短语(加权项)
  12.   

您的查询要求是否符合这些要求?如果您不得不按照描述搜索模式,而没有一致的模式(例如“1%”),那么SQL可能无法使用SARG

  • 您可以使用Boolean语句

C++角度来看,可以从预订,有序和订购后B-Trees访问traversals,并使用Boolean语句搜索B-Tree {1}}。处理速度比字符串比较快得多,布尔值至少提供了改进的性能。

我们可以在以下两个选项中看到这一点:

<强> PATINDEX

  • 仅当您的列不是数字时,因为PATINDEX是为字符串设计的。
  • 返回一个整数(如CHARINDEX),它比字符串更容易处理。

CHARINDEX是一个解决方案

  • CHARINDEX在搜索INT时没有问题,并且再次返回一个数字。
  • 可能需要内置一些额外的案例(即始终忽略第一个数字),但您可以像这样添加它们:CHARINDEX('200', barcode) > 1

我所说的证明,让我们回到旧的[AdventureWorks2012].[Production].[TransactionHistory]。我们有TransactionID,其中包含我们想要的项目数量,并且假设您希望每个事务ID最终都有200个。

-- 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

注意CHARINDEX会在搜索中删除值 200200 ,因此您可能需要相应地调整代码。但看看结果:

Amazing Awesomeness

  • 显然,布尔值和数字的比较速度更快。
  • LIKE使用字符串比较,处理起来要慢得多。

我对差异的大小感到有些惊讶,但基本面是相同的。与字符串比较相比,IntegersBoolean语句始终处理速度更快。

答案 5 :(得分:0)

你没有包含很多约束,这意味着你想在字符串中搜索字符串 - 如果有一种方法可以优化索引来搜索字符串中的字符串,那么它就是内置的!

其他使得难以给出具体答案的事情:

  • 目前尚不清楚&#34;巨大的&#34;并且&#34;太长了&#34;意思。

  • 目前尚不清楚您的应用程序是如何运作的。您是否在添加1,000个新产品时批量搜索?您是否允许用户在搜索框中输入部分条形码?

我可以提出一些建议,可能会对您的情况有所帮助。

加快部分查询

我有一个包含许多牌照的数据库;有时一名军官想要搜索盘子的最后三个字符。为了支持这一点,我将版权卡反向存储,然后使用LIKE ('ZYX%')匹配ABCXYZ。在进行搜索时,他们可以选择包含&#39;搜索(像你一样),这是一个很慢,或选择做&#39;开始/结束&#39;这是因为索引超级。这可以在某些时候解决你的问题(这可能足够好),特别是如果这是一个普遍的需要。

并行查询

索引有效,因为它组织数据,索引无法帮助字符串中的字符串,因为没有组织。速度似乎是您优化的重点,因此您可以以并行搜索的方式存储/查询数据。示例:如果按顺序搜索1000万行需要10秒钟,那么拥有10个并行进程(因此进程搜索100万个)将使您从10秒到1秒(种类&#39; a-sort&# 39;一个)。把它想象成扩展。在您的单个SQL实例(尝试数据分区)或跨多个SQL Server(如果这是一个选项)中有各种选项。

奖励:如果你没有使用RAID设置,这可以帮助阅读,因为它可以有效地并行阅读。

减少瓶颈

搜索&#34;巨大&#34;数据集需要太长时间&#34;是因为需要从磁盘读取所有数据,这总是很慢。您可以跳过磁盘,并使用InMemory表。因为&#34;巨大的&#34;没有定义,这可能不起作用。

答案 6 :(得分:0)

我来晚了,但这是本着@MtwStark第二个回答的精神,获取全文索引的另一种方法。

这是使用搜索表联接

的解决方案
drop table if exists #numbers
select top 10000 row_number() over(order by t1.number) as n 
into #numbers
from master..spt_values t1 
    cross join master..spt_values t2


drop table if exists [like_test]
create TABLE [like_test](
    [N] INT IDENTITY(1,1) not null,
    [barcode] [varchar](40) not null,
    constraint pk_liketest primary key ([N])
) 

insert into dbo.like_test (barcode)
select top (1000000) replace(convert(varchar(40), NEWID()), '-', '') barcode 
from #numbers t,#numbers t2


drop table if exists barcodesearch
select distinct ps.n, trim(substring(ps.barcode,ty.n,100)) as searchstring
    into barcodesearch
from like_test ps
inner join #numbers ty on ty.n < 40
where len(ps.barcode) > ty.n

create clustered index idx_barcode_search_index on barcodesearch (searchstring)

最终搜索应如下所示:

declare @s varchar(20) = 'D34F15'
select distinct lt.* from dbo.like_test lt
inner join barcodesearch bs on bs.N = lt.N
where bs.searchstring like @s+'%'

如果您可以选择全文搜索,则可以通过直接在条形码表中添加全文搜索列来进一步加快搜索速度

drop table if exists #liketestupdates
select n, string_agg(searchstring, ' ') 
    within group (order by reverse(searchstring)) as searchstring
into #liketestupdates
from barcodesearch
group by n

alter table dbo.like_test add search_column varchar(559)

update lt
    set search_column = searchstring
from like_test lt
inner join #liketestupdates lu on lu.n = lt.n


CREATE FULLTEXT CATALOG ftcatalog as default;  

create fulltext index on dbo.like_test  ( search_column )  
key index pk_liketest

最终的全文本搜索应如下所示:

declare @s varchar(20) = 'D34F15'
set @s = '"*' + @s + '*"'
select n,barcode from dbo.like_test where contains(search_column, @s)

我知道估算的成本并不是衡量预期效果的最好方法,但在这里并不是个数字。

通过搜索表联接估计的子树成本为2.13

通过全文搜索估计的子树成本为0.008

答案 7 :(得分:-1)

全文旨在提供更大的文本,让我们说出超过100个字符的文本。您可以使用LIKE&#39;%string%&#39;。 (但这取决于条形码列的定义方式。)您是否有条形码索引?如果没有,那么创建一个,它将改善您的查询。

答案 8 :(得分:-1)

首先在你必须作为where子句放置的列上创建索引。

其次,对于where子句中使用的列的数据类型,将它们作为Char代替Varchar,这将在表和包含该列的索引中节省一些空间。 varchar(1)列需要在char(1)

上多一个字节

请仅提取您需要尝试避免的列数*,特定于您要选择的列数。 不要写为

从产品中选择*

代替它写为

 Select Col1, Col2 from products with (Nolock)