天天看點

PostgreSQL Oracle 相容性之 - 全局臨時表 global temp table

标簽

PostgreSQL , 臨時表 , 全局臨時表 , unlogged table , advisory lock

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E8%83%8C%E6%99%AF 背景

PostgreSQL 暫時不支援類似Oracle風格的臨時表。

PostgreSQL 臨時表結構是會話級别的,而在Oracle中,臨時表的結構是全局有效的,隻是資料會話之間獨立。

Oracle 全局臨時表可以指定SCHEMA,而PostgreSQL的臨時表不能指定SCHEMA,自動在temp臨時SCHEMA中建立。

細節差異詳見:

https://www.codeproject.com/Articles/1176045/Oracle-style-global-temporary-tables-for-PostgreSQL

為了讓PostgreSQL臨時表的使用與Oracle相容,除了核心層面相容之外,目前隻能在使用時注意。

CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [    
  { column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ]    
    | table_constraint    
    | LIKE source_table [ like_option ... ] }    
    [, ... ]    
] )    
...........    
[ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ]      
    
-- 送出後,保留記錄        PRESERVE ROWS    
-- 送出後,清空臨時表記錄  DELETE ROWS    
-- 送出後,删除臨時表      DROP    
           

使用者可以使用以下方式來使用臨時表:

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E6%96%B9%E6%B3%951%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8%E4%BD%BF%E7%94%A8-trigger--inherit-%E4%BB%A3%E6%9B%BF%E4%B8%B4%E6%97%B6%E8%A1%A8-%E7%94%A8%E6%88%B7%E4%BD%BF%E7%94%A8%E5%BD%A2%E6%80%81%E4%B8%8Eoracle%E4%B8%80%E8%87%B4 方法1(推薦使用)、使用 trigger + inherit 代替臨時表 (使用者使用形态與ORACLE一緻)

思想來自如下,與之不一樣的地方是,直接使用的表觸發器+繼承功能來實作的。

以上這個連結的方法問題:1、不支援truncate, copy。2、使用函數傳回所有記錄,會有性能問題。3、無法使用遊标。4、索引使用比較麻煩。

本文下面的方法沒有以上問題(直接TRUNCATE的支援需要打個PATCH, 社群已于2018.12.27後支援

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1e504f01da11db0181d7b28bb30cb5eeb0767184),所有操作(insert,update,delete,select)與直接使用臨時表一樣。INSERT性能會有下降(使用本方法88417/s,直接寫表1111111/s),一般的使用性能也是足夠夠的;

對于PG 10以上版本,使用中間表可以增強性能 

《PostgreSQL 10.0 preview 功能增強 - 觸發器函數内置中間表》

例子

建立一個臨時表 stage.abc。

1、建立一個schema,放臨時表

create schema IF NOT EXISTS stage;    
           

2、建立表stage.abc

drop table if exists stage.abc;    
    
create table if not exists stage.abc (id int primary key, info text, crt_time timestamp);     
           

3、建立這個"stage.abc表"的觸發器,自動基于它建立臨時表,并加入它的繼承關系

所有PG版本通用,自定義insert before觸發器函數

create or replace function public.abc_tg1() returns trigger as $$    
declare    
begin    
  -- 如果臨時表的定義修改,修改這個觸發器的内容,即表名abc,使用你的實際名字替換    
  -- 注意這裡不要加schema.,否則就寫入非臨時表了。    
  insert into abc values (new.*);      
  return null;    
  
  -- 第一次插入會插入普通父表,是以建立父表的AFTER觸發器,報錯,即回到這裡處理。  
  exception when others then  
    -- 根據臨時表的業務需要使用  on commit PRESERVE|DELETE rows       
    execute format('create temp table if not exists %I (like %I.%I including all) inherits (%I.%I) on commit PRESERVE ROWS',       
      TG_TABLE_NAME, TG_TABLE_SCHEMA, TG_TABLE_NAME, TG_TABLE_SCHEMA, TG_TABLE_NAME);    
      
    -- 如果臨時表的定義修改,修改這個觸發器的内容,即表名abc,使用你的實際名字替換    
    -- 注意這裡不要加schema.,否則就寫入非臨時表了。    
    insert into abc values (new.*);       
    return null;     
end;    
$$ language plpgsql strict set client_min_messages = error;    
           

10以後的版本可用批量寫入臨時表加速,自定義insert before觸發器函數

略...  
           

自定義insert after觸發器函數,用于加速insert

(避免每次都要執行

perform 1 from pg_class where relpersistence='t' and relname=TG_TABLE_NAME and pg_table_is_visible(oid);

)

create or replace function public.abc_tg2() returns trigger as $$    
declare    
begin    
  RAISE EXCEPTION 'Do not insert direct to parent persistence table.';   
  return null;     
end;    
$$ language plpgsql strict set client_min_messages = error;    
           

4、建立stage.abc的insert before觸發器

create trigger tg1 before insert on stage.abc for each row execute function public.abc_tg1();     
  
create trigger tg2 after insert on stage.abc for each row execute function public.abc_tg2();     
           

5、測試,完全OK

insert into stage.abc values (1,'test',now());    
insert into stage.abc values (2,'test',now());    
    
postgres=# select tableoid, * from stage.abc;    
 tableoid | id | info |          crt_time            
----------+----+------+----------------------------  
 32224674 |  1 | test | 2018-12-25 09:38:34.252316  
 32224674 |  2 | test | 2018-12-25 09:38:34.257408  
(2 rows)  
  
postgres=# select tableoid, * from only stage.abc;    
 tableoid | id | info | crt_time   
----------+----+------+----------  
(0 rows)  
  
postgres=# insert into stage.abc select generate_series(3,10000000);  
INSERT 0 0  
Time: 113095.297 ms (01:53.095)  
postgres=# select count(*) from only stage.abc;  
 count   
-------  
     0  
(1 row)  
  
Time: 0.464 ms  
postgres=# select count(*) from stage.abc;  
  count     
----------  
 10000000  
(1 row)  
  
Time: 2109.900 ms (00:02.110)  
  
postgres=# truncate abc;  
TRUNCATE TABLE  
Time: 149.441 ms  

postgres=# insert into abc select generate_series(1,10000000);  
INSERT 0 10000000  
Time: 9005.758 ms (00:09.006)  
postgres=# select 10000000/9.0;  
       ?column?         
----------------------  
 1111111.111111111111  
(1 row)  
  
Time: 0.276 ms  
postgres=# select 10000000/113.1;  
      ?column?        
--------------------  
 88417.329796640141  
(1 row)  
  
Time: 0.287 ms  
  
postgres=# begin;
BEGIN
postgres=# declare a cursor for select * from stage.abc;
DECLARE CURSOR
postgres=# fetch 1 from a;
 id | info | crt_time 
----+------+----------
  2 |      | 
(1 row)

postgres=# update stage.abc set info='abc',crt_time=now() where CURRENT OF a;
UPDATE 1
postgres=# end;
COMMIT
postgres=# select * from stage.abc where id=2;
 id | info |          crt_time          
----+------+----------------------------
  2 | abc  | 2018-12-25 14:35:30.043199
(1 row)

postgres=# select * from only stage.abc where id=2;
 id | info | crt_time 
----+------+----------
(0 rows)
           

select, delete, update, copy, 遊标 均使用正常。

6、後期對臨時表加索引,直接操作stage.abc即可。因為我們在建立臨時表時,使用了including all子句。

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E5%B0%81%E8%A3%85%E6%88%90%E6%8E%A5%E5%8F%A3%E5%87%BD%E6%95%B0%E4%BD%BF%E7%94%A8%E6%96%B9%E4%BE%BF 封裝成接口函數使用友善。

create or replace function create_temptable(  
  v_schemaname name,  
  v_tablename name,  
  v_on_commit_action text default 'PRESERVE ROWS',  
  v_tg_schemaname name default 'public'  
) returns void as $$  
declare  
  tg_name1 text := 'v'||md5((clock_timestamp()::text||random()::text));  
  tg_name2 text := 'tg_for_temptable_after_insert_error';  -- 這個函數隻需要一個通用的即可  
  v_sql1 text;  
  v_sql2 text;  
begin  
  v_sql1 := format($_$  
    create or replace function %I.%I() returns trigger as $__$   -- v_tg_schemaname, tg_name1   
    declare    
    begin    
      insert into %I values (new.*);    -- v_tablename  
      return null;    
    exception when others then    
      execute format('create temp table if not exists %%I (like %%I.%%I including all) inherits (%%I.%%I) on commit %s',     --  v_on_commit_action  
        TG_TABLE_NAME, TG_TABLE_SCHEMA, TG_TABLE_NAME, TG_TABLE_SCHEMA, TG_TABLE_NAME);    
    insert into %I values (new.*);   --   v_tablename  
    return null;     
    end;    
    $__$ language plpgsql strict set client_min_messages = error;   
  $_$, v_tg_schemaname, tg_name1, v_tablename, v_on_commit_action, v_tablename);  
  execute v_sql1;  
  
  v_sql2 := format($_$  
    create or replace function %I.%I() returns trigger as $__$  -- v_tg_schemaname, tg_name2   
    declare    
    begin    
      RAISE EXCEPTION 'Do not insert direct to parent persistence table.';   
      return null;     
    end;    
    $__$ language plpgsql strict set client_min_messages = error;    
  $_$, v_tg_schemaname, tg_name2 );  
  execute v_sql2;  
  
  execute format($_$create trigger tg1 before insert on %I.%I for each row execute function %I.%I();$_$, v_schemaname, v_tablename, v_tg_schemaname, tg_name1);     
  execute format($_$create trigger tg2 after insert on %I.%I for each row execute function %I.%I();$_$, v_schemaname, v_tablename, v_tg_schemaname, tg_name2);       
  
end;  
$$ language plpgsql strict;  
           

使用舉例

postgres=# drop table stage.abc;  
DROP TABLE  
  
postgres=# create table stage.abc(id int primary key, info text, crt_time timestamp);  
CREATE TABLE  
  
postgres=# select create_temptable('stage','abc');  
 create_temptable   
------------------  
   
(1 row)  
  
postgres=# \d+ stage.abc  
                                                Table "stage.abc"  
  Column  |            Type             | Collation | Nullable | Default | Storage  | Stats target | Description   
----------+-----------------------------+-----------+----------+---------+----------+--------------+-------------  
 id       | integer                     |           | not null |         | plain    |              |   
 info     | text                        |           |          |         | extended |              |   
 crt_time | timestamp without time zone |           |          |         | plain    |              |   
Indexes:  
    "abc_pkey" PRIMARY KEY, btree (id)  
Triggers:  
    tg1 BEFORE INSERT ON stage.abc FOR EACH ROW EXECUTE PROCEDURE v70c22a86a17342eb6cb571349c85274b()  
    tg2 AFTER INSERT ON stage.abc FOR EACH ROW EXECUTE PROCEDURE tg_for_temptable_after_insert_error()  
           

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E4%BD%BF%E7%94%A8%E9%99%90%E5%88%B6 使用限制

1、truncate暫不支援,請使用delete代替 (或者打patch支援truncate, 社群已支援,如果你使用的是2018.12.27後釋出的版本,不需要打PATCH)

postgres=# truncate stage.abc;    
ERROR:  0A000: cannot truncate temporary tables of other sessions    
LOCATION:  truncate_check_rel, tablecmds.c:1743    
Time: 0.626 ms    
    
    
-- delete正常    
delete from stage.abc;    
           

修正這個truncate問題的PATCH如下

https://www.postgresql.org/message-id/flat/20181225004545.GB2334%40paquier.xyz#b08f63fab9997cdf09d879aaaa5a01d3
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c  
index ad8c176793..9dabfc0d36 100644  
--- a/src/backend/commands/tablecmds.c  
+++ b/src/backend/commands/tablecmds.c  
@@ -1416,6 +1416,19 @@ ExecuteTruncate(TruncateStmt *stmt)  
   
 				/* find_all_inheritors already got lock */  
 				rel = heap_open(childrelid, NoLock);  
+  
+				/*  
+				 * It is possible that the parent table has children that are temp  
+				 * tables of other backends.  We cannot safely access such tables  
+				 * (because of buffering issues), and the best thing to do seems  
+				 * to be to silently ignore them.  
+				 */  
+				if (RELATION_IS_OTHER_TEMP(newrelation))  
+				{  
+					heap_close(rel);  
+					continue;  
+				}  
+  
 				truncate_check_rel(RelationGetRelid(rel), rel->rd_rel);  
 				truncate_check_activity(rel);  
           

2、copy to 不支援 (copy from 正常)

原因:copy時,不讀取INHERIT表的内容。

postgres=# copy stage.abc to '/tmp/abc';    
COPY 0    
           

是以,直接使用臨時表名可以COPY TO

postgres=# copy abc to '/tmp/abc';    
COPY 1000    
    
    
postgres=# copy stage.abc from '/tmp/abc';    
COPY 0    
postgres=# select count(*) from stage.abc;    
 count     
-------    
  1000    
(1 row)    
           

3、由于使用了insert觸發器,INSERT時,資料都寫入了繼承的臨時表,是以returning文法無法獲得傳回行數,記錄。

postgres=# insert into stage.abc values (-1) returning *;    
 id | info | crt_time     
----+------+----------    
(0 rows)    
    
INSERT 0 0    
           

4、如果需要修改臨時表的表名,必須同時修改觸發器函數的内容。

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E6%96%B9%E6%B3%952%E7%94%A8%E4%B8%B4%E6%97%B6%E8%A1%A8%E6%97%B6%E6%8F%90%E5%89%8D%E5%88%9B%E5%BB%BA 方法2、用臨時表時提前建立

1、建立臨時表模闆(一次性建立)

-- 臨時表模闆    
create table tmp1_template(xxxx);      
           

2、以後每次使用某臨時表之前,使用這個模闆表建立臨時表。

create temp table if not exists tmp_xxx (like tmp1_template including all) ON COMMIT DELETE ROWS;        
           

3、以後要修改臨時表的結果,直接修改模闆表

alter table tmp_xxx add column c1 int;    
           
-- 建立臨時表模闆表    
create table tmp1_template (id int8 primary key, info text, crt_time timestamp);      
    
-- 每次使用臨時表前,先使用模闆建立      
create temp table if not exists tbl_tmp (like tmp1_template including all) ON COMMIT DELETE ROWS;       
    
-- 以後要修改臨時表的結果,直接修改模闆表    
alter table tmp1_template add column c1 int;    
           

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E6%96%B9%E6%B3%953plpgsql%E4%B8%AD%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%951%E4%B9%9F%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8array%E4%BB%A3%E6%9B%BF%E4%B8%B4%E6%97%B6%E8%A1%A8 方法3、plpgsql中,可以使用方法1,也可以使用ARRAY代替臨時表

建立普通表(預設會建立對應的複合類型),

使用複合類型數組代替臨時表

do language plpgsql $$        
declare        
  res tbl[]; x tbl;        
begin        
  select array_agg(t::tbl) into res from (select id, random()::text, clock_timestamp() from generate_series(1,10) t(id)) t;        
  raise notice 'res: %', res;         
  foreach x in array res loop         
    raise notice 'x: %', x;         
  end loop;          
end;        
$$;        
NOTICE:  res: {"(1,0.0940282950177789,\"2018-07-15 23:14:44.060389\")","(2,0.922331794165075,\"2018-07-15 23:14:44.060404\")","(3,0.857550186105072,\"2018-07-15 23:14:44.060406\")","(4,0.373486907221377,\"2018-07-15 23:14:44.060408\")","(5,0.973780393600464,\"2018-07-15 23:14:44.060409\")","(6,0.502839601133019,\"2018-07-15 23:14:44.060411\")","(7,0.217925263568759,\"2018-07-15 23:14:44.060412\")","(8,0.733274032827467,\"2018-07-15 23:14:44.060413\")","(9,0.62150136847049,\"2018-07-15 23:14:44.060416\")","(10,0.241393140517175,\"2018-07-15 23:14:44.060418\")"}        
NOTICE:  x: (1,0.0940282950177789,"2018-07-15 23:14:44.060389")        
NOTICE:  x: (2,0.922331794165075,"2018-07-15 23:14:44.060404")        
NOTICE:  x: (3,0.857550186105072,"2018-07-15 23:14:44.060406")        
NOTICE:  x: (4,0.373486907221377,"2018-07-15 23:14:44.060408")        
NOTICE:  x: (5,0.973780393600464,"2018-07-15 23:14:44.060409")        
NOTICE:  x: (6,0.502839601133019,"2018-07-15 23:14:44.060411")        
NOTICE:  x: (7,0.217925263568759,"2018-07-15 23:14:44.060412")        
NOTICE:  x: (8,0.733274032827467,"2018-07-15 23:14:44.060413")        
NOTICE:  x: (9,0.62150136847049,"2018-07-15 23:14:44.060416")        
NOTICE:  x: (10,0.241393140517175,"2018-07-15 23:14:44.060418")        
DO        
           

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E6%96%B9%E6%B3%954%E9%A2%84%E5%88%9B%E5%BB%BA%E8%A1%A8%E7%BB%93%E6%9E%84%E4%BD%BF%E7%94%A8%E8%B5%B7%E6%9D%A5%E6%AF%94%E8%BE%83%E5%A4%8D%E6%9D%82%E4%B8%8D%E6%8E%A8%E8%8D%90 方法4、預建立表結構,使用起來比較複雜,不推薦

建立父表

預建立一些繼承表

使用時,使用advisory lock保護,挑選其中一個繼承表使用

-- 建立父表        
        
create table tmp1(id int, info text, crt_time timestamp);        
        
-- 建立100個子表        
        
do language plpgsql $$        
declare        
begin        
  for i in 1..100 loop        
    execute format('create unlogged table tmp1_%s (like tmp1 including all) inherits (tmp1)', i);        
  end loop;        
end;        
$$;        
           

建立加鎖函數,傳回值即字尾

create or replace function get_lock() returns int as $$        
declare        
begin        
  for i in 1..100 loop        
    if pg_try_advisory_lock(i) then        
      return i;        
    end if;        
  end loop;        
  return '-1';        
end;        
$$ language plpgsql strict;        
           

加鎖,傳回1則使用字尾為1的臨時表

postgres=# select get_lock();        
 get_lock         
----------        
        1        
(1 row)        
           

使用臨時表

truncate tmp1_1;        
        
... 使用 tmp1_1        
           

釋放鎖

postgres=# select pg_advisory_unlock(1);        
 pg_advisory_unlock         
--------------------        
 t        
(1 row)        
           

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E5%8F%AF%E4%BB%A5%E7%B2%BE%E7%BB%86%E5%8C%96 可以精細化

1、維護1張表,字尾ID為PK,這樣的話advisory lock id在全局都不會沖突

create table catalog_tmp (        
  tmp_tbl name,        
  prefix name,        
  suffix int primary key        
);        
        
create index idx_catalog_tmp_1 on catalog_tmp(tmp_tbl);        
           
insert into catalog_tmp select 'tmp1','tmp1',generate_series(1,100);        
           

2、申請臨時表鎖時,使用一個函數,從前面的表中擷取前字尾,直接傳回表名。

create or replace function get_tmp(name) returns text as $$        
declare        
  i int;        
  v name;        
begin        
  for i,v in select suffix,prefix from catalog_tmp where tmp_tbl=$1         
  loop        
    if pg_try_advisory_lock(i) then        
      return v||'_'||i;        
    end if;        
  end loop;        
end;        
$$ language plpgsql strict;        
           

3、申請臨時表,傳回的就是目前會話可以使用的臨時表名

postgres=# select get_tmp('tmp1');        
 get_tmp         
---------        
 tmp1_1        
(1 row)        
           

4、釋放臨時表的函數。

create or replace function release_tmp(name) returns void as $$      
declare      
begin      
  loop      
    if not pg_advisory_unlock(substring($1,'_(\d*)$')::int) then      
      return;      
    end if;      
  end loop;      
end;      
$$ language plpgsql strict;      
           

釋放臨時表(注意,不釋放的話,其他會話就不可用使用這個臨時表)

select release_tmp('tmp1_1');      
           

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E6%96%B9%E6%B3%955%E5%85%B6%E4%BB%96%E5%86%85%E6%A0%B8%E5%B1%82%E9%9D%A2%E6%94%AF%E6%8C%81%E4%B8%B4%E6%97%B6%E8%A1%A8 方法5、其他,核心層面支援臨時表

https://postgrespro.com/roadmap/56516

https://github.com/digoal/blog/blob/master/201807/20180715_01.md#%E5%8F%82%E8%80%83 參考