MergeTestExamples

来自 PostgreSQL wiki
跳转到导航跳转到搜索

此页面包含一些示例,可用于测试 GSoC 2010 中开发的 MERGE 命令

此命令从未集成到 PostgreSQL 中,需要大量工作才能达到生产质量

示例表

为了测试 MERGE 命令,我们首先创建三个示例表。


1. Stock 表,记录每种商品的库存数量。

CREATE TABLE Stock(item_id int UNIQUE, balance int);

INSERT INTO Stock VALUES (10, 2200);
INSERT INTO Stock VALUES (20, 1900);

SELECT * FROM Stock;
 item_id | balance
---------+---------
      10 |    2200
      20 |    1900
(2 rows)


2. Buy 表,记录我们今天为每种商品购买的数量。

CREATE TABLE Buy(item_id int, volume int);

INSERT INTO Buy values(10, 1000);
INSERT INTO Buy values(30, 300);

SELECT * FROM Buy;
 item_id | volume
---------+--------
      10 |   1000
      30 |    300
(2 rows)


3. Sale 表,记录我们今天为每种商品销售的数量。

CREATE TABLE Sale(item_id int, volume int);

INSERT INTO Sale VALUES (10, 2200);
INSERT INTO Sale VALUES (20, 1000);

SELECT * FROM Sale;
 item_id | volume
---------+--------
      10 |   2200
      20 |   1000
(2 rows)

MERGE 的简单使用

第一个示例

我们可以使用 MERGE 命令将我们今天购买的商品添加到 Stock 表中。

BEGIN;

MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume
 WHEN NOT MATCHED THEN INSERT VALUES (Buy.item_id, Buy.volume);
 MERGE 2

SELECT * FROM Stock ORDER BY item_id;
 item_id | balance
---------+---------
      10 |	3200
      20 |	1900
      30 |	 300
(3 rows)
  
ROLLBACK;

如我们所见,商品 10 的数量增加了 1000,商品 30 被插入到库存中。

带限定条件的合并操作

合并操作可以指定其附加限定条件。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0 THEN UPDATE SET balance = balance - volume
   WHEN MATCHED THEN DELETE;
MERGE 2
   
SELECT * FROM Stock ORDER BY item_id;
  item_id | balance
 ---------+---------
       20 |	  900
  (1 row)


ROLLBACK;

在这个例子中,Sale 的数量从 Stock 的余额中减去。


对于商品 20,原始余额为 1900,我们今天销售了 1000。剩余数量为 900,大于 0。因此,此元组满足 UPDATE 操作的条件,并相应更新。请注意,DELETE 操作不会对商品 20 进行,因为它具有较低的优先级,即使它的条件也满足。


对于商品 10,剩余余额为 3200 - 3200 = 0,因此它被删除。

DO NOTHING 操作

在 MERGE 命令中,用户可以指定一个特殊的 "DO NOTHING" 操作。所有被此操作捕获的元组将被忽略。DO NOTHING 也可以有附加限定条件,并在 MATCHED 和 NOT MATCHED 中工作。

在以下示例中,商品 20 匹配 DO NOTHING 操作的要求。因此它保持不变,而商品 10 被 DELETE 操作删除。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
  WHEN MATCHED AND balance - volume > 0 THEN DO NOTHING
  WHEN MATCHED THEN DELETE;
MERGE 1

SELECT * FROM Stock;
 item_id | balance
---------+---------
      20 |    1900
(1 row)

ROLLBACK;

DO NOTHING 也在 NOT MATCHED 情况下工作

BEGIN;

MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume
 WHEN NOT MATCHED THEN DO NOTHING;
MERGE 1

SELECT * FROM Stock;
 item_id | balance
---------+---------
      20 |    1900
      10 |    3200
(2 rows)

ROLLBACK;

在这个例子中,MATCHED 商品(商品 10)被更新,而 NOT MATCHED 商品(商品 30)被忽略。

RAISE ERROR 操作

MERGE 中的另一个特殊操作是 RAISE ERROR。符合此操作的元组会导致错误。目前,错误处理只是抛出一个简短的错误消息。这将在将来扩展。MERGE 命令的主循环将在这种错误的情况下停止。RAISE ERROR 的语法类似于 DO NOTHING。

1. 它可以适合 MATCHED 和 NOT MATCHED 情况

2. 它不会改变元组。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0 THEN RAISE ERROR
   WHEN MATCHED THEN DELETE;
NOTICE:  one tuple is ERROR
MERGE 1


SELECT * FROM Stock;
 item_id | balance
---------+---------
      20 |    1900
(1 row)

ROLLBACK;

例如,在上面的查询中,商品 20 的元组导致错误,而商品 10 在错误之后被删除。

RAISE ERROR 是对与任何操作都不匹配的元组的默认操作。

BEGIN;

MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume;
NOTICE:  one tuple is ERROR
MERGE 1

Select * FROM stock;
 item_id | balance
---------+---------
      20 |    1900
      10 |    3200
(2 rows)

ROLLBACK;

如我们所见,有一个 NOT MATCHED 元组(商品 30),它被用户指定的操作遗漏。因此,它引发错误。

更复杂的用法

使用查询作为源表

源表可以是带别名的查询,如下面的示例所示。

BEGIN;

MERGE INTO Stock USING
	(SELECT Buy.item_id, (Buy.volume - Sale.volume) as v
	FROM Buy, Sale
	WHERE Buy.item_id = Sale.item_id) 
        AS BS
ON Stock.item_id = BS.item_id
    WHEN MATCHED THEN UPDATE SET balance = balance + BS.v;
MERGE 1
	
SELECT * FROM STOCK ORDER BY item_id;
 item_id | balance
---------+---------
      10 |	  1000
      20 |	  1900
(2 rows)
	
ROLLBACK;

在这里,我们想将 Buy 和 Sale 的数量加起来,并将结果合并到 Stock 中。我们今天为商品 10 购买了 1000 并销售了 2200。商品 10 的旧余额为 2200。因此,我们只剩下 1000。商品 20 没有改变,因为源查询 "BS" 只有商品 10 的元组。

子计划/子链接在操作中的应用

例如,假设有一个 Extra 表,记录股票的所有琐碎交易。当我们通过 MERGE 命令更新股票余额时,有必要包含这些琐碎的交易。

CREATE TABLE Extra (item_id int, volume int);

INSERT INTO Extra VALUES (10, 20);
INSERT INTO Extra VALUES (10, -7);
INSERT INTO Extra VALUES (20, 16);
INSERT INTO Extra VALUES (20, 5);
INSERT INTO Extra VALUES (30, 9);

那么第一个示例的新合并查询变为

BEGIN;

MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET 
      balance = balance + Buy.volume + (SELECT sum(volume) FROM Extra WHERE Extra.item_id = Buy.item_id)
 WHEN NOT MATCHED THEN INSERT VALUES 
     (Buy.item_id, Buy.volume + (SELECT sum(volume) FROM Extra WHERE Extra.item_id = Buy.item_id));
MERGE 2

SELECT * FROM Stock;
 item_id | balance
---------+---------
      20 |    1900
      10 |    3213
      30 |     309
(3 rows)

ROLLBACK;

在这个例子中,我们添加了所有琐碎交易的总和。在这里我们可以看到,商品 10 的新余额变为 3213(原来是 3200),其中额外的 13 来自 Extra 表。

继承的 MERGE

MERGE 也可以应用于继承表。目标表的子表将默认扫描和修改。

例如,我们创建一个新的股票表,以及一个从它继承的子表。为了使情况更加复杂,我们在创建表之后添加了一个属性。

CREATE TABLE p_Stock(item_id int UNIQUE);
CREATE TABLE c_Stock(ex int) INHERITS (p_Stock);
ALTER TABLE p_Stock ADD COLUMN balance int;

INSERT INTO p_Stock VALUES (10, 2200);
INSERT INTO p_Stock VALUES (20, 1900);

INSERT INTO c_Stock VALUES (30, 0, 700);

现在 p_Stock 表有两个属性,c_stock 表有三个属性。请注意,balance 是 p_stock 中的第二个属性,但它是 c_stock 中的第三个属性。

SELECT * FROM ONLY p_Stock;
 item_id | balance
---------+---------
      10 |    2200
      20 |    1900
(2 rows)

SELECT * FROM c_Stock;
 item_id | ex | balance
---------+----+---------
      30 |  0 |     700
(1 row)

现在我们将 MERGE 命令应用于 Stock 和 Buy。

BEGIN;
MERGE INTO p_Stock USING Buy ON p_Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume;
NOTICE:  one tuple is ERROR
NOTICE:  one tuple is ERROR
MERGE 2

SELECT * FROM ONLY p_Stock;
 item_id | balance
---------+---------
      20 |    1900
      10 |    3200
(2 rows)

SELECT * FROM c_Stock;
 item_id | ex | balance
---------+----+---------
      30 |  0 |    1000
(1 row)

ROLLBACK;

在这个查询中,p_stock 中的商品 10 被更新为 3200 的余额,而 c_stock 中的商品 30 被更新为 1000 的余额。

解释 MERGE

一般来说,MERGE 命令的 EXPLAIN 结果有 4 部分

1. 标题:第一行是“MERGE”的标题,包含整个计划的成本(如果成本显示未关闭)。

2. 初始化计划:如果有任何初始化计划,将显示所有初始化计划。初始化计划首先执行,因此它们首先显示。

3. 合并操作:在初始化计划之后,是所有合并操作的列表,按照用户输入的顺序排列。每个操作将告诉其操作类型、操作限定条件和操作返回目标列表(如果 VERBOSE 处于开启状态)。如果某个操作中涉及任何子计划,它们将立即在操作下方打印出来。

4. 主计划:源表 LEFT JOIN 目标表 的连接计划

例如,一个简单的 MERGE EXPLAIN 可能如下所示

EXPLAIN
MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
  WHEN MATCHED AND balance - volume > 0 THEN UPDATE SET balance = balance - volume
  WHEN MATCHED THEN DELETE;
		               QUERY PLAN
----------------------------------------------------------------------------
 Merge	(cost=58.15..121.65 rows=2140 width=22)
   Action 1: Update When Matched And
     qual: ((stock.balance - sale.volume) > 0)
   Action 2: Delete When Matched
   MainPlan:
   ->  Hash Left Join  (cost=58.15..121.65 rows=2140 width=22)
	 Hash Cond: (sale.item_id = stock.item_id)
	 ->  Seq Scan on sale  (cost=0.00..31.40 rows=2140 width=8)
	 ->  Hash  (cost=31.40..31.40 rows=2140 width=14)
	       ->  Seq Scan on stock  (cost=0.00..31.40 rows=2140 width=14)
(10 rows)


与其他命令一样,EXPLAIN ANALYZE MERGE ... 将执行合并命令并告诉实际运行时间。

BEGIN;

EXPLAIN ANALYZE
MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0 THEN UPDATE SET balance = balance - volume
   WHEN MATCHED THEN DELETE;
				                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
 Merge  (cost=58.15..121.65 rows=2140 width=22) (actual time=0.382..0.382 rows=0 loops=1)
   Action 1: Update When Matched And
   Qual: ((stock.balance - sale.volume) > 0)
   Action 2: Delete When Matched
   MainPlan:
   ->  Hash Left Join  (cost=58.15..121.65 rows=2140 width=22) (actual time=0.25 3..0.265 rows=2 loops=1)
         Hash Cond: (sale.item_id = stock.item_id)
	 ->   Seq Scan on sale  (cost=0.00..31.40 rows=2140 width=8) (actual time=0.080..0.084 rows=2 loops=1)
	 ->   Hash  (cost=31.40..31.40 rows=2140 width=14) (actual time=0.075..0.075 rows=2 loops=1)
		Buckets: 1024  Batches: 1  Memory Usage: 1kB
		->  Seq Scan on stock  (cost=0.00..31.40 rows=2140 width=14) (actual time=0.061..0.065 rows=2 loops=1)
 Total runtime: 0.511 ms
(12 rows)

SELECT * FROM Stock;
 item_id | balance
---------+---------
      20 |	900
(1 row)

ROLLBACK;

一个更复杂(但意义更小)的 MERGE 查询解释如下:

EXPLAIN VERBOSE
MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0 THEN UPDATE
      SET balance = balance - volume + (SELECT sum(volume) FROM Extra WHERE Extra.item_id = Sale.item_id)
   WHEN MATCHED THEN UPDATE SET balance = (SELECT COUNT(*) FROM generate_series(1,10)) ;
						   QUERY PLAN
-------------------------------------------------------------------------------------------------
Merge	(cost=70.67..134.16 rows=2140 width=22)
  InitPlan 2 (returns $1)
    ->  Aggregate   (cost=12.50..12.51 rows=1 width=0)
	  Output: count(*)
	  -> Function Scan on pg_catalog.generate_series    (cost=0.00..10.00 rows=1000 width=0)
	        Output: generate_series.generate_series
  Action 1: Update When Matched And
    qual: ((stock.balance - sale.volume) > 0)
  Output: stock.item_id, ((stock.balance - sale.volume) + (SubPlan 1))
  SubPlan 1
    ->  Aggregate   (cost=36.78..36.79 rows=1 width=4)
	  Output: sum(extra.volume)
	    ->  Seq Scan on public.extra  (cost=0.00..36.75 rows=11 width=4)
		  Output: extra.item_id, extra.volume
		  Filter: (extra.item_id = $0)
  Action 2: Update When Matched
  Output: stock.item_id, $1
  MainPlan:
  ->  Hash Left Join  (cost=58.15..121.65 rows=2140 width=22)
	Output: sale.item_id, sale.volume, stock.item_id, stock.balance, stock.ctid
	Hash Cond: (sale.item_id = stock.item_id)
	->  Seq Scan on public.sale  (cost=0.00..31.40 rows=2140 width=8)
	      Output: sale.item_id, sale.volume
	->  Hash  (cost=31.40..31.40 rows=2140 width=14)
	      Output: stock.item_id, stock.balance, stock.ctid
	      ->  Seq Scan on public.stock  (cost=0.00..31.40 rows=2140 width=14)
		    Output: stock.item_id, stock.balance, stock.ctid
(27 rows)
		

使用规则的 MERGE

由操作触发的规则

如果我们在 MERGE 命令的目标表上定义规则,MERGE 操作将应用该规则。

例如,假设创建一个名为 "count_by_rule" 的表,用于记录对 "Stock" 表的修改次数。

CREATE TABLE count_by_rule (update INT, insert INT, delete INT);

INSERT INTO count_by_rule VALUES (0,0,0);

我们需要创建规则以自动维护计数表。

CREATE RULE "update_count" AS
ON UPDATE TO stock
DO UPDATE count_by_rule SET update=update+1;
CREATE RULE "delete_count" AS
ON DELETE TO stock
DO UPDATE count_by_rule SET delete=delete+1;
CREATE RULE "insert_count" AS
ON INSERT TO stock
DO UPDATE count_by_rule SET insert=insert+1;

现在让我们再次运行第二个示例。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0THEN UPDATE SET balance = balance - volume
   WHEN MATCHED THEN DELETE;
MERGE 2

SELECT * FROM count_by_rule;
 update | insert | delete
--------+--------+--------
      1 |      0 |      1
(1 row)

SELECT * FROM stock;
 item_id | balance
---------+---------
      20 |     900
(1 row)

ROLLBACK;

我们可以看到,count 表中的 update 和 delete 字段都增加了 1,这意味着 "update_count" 和 "delete_count" 规则都被 MERGE 命令触发。

同一类型的多个操作

如果一个 MERGE 命令包含多个相同类型的操作,则该操作类型的规则只会被激活一次。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0 THEN UPDATE SET balance = balance - volume
   WHEN MATCHED AND balance - volume <= 0 THEN UPDATE SET balance = 0;
MERGE 2
	
SELECT * FROM Stock;
 item_id | balance
---------+---------
      10 |       0
      20 |     900
(2 rows)


SELECT * FROM count_by_rule;
 update | insert | delete
--------+--------+--------
      1 |      0 |      0
(1 row)


ROLLBACK;

在上面的例子中,我们在 MERGE 命令中有两个 UPDATE 操作。查询之后,我们可以发现 "count.update" 的值只增加了 1(而不是 2),这意味着 "update_count" 规则已被激活了一次。

INSTEAD 规则

用 INSTEAD 规则替换更新规则。

CREATE OR REPLACE RULE "update_count" AS
ON UPDATE TO stock
DO INSTEAD
UPDATE count_by_rule SET update=update+1;

再次尝试 MERGE 查询。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0THEN UPDATE SET balance = balance - volume
   WHEN MATCHED THEN DELETE;
MERGE 1

SELECT * FROM stock;
 item_id | balance
---------+---------
      20 |    1900
(1 row)

SELECT * FROM count_by_rule;
 update | insert | delete
--------+--------+--------
      1 |      0 |      1
(1 row)

ROLLBACK;

从这个例子我们可以发现,"update_count" 和 "delete_count" 规则正确更新了 count 表。

如我们所见,商品 10 被 DELETE 操作删除。

另一方面,由于更新操作被 INSTEAD 规则替换,因此商品 20 的余额没有改变。

使用触发器的 MERGE

行级触发器

MERGE 目标表的行级触发器将被相同类型的操作激活。例如,假设我们想记录所有进入 Stock 表的新元组。我们可以首先创建一个 mirror_stock 表。如下所示

CREATE TABLE mirror_stock (item_id int, balance int, source text);


然后在 Stock 上创建行级触发器,它将把 INSERT 或 UPDATE 命令创建的所有新元组插入 mirror_stock 中。

CREATE FUNCTION log_insert_stock() RETURNS trigger 
AS 
$$
        BEGIN
                INSERT INTO mirror_stock VALUES (new.item_id, new.balance, 'from INSERT action');
		    RETURN NULL;	
        END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER log_insert
	AFTER INSERT ON Stock
	FOR EACH ROW    	
	EXECUTE PROCEDURE log_insert_stock();

CREATE FUNCTION log_update_stock() RETURNS trigger 
AS 
$$
        BEGIN
                INSERT INTO mirror_stock VALUES (new.item_id, new.balance, 'from UPDATE action');
		    RETURN NULL;	
        END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER log_update
	AFTER UPDATE ON Stock
	FOR EACH ROW    	
	EXECUTE PROCEDURE log_update_stock();

现在,让我们再次尝试 UPDATE+INSERT 示例

BEGIN;

MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume
 WHEN NOT MATCHED THEN INSERT VALUES (Buy.item_id, Buy.volume);
MERGE 2

SELECT * FROM Stock ORDER BY itme_id;
 item_id | balance
---------+---------
      10 |    3200
      20 |    1900
      30 |     300
(3 rows)

SELECT * FROM mirror_stock;
 item_id | balance |       source
---------+---------+--------------------
      10 |    3200 | from UPDATE action
      30 |     300 | from INSERT action
(2 rows)

ROLLBACK;

在这个例子中,两个行级触发器分别被两种不同类型的 MERGE 操作触发,并根据我们的预期在 mirror_stock 中插入日志元组。

语句级触发器

例如,我们可以使用 BEFORE 语句触发器来计算 Stock 被应用于 UPDATE/INSERT 命令的次数。

在我们开始之前,先删除之前创建的 INSTEAD 规则。

DROP RULE update_count ON Stock CASCADE; 


首先,创建一个用于计数的表。

CREATE TABLE count_by_trigger (update int, insert int, delete int);

INSERT INTO count_by_trigger VALUES (0,0,0);

然后,创建触发器。

CREATE FUNCTION count_update_stock() RETURNS trigger 
AS 
$$
        BEGIN
                UPDATE count_by_trigger SET update = update + 1;
		    RETURN NULL;	
        END;
$$ 
LANGUAGE plpgsql;


CREATE TRIGGER count_update
	BEFORE UPDATE ON Stock
	EXECUTE PROCEDURE count_update_stock();

CREATE FUNCTION count_insert_stock() RETURNS trigger 
AS 
$$
        BEGIN
                UPDATE count_by_trigger SET insert = insert + 1;
		    RETURN NULL;	
        END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER count_insert
	BEFORE INSERT ON Stock
	EXECUTE PROCEDURE count_insert_stock();

现在,运行 MERGE 命令。

BEGIN;

MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume
 WHEN NOT MATCHED THEN INSERT VALUES (Buy.item_id, Buy.volume);
MERGE 2

SELECT * FROM count_by_trigger;
 update | insert | delete
--------+--------+--------
      1 |      1 |      0
(1 row)

ROLLBACK;

如我们所见,这两个触发器("count_insert" 触发器和 "count_update" 触发器)都被此语句触发。

同一类型的多个操作

如果一个 MERGE 命令包含多个相同类型的操作,则相应的语句触发器只会被触发一次。

请参见以下示例。MERGE 命令有两个 UPDATE 操作,但 count_by_trigger 表只更新了一次。

BEGIN;

MERGE INTO Stock USING Sale ON Stock.item_id = Sale.item_id
   WHEN MATCHED AND balance - volume > 0 THEN UPDATE SET balance = balance - volume
   WHEN MATCHED AND balance - volume <= 0 THEN UPDATE SET balance = 0;
MERGE 2

SELECT * FROM count_by_trigger;
 update | insert | delete
--------+--------+--------
      1 |      0 |      0
(1 row)

ROLLBACK;

INSTEAD 规则阻止触发器

如果一种操作被 INSTEAD 规则替换,它将不会触发触发器。

让我们创建一个 INSTEAD 规则。

CREATE OR REPLACE RULE "update_cont" AS
ON UPDATE TO stock
DO INSTEAD
UPDATE count_by_rule SET update=update+1;

然后,再次运行同一个示例。

BEGIN;
 
MERGE INTO Stock USING Buy ON Stock.item_id = Buy.item_id
 WHEN MATCHED THEN UPDATE SET balance = balance + Buy.volume
 WHEN NOT MATCHED THEN INSERT VALUES (Buy.item_id, Buy.volume);
MERGE 1

SELECT * FROM count_by_trigger;
 update | insert | delete
--------+--------+--------
      0 |      1 |      0
(1 row)

ROLLBACK;

UPDATE 操作被规则替换,因此它不会触发 UPDATE 触发器。