【问题标题】:Performance tuning on pattern matching query模式匹配查询的性能调优
【发布时间】:2019-06-17 21:32:45
【问题描述】:

我有两张桌子:

表 1:100 行

表 2:1000 万行

例子:

表一:tb100

create table tb100
(
    name varchar(50)
);

insert into tb100 values('Mak John'),('Will Smith'),('Luke W')......100 rows.

表2:tb10mil

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

注意:如果另一个表中存在任何 WORD(John,Smith,Will),我想匹配两个表之间的名称。例如 John 出现在 John A Mark 中。

我的尝试:

首先,我创建了用于将nametb100 拆分为行的用户定义函数。

函数:udf_Split

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 

然后我写了以下查询:

;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

上述查询的执行时间超过 20 分钟。

【问题讨论】:

  • 你为什么要为你的分离器使用WHILE循环?有很多数据集解决方案会快得多。我建议使用 XML 拆分器或 delimitedsplit8k(如果它不是 nvarchar(MAX))。该拆分器很可能是您的(第一个)问题..
  • 首先,SQL 不是一种文本处理语言,所以你不能指望良好的文本分割性能。其次,没有索引就无法提高性能,并且该功能无法使用索引。最后,SQL Server 在 SSIS 中已经具备全文搜索功能以及模糊匹配和查找功能。如果您想在加载 10M 行时模糊匹配名称,请在 SSIS 中使用模糊查找运算符
  • 如果您想将销售表与一组客户记录按名称进行匹配,请在加载数据时执行此操作,并且仅在目标表中存储实际 ID。每次要加入表时,不要尝试重复模糊查找。无论您使用哪种技术,模糊匹配总是比连接两个索引列慢
  • 如果这个查询会被频繁使用,那么最好在主表上维护一个 AFTER 触发器的映射表,并将拆分的数据作为表列值保存。然后可以在这个新的映射表上建立连接
  • 使用 SQL FOR XML PATH 方法分割字符串,下面的 UDF 函数可以替代kodyaz.com/articles/…

标签: sql-server tsql sql-server-2008-r2 database-performance


【解决方案1】:

你可以尝试如下。

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

【讨论】:

  • 我真的怀疑,通过 LIKE 在带有前导 % 的模式上的 JOIN 会很快有数百万行......而且只是提到这一点:如果名称包含禁止字符(如&lt;,&gt; or &amp; 和更多),这可能会在 CAST() 上中断。还有一个关于性能的提示:GROUP BYDISTINCT 快得多。
  • LIKE 与此答案中的性能无关。提高性能的是不同的字符串拆分方法。与加载数据时使用正确的查找相比,这仍然很糟糕
【解决方案2】:

让我们谈谈性能

  • 第一点是:尽量避免 标量函数,并尽可能避免 multi-statement-TVF。唯一的 快速方法是inline-TVF单行语句)。

  • 第二点:尽可能避免循环!

  • 第三点(实际上是第一点):尝试将数据存储在 一种为快速查询优化的格式。存储多个值 在一个细胞内违反了 1NF 和一个巨大的速度杀手。

你可以试试这个:

模拟您的表格

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 

--创建表以单独存储名称片段(这是您实际应该使用的格式)

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

--此查询将返回所有具有公共片段的行
--您可以重新加入源表以取回值
--您可以按 t2.ID_t100 分组以获取较小表的 ID(更快)

SELECT t1.ID_t1M
FROM #t1MSplitted t1
INNER JOIN #t100Splitted t2 ON t1.Fragment=t2.Fragment
GROUP BY t1.ID_t1M
GO

--清理

DROP TABLE #t100;
GO
DROP TABLE #t1M;
GO
DROP TABLE #t100Splitted;
GO
DROP TABLE #t1MSplitted;
GO

在我的系统上,大约 2 分钟处理了 1 个 Mio 行。

更新 - 100 行与 1000 万行的性能测试

(点击率非常高)

  • 只是拆分,将您的数据转换成更好的形状:~17 分钟

  • 最终选择(只是查找):

  • 最终选择,但正在寻找小表的 ID:几秒钟
    (一旦您转换了数据,这将是正常速度

  • PSK 的方法(内联拆分):~ 30 分钟

【讨论】:

    【解决方案3】:

    我试图节省内存,从而通过避免连接来节省处理内存的时间。 我试图在较小的表中使用 4 值在 ~420k 记录上模拟您的问题。

    方法是避免加入并将问题内存空间从m x n限制到至少大于m&n。

    select DISTINCT t2.name
    from tb10mil  t2
    where (SELECT TOP(1) 1 FROM #splitdata where CHARINDEX(data,t2.Problem)>0)=1 
    

    结果:所用方法所用时间的一半。 (reduced from ~28 s to ~14s)

    缺点:只有当其中一张桌子非常小时,这种方法才是有益的

    【讨论】:

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