sql-server – 使用子查询在大型表上缓慢更新
如果SourceTable具有> 15MM记录而Bad_Phrase具有> 3K记录,则以下查询将花费近10个小时在SQL Server 2005 SP4上运行.
UPDATE [SourceTable] SET Bad_Count= ( SELECT COUNT(*) FROM Bad_Phrase WHERE [SourceTable].Name like '%'+Bad_Phrase.PHRASE+'%' ) 在英语中,此查询计算Bad_Phrase中列出的不同短语的数量,这些短语是SourceTable中字段Name的子字符串,然后将该结果放在字段Bad_Count中. 我想知道如何让这个查询运行得更快. 解决方法虽然我同意其他评论者认为这是一个计算上很昂贵的问题,但我认为通过调整你正在使用的SQL有很大的改进空间.为了说明,我创建了一个包含15MM名称和3K短语的假数据集,运行旧方法,并运行新方法.Full script to generate a fake data set and try out the new approach TL; DR 在我的机器和这个假数据集上,原始方法需要大约4个小时才能运行.拟议的新方法大约需要10分钟,这是一项相当大的改进.以下是拟议方法的简短摘要: >对于每个名称,从每个字符偏移量开始生成子字符串(并且以最长的错误短语的长度为上限,作为优化) 原始方法:算法分析 从原始UPDATE语句的计划中,我们可以看到工作量与名称数量(15MM)和短语数量(3K)成线性比例.因此,如果我们将名称和短语的数量乘以10,则整体运行时间将慢约100倍. 查询实际上也与名称的长度成正比;虽然这在查询计划中有点隐藏,但它通过“执行次数”来寻找表盘.在实际计划中,我们可以看到每个名称不仅发生一次,而且名称中每个字符偏移实际上发生一次.因此,这种方法在运行时复杂性方面是O(#names * #words * name length). 新方法:代码 此代码也可在完整的pastebin中找到,但为了方便起见,我将其复制到此处. pastebin还具有完整的过程定义,其中包括您在下面看到的@minId和@maxId变量,用于定义当前批次的边界. -- For each name,generate the string at each offset DECLARE @maxBadPhraseLen INT = (SELECT MAX(LEN(phrase)) FROM Bad_Phrase) SELECT s.id,sub.sub_name INTO #SubNames FROM (SELECT * FROM SourceTable WHERE id BETWEEN @minId AND @maxId) s CROSS APPLY ( -- Create a row for each substring of the name,starting at each character -- offset within that string. For example,if the name is "abcd",this CROSS APPLY -- will generate 4 rows,with values ("abcd"),("bcd"),("cd"),and ("d"). In order -- for the name to be LIKE the bad phrase,the bad phrase must match the leading X -- characters (where X is the length of the bad phrase) of at least one of these -- substrings. This can be efficiently computed after indexing the substrings. -- As an optimization,we only store @maxBadPhraseLen characters rather than -- storing the full remainder of the name from each offset; all other characters are -- simply extra space that isn't needed to determine whether a bad phrase matches. SELECT TOP(LEN(s.name)) SUBSTRING(s.name,n.n,@maxBadPhraseLen) AS sub_name FROM Numbers n ORDER BY n.n ) sub -- Create an index so that bad phrases can be quickly compared for a match CREATE CLUSTERED INDEX IX_SubNames ON #SubNames (sub_name) -- For each name,compute the number of distinct bad phrases that match -- By "match",we mean that the a substring starting from one or more -- character offsets of the overall name starts with the bad phrase SELECT s.id,COUNT(DISTINCT b.phrase) AS bad_count INTO #tempBadCounts FROM dbo.Bad_Phrase b JOIN #SubNames s ON s.sub_name LIKE b.phrase + '%' GROUP BY s.id -- Perform the actual update into a "bad_count_new" field -- For validation,we'll compare bad_count_new with the originally computed bad_count UPDATE s SET s.bad_count_new = COALESCE(b.bad_count,0) FROM dbo.SourceTable s LEFT JOIN #tempBadCounts b ON b.id = s.id WHERE s.id BETWEEN @minId AND @maxId 新方法:查询计划 首先,我们从每个字符偏移量开始生成子字符串 然后在这些子字符串上创建聚簇索引 现在,对于每个坏词,我们都会搜索这些子串以识别任何匹配.然后,我们计算匹配该字符串的一个或多个子串的不同坏短语的数量.这真是关键一步;由于我们对子字符串编制索引的方式,我们不再需要检查坏短语和名称的完整交叉产品.执行实际计算的此步骤仅占实际运行时间的约10%(其余为子串的预处理). 最后,执行实际更新语句,使用LEFT OUTER JOIN将计数0分配给我们未找到任何错误短语的任何名称. 新方法:算法分析 新方法可分为两个阶段,预处理和匹配.让我们定义以下变量: > N =名字# 预处理阶段是O(N * L * LOG(N * L)),以便创建N * L子串然后对它们进行排序. 实际匹配是O(B * LOG(N * L)),以便寻找每个坏词的子串. 通过这种方式,我们创建了一种算法,该算法不会与坏短语的数量成线性关系,当我们扩展到3K短语及以后时,关键性能解锁.换句话说,只要我们从300个坏短语变成3K坏短语,原始实现大约需要10倍.同样地,如果我们从3K坏短语变为30K,那么它将需要另外10倍.然而,新的实现将以亚线性方式向上扩展,实际上,当扩展到30K坏短语时,在3K坏短语上测量的时间不到2倍. 假设/警告 >我将整体工作划分为适度规模的批次.对于这两种方法而言,这可能是一个好主意,但对于新方法尤其重要,因此子串上的SORT对于每个批次都是独立的,并且很容易适合内存.您可以根据需要操作批量大小,但在一个批次中尝试所有15MM行是不明智的. 相关博文 Aaron Bertrand在他的博客文章One way to get an index seek for a leading %wildcard中更详细地探讨了这种类型的解决方案. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |