【问题标题】:Using SQLServer contains for partial words使用 SQLServer 包含部分单词
【发布时间】:2016-10-07 05:29:08
【问题描述】:

我们正在使用部分匹配的条形码在庞大的目录中运行许多产品搜索。

我们从一个简单的类似查询开始

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

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

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

但是,contains 似乎不支持查找部分包含文本的单词,而只能查找完整的单词匹配或前缀。 (但在本例中,我们正在寻找“123456”)。

【问题讨论】:

  • FTS 以语言为导向 - 单词和短语。
  • 如果您想使用CONTAINS 进行全文搜索来查找123456,您可以使用123456"123*" 相当于=123456LIKE '123%'。这就是它的设计方式。
  • @gofr1 谢谢,但我们需要一个真正的 contains 而不仅仅是前缀。
  • @Damien_The_Unbeliever 是否意味着我可以在发送 contains(name, 'il') 时找到“低脂牛奶”?
  • @GuyKorland 这是全文搜索中的真实CONTAINS。没有等同于LIKE

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


【解决方案1】:

我的回答是:@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() 只是我在我的数据库中使用的一个函数(一种tally_table) 快速获取记录。

我有 1000 万条这样的记录:

N   barcode
1   1C333262C2D74E11B688281636FAF0FB
2   3680E11436FC4CBA826E684C0E96E365
3   7763D29BD09F48C58232C7D33551E6C9

让我们声明一个要搜索的变量:

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 的建议,即使用 Binary Collat​​ion 应该加快速度。

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

难以置信! 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 秒

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

【讨论】:

  • 它在你搜索的关键字中起作用,但是当关键字被缩短时,例如“44”(只有2个字符,在这种情况下结果包含190万行返回),执行有和没有排序规则的时间是一样的。
【解决方案2】:

我经常使用 charindex,并且经常进行这种辩论。

事实证明,根据您的结构,您实际上可能会获得显着的性能提升。

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

【讨论】:

  • 感谢 charindex 似乎是一个短期的不错的改进!但是,一旦我拥有数百万条条形码,这可能还不够......
【解决方案3】:

适合您的情况的最佳选择 - 创建您的 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) 将条码切割为克,并将它们中的每一个保存到表中。例如:barcode = '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 是在第二步生成的或存在于 Terms 表中。 BarcodeId - 是条形码的标识符,存储在 Barcodes(或您使用的任何名称)表中。对于每个条形码,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 表用于引用术语和条形码。

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

【讨论】:

  • 我认为打破到克应该是全文索引中的一个选项,就像任何其他搜索引擎一样
  • 没有。 SQL Server 中的默认分词器仅在空格和特殊字符上断开文本。实现 n-gram 或其他算法应该手动完成。
  • 顺便说一句,Guy,据我所知,条形码是数字的,所以你不需要处理大小写和国家字符。您可以为此列设置二进制排序规则并使用 like 进行搜索。这将显着提高扫描速度。是的,在这个字段上创建一个索引(如果你没有它)也会使扫描更快。可能这不是您要寻找的,但与我在答案中建议的解决方案相比,这种修改会很快。
  • @DenisReznik 在我看来,这个创造性的解决方案将比你第一个想法直接 charindex() + collat​​e binary 要贵得多。在使用 HUGE 目录时,添加 5-6 列及其索引,尤其是在表经常更新的情况下,这对于磁盘空间和性能而言都不是最佳解决方案。在这种情况下,条形码似乎只有 6 个字符长,但对于条形码来说,很容易达到 8-13-21 个字符的宽度,这个解决方案不容易扩展
  • @MtwStark 是的,我认为条形码只有 6 个字符。选择这个或那个解决方案应该取决于整体数据大小和获取的数据大小。在最坏的情况下使用此解决方案,您将执行 6 次索引查找。取决于搜索词的长度,这可以优化为仅搜索长度相等或更大的列。例如:如果一个搜索词是“12345”,它只能在 col1 和 col2 中找到,所以我们进行 2 次索引搜索。带有排序规则和 charindex 的第二个选项需要对每次搜索进行全索引扫描。这是考虑的重点,这对您的数据和查询更好。
【解决方案4】:

在进一步测试和阅读并与@DenisReznik 交谈后,我认为最好的选择可能是在条码表中添加虚拟列以拆分条码。

我们只需要从第 2 到第 4 开始位置的列,因为对于第 1 列,我们将使用原始条码列,最后我认为它根本没有用(当 60% 的记录会匹配吗?):

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

地狱快!

  • 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 位)上进行测试,后者的优化器稍好一些,但主要区别在于:

在 express edition 上,您必须在搜索字符串时提取 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))

【讨论】:

    【解决方案5】:

    你没有包含很多约束,这意味着你想在一个字符串中搜索字符串——如果有一种方法可以优化索引以在一个字符串中搜索一个字符串,它就会被内置!

    其他难以给出具体答案的因素:

    • 不清楚“巨大”和“太长”是什么意思。

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

    我可以提出一些对您的情况可能有用也可能没有帮助的建议。

    加快一些查询速度

    我有一个包含大量车牌的数据库;有时警官想通过车牌的最后 3 个字符进行搜索。为了支持这一点,我将车牌反向存储,然后使用LIKE ('ZYX%') 匹配ABCXYZ。在进行搜索时,他们可以选择“包含”搜索(就像您一样),这很慢,或者可以选择“开始/结束于”,因为索引非常好。这会在某些时候解决您的问题(这可能已经足够好了),特别是如果这是一个常见的需求。

    并行查询

    索引之所以有效,是因为它组织了数据,而索引无法帮助处理字符串中的字符串,因为没有组织。速度似乎是您优化的重点,因此您可以以并行搜索的方式存储/查询您的数据。示例:如果顺序搜索 1000 万行需要 10 秒,那么拥有 10 个并行进程(因此进程搜索 100 万行)将使您从 10 秒缩短到 1 秒(kind'a-sort'a) .将其视为向外扩展。对此有多种选择,在您的单个 SQL 实例中(尝试数据分区)或跨多个 SQL Server(如果这是一个选项)。

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

    减少瓶颈

    搜索“巨大”数据集需要“太长时间”的一个原因是因为需要从磁盘读取所有数据,这总是很慢。您可以跳过磁盘,并使用 InMemory Tables。由于没有定义“巨大”,这可能不起作用。

    【讨论】:

      【解决方案6】:

      更新:

      我们知道全文搜索可用于以下内容:

      Full-Text Search - MSDN

      1. 一个或多个特定词或短语(简单术语)
      2. 以指定文本(前缀词)开头的词或词组
      3. 特定词的屈折变化形式(代词)
      4. 与另一个词或词组(邻近词)接近的词或词组
      5. 特定单词的同义词(同义词库)
      6. 使用加权值(加权项)的单词或短语

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

      • 您可以使用Boolean 语句

      C++ 的角度来看,B-Trees 可从预购、订购和后购traversals 访问,并利用Boolean 语句搜索B-Tree。布尔值的处理速度比字符串比较快得多,至少可以提高性能。

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

      PATINDEX

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

      CHARINDEX 是一种解决方案

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

      证明我在说什么,让我们回到旧的[AdventureWorks2012].[Production].[TransactionHistory]。我们有 TransactionID,其中包含我们想要的项目数,为了好玩,假设您想要每个最后有 200 的 transactionID。

      -- 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,因此您可能需要适当地调整您的代码。但是看看结果:

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

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

      【讨论】:

      • 条形码是字符串,因为它们可能有前导零
      • @GuyKorland 不是他们不必是,尽管这无关紧要。不是更新来证明我的观点。 :)
      • SQL Server 中字符串的 LIKE 比较类型取决于排序规则。最快的比较 - 二进制比较。如果列使用二进制排序规则,LIKE 将快得像地狱一样。
      • @clifton_h 不。看起来你做错了什么。我说的是您使用 LIKE 谓词搜索的列的 COLLATION。如果将其更改为某种二进制排序规则,LIKE 会运行得更快。
      • @clifton_h 这并非未经证实,这是一种众所周知的方法。如果我的解释对您来说还不够,请查看这篇文章:aboutsqlserver.com/2015/01/20/…
      【解决方案7】:

      我迟到了,但这里有另一种方法可以按照@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

      【讨论】:

        【解决方案8】:

        全文是针对更大的文本,比如说超过 100 个字符的文本。您可以使用 LIKE '%string%'。 (但这取决于条形码列的定义方式。)您有条形码索引吗?如果没有,请创建一个,它会改进您的查询。

        【讨论】:

        • 如果搜索字符串的两端都有通配符,那么索引将无济于事。 stackoverflow.com/questions/1388059/…
        • 条形码字段上的索引将有很大帮助,因为此索引扫描将比聚集索引扫描更快。但这可能不是您正在寻找的解决方案。
        【解决方案9】:

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

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

        只提取您需要尽量避免的列数 * ,具体到您希望选择的列数。 不要写成

        从产品中选择 *

        代替它写成

         Select Col1, Col2 from products with (Nolock)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-12-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多