AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / dba / 问题 / 22217
Accepted
Geoff Patterson
Geoff Patterson
Asked: 2012-08-10 01:48:11 +0800 CST2012-08-10 01:48:11 +0800 CST 2012-08-10 01:48:11 +0800 CST

SQL Server 不会优化两个等效分区表上的并行合并连接

  • 772

提前为非常详细的问题道歉。我已经包含查询以生成完整的数据集以重现问题,并且我在 32 核机器上运行 SQL Server 2012。但是,我不认为这特定于 SQL Server 2012,并且我已为此特定示例强制 MAXDOP 为 10。

我有两个使用相同分区方案分区的表。当在用于分区的列上将它们连接在一起时,我注意到 SQL Server 无法像预期的那样优化并行合并连接,因此选择使用 HASH JOIN。在这种特殊情况下,我可以通过根据分区函数将查询拆分为 10 个不相交的范围并在 SSMS 中同时运行每个查询来手动模拟更优化的并行 MERGE JOIN。使用 WAITFOR 在完全相同的时间运行它们,结果是所有查询在原始并行 HASH JOIN 使用的总时间的约 40% 内完成。

在等效分区表的情况下,有什么方法可以让 SQL Server 自行进行这种优化?我知道 SQL Server 通常可能会产生大量开销以使 MERGE JOIN 并行,但在这种情况下似乎有一种非常自然的分片方法,开销最小。也许这只是优化器还不够聪明识别的特殊情况?

这是设置简化数据集以重现此问题的 SQL:

/* Create the first test data table */
CREATE TABLE test_transaction_properties 
    ( transactionID INT NOT NULL IDENTITY(1,1)
    , prop1 INT NULL
    , prop2 FLOAT NULL
    )

/* Populate table with pseudo-random data (the specific data doesn't matter too much for this example) */
;WITH E1(N) AS (
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
    UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
, E2(N) AS (SELECT 1 FROM E1 a CROSS JOIN E1 b)
, E4(N) AS (SELECT 1 FROM E2 a CROSS JOIN E2 b)
, E8(N) AS (SELECT 1 FROM E4 a CROSS JOIN E4 b)
INSERT INTO test_transaction_properties WITH (TABLOCK) (prop1, prop2)
SELECT TOP 10000000 (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 5) + 1 AS prop1
                , ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) * rand() AS prop2
FROM E8

/* Create the second test data table */
CREATE TABLE test_transaction_item_detail
    ( transactionID INT NOT NULL
    , productID INT NOT NULL
    , sales FLOAT NULL
    , units INT NULL
    )

 /* Populate the second table such that each transaction has one or more items
     (again, the specific data doesn't matter too much for this example) */
INSERT INTO test_transaction_item_detail WITH (TABLOCK) (transactionID, productID, sales, units)
SELECT t.transactionID, p.productID, 100 AS sales, 1 AS units
FROM test_transaction_properties t
JOIN (
    SELECT 1 as productRank, 1 as productId
    UNION ALL SELECT 2 as productRank, 12 as productId
    UNION ALL SELECT 3 as productRank, 123 as productId
    UNION ALL SELECT 4 as productRank, 1234 as productId
    UNION ALL SELECT 5 as productRank, 12345 as productId
) p
    ON p.productRank <= t.prop1

/* Divides the transactions evenly into 10 partitions */
CREATE PARTITION FUNCTION [pf_test_transactionId] (INT)
AS RANGE RIGHT
FOR VALUES
(1,1000001,2000001,3000001,4000001,5000001,6000001,7000001,8000001,9000001)

CREATE PARTITION SCHEME [ps_test_transactionId]
AS PARTITION [pf_test_transactionId]
ALL TO ( [PRIMARY] )

/* Apply the same partition scheme to both test data tables */
ALTER TABLE test_transaction_properties
ADD CONSTRAINT PK_test_transaction_properties
PRIMARY KEY (transactionID)
ON ps_test_transactionId (transactionID)

ALTER TABLE test_transaction_item_detail
ADD CONSTRAINT PK_test_transaction_item_detail
PRIMARY KEY (transactionID, productID)
ON ps_test_transactionId (transactionID)

现在我们终于准备好重现次优查询了!

/* This query produces a HASH JOIN using 20 threads without the MAXDOP hint,
    and the same behavior holds in that case.
    For simplicity here, I have limited it to 10 threads. */
SELECT COUNT(*)
FROM test_transaction_item_detail i
JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
OPTION (MAXDOP 10)

在此处输入图像描述

在此处输入图像描述

但是,使用单个线程来处理每个分区(下面的第一个分区示例)将导致更有效的计划。我通过在完全相同的时刻对 10 个分区中的每一个运行如下查询来测试这一点,所有 10 个分区都在 1 秒多的时间内完成:

SELECT COUNT(*)
FROM test_transaction_item_detail i
INNER MERGE JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
WHERE t.transactionID BETWEEN 1 AND 1000000
OPTION (MAXDOP 1)

在此处输入图像描述 在此处输入图像描述

sql-server join
  • 2 2 个回答
  • 6835 Views

2 个回答

  • Voted
  1. Best Answer
    Paul White
    2012-08-30T02:31:04+08:002012-08-30T02:31:04+08:00

    你是对的,SQL Server 优化器不喜欢生成并行MERGE连接计划(这种替代方案的成本非常高)。并行MERGE总是需要在两个连接输入上重新分区交换,更重要的是,它要求在这些交换中保留行顺序。

    当每个线程可以独立运行时,并行性是最有效的;订单保留通常会导致频繁的同步等待,并最终可能导致交换溢出以tempdb解决查询内死锁条件。

    这些问题可以通过在每个线程上运行整个查询的多个实例来规避,每个线程处理专有范围的数据。然而,这不是优化器本机考虑的策略。实际上,用于并行性的原始 SQL Server 模型会在交换时中断查询,并在多个线程上运行由这些拆分形成的计划段。

    有一些方法可以实现在多个线程上在专有数据集范围内运行整个查询计划,但它们需要一些技巧,并不是每个人都会满意(并且不会得到 Microsoft 的支持或保证将来可以工作)。一种这样的方法是遍历分区表的分区,并为每个线程分配生成小计的任务。结果是SUM每个独立线程返回的行数:

    从元数据中获取分区号很容易:

    DECLARE @P AS TABLE
    (
        partition_number integer PRIMARY KEY
    );
    
    INSERT @P (partition_number)
    SELECT
        p.partition_number
    FROM sys.partitions AS p 
    WHERE 
        p.[object_id] = OBJECT_ID(N'test_transaction_properties', N'U')
        AND p.index_id = 1;
    

    然后我们使用这些数字来驱动一个关联连接 ( APPLY),以及将$PARTITION每个线程限制为当前分区号的函数:

    SELECT
        row_count = SUM(Subtotals.cnt)
    FROM @P AS p
    CROSS APPLY
    (
        SELECT
            cnt = COUNT_BIG(*)
        FROM dbo.test_transaction_item_detail AS i
        JOIN dbo.test_transaction_properties AS t ON
            t.transactionID = i.transactionID
        WHERE 
            $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
            AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
    ) AS SubTotals;
    

    查询计划显示MERGE为 table 中的每一行执行连接@P。聚集索引扫描属性确认每次迭代只处理一个分区:

    应用串行计划

    不幸的是,这只会导致分区的顺序串行处理。在您提供的数据集上,我的 4 核(超线程到 8)笔记本电脑在7 秒内返回正确的结果,所有数据都在内存中。

    为了让MERGE子计划同时运行,我们需要一个并行计划,其中分区 ID 分布在可用线程 ( MAXDOP) 上,每个MERGE子计划使用一个分区中的数据在单个线程上运行。不幸的是,优化器经常MERGE以成本为由决定反对并行,并且没有记录的方法来强制并行计划。有一种未记录(且不受支持)的方式,使用跟踪标志 8649:

    SELECT
        row_count = SUM(Subtotals.cnt)
    FROM @P AS p
    CROSS APPLY
    (
        SELECT
            cnt = COUNT_BIG(*)
        FROM dbo.test_transaction_item_detail AS i
        JOIN dbo.test_transaction_properties AS t ON
            t.transactionID = i.transactionID
        WHERE 
            $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
            AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
    ) AS SubTotals
    OPTION (QUERYTRACEON 8649);
    

    现在,查询计划显示分区号@P以循环方式分布在线程之间。每个线程都在嵌套循环连接的内侧运行单个分区,从而实现我们同时处理不相交数据的目标。现在,我的 8 个超核在3 秒内返回相同的结果,所有 8 个超核的利用率均为 100%。

    并行应用

    我不建议您一定要使用这种技术 - 请参阅我之前的警告 - 但它确实解决了您的问题。

    有关详细信息,请参阅我的文章提高分区表连接性能。

    列存储

    看到您使用的是 SQL Server 2012(并假设它是 Enterprise),您还可以选择使用列存储索引。这显示了在有足够内存可用的情况下批处理模式哈希连接的潜力:

    CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
    ON dbo.test_transaction_properties (transactionID);
    
    CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
    ON dbo.test_transaction_item_detail (transactionID);
    

    有了这些索引,查询...

    SELECT
        COUNT_BIG(*)
    FROM dbo.test_transaction_properties AS ttp
    JOIN dbo.test_transaction_item_detail AS ttid ON
        ttid.transactionID = ttp.transactionID;
    

    ...从优化器中得到以下执行计划,没有任何技巧:

    列存储计划 1

    在2 秒内正确结果,但消除标量聚合的行模式处理有助于更多:

    SELECT
        COUNT_BIG(*)
    FROM dbo.test_transaction_properties AS ttp
    JOIN dbo.test_transaction_item_detail AS ttid ON
        ttid.transactionID = ttp.transactionID
    GROUP BY
        ttp.transactionID % 1;
    

    优化的列存储

    优化的列存储查询在851ms中运行。

    Geoff Patterson 创建了错误报告Partition Wise Joins,但由于无法修复而被关闭。

    • 19
  2. podiluska
    2012-08-10T01:52:11+08:002012-08-10T01:52:11+08:00

    使优化器以您认为更好的方式工作的方法是通过查询提示。

    在这种情况下,OPTION (MERGE JOIN)

    或者你可以全力以赴并使用USE PLAN

    • 0

相关问题

  • INNER JOIN 和 OUTER JOIN 有什么区别?

  • 什么时候应该使用唯一约束而不是唯一索引?

  • 死锁的主要原因是什么,可以预防吗?

  • JOIN 语句的输出是什么样的?

  • 如何确定是否需要或需要索引

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    如何查看 Oracle 中的数据库列表?

    • 8 个回答
  • Marko Smith

    mysql innodb_buffer_pool_size 应该有多大?

    • 4 个回答
  • Marko Smith

    列出指定表的所有列

    • 5 个回答
  • Marko Smith

    从 .frm 和 .ibd 文件恢复表?

    • 10 个回答
  • Marko Smith

    如何在不修改我自己的 tnsnames.ora 的情况下使用 sqlplus 连接到位于另一台主机上的 Oracle 数据库

    • 4 个回答
  • Marko Smith

    你如何mysqldump特定的表?

    • 4 个回答
  • Marko Smith

    如何选择每组的第一行?

    • 6 个回答
  • Marko Smith

    使用 psql 列出数据库权限

    • 10 个回答
  • Marko Smith

    如何从 PostgreSQL 中的选择查询中将值插入表中?

    • 4 个回答
  • Marko Smith

    如何使用 psql 列出所有数据库和表?

    • 7 个回答
  • Martin Hope
    Mike Walsh 为什么事务日志不断增长或空间不足? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland 列出指定表的所有列 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney MySQL 能否合理地对数十亿行执行查询? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx 如何监控大型 .sql 文件的导入进度? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison 你如何mysqldump特定的表? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    pedrosanta 使用 psql 列出数据库权限 2011-08-04 11:01:21 +0800 CST
  • Martin Hope
    Jonas 如何使用 psql 对 SQL 查询进行计时? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas 如何从 PostgreSQL 中的选择查询中将值插入表中? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas 如何使用 psql 列出所有数据库和表? 2011-02-18 00:45:49 +0800 CST
  • Martin Hope
    bernd_k 什么时候应该使用唯一约束而不是唯一索引? 2011-01-05 02:32:27 +0800 CST

热门标签

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve