SQL Server 2008 R2:优化查询性能

Posted

技术标签:

【中文标题】SQL Server 2008 R2:优化查询性能【英文标题】:SQL Server 2008 R2: Optimizing query performance 【发布时间】:2015-03-13 14:49:33 【问题描述】:

在运行相对简单的查询时,我遇到了一些性能问题。情况如下:

我有一张桌子,我们把它命名为[Old_Table],它是这样设置的:

Document ID  | Parsed_Codes
-------------+-------------
Document_1   |  a
Document_1   |  b
Document_1   |  c
Document_2   |  a
Document_2   |  d
Document_3   |  a
Document_3   |  c

此表共有 250 万行和大约 500k 个唯一的 [Document ID] 值。

我想要做的是将这个表聚合成一个名为[New_Table] 的新表,应该如下所示:

Document ID  | New_Parsed_Codes
-------------+-----------------
Document_1   |  a; b; c
Document_2   |  a; d
Document_3   |  a; c

为此,我创建了以下查询:

SELECT 
    t1.[Document ID] as [Document ID],
    Stuff((SELECT '; ' + CONVERT(NVARCHAR(max), Parsed_Codes)
           FROM dbo.[Old_Table] t2
           WHERE t2.[Document ID] = t1.[Document ID]
           FOR XML PATH('')), 1, 2, '') as [New_Parsed_Codes]
INTO dbo.[New_Table]
FROM dbo.[Old_Table] t1
GROUP BY t1.[Document ID]

现在的问题是这些数字似乎并不算大,但查询很容易需要 16 到 32 小时才能完成。我运行它的机器有 120GB RAM 和 24 个内核。

现在的问题是;有什么方法可以改变查询以提高效率。或者可能有不同的方法一起使用

【问题讨论】:

表格“按字节”有多大? (例如 sp_spaceused 的数据列的结果)。从我“推断”的示例中,我们谈论的是 250 万行,每行大约 40 个字节,提供少于 100Mb 的(相关)数据。我假设您简化了示例? 【参考方案1】:

执行group_concat 的另一种方法是使用cross apply 而不是correlated subquery。试试这个。

SELECT t1.[Document ID] AS [Document ID],
LEFT(cs.New_Parsed_Codes, Len(cs.New_Parsed_Codes) - 1) AS New_Parsed_Codes
FROM   Old_Table t1
       CROSS APPLY (SELECT '; ' + CONVERT(NVARCHAR(max), Parsed_Codes)
                    FROM   dbo.[Old_Table] t2
                    WHERE  t2.[Document ID] = t1.[Document ID]
                    FOR XML PATH('')) cs ([New_Parsed_Codes])
GROUP  BY t1.[Document ID],
          LEFT(cs.New_Parsed_Codes, Len(cs.New_Parsed_Codes) - 1) 

【讨论】:

谢谢。我目前正在运行查询,如果它提高了性能,我会通知您。【参考方案2】:

由于所涉及的数字(250 万行,看似很小的记录)听起来并不过分,而且所描述的机器似乎令人印象深刻,我想知道这在我的笔记本电脑上运行起来会有多糟糕。因此,我创建了这个“模拟”问题的测试:

IF DB_ID('test') IS NULL CREATE DATABASE test
GO
USE test
GO

SET NOCOUNT ON

PRINT Convert(varchar, current_timestamp, 113) + ' - starting up, creating t_old_table...'

IF OBJECT_ID('t_old_table') IS NOT NULL DROP TABLE t_old_table

GO
CREATE TABLE t_old_table (row_id int IDENTITY(1, 1) PRIMARY KEY, 
                          document_id nvarchar(50) NOT NULL, 
                          parsed_codes nvarchar(50) NOT NULL)
GO

PRINT Convert(varchar, current_timestamp, 113) + ' - starting up, creating docuemnt_id''s...'

-- create unique document_id's first
DECLARE @counter int = 1,
        @target  int = 500000,
        @block   int = 10000

INSERT t_old_table (document_id, parsed_codes) VALUES (Reverse(Convert(nvarchar(50), NewID())), Convert(nvarchar(50), BINARY_CHECKSUM(NewID())))

WHILE @counter < @target
    BEGIN
        INSERT t_old_table (document_id, parsed_codes) 
        SELECT TOP (CASE WHEN @counter + @block > @target THEN @target - @counter ELSE @block END)
               Reverse(Convert(nvarchar(50), NewID())), 
               Convert(nvarchar(50), BINARY_CHECKSUM(NewID()))
          FROM t_old_table

        SELECT @counter = @counter + @@ROWCOUNT
    END

PRINT Convert(varchar, current_timestamp, 113) + ' - starting up, adding parsed codes...'

-- add parsed-codes to existing document_id's
SELECT @target = @target * 5

WHILE @counter < @target
    BEGIN
        INSERT t_old_table (document_id, parsed_codes) 
        SELECT TOP (CASE WHEN @counter + @block > @target THEN @target - @counter ELSE @block END)
               document_id, 
               Convert(nvarchar(50), BINARY_CHECKSUM(NewID()))
          FROM t_old_table
         ORDER BY NewID() -- some document_id's will have more, some will have less

        SELECT @counter = @counter + @@ROWCOUNT
    END

UPDATE STATISTICS t_old_table

PRINT Convert(varchar, current_timestamp, 113) + ' - Creating t_new_table...'

GO
IF OBJECT_ID('t_new_table') IS NOT NULL DROP TABLE t_new_table
GO
CREATE TABLE t_new_table (document_id nvarchar(50) NOT NULL PRIMARY KEY, 
                          parsed_codes_list nvarchar(max) NOT NULL)

GO

在我的 (t)rusty i5 笔记本电脑上运行此程序大约需要 6 分钟

2015 年 3 月 13 日 22:20:09:053 - 启动,t_old_table... 2015 年 3 月 13 日 22:20:09:073 - 启动,创建文档 ID... 2015 年 3 月 13 日 22:20:13:273 - 启动,添加解析代码... 2015 年 3 月 13 日 22:26:27:023 - 创建 t_new_table...

接下来我采取了以下方法:

PRINT Convert(varchar, current_timestamp, 113) + ' - Creating #numbered table...'

-- step 1, make temp-table that holds 'correct' order
IF OBJECT_ID('tempdb..#numbered') IS NOT NULL DROP TABLE #numbered
GO
SELECT document_id,
       parsed_codes,
       order_nbr = ROW_NUMBER() OVER (PARTITION BY document_id ORDER BY parsed_codes)
  INTO #numbered
  FROM t_old_table  
 WHERE 1 = 2

CREATE UNIQUE CLUSTERED INDEX uq0 ON #numbered (order_nbr, document_id)

INSERT #numbered
SELECT document_id,
       parsed_codes,
       order_nbr = ROW_NUMBER() OVER (PARTITION BY document_id ORDER BY parsed_codes )
  FROM t_old_table

GO
-- extract parsed codes
DECLARE @nbr int = 1,
        @rowcount int 

SET NOCOUNT OFF
PRINT Convert(varchar, current_timestamp, 113) + ' - Converting to t_new_table, step ' + convert(varchar, @nbr) + '...'

INSERT t_new_table (document_id, parsed_codes_list)
SELECT document_id, parsed_codes
  FROM #numbered
 WHERE order_nbr = @nbr

SELECT @rowcount = @@ROWCOUNT

UPDATE STATISTICS t_new_table

WHILE @rowcount > 0
    BEGIN
        SELECT @nbr = @nbr + 1

        PRINT Convert(varchar, current_timestamp, 113) + ' - Converting to t_new_table, step ' + convert(varchar, @nbr) + '...'

        UPDATE t_new_table
           SET parsed_codes_list = parsed_codes_list + ';' + n.parsed_codes
          FROM t_new_table
          JOIN #numbered n
            ON n.document_id = t_new_table.document_id
           AND n.order_nbr = @nbr

        SELECT @rowcount = @@ROWCOUNT
    END

GO

-- all done
PRINT Convert(varchar, current_timestamp, 113) + ' - All done.'

这确实需要预先进行一些排序,但是一旦事情开始循环,连接实际上就非常简单和快速。事实上,整个过程不到一分钟就完成了。

2015 年 3 月 13 日 22:26:27:030 - 创建 #numbered 表... 2015 年 3 月 13 日 22:26:41:307 - 转换为 t_new_table,步骤 1... (500000 行受影响) 2015 年 3 月 13 日 22:26:45:543 - 转换为 t_new_table,第 2 步... (400986 行受影响) 2015 年 3 月 13 日 22:26:49:863 - 转换为 t_new_table,第 3 步... (受影响的 322042 行) [...] 2015 年 3 月 13 日 22:27:15:713 - 转换为 t_new_table,步骤 62... (1 行受影响) 2015 年 3 月 13 日 22:27:15:900 - 转换为 t_new_table,步骤 63... (0 行受影响) 2015 年 3 月 13 日 22:27:15:940 - 全部完成。

由于我无法相信我的笔记本电脑/解决方案比您拥有的要快得多,因此我运行了您的查询

SELECT 
    t1.document_id as document_id,
    Stuff((SELECT '; ' + CONVERT(NVARCHAR(max), parsed_codes)
           FROM t_old_table t2
           WHERE t2.document_id = t1.document_id
           FOR XML PATH('')), 1, 2, '') as [New_parsed_codes]
INTO dbo.[New_Table]
FROM t_old_table t1
GROUP BY t1.document_id

它在 40 秒内运行。

这让我相信你需要进一步解释一下情况(大小确实很重要=),这样我们才能更好地掌握时间花在哪里;或者你有硬件问题......

【讨论】:

以上是关于SQL Server 2008 R2:优化查询性能的主要内容,如果未能解决你的问题,请参考以下文章

优化 SQL Server 2008 查询

如何在SQL Server 2008R2中查找未使用的列

SQL Server 2008R2 用户定义函数(表值)性能

ODBC 性能 Access 2010 和 SQL Server 2008 R2

关于视图和索引 (SQL Server 2008 R2)

SQL Server 2008r2 提交性能问题