SQL 服务器 - 查询优化 'like' 导致大多数 cpu 使用 100%
SQL SERVER - Query optimization 'like' causing most cpu uses 100%
我在数据库 Products 和 Filters 中有两个 table。
架构:
我创建了一个查询,从过滤器 table 中查找所有记录,循环处理每条记录并调用一个过程来设置产品 table 的类别 ID。
过滤table数据如下
过滤器选择查询如下..
DECLARE @TotalRecords INT, @Start INT, @Limit INT, @CatId INT, @Merchants NVARCHAR(max), @NotMatch NVARCHAR(max), @WillMatch NVARCHAR(max);
SELECT @TotalRecords = COUNT(*) FROM filters;
SET @Limit = 1;
SET @Start = 0;
WHILE(@TotalRecords > 0)
BEGIN
SELECT @CatId = category_id, @Merchants = merchant_name, @NotMatch = not_match, @WillMatch = will_match FROM
(
SELECT TOP (@Start + @Limit) *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rnum
FROM filters
) a
WHERE rnum > @Start;
-- call filter procedure.
exec procSetProductCategory @CatId = @CatId, @Merchants = @Merchants, @WillMatch = @WillMatch, @NotMatch = @NotMatch;
SET @Start += 1;
SET @TotalRecords -= 1;
END
procSetProductCategory如下..
CREATE PROC [dbo].[procSetProductCategory]
(
@CatId INT = NULL,
@Merchants NVARCHAR(max),
@NotMatch NVARCHAR(max),
@WillMatch NVARCHAR(max)
)
AS
BEGIN
SET NOCOUNT ON
declare @query nvarchar(max), @orToken nvarchar(max), @andToken nvarchar(max);
set @query = 'UPDATE Products SET category_id = '+ convert(nvarchar(20), @CatId) + ' WHERE category_id IS NULL AND merchant_name IN(' + @Merchants + ')';
if(@WillMatch is not null AND LTRIM(RTRIM(@WillMatch)) != '')
BEGIN
set @andToken = '%'' AND product_name LIKE ''%';
set @WillMatch = REPLACE(@WillMatch, '+', @andToken);
set @orToken = '%'') OR (product_name LIKE ''%';
set @query = @query + ' AND ((product_name LIKE '''+ '%' + REPLACE(@WillMatch, ',', @orToken) + '%''))';
END
if(@NotMatch is not null AND LTRIM(RTRIM(@NotMatch)) != '')
BEGIN
set @andToken = '%'' AND product_name NOT LIKE ''%';
set @NotMatch = REPLACE(@NotMatch, '+', @andToken);
set @orToken = '%'') OR (product_name NOT LIKE ''%';
set @query = @query + ' AND ((product_name NOT LIKE '''+ '%' + REPLACE(@NotMatch, ',', @orToken) + '%''))';
END
EXECUTE sp_executesql @query;
END
它生成如下 sql 查询...
Query #1
-------------------------------------------------------------------------------------------------------
UPDATE Products SET category_id = 101 WHERE merchant_name IN('merchant 1','merchant 4','merchant 3') AND
(
(product_name LIKE '%abcd%' AND product_name LIKE '%efhg%')
) AND (
(product_name NOT LIKE '%3258%')
OR (product_name NOT LIKE '%yxzs%')
)
Query #2
-------------------------------------------------------------------------------------------------------
UPDATE Products SET category_id = 102 WHERE merchant_name IN('merchant 3', 'merchant 4') AND
(
(product_name LIKE '%1258%') OR (product_name LIKE '%abcd%')
)
注意这里使用了一些技巧。
[]用于区分匹配词组。
[+] 在用于两个具有 AND 条件的匹配短语的匹配字段中。
这些查询与我需要的一样..
问题是,当我 运行 这个查询有 500 000 个产品时,它使用了大约 100% CPU。
我们如何优化不影响结果但可以减少 CPU 使用率的查询?
没有查询计划,很难确定,但我猜这是因为你在 '%something%'
上进行匹配,这意味着查询必须检查每一行。
这总是很慢,而且您也无能为力来帮助建立索引。
如果您要进行文本比较,使用 SQL 服务器的 full text matching 功能可能会获得更好的性能。
首先,正如已经指出的那样:这里的逻辑确实有问题。也就是说,假设您坚持使用它,那么您可能想尝试一些事情。
我的第一个问题是:这个东西 运行 需要多长时间?你不必太担心它需要 100% CPU;问题是完成需要多少时间。
查询 1:
您似乎在 filters
table 上创建一个循环,逐行获取每一行。
- SQL 未针对逐行操作进行优化;你真的应该考虑将逻辑更改为基于集合的东西
- 如果您真的想逐行执行某些操作,请使用
CURSOR
而不是 当前方法 。
- 首先你遍历整个table来计算有多少个过滤器
- 然后你遍历整个 table 并按
SELECT 1
排序记录
- 从已排序的列表中选择
rnum
大于您的计数器的列表
=> 这在很多方面都是错误的,它实际上伤害了 =(
- 如果你sort/order按
SELECT 1
那么它可以return第一次按ABCD顺序记录,第二次按BADC顺序记录;并且两个答案都是正确的,因为您按常量排序:记录的实际顺序无关紧要!
- 每次循环,服务器都必须对整个 table 进行排序,然后才能判断哪些
rnum
值符合大于 [=19= 的要求];每一次!
- 将有很多记录适合
rnum > @start
,用于填充记录的 returned 记录可以是其中任何一个!
对于'fix',我建议使用以下方法:
DECLARE @TotalRecords INT,
@Start INT,
@Limit INT,
@CatId INT,
@Merchants NVARCHAR(max),
@NotMatch NVARCHAR(max),
@WillMatch NVARCHAR(max);
DECLARE filter_loop CURSOR LOCAL FAST_FORWARD
FOR SELECT category_id,
merchant_name,
not_match,
will_match
FROM filters
ORDER BY id -- not required but makes debugging easier
OPEN filter_loop
FETCH NEXT FROM filter_loop INTO @CatId, @Merchants, @NotMatch, @WillMatch
WHILE @@FETCH_STATUS = 0
BEGIN
-- call filter procedure.
exec procSetProductCategory @CatId = @CatId, @Merchants = @Merchants, @WillMatch = @WillMatch, @NotMatch = @NotMatch;
-- get next filter
FETCH NEXT FROM filter_loop INTO @CatId, @Merchants, @NotMatch, @WillMatch
END
CLOSE filter_loop
DEALLOCATE filter_loop
查询 2:
乍一看,我对存储过程本身无能为力。有一些动态 sql 字符串构建可能会优化一点,但我非常怀疑它会产生很大的影响。因为它现在是相当可读的,所以我会保持原样。
生成的查询确实看起来像这样:
UPDATE Products
SET category_id = 101
WHERE merchant_name IN ('merchant 1','merchant 4','merchant 3')
AND ((product_name LIKE '%abcd%' AND product_name LIKE '%efhg%') )
AND ((product_name NOT LIKE '%3258%') OR (product_name NOT LIKE '%yxzs%'))
我建议为此创建以下索引:
CREATE INDEX idx_test ON Products (merchant_name) INCLUDE product_name)
事后思考
即使进行了上述更改,在处理超过 100k 条记录时,这仍然会 运行 相当长一段时间。唯一真正解决这个问题的方法是使用基于集合的方法,但需要一个庞大的动态 sql 字符串;或者对数据本身有更好的了解。例如。您可以尝试组合具有相同 Merchants
value 但不同 Match
/NoMatch
的不同 Filters
记录...可能不会太多很难,但我建议先从上面的建议开始,然后再看看你的结局。
我在数据库 Products 和 Filters 中有两个 table。
架构:
我创建了一个查询,从过滤器 table 中查找所有记录,循环处理每条记录并调用一个过程来设置产品 table 的类别 ID。
过滤table数据如下
过滤器选择查询如下..
DECLARE @TotalRecords INT, @Start INT, @Limit INT, @CatId INT, @Merchants NVARCHAR(max), @NotMatch NVARCHAR(max), @WillMatch NVARCHAR(max);
SELECT @TotalRecords = COUNT(*) FROM filters;
SET @Limit = 1;
SET @Start = 0;
WHILE(@TotalRecords > 0)
BEGIN
SELECT @CatId = category_id, @Merchants = merchant_name, @NotMatch = not_match, @WillMatch = will_match FROM
(
SELECT TOP (@Start + @Limit) *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rnum
FROM filters
) a
WHERE rnum > @Start;
-- call filter procedure.
exec procSetProductCategory @CatId = @CatId, @Merchants = @Merchants, @WillMatch = @WillMatch, @NotMatch = @NotMatch;
SET @Start += 1;
SET @TotalRecords -= 1;
END
procSetProductCategory如下..
CREATE PROC [dbo].[procSetProductCategory]
(
@CatId INT = NULL,
@Merchants NVARCHAR(max),
@NotMatch NVARCHAR(max),
@WillMatch NVARCHAR(max)
)
AS
BEGIN
SET NOCOUNT ON
declare @query nvarchar(max), @orToken nvarchar(max), @andToken nvarchar(max);
set @query = 'UPDATE Products SET category_id = '+ convert(nvarchar(20), @CatId) + ' WHERE category_id IS NULL AND merchant_name IN(' + @Merchants + ')';
if(@WillMatch is not null AND LTRIM(RTRIM(@WillMatch)) != '')
BEGIN
set @andToken = '%'' AND product_name LIKE ''%';
set @WillMatch = REPLACE(@WillMatch, '+', @andToken);
set @orToken = '%'') OR (product_name LIKE ''%';
set @query = @query + ' AND ((product_name LIKE '''+ '%' + REPLACE(@WillMatch, ',', @orToken) + '%''))';
END
if(@NotMatch is not null AND LTRIM(RTRIM(@NotMatch)) != '')
BEGIN
set @andToken = '%'' AND product_name NOT LIKE ''%';
set @NotMatch = REPLACE(@NotMatch, '+', @andToken);
set @orToken = '%'') OR (product_name NOT LIKE ''%';
set @query = @query + ' AND ((product_name NOT LIKE '''+ '%' + REPLACE(@NotMatch, ',', @orToken) + '%''))';
END
EXECUTE sp_executesql @query;
END
它生成如下 sql 查询...
Query #1
-------------------------------------------------------------------------------------------------------
UPDATE Products SET category_id = 101 WHERE merchant_name IN('merchant 1','merchant 4','merchant 3') AND
(
(product_name LIKE '%abcd%' AND product_name LIKE '%efhg%')
) AND (
(product_name NOT LIKE '%3258%')
OR (product_name NOT LIKE '%yxzs%')
)
Query #2
-------------------------------------------------------------------------------------------------------
UPDATE Products SET category_id = 102 WHERE merchant_name IN('merchant 3', 'merchant 4') AND
(
(product_name LIKE '%1258%') OR (product_name LIKE '%abcd%')
)
注意这里使用了一些技巧。
[]用于区分匹配词组。 [+] 在用于两个具有 AND 条件的匹配短语的匹配字段中。
这些查询与我需要的一样..
问题是,当我 运行 这个查询有 500 000 个产品时,它使用了大约 100% CPU。
我们如何优化不影响结果但可以减少 CPU 使用率的查询?
没有查询计划,很难确定,但我猜这是因为你在 '%something%'
上进行匹配,这意味着查询必须检查每一行。
这总是很慢,而且您也无能为力来帮助建立索引。
如果您要进行文本比较,使用 SQL 服务器的 full text matching 功能可能会获得更好的性能。
首先,正如已经指出的那样:这里的逻辑确实有问题。也就是说,假设您坚持使用它,那么您可能想尝试一些事情。 我的第一个问题是:这个东西 运行 需要多长时间?你不必太担心它需要 100% CPU;问题是完成需要多少时间。
查询 1:
您似乎在 filters
table 上创建一个循环,逐行获取每一行。
- SQL 未针对逐行操作进行优化;你真的应该考虑将逻辑更改为基于集合的东西
- 如果您真的想逐行执行某些操作,请使用
CURSOR
而不是 当前方法 。- 首先你遍历整个table来计算有多少个过滤器
- 然后你遍历整个 table 并按
SELECT 1
排序记录
- 从已排序的列表中选择
rnum
大于您的计数器的列表
=> 这在很多方面都是错误的,它实际上伤害了 =(
- 如果你sort/order按
SELECT 1
那么它可以return第一次按ABCD顺序记录,第二次按BADC顺序记录;并且两个答案都是正确的,因为您按常量排序:记录的实际顺序无关紧要! - 每次循环,服务器都必须对整个 table 进行排序,然后才能判断哪些
rnum
值符合大于 [=19= 的要求];每一次! - 将有很多记录适合
rnum > @start
,用于填充记录的 returned 记录可以是其中任何一个!
对于'fix',我建议使用以下方法:
DECLARE @TotalRecords INT,
@Start INT,
@Limit INT,
@CatId INT,
@Merchants NVARCHAR(max),
@NotMatch NVARCHAR(max),
@WillMatch NVARCHAR(max);
DECLARE filter_loop CURSOR LOCAL FAST_FORWARD
FOR SELECT category_id,
merchant_name,
not_match,
will_match
FROM filters
ORDER BY id -- not required but makes debugging easier
OPEN filter_loop
FETCH NEXT FROM filter_loop INTO @CatId, @Merchants, @NotMatch, @WillMatch
WHILE @@FETCH_STATUS = 0
BEGIN
-- call filter procedure.
exec procSetProductCategory @CatId = @CatId, @Merchants = @Merchants, @WillMatch = @WillMatch, @NotMatch = @NotMatch;
-- get next filter
FETCH NEXT FROM filter_loop INTO @CatId, @Merchants, @NotMatch, @WillMatch
END
CLOSE filter_loop
DEALLOCATE filter_loop
查询 2:
乍一看,我对存储过程本身无能为力。有一些动态 sql 字符串构建可能会优化一点,但我非常怀疑它会产生很大的影响。因为它现在是相当可读的,所以我会保持原样。 生成的查询确实看起来像这样:
UPDATE Products
SET category_id = 101
WHERE merchant_name IN ('merchant 1','merchant 4','merchant 3')
AND ((product_name LIKE '%abcd%' AND product_name LIKE '%efhg%') )
AND ((product_name NOT LIKE '%3258%') OR (product_name NOT LIKE '%yxzs%'))
我建议为此创建以下索引:
CREATE INDEX idx_test ON Products (merchant_name) INCLUDE product_name)
事后思考
即使进行了上述更改,在处理超过 100k 条记录时,这仍然会 运行 相当长一段时间。唯一真正解决这个问题的方法是使用基于集合的方法,但需要一个庞大的动态 sql 字符串;或者对数据本身有更好的了解。例如。您可以尝试组合具有相同 Merchants
value 但不同 Match
/NoMatch
的不同 Filters
记录...可能不会太多很难,但我建议先从上面的建议开始,然后再看看你的结局。