神奇的SQL MERGE语句(详细指南)

SQL MERGE语句是一个神秘的设备,它的威力仅次于它的力量。 根据标准SQL,一个简单的例子显示了它的全部力量。想象一下,你有一个产品价格的生产表,以及一个你想从其中加载最新价格的暂存表。这一次,我使用了Db2 LuW MERGE语法,因为这是目前最符合标准的语法(在我们jOOQ支持的方言中):


DROP TABLE IF EXISTS prices;
DROP TABLE IF EXISTS staging;

CREATE TABLE prices (
  product_id BIGINT NOT NULL PRIMARY KEY,
  price DECIMAL(10, 2) NOT NULL,
  price_date DATE NOT NULL,
  update_count BIGINT NOT NULL
);

CREATE TABLE staging (
  product_id BIGINT NOT NULL PRIMARY KEY,
  price DECIMAL(10, 2) NOT NULL
);

DELETE FROM prices;
DELETE FROM staging;
INSERT INTO staging 
VALUES (1, 100.00),
       (2, 125.00),
       (3, 150.00);

所以,我们已经在我们的暂存表中加载了一些记录,我们现在想把它们合并到价格表中。我们可以直接插入这些记录,很容易,但我们以后会有更多的价格,例如这些:

DELETE FROM staging;
INSERT INTO staging 
VALUES (1, 100.00),
       (2,  99.00),
       (4, 300.00);

我们希望我们的逻辑是这样的:

  • 分期表中的所有新ID应该直接插入到价格表中。
  • 现有的ID应该被更新,当且仅当价格发生了变化。在这种情况下,update_count应该增加。
  • 在暂存表中不再遇到的价格应该从价格表中删除,以实现完全同步,而不是三角洲同步,为了这个例子的目的。我们还可以添加一个 "命令 "列,其中包含关于数据是否应该被更新或删除的指令,以实现delta同步。

所以,这就是我们用于工作的Db2(符合标准)的MERGE语句:

MERGE INTO prices AS p
USING (
  SELECT COALESCE(p.product_id, s.product_id) AS product_id, s.price
  FROM prices AS p
  FULL JOIN staging AS s ON p.product_id = s.product_id
) AS s
ON (p.product_id = s.product_id)
WHEN MATCHED AND s.price IS NULL THEN DELETE
WHEN MATCHED AND p.price != s.price THEN UPDATE SET 
  price = s.price,
  price_date = CURRENT_DATE,
  update_count = update_count + 1
WHEN NOT MATCHED THEN INSERT 
  (product_id, price, price_date, update_count)
VALUES 
  (s.product_id, s.price, CURRENT_DATE, 0);

如果你一辈子都没有写过MERGE语句,那就没那么简单了。如果是这样,不要害怕。和大多数SQL一样,可怕的部分是语法(关键字,UPPER CASE,等等)。底层概念比一开始看起来要简单。让我们一步一步地去了解它。它有4个部分:1.目标表就像INSERT语句一样,我们可以定义我们想把数据合并到哪里。这是最简单的部分:

MERGE INTO prices AS p
-- ...

2.源表USING关键字包装了一个我们要合并的源表。我们可以直接把暂存表放在这里,但我想先用一些额外的数据来充实源数据。我使用FULL JOIN来产生旧数据(价格)和新数据(暂存)之间的匹配。如果在第二次填充中转表之后,但在运行MERGE语句之前,我们单独运行USING子句(为了说明问题做了一些小的修改):

SELECT 
  COALESCE(p.product_id, s.product_id) AS product_id, 
  p.price AS old_price, 
  s.price AS new_price
FROM prices AS p
FULL JOIN staging AS s ON p.product_id = s.product_id
ORDER BY product_id

那么我们会得到这样的结果:

PRODUCT_ID|OLD_PRICE|NEW_PRICE|
----------|---------|---------|
         1|   100.00|   100.00| <-- same price
         2|   125.00|    99.00| <-- updated price
         3|   150.00|         | <-- deleted price
         4|         |   300.00| <-- added price

Neat!3.ON子句接下来,我们使用ON子句对目标表和源表进行RIGHT JOIN,就像普通JOIN一样:

ON (p.product_id = s.product_id)

MERGE总是使用RIGHT JOIN的语义,这就是为什么我在源表中放置一个FULL JOIN,即USING子句。我们完全可以用不同的方式来写,这样就可以避免两次访问价格表,但我想展示这个语句的全部威力。注意,SQL Server使用FULL JOIN连接源表和目标表,我将进一步解释。我也会马上解释为什么要用RIGHT JOIN**。**4.WHEN子句现在,有趣的部分来了!两个表(目标表和源表)之间可以有一个匹配,就像我们得到INNER JOIN的结果一样,或者没有这样的匹配,因为源表包含一条没有被目标表匹配的记录(RIGHT JOIN的语义)。在我们的例子中,PRODUCT_ID IN (1, 2, 3)将产生一个匹配(包含在源表和目标表中),而PRODUCT_ID = 4将不会产生一个匹配(还不包含在目标表中)。 为我们的源数据集着色:

PRODUCT_ID|OLD_PRICE|NEW_PRICE|
----------|---------|---------|

下面是一连串的匹配指令,这些指令将按照出现的顺序,对前一个RIGHT JOIN产生的每一条记录进行执行:

-- With my FULL JOIN, I've produced NULL price values
-- whenever a PRODUCT_ID is in the target table, but not
-- in the source table. These rows, we want to DELETE
WHEN MATCHED AND s.price IS NULL THEN DELETE

-- When there is a price change (and only then), we 
-- want to update the price information in the target table.
WHEN MATCHED AND p.price != s.price THEN UPDATE SET 
  price = s.price,
  price_date = CURRENT_DATE,
  update_count = update_count + 1

-- Finally, when we don't have a match, i.e. a row is
-- in the source table, but not in the target table, then
-- we simply insert it into the target table.
WHEN NOT MATCHED THEN INSERT 
  (product_id, price, price_date, update_count)
VALUES 
  (s.product_id, s.price, CURRENT_DATE, 0);

这不是太复杂,只是有很多关键词和语法。因此,在中转表的第二组数据上运行这个MERGE后,我们将在价格表中得到这样的结果:

扫描二维码关注公众号,回复: 14422430 查看本文章
PRODUCT_ID|PRICE |PRICE_DATE|UPDATE_COUNT|
----------|------|----------|------------|
         1|100.00|2020-04-09|           0|
         2| 99.00|2020-04-09|           1|
         4|300.00|2020-04-09|           0|

我表达这个MERGE语句的方式,它是同位的,也就是说,我可以在相同的中转表内容上再次运行它,它不会修改价格表中的任何数据--因为没有一个WHEN语句适用。同位素不是MERGE的属性,我只是把语句写成这样。

方言的具体内容

有一些方言支持MERGE。在jOOQ 3.13所支持的方言中,至少有:

  • Db2
  • Derby
  • Firebird
  • H2
  • HSQLDB
  • Oracle
  • SQL Server
  • Sybase SQL Anywhere
  • Teradata
  • Vertica

遗憾的是,这一次,这个列表没有包括PostgreSQL。但即使是这个列表中的方言,也没有就MERGE的真正含义达成一致。SQL标准规定了3个功能,每一个都是可选的:

  • F312 MERGE 语句
  • F313 增强型MERGE语句
  • F314 带有DELETE分支的MERGE语句

但是,与其看标准和它们的要求,不如看看方言提供了什么,以及如果没有的东西,如何模仿它。

AND子句

你可能已经注意到,这篇文章使用了语法。

WHEN MATCHED AND <some predicate> THEN

也可以指定

WHEN NOT MATCHED AND <some predicate> THEN

除了Teradata之外,大多数方言都支持这些AND子句(Oracle有一个使用WHERE的特殊语法,我将在后面讲到)。 这些子句的意义在于能够有几个这样的WHEN MATCHED或WHEN NOT MATCHED子句,实际上是任意数量的子句。不幸的是,这并不是所有的方言都支持的。有些方言只支持每种类型的一个子句(INSERT, UPDATE, DELETE)。严格来说,支持几个子句并不是必须的,但正如我们将在下面看到的那样,它更方便。 这些方言不支持多个WHEN MATCHED或WHEN NOT MATCHED子句:

  • HSQLDB
  • Oracle
  • SQL Server
  • Teradata

如果一种方言不支持AND,或者不支持多个WHEN MATCHED子句,只需将这些子句翻译成case表达式。我们将得到的不是我们之前的WHEN子句,而是:

-- The DELETE clause doesn't make much sense without AND,
-- So there's not much we can do about this emulation in Teradata.
WHEN MATCHED AND s.price IS NULL THEN DELETE

-- Repeat the AND clause in every branch of the CASE
-- Expression where it applies
WHEN MATCHED THEN UPDATE SET 
  price = CASE

    -- Update the price if the AND clause applies
    WHEN p.price != s.price THEN s.price,

    -- Otherwise, leave it untouched
    ELSE p.price
  END

  -- Repeat for all columns
  price_date = CASE
    WHEN p.price != s.price THEN CURRENT_DATE
    ELSE p.price_date
  END,
  update_count = CASE
    WHEN p.price != s.price THEN update_count + 1
    ELSE p.update_count
  END

-- Unchanged, in this case
WHEN NOT MATCHED THEN INSERT 
  (product_id, price, price_date, update_count)
VALUES 
  (s.product_id, s.price, CURRENT_DATE, 0);

形式主义是这样的:如果没有AND,则添加AND这些都是一样的:

WHEN MATCHED THEN [ UPDATE | DELETE ]
WHEN MATCHED AND 1 = 1 THEN [ UPDATE | DELETE ]

在Firebird(它在这方面有一个bug)和SQL Server(它不允许在没有AND子句的WHEN MATCHED子句之后有WHEN MATCHED子句,这有点像一个linting错误)可能需要这种替换。你可以跳过所有后续的WHEN MATCHED分支,而不是模仿东西,因为它们不会应用。每条记录只更新一次,即只通过一个WHEN子句:每条记录只更新一次按照标准要求,确保在仿真中没有一条记录被更新超过一次。在写这个的时候:

WHEN MATCHED AND p1 THEN UPDATE SET c1 = 1
WHEN MATCHED AND p2 THEN DELETE
WHEN MATCHED AND p3 THEN UPDATE SET c1 = 3, c2 = 3
WHEN MATCHED AND p4 THEN DELETE

这实际上意味着与:

WHEN MATCHED AND p1 THEN UPDATE SET c1 = 1
WHEN MATCHED AND NOT p1 AND p2 THEN DELETE
WHEN MATCHED AND NOT p1 AND NOT p2 AND p3 THEN UPDATE SET c1 = 3,c2 = 3
WHEN MATCHED AND NOT p1 AND NOT p2 AND NOT p3 AND p4 THEN DELETE

要模拟上述情况,就写成这样:

WHEN MATCHED AND 
  p1 OR
  NOT p1 AND NOT p2 AND p3
THEN UPDATE SET 
  c1 = CASE 
    WHEN p1                       THEN 1
    WHEN NOT p1 AND NOT p2 AND p3 THEN 3
                                  ELSE c1
  END,
  c2 = CASE
    WHEN NOT p1 AND NOT p2 AND p3 THEN 3
                                  ELSE c2
  END
WHEN MATCHED AND 
  NOT p1 AND p2 OR
  NOT p1 AND NOT p2 AND NOT p3 AND p4
THEN DELETE

相当费劲,但事实就是如此。

H2和HSQLDB

注意,H2和HSQLDB都没有遵循 "每行只更新一次 "的规则。我已经向H2报告了这一点:https://github.com/h2database/h2database/issues/2552。如果你想符合标准(jOOQ 3.14将为你模拟这一点,不用担心),那么你必须在这些方言中疯狂地执行上述CASE表达式,或者,在H2中(HSQLDB不支持同一类型的多个WHEN MATCHED子句)加强所有的WHEN MATCHED AND子句,正如我之前说明的:

WHEN MATCHED AND p1 THEN UPDATE SET c1 = 1
WHEN MATCHED AND NOT p1 AND p2 THEN DELETE
WHEN MATCHED AND NOT p1 AND NOT p2 AND p3 THEN UPDATE SET c1 = 3,c2 = 3
WHEN MATCHED AND NOT p1 AND NOT p2 AND NOT p3 AND p4 THEN DELETE

Oracle

Oracle在这里不支持AND,但支持一些有趣的供应商特定语法。乍看起来很合理,但它真的很古怪:

  • 在UPDATE之后,你可以添加一个WHERE子句,这和AND是一回事。到目前为止还不错。
  • 你也可以添加一个DELETE WHERE子句,但只能和UPDATE一起。所以你不能在不更新的情况下DELETE。好吧,在我们的例子中,我们并不打算这样做。
  • 然而,有趣的是,UPDATE/DELETE命令是一起执行的,而且DELETE发生在UPDATE之后。所以同一行被处理了两次。如果你在UPDATE中使用WHERE,那么只有UPDATE中包含的记录才能被DELETE中包含。我的意思是,为什么你要在删除之前先更新记录?

这意味着,我们的标准子句:

WHEN MATCHED AND p1 THEN UPDATE SET c1 = 1
WHEN MATCHED AND p2 THEN DELETE
WHEN MATCHED AND p3 THEN UPDATE SET c1 = 3, c2 = 3
WHEN MATCHED AND p4 THEN DELETE

将需要像这样被模仿:

WHEN MATCHED 
THEN UPDATE SET 
  c1 = CASE 
    WHEN p1 THEN 1  -- Normal update for WHEN MATCHED AND p1 clause
    WHEN p2 THEN c1 -- "Touch" record for later deletion
    WHEN p3 THEN 3  -- Normal update for WHEN MATCHED AND p3 clause
    WHEN p4 THEN c1 -- "Touch" record for later deletion
            ELSE c1
  END,
  c2 = CASE
    WHEN p1 THEN c2 -- p1 is not affecting c2
    WHEN p2 THEN c2 -- "Touch" record for later deletion
    WHEN p3 THEN 3  -- Normal update for WHEN MATCHED AND p3 clause
    WHEN p4 THEN c2 -- "Touch" record for later deletion
            ELSE c2
  END

-- Any predicate from any AND clause, regardless if UPDATE or DELETE
WHERE p1 OR p2 OR p3 OR p4

-- Repeat the predicates required for deletion
DELETE WHERE 
  NOT p1 AND p2 OR
  NOT p1 AND NOT p2 AND NOT p3 AND p4

这只是一个简单的标准SQL语法的MERGE语句!这里还有一个额外的棘手问题,我不会在这篇博文中涉及(但我们可能会在jOOQ中处理)。在Oracle中,DELETE WHERE子句已经可以看到UPDATE子句所执行的更新。这意味着,如果,例如,p2依赖于c1的值:

  c1 = CASE 
    ...
    WHEN p2 THEN c1 -- "Touch" record for later deletion
    ...
  END,

那么在DELETE WHERE中对p2的评估就会受到这个影响:

DELETE WHERE 
  NOT p1 AND p2 OR
  NOT p1 AND NOT p2 AND NOT p3 AND p4

这些p2表达式中的c1将与UPDATE子句中的c1不一样。显然,在某种程度上也可以通过变量替换来管理这个问题。

SQL Server BY SOURCE 和 BY TARGET

SQL Server对WHEN NOT MATCHED子句有一个有用的扩展,我认为这属于SQL标准的范畴 有了这个扩展,你可以指定是在WHEN NOT MATCHED [BY TARGET](其他人也支持的默认值)时执行INSERT操作,还是在WHEN NOT MATCHED BY SOURCE(在这种情况下,你可以执行另一个UPDATE或DELETE操作。 BY TARGET子句意味着我们在源表中找到一条记录,但在目标表中没有。BY SOURCE子句意味着我们在目标表中找到了一条记录,但在源表中没有。这意味着在SQL Server中,目标表和源表是FULL OUTER JOINed,而不是RIGHT OUTER JOINed,这将意味着我们原来的语句可以被大大简化。

MERGE INTO prices AS p
USING staging AS s
ON (p.product_id = s.product_id)
WHEN NOT MATCHED BY SOURCE THEN DELETE
WHEN MATCHED AND p.price != s.price THEN UPDATE SET 
  price = s.price,
  price_date = getdate(),
  update_count = update_count + 1
WHEN NOT MATCHED BY TARGET THEN INSERT 
  (product_id, price, price_date, update_count)
VALUES 
  (s.product_id, s.price, getdate(), 0);

我们可以将这里遇到的行再次着色:

PRODUCT_ID|  P.PRICE|  S.PRICE|
----------|---------|---------|

可以看出,这其实就是FULL OUTER JOIN的工作方式。 将这些子句模拟成标准SQL也是很费力的,因为我们必须明确地模拟这个FULL OUTER JOIN。我想这是可能的,但我们可能不会在jOOQ中实现它。

Vertica

只有Vertica似乎不支持DELETE分支,这意味着你不能用MERGE语句从目标表中DELETE数据。你只能用它来INSERT或UPDATE数据,这在几乎所有情况下都是足够好的。奇怪的是,Teradata支持DELETE,但不支持AND,这似乎有点毫无意义,因为DELETE和UPDATE不能以这种方式组合。

结论

MERGE语句是一个神秘的装置,其威力仅次于它。在一个简单的形式中(没有AND或WHERE子句,没有DELETE子句),所有的方言几乎都同意,这已经是一个非常有用的功能集,jOOQ已经支持了很长时间。从jOOQ 3.14开始,我们还将处理本文中列出的所有其他功能,以帮助你编写复杂的、与厂商无关的MERGE语句,并在所有支持MERGE的方言中模拟它们。 现在就想玩一玩吗?请看我们免费的在线SQL翻译工具

猜你喜欢

转载自juejin.im/post/7126379610652016670