更新/删除查询的性能

Posted

技术标签:

【中文标题】更新/删除查询的性能【英文标题】:Performance on an update/delete query 【发布时间】:2013-05-06 13:33:43 【问题描述】:

我想用同一张表生成的值更新我的表。

我的目标是使用 proftxt 搜索所有行,例如 _NS 和 _WP 并使用相同的 ao,总结它们, 将该值除以该 ao 的 _H,_G,_L 元素的数量,并将该值添加到该 ao 的 _H、_G 和 _L 对象中。

一个 ao 可能只有 _NS 和 _WP 行。比例程要跳过这个ao。

例子:

我的数据如下:

an, ao, proftxt, value, year  
101 , 1, 'e_NSe', 5, 2006  
102 , 1, 'e_Ha', 1, 2006  
103 , 1, 'w_NSr', 4, 2006  

104 , 2, 'w_NSr', 2, 2006  
105 , 2, 'x_H05r', 4, 2006   
106 , 2, 'w_Gr', 2, 2006   
107 , 2, 'a_WPr', 4, 2006 

108 , 3, 'a_WPr', 4, 2006 

我的数据应该是这样的:

an, ao, proftxt, value, year  
102 , 1, 'e_Ha', 10 2006  

103 , 2, 'x_H05r', 7, 2006  
103 , 2, 'w_Gr', 5, 2006  

108 , 3, 'a_WPr', 4, 2006  

我的例程适用于少量测试数据。

更新功能结束,在真实数据库上工作,13小时成功后。 但它只编辑了 210000 行中的 5000 行。

DECLARE @ENDYEAR INT
DECLARE @AO BIGINT
DECLARE @YEAR INT
DECLARE @ELEMENTS INT

--Parameter festlegen
SET @YEAR = 2006
SET @ENDYEAR = 2013 --Endyear+1  
SET @AO = 2  

WHILE(@YEAR<@ENDYEAR)
BEGIN     
WHILE (@AO >1)  --Do as long as Cursor is inside table
BEGIN  
    SET @AO = (SELECT TOP 1 ao FROM tbl_slp -- Search ao with _WP _NS
                 WHERE (proftxt LIKE '%[_]WP%' 
                         OR proftxt LIKE '%[_]NS%') 
                         AND year = @YEAR 
                         AND ao > @AO );

    SET @ELEMENTS = (SELECT COUNT(proftxt) --Count Number of _H, _G, _L elements
                            FROM tbl_SLP  
                            WHERE ao = @AO AND year = @YEAR AND  
                                (proftxt LIKE '%[_]H%' OR proftxt = NULL
                                  OR proftxt LIKE '%[_]G%'
                                  OR proftxt LIKE '%[_]L%'))

    IF (@ELEMENTS != 0)
    BEGIN
        UPDATE tbl_SLP --Update _H, _G, _L rows
        SET value = value + (SELECT SUM(CONVERT(float, value)) 
                            FROM tbl_SLP
                            WHERE (proftxt LIKE '%[_]WP%'
                                                            OR proftxt LIKE '%[_]NS%')
                                                            AND year = @YEAR 
                                                            AND ao = @AO)
                            /@ELEMENTS
        WHERE ao = @AO AND year = @YEAR 


        DELETE FROM tbl_SLP --delete_WP _NS rows
            WHERE ao= @AO 
                  AND year = @YEAR 
                  AND (proftxt LIKE '%[_]WP%' OR proftxt LIKE '%[_]NS%')

    END

    SET @AO = @AO +1
    END
    SET @YEAR = @YEAR +1
END

我知道例行程序超级慢,但我该怎么办?

【问题讨论】:

桌子上有索引吗? 不,an 和 year 是我的主键。我会检查它... 【参考方案1】:

SQL 是为基于集合的操作而设计的,而不是像您的例程那样的过程控制流式逻辑。这是一种基于集合的方式,我猜它会比程序方式快得多:

SET XACT_ABORT ON
SET NOCOUNT ON

BEGIN TRANSACTION

-- Create a temp table with each ao-year's sums and counts (sums of _NS and _WP record values and counts of _H, _G, and _L records)
SELECT T.ao, T.year, SUM(T.value) AS SumVals, (SELECT COUNT(*) FROM tbl_slp A WHERE A.ao = T.ao AND A.year = T.year AND (A.proftxt = NULL OR A.proftxt LIKE '%[_]H%' OR A.proftxt LIKE '%[_]G%' OR A.proftxt LIKE '%[_]L%')) AS CountOther
INTO #temp1 
FROM tbl_slp T
WHERE (T.proftxt LIKE '%[_]WP%' OR T.proftxt LIKE '%[_]NS%')
GROUP BY T.ao, T.year

-- Add "sum/count" for each ao-year to the _H, _G, and _L records for that year
UPDATE A
SET value = value + CONVERT(FLOAT, T.SumVals) / T.CountOther
FROM tbl_slp A 
INNER JOIN #temp1 T ON A.ao = T.ao AND A.year = T.year
WHERE (A.proftxt = NULL OR A.proftxt LIKE '%[_]H%' OR A.proftxt LIKE '%[_]G%' OR A.proftxt LIKE '%[_]L%')

-- Now that we've distributed the _WP and _NS values, delete those records
DELETE A
FROM tbl_slp A
INNER JOIN #temp1 T ON A.ao = T.ao AND A.year = T.year
WHERE (A.proftxt LIKE '%[_]WP%' OR A.proftxt LIKE '%[_]NS%')
 AND T.CountOther > 0

COMMIT TRANSACTION

对于您提供的样本集,这会产生完全相同的结果(an 列除外,我认为这是一个错字)。

完全披露,这在样本集上比您的例程花费的时间更长(17 毫秒与您的 3 毫秒相比),但它应该更好地扩展到大数据。为了正确起见,我将其放入事务中,但我不确定您的确切用例是什么,因此这可能是我的方式的一个缺点,因为它会一直锁定页面(并可能升级到整个表)。但是,您的例程没有任何事务,这可能会导致错误数据,因此如果您按自己的方式行事,请确保将每个更新-删除对放在自己的事务中。

另外,如果您在proftxt 上没有索引,请添加一个!这将对两种解决方案产生巨大影响。

祝你好运。这里是the SQL Fiddle I used。

【讨论】:

感谢您的帮助!这比我的想法更快更好。完美运行! :)【参考方案2】:

首先,我看到了几个与 NULL 相关的问题。例如,您的内部循环显然在等待 @AO 变为 NULL 才能完成:

WHILE (@AO >1)

当您将@AO 设置为不存在但难以阅读的内容时,这将起作用,并且您可能想要编写更明确的逻辑。

接下来,这个条件永远为假:

OR proftxt = NULL

NULL 值不等于自身。要测试这种情况,您必须编写:

OR proftxt IS NULL

此外,您的 COUNT(proftxt) 中将省略任何 NULL 值。尝试运行以下示例查询。它返回 1 以及消息“警告:空值已被聚合或其他 SET 操作消除”。

SELECT COUNT(fieldname) FROM (SELECT 1 AS fieldname UNION SELECT NULL AS fieldname) AS tablename

最后,为 proftxt 列建立索引并不能解决您的性能问题,因为带有前导通配符的 LIKE 条件不能使用该索引。您可以将索引想象成按姓氏字母顺序排列的电话簿。如果您正在寻找 LastName LIKE '%mann',那么索引不会帮助您。您仍然需要通读电话簿中的每个条目,才能找到所有以“mann”结尾的姓氏。在数据库术语中,这称为“表扫描”,而且速度很慢。

我会添加一个新列,您可以将其称为 proftxttype。

UPDATE tbl_SLP
SET proftxttype = 1
WHERE proftxt LIKE '%[_]WP%' 
OR proftxt LIKE '%[_]NS%'

UPDATE tbl_SLP
SET proftxttype = 2
WHERE proftxt LIKE '%[_]H%'
OR proftxt LIKE '%[_]G%'
OR proftxt LIKE '%[_]L%'
OR proftxt IS NULL

然后索引该列:

CREATE NONCLUSTERED INDEX [IX_PROFTXTTYPE] ON [dbo].[TBL_SLP] (PROFTXTTYPE ASC) ON [PRIMARY]

现在根据 proftxttype 重写您的更新。当然,每当您插入或更新 proftxt 时,您还必须更新 proftxttype。这是不可避免的,但 SQL Server 会负责使索引保持最新,因此您不必担心索引。

我知道这听起来需要做很多工作,但问题的核心是每次要查找带有前导通配符的 proftxt 值时,您都在扫描整个表。

【讨论】:

谢谢!如您在上面所见,我创建了索引并纠正了我的错误。完美运行! :) 不客气@user2354590!如果我的回答对您有帮助,您可以点个赞。【参考方案3】:

我结合了两个(真的很有帮助!)答案。正如criticalfix 告诉我的那样,我添加了一个用于在表上设置索引的coloum proftype:

ALTER TABLE
ADD proftype CHAR(1)
GO

UPDATE tbl_SLPverrechnetWPNSP 
SET proftype = 'W'
WHERE proftxt LIKE '%[_]WP%' 

UPDATE tbl_SLP 
SET proftype = 'N'
WHERE proftxt LIKE '%[_]NS%'

UPDATE tbl_SLP
SET proftype = 'H'
WHERE proftxt LIKE '%[_]H%'
    OR proftxt IS NULL

UPDATE tbl_SLP 
SET proftype = 'G'
WHERE proftxt LIKE '%[_]G%'

UPDATE tbl_SLP
SET proftype = 'L'
WHERE proftxt LIKE '%[_]L%'

--set index on proftype
CREATE NONCLUSTERED INDEX [IX_PROFTYPE] ON [dbo].[tbl_SLP] (proftype ASC) ON [PRIMARY]
GO

接下来我使用来自 bob 的代码来编辑我的表格。

SET XACT_ABORT ON
SET NOCOUNT ON

BEGIN TRANSACTION

-- Create a temp table with each ao-year's sums and counts (sums of N and W record values and counts of H, G, and L records)
SELECT T.ao, T.year, SUM(CONVERT(float, T.value)) AS SumVals, (SELECT COUNT(*) 
                                                FROM tbl_slp A 
                                                WHERE A.ao = T.ao 
                                                    AND A.year = T.year 
                                                    AND (A.proftype ='G' OR A.proftype = 'H' OR A.proftype = 'L' )) 
                                                AS CountOther
INTO #temp1 
FROM tbl_slp T
WHERE (T.proftype = 'W' OR T.proftype = 'N')
GROUP BY T.ao, T.year

-- Add "sum/count" for each ao-year to the H, G, and L records for that year
UPDATE A
SET value = value + CONVERT(FLOAT, T.SumVals) / T.CountOther
FROM tbl_slp A 
INNER JOIN #temp1 T ON A.ao = T.ao AND A.year = T.year
WHERE (A.proftype = 'H' OR A.proftype = 'G' OR A.proftype LIKE 'L')

-- Now that we've distributed the W and N values, delete those records
DELETE A
FROM tbl_slp A
INNER JOIN #temp1 T ON A.ao = T.ao AND A.year = T.year
WHERE (A.proftype = 'W' OR A.proftype = 'N')
 AND T.CountOther > 0

 DROP TABLE #temp1

COMMIT TRANSACTION

非常感谢您的帮助!例程只跑了 3,5 分钟!!!

【讨论】:

以上是关于更新/删除查询的性能的主要内容,如果未能解决你的问题,请参考以下文章

查询的性能调优

使用子查询更新与使用连接更新 - 性能更好

在单个查询中组合 2 个更新查询 - 性能

聚合查询中的性能更新

删除查询中包含大表的 IN 子句中的子查询性能

更新同一张表的 JPQL 查询的性能改进