PostgreSQL TOAST 技術解析

liguangxian2018發表於2018-06-01

轉載:https://cloud.tencent.com/developer/article/1004455

TOAST是“The Oversized-Attribute Storage Technique”的縮寫,主要用於儲存一個大欄位的值。要理解TOAST,我們要先理解頁(BLOCK)的概念。在PG中,頁是資料在檔案儲存中的基本單位,其大小是固定的且只能在編譯期指定,之後無法修改,預設的大小為8KB。同時,PG不允許一行資料跨頁儲存,那麼對於超長的行資料,PG就會啟動TOAST,具體就是採用壓縮和切片的方式。如果啟用了切片,實際資料儲存在另一張系統表的多個行中,這張表就叫TOAST表,這種儲存方式叫行外儲存。

在深入細節之前,我們要先了解,在PG中每個表欄位有四種TOAST的策略:

  • PLAIN:避免壓縮和行外儲存。只有那些不需要TOAST策略就能存放的資料型別允許選擇(例如int型別),而對於text這類要求儲存長度超過頁大小的型別,是不允許採用此策略的

  • EXTENDED:允許壓縮和行外儲存。一般會先壓縮,如果還是太大,就會行外儲存

  • EXTERNA:允許行外儲存,但不許壓縮。類似字串這種會對資料的一部分進行操作的欄位,採用此策略可能獲得更高的效能,因為不需要讀取出整行資料再解壓。

  • MAIN:允許壓縮,但不許行外儲存。不過實際上,為了保證過大資料的儲存,行外儲存在其它方式(例如壓縮)都無法滿足需求的情況下,作為最後手段還是會被啟動。因此理解為:儘量不使用行外儲存更貼切。 現在我們通過實際操作來研究TOAST的細節:

首先建立一張blog表:

postgres=# create table blog(id int, title text, content text);
CREATE TABLE
postgres=# \d+ blog;
                          Table "public.blog"
 Column  |  Type   | Modifiers | Storage  | Stats target | Description 
---------+---------+-----------+----------+--------------+-------------
 id      | integer |           | plain    |              | 
 title   | text    |           | extended |              | 
 content | text    |           | extended |              |

可以看到,interger預設TOAST策略為plain,而text為extended。PG資料告訴我們,如果表中有欄位需要TOAST,那麼系統會自動建立一張TOAST表負責行外儲存,那麼這張表在哪裡?

postgres=# select relname,relfilenode,reltoastrelid from pg_class where relname='blog';
 relname | relfilenode | reltoastrelid 
---------+-------------+---------------
 blog    |       16441 |         16444
(1 row)

通過上訴語句,我們查到blog表的oid為16441,其對應TOAST表的oid為16444(關於oid和pg_class的概念,請參考PG官方文件),那麼其對應TOAST表名則為:pg_toast.pg_toast_16441(注意這裡是blog表的oid),我們看下其定義:

postgres=# \d+ pg_toast.pg_toast_16441;
TOAST table "pg_toast.pg_toast_16441"
   Column   |  Type   | Storage 
------------+---------+---------
 chunk_id   | oid     | plain
 chunk_seq  | integer | plain
 chunk_data | bytea   | plain

TOAST表有3個欄位:

  • chunk_id:用來表示特定TOAST值的OID,可以理解為具有同樣chunk_id值的所有行組成原表(這裡的blog)的TOAST欄位的一行資料

  • chunk_seq:用來表示該行資料在整個資料中的位置

  • chunk_data:實際儲存的資料。 現在我們來實際驗證下:

postgres=# insert into blog values(1, 'title', '0123456789');
INSERT 0 1
postgres=# select * from blog;
 id | title |  content   
----+-------+------------
  1 | title | 0123456789
(1 row)

postgres=# select * from pg_toast.pg_toast_16441;
 chunk_id | chunk_seq | chunk_data 
----------+-----------+------------
(0 rows)

可以看到因為content只有10個字元,所以沒有壓縮,也沒有行外儲存。然後我們使用如下SQL語句增加content的長度,每次增長1倍,同時觀察content的長度,看看會發生什麼情況?

postgres=# update blog set content=content||content where id=1;
UPDATE 1
postgres=# select id,title,length(content) from blog;
 id | title | length 
----+-------+--------
  1 | title |     20
(1 row)
postgres=# select * from pg_toast.pg_toast_16441;
 chunk_id | chunk_seq | chunk_data 
----------+-----------+------------
(0 rows)

反覆執行如上過程,直到pg_toast_16441表中有資料:

postgres=# select id,title,length(content) from blog;
 id | title | length 
----+-------+--------
  1 | title | 327680
(1 row)

postgres=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_16441;
 chunk_id | chunk_seq | length 
----------+-----------+--------
    16439 |         0 |   1996
    16439 |         1 |   1773
(2 rows)

可以看到,直到content的長度為327680時(已遠遠超過頁大小8K),對應TOAST表中才有了2行資料,且長度都是略小於2K,這是因為extended策略下,先啟用了壓縮,然後才使用行外儲存

下面我們將content的TOAST策略改為EXTERNA,以禁止壓縮。

postgres=# alter table blog alter content set storage external;
ALTER TABLE
postgres=# \d+ blog;
                          Table "public.blog"
 Column  |  Type   | Modifiers | Storage  | Stats target | Description 
---------+---------+-----------+----------+--------------+-------------
 id      | integer |           | plain    |              | 
 title   | text    |           | extended |              | 
 content | text    |           | external |              |

然後我們再插入一條資料:

postgres=# insert into blog values(2, 'title', '0123456789');
INSERT 0 1
postgres=# select id,title,length(content) from blog;
 id | title | length 
----+-------+--------
  1 | title | 327680
  2 | title |     10
(2 rows)

然後重複以上步驟,直到TOAST表中產生新的行:

postgres=# update blog set content=content||content where id=2;
UPDATE 1
postgres=# select id,title,length(content) from blog;
 id | title | length 
----+-------+--------
  2 | title |   2560
  1 | title | 327680
(2 rows)

postgres=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_16441;
 chunk_id | chunk_seq | length 
----------+-----------+--------
    16447 |         0 |   1996
    16447 |         1 |   1773
    16448 |         0 |   1996
    16448 |         1 |    564
(4 rows)

這次我們看到當content長度達到2560(按照官方文件,應該是超過2KB左右),TOAST表中產生了新的2條chunk_id為16448的行,且2行資料的chunk_data的長度之和正好等於2560。通過以上操作得出以下結論:

  • 如果策略允許壓縮,則TOAST優先選擇壓縮
  • 不管是否壓縮,一旦資料超過2KB左右,就會啟用行外儲存
  • 修改TOAST策略,不會影響現有資料的儲存方式

相關文章