MergeTestExamples

From PostgreSQL wiki

Revision as of 20:09, 24 February 2014 by Pierce (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

This page contains examples that can be used to test the MERGE command as developed during the GSoC 2010.

This was never integrated into PostgreSQL, and requires significant work to be production quality

Contents

Sample Tables

For testing the MERGE command, we create three sample tables firstly.


1. The Stock table, which records the amount of each item on hand.

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. The Buy table, which records the amount we bought today for each item.

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. The Sale table, which records the amount we sold today for each item.

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)

Simple use of MERGE

The first example

We can use a MERGE command to add the items we bought today into the Stock table.

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;

As we can see, the item 10 has increase by 1000 and itme 30 is inserted into stock.

Merge Action with Quals

The merge action can specify their additional quals.

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;

In this example, the volume of Sale is subtracted from the balance in Stock.


For item 20, the original balance is 1900 and we sold 1000 today. The remaining amount is 900 which is larger than 0. Thus, this tuple fulfills the condition of the UPDATE action and is updated accordingly. Note that the DELETE action is not taken on item 20 because it has a lower priority, although its condition is also fulfilled.


For item 10, the remaining balance is 3200 - 3200 = 0, so it is deleted.

DO NOTHING action

In MERGE command, user can specify a spectial "DO NOTHING" action. All the tuples caught by this action will be ignored. DO NOTHING can also have additional quals, and works in both MATCHED and NOT MATCHED.

In the following example, item 20 matches the requirement of the DO NOTHING action. So it remains unchanged, while item 10 is deleted by the DELETE action.

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 also works in the NOT MATCHED case:

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;

In this example, the MATCHED item (item 10) is updated, while the NOT MATCHED item (item 30) is ignored.

RAISE ERROR action

Another special action in MERGE is RAISE ERROR. The tuple fit this action will cause an ERROR. Currently, the ERROR handling is just to throw a short error message. This will be extended in future. The main loop of the MERGE command will stop on this kind of ERROR. The syntax of RAISE ERROR is similar to that of DO NOTHING.

1. It can fit in both MATCHED and NOT MATCHED situation

2. It will not change the tuple.

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;

For example, in the above query, the tuple of item 20 leads to an ERROR, while item 10 is deleted after the ERROR.

RAISE ERROR is the default action for the tuples that match with no actions.


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;

As we can see, there is a NOT MATCHED tuple (item 30) which is missed by the user-specified action. So, it raises an error.

More complicated use

Use query as source table

The source table could be a query with alias, as shown in the following example.

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;

Here we want to sum up the Buy and Sale volume together and merge the result in Stock. We bought 1000 and sold 2200 for item 10 today. The old balance of item 10 is 2200. Thus, we have only 1000 remained. Item 20 is not changed, because the source query "BS" has only the tuple for item 10.

Subplan/sublinks in action

For example, suppose there is a table Extra that records all the trivial transactions of the stocks. When we update the stock balance by MERGE command, it is necessary to include these trivial transactions.


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);

Then new merge query of the first example becomes:


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;

In this example, we add the sum of all the trivial transactions. Here we can see, the new balance of item 10 becomes 3213 (used to be 3200), where the extra 13 come from the Extra table.

MERGE on inheritance

MERGE can also be applied on inherited tables. The child tables of the target table will be scanned and modified by default.

For example, we create a new stock table and a child table inheriting from it. To make the situation more complex, we add one attribute after the tables are created.

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);

Now the p_Stock table has two attribute and c_stock table has three attibutes. Note that balance is the second attribute in p_stock but the third attribute in 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)

Now we apply the MERGE command on Stock and 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;

In this query, the item 10 in p_stock is updated to a balance of 3200 and the item 30 in c_stock is updated to a balance of 1000.

Explain Merge

In general, the EXPLAIN result of a MERGE command has 4 parts:

1. Title: the first line is a title of “MERGE” which contains the costs of the whole plan (if cost display is not off).

2. Init plans: all the Init Plans will be displayed, if any. Init plans are executed first , so they are displayed first.

3 Merge actions: After init plans, there is the list of all merge actions, in the order of user’s input. Each action will tell its action type, action qual and action return target list (if VERBOSE is on). If any subplan is involved in one action, they will be printed out immediately under the action.

4. Main Plan: the join plan for source table LEFT JOIN target table

For example, a simple MERGE EXPLAIN may like the following:

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)


As other command, EXPLAIN ANALYZE MERGE ... will execute the merge command and tell the real running time.


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;

A more complicated (but less meaningful) MERGE query is explained as :


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 with rules

Rules fired by actions

If we define rules on the target table of MERGE command, the MERGE actions will apply the rule.

For example, suppose a table "count_by_rule" is created for recording the times of modification on the "Stock" table.

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

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

We need to create rules for maintaining the count table automatically.

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;

Now let's run the second example again.

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;

We can see that the update and delete field in count table has increased by 1, which means the rule of “update_count” and “delete_count” are both triggered by the MERGE command.

Multiple actions of the same type

If one MERGE command has multiple actions of the same type, the rules of this action type will only be activated once.

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;

In the above example, we have two UPDATE actions in the MERGE command. After the query, we can find that the value of "count.update" is just increased by 1 (not 2), which means the "update_count" rule has been activated for one time.

INSTEAD rules

Replace the update rule with a INSTEAD rule.


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

Try the MERGE query again.


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;

From this example we can find that, the "update_count" and "delete_count" rules updated the count table correctly.

As we can see, the item 10 is deleted by the DELETE action.

On the other hand, since the update action is replaced by INSTEAD rule, the balance of item 20 is not changed.

MERGE with triggers

Row level triggers

The row level triggers of the target table of a MERGE will be activated by the action of the same type. For example, suppose we want to log all the new tuples come to Stock table. We can firstly create a mirror_stock table.As shown below:

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


Then create row level triggers on Stock, which will insert all new tuples created by INSERT or UPDATE command in to 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();

Now, let try the UPDATE+INSERT example again:

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;

In this example, the two row level triggers are fired by the two different types of MERGE actions respectively and insert log tuples in mirror_stock as we expected.

Statement level triggers

For example, we can use BEFORE statement triggers to count how many times Stock is applied to the command of UPDATE/INSERT.

Before we start, drop the INSTEAD rules we create before.

DROP RULE update_count ON Stock CASCADE; 


Firstly, create a table for counting.


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

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

Then, create the triggers.

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();

Now, run the MERGE command.

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;

As we can see, both triggers (the "count_insert" trigger and "count_update" trigger) are fired for this statement.

Multiple actions of same type

If a MERGE command has more than one action of the same type, the corresponding statement trigger will be fired only once.

See the following example. The MERGE command has two UPDATE actions, but the count_by_trigger table is updated only once.

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 rule blocks the triggers

If one kind of action is replaced by INSTEAD rules, it will not fire triggers.

Let's create an INSTEAD rule.

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

Then, run the same example again.

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;

The UPDATE action is replaced by rule, so it will not fire the UPDATE triggers.

Personal tools