【问题标题】:What is the optimal JOIN approach here?这里的最佳 JOIN 方法是什么?
【发布时间】:2012-06-18 21:15:32
【问题描述】:

这里有两个查询返回相同的结果集,但哪个是最佳语句或无关紧要?

SELECT A.id, B.somefield FROM (
   SELECT id from table1
   UNION
   SELECT id from table2
) A LEFT JOIN table3 B on A.id = B.id

SELECT A.id, B.somefield FROM table1 A LEFT JOIN table3 B on A.id = B.id
UNION
SELECT A.id, B.somefield FROM table2 B LEFT JOIN table3 B on A.id = B.id

我意识到我可以将它们充满数据并运行一些测试,但我同样对“为什么”感兴趣,如果一个更快? (我正在使用 postgresql,以防它影响事情)。

谢谢。

【问题讨论】:

  • 作为切线点,如果您无法在 table1 和 table2 之间获取重复的 id 值,UNION ALL 将比普通的 UNION 更快,因为它不需要检查重复项.

标签: sql postgresql join


【解决方案1】:

使用UNION的执行计划首先显示的步骤少了很多,不幸的是,执行计划并不是全部,还有表扫描、逻辑读取、CPU使用率,所以这不是全部,结束它很大程度上取决于您的数据和指标。

第一个查询应该执行得更好,因为UNION 删除重复项发生在连接之前,导致对表 3 的表扫描次数减少。如果表 1 和表 2 中没有重复项,那么应该没有区别。

这可以通过一些示例数据来证明。我的所有示例都使用以下 5 个表(T4 和 T5 只是将输出转储到其中,因此您不必在 SQL fiddle 页面向下滚动数英里即可查看执行计划)

CREATE TABLE T1 (ID INT NOT NULL);
CREATE TABLE T2 (ID INT NOT NULL);
CREATE TABLE T3 (FK INT NOT NULL, SomeValue VARCHAR(10) NOT NULL);
CREATE TABLE T4 (ID INT NOT NULL, SomeValue VARCHAR(10) NULL);
CREATE TABLE T5 (ID INT NOT NULL, SomeValue VARCHAR(10) NULL);

并且全部使用以下进行测试(也反向完成以消除任何查询计划缓存):

INSERT INTO T4
SELECT  ID, SomeValue
FROM    T1
        LEFT JOIN T3
            ON ID = FK
UNION 
SELECT  ID, SomeValue
FROM    T2
        LEFT JOIN T3
            ON ID = FK;

INSERT INTO T5
SELECT  ID, SomeValue
FROM    (   SELECT  ID
            FROM    T1
            UNION
            SELECT  ID
            FROM    T2
        ) T
        LEFT JOIN T3
            ON ID = FK;

示例 1 - T1 包含也在 T2 中的行

INSERT INTO T1 (ID)
SELECT  *
FROM    GENERATE_SERIES(0, 40000);

INSERT INTO T2 (ID)
SELECT  *
FROM    GENERATE_SERIES(20000, 60000);

INSERT INTO T3 (FK, SomeValue)
SELECT  *, 'VALUE'
FROM    GENERATE_SERIES(10000, 50000);

Example on SQL Fiddle 表明插入到 T4(UNION 之前 JOIN)表现更好。我已经运行了 25 次,其中 22 次插入到 T4 运行得更快。没有足够的数据从等式中消除服务器负载,因此正如预期的那样,存在一些异常情况。在this example 中插入的顺序颠倒了,再次看到了类似的结果。

示例 2 - table1 和 table2 中没有重复项

INSERT INTO T1 (ID)
SELECT  *
FROM    GENERATE_SERIES(0, 30000);

INSERT INTO T2 (ID)
SELECT  *
FROM    GENERATE_SERIES(30001, 60000);

INSERT INTO T3 (FK, SomeValue)
SELECT  *, 'VALUE'
FROM    GENERATE_SERIES(10000, 50000);

在这个例子中,执行时间更接近,并且经常在哪个方法执行得更快之间切换。

Sample Data

Sample Data 2

最后,重申已经提出的观点,如果您不期待被欺骗/不关心被欺骗,那么UNION ALL 将提高性能,但由于没有欺骗,两种方法的性能应该大致相似这应该在同等程度上改进这两种方法。我没有对此进行测试,但更改我用来检查的测试数据应该不是一项艰巨的任务。

编辑

我刚刚尝试了 SQL Fiddle 上的查询,它们显示出的差异比在我的本地计算机上的差异要大得多,因此请用少量盐来处理这些示例并在您自己的服务器上进行测试,这样更容易创造一个公平的测试环境!

【讨论】:

  • 仅供参考 - 基于 sql fiddle 的实际执行时间的性能测试非常有限,价值不大。太多的因素(CPU 负载、操作系统配置、硬件设置、有限的数据大小等)将与 sql fiddle 不同,然后将在真实环境中。关于性能,我建议您使用 SQL fiddle 的唯一一件事是比较执行计划。
  • @Gareth 感谢您的所有努力 - !标记了您,但选择了 Keith 的答案,因为我觉得它确实检查了发生的事情背后的核心数学。
【解决方案2】:

好的,首先,选择列表中的id 有歧义;我们要A.id 还是B.id

其次,假设id是所有表中的索引字段,去重和join都是NlogM操作,其中N是“左侧”的行数,M是“右侧”的行数.对于 N 中的每一行,必须找到或未找到 M 中的匹配行(连接时,将 M 中找到的行包含在结果中;合并时,排除 M 中找到的行)。这意味着最小化左侧的基数将提供最佳性能。

因此,任一查询的复杂性很大程度上取决于表 1 和表 2 之间有多少共享 ID。在零共性(没有相同的行 ID)和每个表 100 行的情况下,第一个查询将执行 100log100 union 然后是 200log100 join,第二个查询将执行两个 100log100 join,然后是 100log100 union,这将在相同的时间内执行。然而,在 100% 的通用性下(表 1 中的每一行也在 2 中),第一个查询将执行 100log100 联合,然后是 100log100 连接(因为 1 和 2 的 UNION 将等同于表 1),而第二个查询仍将执行两个 100log100 连接和一个 100log100 联合。由于最坏情况相同,但查询 1 的最佳情况是查询 2 的三分之二,所以我选择查询 1。

但是,正如评论者所说,如果您不期望任何欺骗,则 UNION ALL 在两个查询中都会表现得更好。 A 和 B 的 UNION ALL 的结果是 A+B,它只受每个集合的访问时间的约束(我没有考虑过)。由于不希望出现欺骗,这两个查询都可以减少到第一个查询的最佳性能。

【讨论】:

  • 谢谢 - 我发现这真的很有用,实际上有点令人着迷(我知道很难过!)
【解决方案3】:

对每个查询使用EXPLAIN ANALYZE SELECT A.id, ... 并比较结果。他们可能是一样的。

【讨论】:

    【解决方案4】:

    为每个查询运行 postgres'EXPLAIN 并查看执行成本。

    【讨论】:

      【解决方案5】:

      下面的查询生成的查询计划与@GarethD 的两个不同,但执行大致相同(添加主键后):

      -- EXPLAIN ANALYZE
      WITH ttt AS (
          SELECT COALESCE(t1.id,t2.id) AS id
              FROM    t1
              FULL OUTER JOIN t2 ON t1.id = t2.id
          )   
      INSERT INTO t6 (id, somevalue)
      SELECT tx.id AS id
          , t3.somevalue AS somevalue
      FROM ttt tx
      LEFT JOIN t3 ON tx.id = t3.fk
              ;
      

      注意:在查询计划中,COALESCE() 函数不存在,因此它实际上被视为运算符(这很好):

                                                             QUERY PLAN                                                         
      ---------------------------------------------------------------------------------------------------------------------------
       Insert on t6  (cost=3761.65..5761.69 rows=40001 width=10) (actual time=1643.429..1643.429 rows=0 loops=1)
         CTE ttt
           ->  Hash Full Join  (cost=1004.80..2488.62 rows=40001 width=8) (actual time=129.768..399.898 rows=60001 loops=1)
                 Hash Cond: (t1.id = t2.id)
                 ->  Seq Scan on t1  (cost=0.00..557.01 rows=40001 width=4) (actual time=0.008..60.693 rows=40001 loops=1)
                 ->  Hash  (cost=533.80..533.80 rows=37680 width=4) (actual time=129.737..129.737 rows=40001 loops=1)
                       Buckets: 4096  Batches: 1  Memory Usage: 938kB
                       ->  Seq Scan on t2  (cost=0.00..533.80 rows=37680 width=4) (actual time=0.016..62.236 rows=40001 loops=1)
         ->  Hash Left Join  (cost=1273.02..3273.06 rows=40001 width=10) (actual time=265.672..999.408 rows=60001 loops=1)
               Hash Cond: (tx.id = t3.fk)
               ->  CTE Scan on ttt tx  (cost=0.00..800.02 rows=40001 width=4) (actual time=129.776..603.317 rows=60001 loops=1)
               ->  Hash  (cost=597.01..597.01 rows=40001 width=10) (actual time=135.854..135.854 rows=40001 loops=1)
                     Buckets: 4096  Batches: 2  Memory Usage: 627kB
                     ->  Seq Scan on t3  (cost=0.00..597.01 rows=40001 width=10) (actual time=0.009..66.008 rows=40001 loops=1)
       Total runtime: 1644.480 ms
      

      通过更好的调整(更少的 work_mem,强制基于磁盘的执行,以及更低的 random_page_cost 以促进索引的使用),计划甚至变得更好:

      SET work_mem = 64;
      SET random_page_cost = 2.1;
      SET seq_page_cost = 2;
      
      
                                                                    QUERY PLAN                                                              
      --------------------------------------------------------------------------------------------------------------------------------------
       Insert on t6  (cost=4404.54..6654.58 rows=40001 width=10) (actual time=1573.465..1573.465 rows=0 loops=1)
         CTE ttt
           ->  Merge Full Join  (cost=0.00..2758.52 rows=40001 width=8) (actual time=0.048..348.906 rows=60001 loops=1)
                 Merge Cond: (t1.id = t2.id)
                 ->  Index Scan using t1_pkey on t1  (cost=0.00..1103.37 rows=40001 width=4) (actual time=0.022..67.840 rows=40001 loops=1)
                 ->  Index Scan using t2_pkey on t2  (cost=0.00..1084.15 rows=37680 width=4) (actual time=0.018..68.583 rows=40001 loops=1)
         ->  Hash Left Join  (cost=1646.02..3896.06 rows=40001 width=10) (actual time=170.840..957.899 rows=60001 loops=1)
               Hash Cond: (tx.id = t3.fk)
               ->  CTE Scan on ttt tx  (cost=0.00..800.02 rows=40001 width=4) (actual time=0.055..544.544 rows=60001 loops=1)
               ->  Hash  (cost=794.01..794.01 rows=40001 width=10) (actual time=170.130..170.130 rows=40001 loops=1)
                     Buckets: 1024  Batches: 32  Memory Usage: 42kB
                     ->  Seq Scan on t3  (cost=0.00..794.01 rows=40001 width=10) (actual time=0.009..79.751 rows=40001 loops=1)
       Total runtime: 1574.108 ms
      (13 rows)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-12
        • 2021-12-25
        • 2020-06-28
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多