這一章節里繼續討論表的內容,畢竟后面再高深的內容都是建立在處理表數據的基礎之上的。
表繼承
表繼承其實是屬于表創建相關的內容,但是如果這些內容都放在表創建那一部分,就顯得有點太長了,看的心煩,就在這里單獨來講講。表繼承,從字面意思上來看,就是從其他位置繼承一些東西過來,實際應用上也是這樣,表繼承就是以其他表為模板來生成新的表。基本命令格式如下:
create table table_child (like table_father);
我們來看一個示例:
postgres=# \d testdb1;
Table "public.testdb1"
Column | Type | Modifiers
-----------+-----------------------+-----------------------------------
id | integer | not null
comments | character varying(20) | default 'test'::character varying
parent_id | integer |
Indexes:
"testdb1_pkey" PRIMARY KEY, btree (id)
postgres=# select * from testdb1;
id | comments | parent_id
----+----------+-----------
1 | my test | 5
(1 row)
上面是一張表的結構以及表中的數據,我們以這張表為模板生成一張新表testdb2:
postgres=# create table testdb2 (like testdb1);
CREATE TABLE
postgres=# \d testdb2;
Table "public.testdb2"
Column | Type | Modifiers
-----------+-----------------------+-----------
id | integer | not null
comments | character varying(20) |
parent_id | integer |
postgres=# select * from testdb2;
id | comments | parent_id
----+----------+-----------
(0 rows)
從上面的代碼中我們可以看到,生成的表testdb2和testdb1的結構一模一樣,但是默認情況下沒有繼承表testdb1中的數據以及comments字段的默認值。通過查詢官網文檔發現,如果想繼承原表的各種屬性以及數據,需要用到including和with關鍵字,用法分別是:
- including defaults 繼承默認值
- including constraint 繼承約束
- including indexes 繼承索引
- including storage 繼承存儲
- including comments 繼承注釋
- including all 繼承原表的所有屬性
- create table table_child as select * from table_father [with no data];
我們來嘗試一下,分別來生成一個繼承默認值和一個繼承原表所有屬性的新表。
postgres=# create table testdb3 (like testdb1 including defaults);
CREATE TABLE
postgres=# \d testdb3
Table "public.testdb3"
Column | Type | Modifiers
-----------+-----------------------+-----------------------------------
id | integer | not null
comments | character varying(20) | default 'test'::character varying
parent_id | integer |
postgres=# select * from testdb3;
id | comments | parent_id
----+----------+-----------
(0 rows)
從上面可以看到繼承了默認值,沒有繼承表中數據以及主鍵索引
postgres=# create table testdb4 (like testdb1 including all);
CREATE TABLE
postgres=# \d testdb4;
Table "public.testdb4"
Column | Type | Modifiers
-----------+-----------------------+-----------------------------------
id | integer | not null
comments | character varying(20) | default 'test'::character varying
parent_id | integer |
Indexes:
"testdb4_pkey" PRIMARY KEY, btree (id)
postgres=# select * from testdb4;
id | comments | parent_id
----+----------+-----------
(0 rows)
從上面可以看到,including all關鍵字會繼承所有屬性,但是不會繼承原表的數據。
postgres=# create table testdb5 as select * from testdb1;
SELECT 1
postgres=# \d testdb5;
Table "public.testdb5"
Column | Type | Modifiers
-----------+-----------------------+-----------
id | integer |
comments | character varying(20) |
parent_id | integer |
postgres=# select * from testdb5;
id | comments | parent_id
----+----------+-----------
1 | my test | 5
(1 row)
這個示例使用as關鍵字創建新表testdb5,不帶with子句。從中可以看到默認情況下,as語句會將原來表里的數據復制到新表中,但是不會復制原來表中的屬性,例如各種索引、約束等。那能不能通過把這兩種命令形式結合起來,既可以繼承原表結構,又可以復制原表數據呢?目前還是沒有這種命令的,只能通過完整的復制數據,再添加索引、約束等,或者先復制完整的表結構,再導入數據。
臨時表
PostgreSQL支持兩種類型的臨時表,分別是會話級別臨時表和事務級別臨時表。同時,表中的數據也分為兩個級別,一個是會話級別,一個是事務級別。因此就存在三種形式的臨時表。分別是:
- 會話級別表、會話級別數據
- 會話級別表、事務級別數據
- 事務級別表、事務級別數據
第一種表和數據在會話周期都存在,會話銷毀,表和數據銷毀。第二種表在會話周期存在,數據在事務周期存在,事務周期結束數據銷毀,會話結束表銷毀。第三種表和數據在事務結束以后都會銷毀。
臨時表的創建
臨時表的創建和普通表的創建唯一的區別就是在創建語句中加了一個temporary關鍵字(也可以簡寫為temp),基本命令格式如下:
create temporary table table_name (field1 type1, field2 type...);
也可以使用我們上面表繼承的語法來創建臨時表:
create temp table table_name(like table1);
或
create temp table table_name as select * from table1;
根據前面說的,臨時表是分成三種類型的,而采用上面的語句,默認情況下創建的是會話級別的表和數據。而如果想創建其他級別的表,則需要使用下面幾個子句:
-
on commit perserve rows
這個子句和默認情況相同,創建會話級別的表和數據,插入的數據保留到。 -
on commit delete rows
這個子句用來創建會話級別表和事務級別數據,事務銷毀后表中數據銷毀。 -
on commit drop
這個子句只能用在事務中創建臨時表,在會話中創建只會顯示表格創建的提示消息,但是看不到表格,因為表格一創建成功就立即銷毀了。
通過下面幾個示例來看一下:
postgres=# create temp table temp_1 as select * from testdb1;
SELECT 1
postgres=# \d
List of relations
Schema | Name | Type | Owner
-----------+---------+-------+----------
pg_temp_2 | temp_1 | table | postgres
public | testdb1 | table | postgres
public | testdb2 | table | postgres
(3 rows)
postgres=# create temp table temp_2 as select id, parent_id from testdb1;
SELECT 1
postgres=# \d temp_2;
Table "pg_temp_2.temp_2"
Column | Type | Modifiers
-----------+---------+-----------
id | integer |
parent_id | integer |
postgres=# create temp table temp_3 (like testdb1);
CREATE TABLE
postgres=# \d temp_3;
Table "pg_temp_2.temp_3"
Column | Type | Modifiers
-----------+-----------------------+-----------
id | integer | not null
comments | character varying(20) |
parent_id | integer |
postgres=# \d
List of relations
Schema | Name | Type | Owner
-----------+---------+-------+----------
pg_temp_2 | temp_1 | table | postgres
pg_temp_2 | temp_2 | table | postgres
pg_temp_2 | temp_3 | table | postgres
public | testdb1 | table | postgres
public | testdb2 | table | postgres
(5 rows)
從上面可以看到,三張臨時表都是處在一個特殊的模式下,模式名稱是pg_temp_2。而在postgresql中,每個會話中創建的臨時表都是處在特殊的模式下,模式名稱一般都是pg_temp_x,x是根據不同的會話來分配的數字。我們來嘗試創建一下其他級別的表:
- 創建會話級別臨時表和事務級別數據。
postgres=# create temp table temp_4 (like temp_1 including all) on commit delete rows;
CREATE TABLE
postgres=# select * from temp_4;
id | comments | parent_id
----+----------+-----------
(0 rows)
postgres=# insert into temp_4 values(1, 'test4', 4);
INSERT 0 1
postgres=# select * from temp_4;
id | comments | parent_id
----+----------+-----------
(0 rows)
postgres=# begin;
BEGIN
postgres=# insert into temp_4 values(1, 'test4', 4);
INSERT 0 1
postgres=# select * from temp_4;
id | comments | parent_id
----+----------+-----------
1 | test4 | 4
(1 row)
postgres=# end;
COMMIT
postgres=# select * from temp_4;
id | comments | parent_id
----+----------+-----------
(0 rows)
上面的示例中,我們操作依次是:
(1) 創建臨時表temp_4,表是會話級別,數據是事務級別
(2)查看temp_4表,沒有任何數據
(3)插入一條數據,再看表,還是沒有數據
(4)begin啟動一個事務,往表中插入數據,查看表,發現有一條數據
(5)end結束事務,再查看表,表中數據消失。
示例清楚地解釋了這種臨時表的特性
- 事務臨時表和事務數據
postgres=# create temp table temp_5 (like temp_1 including all) on commit drop;
CREATE TABLE
postgres=# \d
List of relations
Schema | Name | Type | Owner
-----------+---------+-------+----------
pg_temp_2 | temp_1 | table | postgres
pg_temp_2 | temp_2 | table | postgres
pg_temp_2 | temp_3 | table | postgres
pg_temp_2 | temp_4 | table | postgres
public | testdb1 | table | postgres
public | testdb2 | table | postgres
(6 rows)
上面代碼中我們創建了一個事務級別表,提示創建成功,但是看不到這個臨時表。說明事務級別表在會話中沒法存在,或者說創建之后就銷毀。我們在事務中再來看:
postgres=# begin;
BEGIN
postgres=# create temp table temp_5 (like temp_1 including all) on commit drop;
CREATE TABLE
postgres=# \d
List of relations
Schema | Name | Type | Owner
-----------+---------+-------+----------
pg_temp_2 | temp_1 | table | postgres
pg_temp_2 | temp_2 | table | postgres
pg_temp_2 | temp_3 | table | postgres
pg_temp_2 | temp_4 | table | postgres
pg_temp_2 | temp_5 | table | postgres
public | testdb1 | table | postgres
public | testdb2 | table | postgres
(7 rows)
postgres=# select * from temp_5;
id | comments | parent_id
----+----------+-----------
(0 rows)
postgres=# insert into temp_5 values(1, 'test5', 5);
INSERT 0 1
postgres=# select * from temp_5;
id | comments | parent_id
----+----------+-----------
1 | test5 | 5
(1 row)
postgres=# end;
COMMIT
postgres=# \d
List of relations
Schema | Name | Type | Owner
-----------+---------+-------+----------
pg_temp_2 | temp_1 | table | postgres
pg_temp_2 | temp_2 | table | postgres
pg_temp_2 | temp_3 | table | postgres
pg_temp_2 | temp_4 | table | postgres
public | testdb1 | table | postgres
public | testdb2 | table | postgres
(6 rows)
上面的代碼是一個完整的事務,代碼功能依次是:
(1)在事務中先創建臨時表temp_5,查看表,確實存在。
(2)查看表中沒有任何數據,往表中插入一條數據,再看表,有一條數據
(3)退出事務,再看表信息,temp_5這個臨時表已經消失了。
相信經過這幾個例子,大家對臨時表的特性已經有所了解了。
表分區
表分區是通過某種標準將一個大表劃分成若干個小表,然后創建觸發器,在插入數據的時候,觸發器根據指定的標準,將數據插入到不同的小表中去。可以提高性能,以及查詢速度。標準可以是日期、時間、地區等等。
表分區的實現依賴于表繼承,這里的表繼承和上面所說的表繼承略有區別,上面的表繼承是通過復制父表的結構生成子表,而這里的表繼承,指的是完全繼承父表的表結構,同時還可以添加自己的字段來生成一張新的子表。如下所示:
postgres=# create table father(name text, age int);
CREATE TABLE
postgres=# create table child(language text) inherits (father);
CREATE TABLE
postgres=# \d father;
Table "public.father"
Column | Type | Modifiers
--------+---------+-----------
name | text |
age | integer |
Number of child tables: 1 (Use \d+ to list them.)
postgres=# \d child;
Table "public.child"
Column | Type | Modifiers
----------+---------+-----------
name | text |
age | integer |
language | text |
Inherits: father
在上面的示例種,子表child繼承父表father,同時添加了自己字段language。
而且這種表繼承還具有不同的特性,這種類型的表繼承中,查詢父表,可以查詢到所有子表中對應字段的內容。我們通過幾個例子來看一下這種表繼承的特性:
postgres=# select * from father;
name | age
------+-----
(0 rows)
postgres=# slect * from child
postgres-# ;
ERROR: syntax error at or near "slect"
LINE 1: slect * from child
^
postgres=# select * from child;
name | age | language
------+-----+----------
(0 rows)
postgres=# insert into child values('張三', 16, '中文');
INSERT 0 1
postgres=# insert into child values('tom', 19, 'English');
INSERT 0 1
postgres=# select * from father;
name | age
------+-----
張三 | 16
tom | 19
(2 rows)
postgres=# select * from child;
name | age | language
------+-----+----------
張三 | 16 | 中文
tom | 19 | English
(2 rows)
從上面的例子中可以看到,往子表中插入的數據,在父表中可以看到對應字段的數據。再看下面這個例子:
postgres=# insert into father values('李四', 25);
INSERT 0 1
postgres=# select * from father;
name | age
------+-----
李四 | 25
張三 | 16
tom | 19
(3 rows)
postgres=# select * from child;
name | age | language
------+-----+----------
張三 | 16 | 中文
tom | 19 | English
(2 rows)
可以看到,往父表中插入的數據,在子表中卻看不到。
而這種類型的表繼承的特性恰好是表分區實現的基礎,可以通過查詢主表來查詢主表下所有分區表的內容。往分區表里插入數據,相當于是往主表中插入數據。在這里,主表相當于是子表,分區表相當于是父表。同時,還要通過觸發器來實現往主表里插入數據或查詢數據時,自動轉到對應的分區表上。因此還要了解觸發器的創建規則。因此這部分內容在學完觸發器的知識后再來看。
表的存儲屬性
表的存儲屬性指的是
TOAST用于存儲大字段的技術,在理解這個技術之前,先了解一下頁的概念。頁在PostgreSQL中是數據在文件存儲中的單位,其大小是固定的且只能在編譯時指定,之后無法修改,默認的大小為8 KB 。同時,PG 不允許一行數據跨頁存儲,那么對于超長的行數據,PG 就會啟動 TOAST ,具體就是采用壓縮和切片的方式。如果啟用了切片,實際上行數據存儲在另一張系統表的多個行中,這張表就叫 TOAST 表,這種存儲方式叫行外存儲。
而一般情況下,只有變長類型才會支持TOAST技術,在變長類型中,前4個字節是長度字。長度字的前2位是標志位,后30位是長度值。長度值包含自身占用的4個字節,因此,TOAST數據類型的長度最大應該是30b,及1GB(2^30-1)個字節。
前2位標志位,第一個表示是否壓縮,第二個表示是否是行外存儲。兩個都是0表示不壓縮,不是行外存儲。第一個為1表示壓縮過,使用之前必須解壓。第二個為1表示行外存儲,此時,長度字后面存儲的只是一個指針。不管數據是否壓縮,長度位后30bit都表示的是數據的真實大小,而不會是壓縮以后的大小。
如果一張表A有對應的toast表,那么在系統表pg_class里面就會有一個表名A對應一個toast表的OID,然后根據這個oid就能找到對應的toast表pg_toast_oid。如果么某個大字段的數據存放在toast表中,那么這個字段的數據就會被切片成頁面大小1/4大小的塊。然后將這些數據塊存放在toast表中。每個toast表有chunk_id、chunk_seq和chunk_data字段,數據塊就放在chunk_data字段中。在chunk_id和chunk_seq上有一個唯一的索引,用于對數據的快速檢索。因此在前面變長類型的字段的長度位中,如果存儲的是指針的話,那么該指針就包含toast表的OID和特定數值的chunk_id,以及數據的實際長度。
在 PG 中每個表字段有四種 TOAST 的策略:
- PLAIN :避免壓縮和行外存儲。只有那些不需要 TOAST 策略就能存放的數據類型允許選擇(例如 int 類型),而對于 text 這類要求存儲長度超過頁大小的類型,是不允許采用此策略的。
- EXTENDED :允許壓縮和行外存儲。一般會先壓縮,如果還是太大,就會行外存儲
- EXTERNA :允許行外存儲,但不許壓縮。類似字符串這種會對數據的一部分進行操作的字段,采用此策略可能獲得更高的性能,因為不需要讀取出整行數據再解壓。
- MAIN :允許壓縮,但不許行外存儲。不過實際上,為了保證過大數據的存儲,行外存儲在其它方式(例如壓縮)都無法滿足需求的情況下,作為最后手段還是會被啟動。因此理解為:盡量不使用行外存儲更貼切。
在PG中,會為每個允許TOAST的字段設置一個默認的策略。但是可以使用ALTER TABLE SET STORAGE
命令來修改系統設置的默認策略。
默認情況下,只有當一個字段數據的大小超過頁大小的1/4的時候,才會觸發壓縮策略。即對于8k頁大小的數據庫來說,字段數據大小超過2k時才會觸發壓縮策略。在這種情況下,如果兩張結構相同的表A和B,A中某個字段存儲的數據小于2k,而B中該字段的數據大于2k,會出現B表所占用的空間反而小于A表。
從上面這些內容來看,所謂的存儲屬性,應該指的就是針對字段設置的各種策略。此外,還可以針對表設置fillfactor和toast.fillfactor這兩個值。這兩個值表示表的填充因子和toast表的填充因子,填充因子表示的是一個數據塊的填充比例,即一個數據庫填充到多少以后就不填充數據了,剩下的空間留做數據更新時使用。
在PG中,更新舊數據時,不會刪除舊數據,而是在保留的空間里寫入一條新數據,然后在舊數據和新數據之間創建一個鏈表,這樣就不用更新索引,索引找到舊數據時,會根據鏈表找到新數據。但是如果填充因子設置的過大,更新舊數據時,保留的空間不足以存放更新的數據,且數據不能跨頁存放,因此必須放在新的數據塊,此時舊需要更新索引。會更耗時,因此,對于數據更新頻繁的表,這個值可以設置的小一點。