1. 程式人生 > >PostgreSQL 動態表複製(CREATE TABLE...LIKE)

PostgreSQL 動態表複製(CREATE TABLE...LIKE)

前言

專案中有表複製的需求,而且是動態複製,即在儲存過程里根據引數陣列的值迴圈複製n張結構(約束、索引等)等一致的一組表,PostgreSQL提供了兩種語法來進行表複製,分別是:

  1. CREATE TABLE AS
  2. CREATE TABLE LIKE

下面就通過一個例子來看看究竟哪一種更好或者說更符合我們的需求。

CREATE TABLE AS

首先看看CREATE TABLE AS的用法,在這之前結合一個具體的例子看看,我們需要複製的是這樣一張表:
這裡寫圖片描述

如上圖所示,在PowerDesigner的物理模型(pdm)中我們可以看到這張表定義了主鍵和一個外來鍵,再看看它的ddl語句:

drop table t_key_event_file_student;

/*==============================================================*/
/* Table: t_key_event_file_student                              */
/*==============================================================*/
create table t_key_event_file_student (
id                   SERIAL not
null, key_event_score_student_id INT4 not null, file_name varchar(100) not null, file_path varchar(100) not null, constraint PK_T_KEY_EVENT_FILE_STUDENT primary key (id) );
comment on table t_key_event_file_student is '關鍵事件業務表(附件)'; comment on column t_key_event_file_student.id is '主鍵'; comment on column t_key_event_file_student.key_event_score_student_id is '關鍵事件錄入ID'; comment on column t_key_event_file_student.file_name is '附件檔名稱'; comment on column t_key_event_file_student.file_path is '附件檔案路徑'; alter
table t_key_event_file_student add constraint FK_T_KEY_EV_REF16_T_KEY_EV foreign key (key_event_score_student_id) references t_key_event_score_student (id) on delete restrict on update restrict;

如上所示,首先理一下這張表都包含了什麼東西,我們複製表的同時應帶上什麼東西。
首先,id定義成了SERIAL型別,那就意味著建表的同時會為我們自動建立一個序列,那麼這個序列在表複製的時候是肯定不能copy的,因為那樣的話將意味著原表和複製的表公用一個序列,明顯不合理。其次是約束,我們可以看到上面的DDL語句中出現了三種約束,分別是:主鍵(Primary Key)約束、外來鍵(Foreign Key)約束以及非空(Not Null)約束,很顯然,表複製的同時這三種約束都應存在,中間的語句還有若干條comment(註釋),理論上註釋內容在表複製的同時也應該存在,所以簡單總結一下我們做表複製的取捨:

  1. 所有約束、索引和註釋在複製時都應被拷貝。
  2. 序列不應拷貝,應當為每一張複製的表單獨建立一個新的序列。

搞清楚這些問題後接下來看看PostgreSQL的相關支援能為我們實現什麼,首先看一下CREATE TABLE AS,官方是這樣描述的:
這裡寫圖片描述

如上圖所示,CREATE TABLE AS主要做兩件事情,分別是建表(CREATE DATA)和填充資料(FILL DATA),下面我們就通過CREATE TABLE AS複製一張表試試看。本篇blog的示例都會用t_key_event_file_student這張表,首先給這張表插入3條資料:
這裡寫圖片描述

接下來執行CREATE TABLE AS來複制該表:

create table t_key_event_file_student_100 as select * from t_key_event_file_student;

建立成功後看看它的DDL語句:
這裡寫圖片描述

再看一下這張表的資料:
這裡寫圖片描述

如上圖,首先第一張圖可以看到拷貝後的表結構,那我們再回頭看看原始表的表結構好做對比:
這裡寫圖片描述

如上圖,這樣一比較發現差距還挺大的,CREATE TABLE AS複製出來的表,所有約束、註釋和序列都沒有被拷貝,但資料成功拷貝了,就如同官方文件中的描述,顯而易見,這與我們的預期相差甚遠,所以就不做過多考慮了,接下來看看第二種複製方式——CREATE TABLE LIKE。

CREATE TABLE LIKE

如題,LIKE不同於CREATE TABLE AS 語句,它是標準CREATE TABLE語句的一個引數項,在官方文件中可以看到:
這裡寫圖片描述

後面還有對like_options的引數值列舉:
這裡寫圖片描述

如上圖,用法很簡單,即INCLUDING後面6個值或者EXCLUDING後面6個值,例如:INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING COMMENTS,這就是一種配置方式。直觀起見我們依舊通過舉例說明,下面通過CREATE TABLE LIKE來完成複製:

create table t_key_event_file_student_101 (like t_key_event_file_student);

複製成功後再看一下表結構的DDL語句和資料:
這裡寫圖片描述
這裡寫圖片描述

如上圖,同CREATE TABLE AS不同的是這次複製成功拷貝了所有NOT-NULL約束,並且沒有拷貝表資料,這也漸漸接近了我們的需求,並且驗證了一點,就是CREATE TABLE LIKE並不會複製任何資料,而CREATE TABLE AS則會複製資料。回顧一下我們的需求:

  1. 所有約束、索引和註釋在複製時都應被拷貝。
  2. 序列不應拷貝,應當為每一張複製的表單獨建立一個新的序列。

接下來就要通過LIKE選項的INCLUDING關鍵字來實現了後續需求了,官方文件中對於CREATE TABLE的like_options有幾小段詳細的解釋:

LIKE source_table [ like_option … ]
The LIKE clause specifies a table from which the new table automatically copies all column names, their data types, and their not-null constraints.

如上所示,當使用LIKE子句做表複製時,預設會自動拷貝所有欄位、欄位型別以及它們的NOT-NULL約束,這也就解釋了剛才為什麼會成功複製NOT-NULL約束。

Default expressions for the copied column definitions will only be copied if INCLUDING DEFAULTS is specified. The default behavior is to exclude default expressions, resulting in the copied columns in the new table having null defaults.

如上所示,當指定了INCLUDING DEFAULTS時預設的列定義均會被拷貝,這麼說的話由於原始表的主鍵是SERIAL型別,建立後id會繫結序列,那麼序列是否也會被拷貝呢?測試一下:

create table t_key_event_file_student_102 (like t_key_event_file_student INCLUDING DEFAULTS);

接下來看一下DDL語句:
這裡寫圖片描述

沒錯,與官方的說法一致,由於序列是指定在了列定義(column definitions )上,所以當使用了INCLUDING DEFAULTS時它自然會被複制,但這與我們的需求不符,因為我們的需求是每張被複制的表都應建立一個其專屬的唯一序列,所以結論就是不能用INCLUDING DEFAULTS,繼續往下看:

Not-null constraints are always copied to the new table. CHECK constraints will be copied only if INCLUDING CONSTRAINTS is specified. Indexes, PRIMARY KEY, and UNIQUE constraints on the original table will be created on the new table only if the INCLUDING INDEXES clause is specified. No distinction is made between column constraints and table constraints.

如上所示,NOT-NULL約束always copied to the new table,這一點在上面也提過了,它總會被複制。CHECK約束只有在指定了INCLUDING CONSTRAINTS時才會被拷貝,這很好理解,由於我們的原始表並沒有CHECK約束,所以暫不考慮。如果希望索引、主鍵約束和唯一約束被複制的話,那麼需要指定INCLUDING INDEXES,顯然這是我們需要的,因為我們的原始表指定了主鍵約束,還有最後一段:

Comments for the copied columns, constraints, and indexes will only be copied if INCLUDING COMMENTS is specified. The default behavior is to exclude comments, resulting in the copied columns and constraints in the new table having no comments.

如果希望複製註釋,那麼需要指定INCLUDING COMMENTS,很明顯,這也是我們需要的。至此我們已經可以篩選出我們需要的東西了,下面通過標記看一下:

  1. including constraints :沒有CHECK約束,所以不考慮
  2. including indexes :需要主鍵約束
  3. including comments:需要註釋
  4. including defaults:不需要複製序列,所以不要

結論是我們的LIKE選項為:INCLUDING INDEXES INCLUDING COMMENTS,所以這次就能複製一個“最貼近我們需求”的表了:

create table t_key_event_file_student_103 (like t_key_event_file_student INCLUDING INDEXES INCLUDING COMMENTS);

依舊看一下DDL語句:
這裡寫圖片描述

如上圖,可以看到這次複製的有NOT-NULL約束、主鍵約束以及註釋,這樣就完成了我們的表複製,可剛才為什麼說建立的是最貼近我們需求的表呢?因為到此為止對比需求發現我們可能還少了點東西,原始表中有外來鍵約束,那麼該如何複製呢?答案是無法複製,PostgreSQL官方並不提供外來鍵約束的複製,所以只能自己通過alter語句去新增外來鍵約束了,同樣序列也是,通過語句手動建立即可,最後就看一下通過PostgreSQL的自定義函式完成動態表複製的全過程。

自定義函式實現動態複製

如題,需求是傳入一個字串陣列,根據陣列的大小(n)來動態複製n張表,接下來直接看一下完整的自定義函式程式碼:

CREATE OR REPLACE FUNCTION "public"."f_inittables1"(arr _text)
  RETURNS "pg_catalog"."void" AS $BODY$
DECLARE
     scount INTEGER;
   rownum integer := 1;
   currsnum text;
   strSQL text;
BEGIN
        scount:=array_length(arr,1);
      while rownum <= scount LOOP
            currsnum:=arr[rownum];
            RAISE NOTICE '這裡是%', currsnum;
          -- 開始複製
      ----建表
      strSQL := 'CREATE TABLE t_self_evaluation'||'_'||currsnum||'
                        (like t_self_evaluation including constraints including indexes including comments);';
      EXECUTE strSQL;
      ----新增外來鍵約束
      strSQL :='alter table t_self_evaluation'||'_'||currsnum||'
                                add constraint FK_T_SELF_E_REF12_T_EVALUA_'||currsnum||' foreign key (scheme_id)
                                references t_evaluation_scheme (id)
                                on delete restrict on update restrict;';
            EXECUTE strSQL;
      ----指定序列
      strSQL :='create sequence t_self_evaluation_'||currsnum||'_id_seq increment by 1
                                minvalue 1 maxvalue 9223372036854775807 start with 1
                                owned by t_self_evaluation_'||currsnum||'.id';
            EXECUTE strSQL;
            rownum := rownum + 1;
    end LOOP;
END;
$BODY$
  LANGUAGE 'plpgsql' VOLATILE COST 100
;

ALTER FUNCTION "public"."f_inittables1"(arr _text) OWNER TO "postgres";

如上所示,遍歷引數陣列,根據陣列的值拼接構造表名,同時構造外來鍵名和序列名,在迴圈的n次中通過EXECUTE關鍵字執行建表語句實現動態建表,下面呼叫一下試試,傳入一個5個字串的陣列:

select f_inittables1('{"021","270","271","070","150"}');

執行結束後可以看到控制檯成功列印了RAISE NOTICE資訊:
這裡寫圖片描述

最後再看一下複製的表:
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

如上圖,可以看到已經完全滿足我們的需求了,至此我們的動態表複製就算全部結束了。

總結

簡單記錄一下PostgreSQL中實現動態表複製的全過程,希望對遇到同樣問題的朋友有所幫助,The End。