iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 19
2
Postgresql 的 ENUM 型態

可以用來取代小的屬性表.來看以下的例子

create table color (
  id int generated always as identity primary key
, name text
);

create table cars (
  brand text
, model text
, color integer references color(id)
);

insert into color(name) values
('blue'),('red'),('gray'),('black');

insert into cars(brand, model, color)
select brand, model, color.id
  from (values 
        ('ferari', 'testarosa', 'red'),
        ('aston martin', 'db2', 'blue'),
        ('bentley', 'mulsanne', 'gray'),
        ('ford', 'T', 'black')
       ) as data(brand, model, color)
  join color
    on color.name = data.color;

這時候在 cars 裡的值

select *
  from cars;

+--------------+-----------+-------+
|    brand     |   model   | color |
+--------------+-----------+-------+
| aston martin | db2       |     1 |
| ferari       | testarosa |     2 |
| bentley      | mulsanne  |     3 |
| ford         | T         |     4 |
+--------------+-----------+-------+

實際使用時,會跟 color 做 join, 取出 color.name

select brand
     , model
     , color.name as color
  from cars
  join color
    on color.id = cars.color;

+--------------+-----------+-------+
|    brand     |   model   | color |
+--------------+-----------+-------+
| aston martin | db2       | blue  |
| ferari       | testarosa | red   |
| bentley      | mulsanne  | gray  |
| ford         | T         | black |
+--------------+-----------+-------+

或是依據上面這道 SQL Command 建立一個 view, 方便使用.

在開發系統的過程中,諸如 color 這類的屬性表,不勝枚舉.
所以就有了這類的資料型態,ENUM, 可以翻譯為 枚舉.

來看怎樣建立 enum , 在 Postgresql 中,要先建立一個 enum 的 type,
在 MySQL 是在建立 table 時,直接用 enum . 好處是方便,但是不易於複用.
Postgresql 的方式,可以複用.
兩者各有千秋,也可以看出兩種資料庫產品的設計哲學差異.

create type color_t as enum('blue', 'red', 'gary', 'black');

drop table if exists cars;

create table cars (
  brand text
, model text
, color color_t
);

insert into cars(brand, model, color) values 
('ferari', 'testarosa', 'red'),
('aston martin', 'db2', 'blue'),
('bentley', 'mulsanne', 'gray'),
('ford', 'T', 'black');

ERROR:  22P02: invalid input value for enum color_t: "gray"
LINE 4: ('bentley', 'mulsanne', 'gray'),
                                ^
LOCATION:  enum_in, enum.c:60

出現錯誤了...因為前面建立 enum 時, gray 拼成 gary 了.
這樣就能看出 enum 的用處之一.

來看怎樣修正 enum

alter type color_t rename value 'gary' to 'gray';

這時候就能順利輸入了,同樣的 insert into ,我就不重複了.

來看 enum 另一個功用,指定排序順序.

不指定 order by 欄位時,隨機排列.
select *
  from cars;
+--------------+-----------+-------+
|    brand     |   model   | color |
+--------------+-----------+-------+
| ferari       | testarosa | red   |
| aston martin | db2       | blue  |
| bentley      | mulsanne  | gray  |
| ford         | T         | black |
+--------------+-----------+-------+

select *
  from cars
 order by color;
+--------------+-----------+-------+
|    brand     |   model   | color |
+--------------+-----------+-------+
| aston martin | db2       | blue  |
| ferari       | testarosa | red   |
| bentley      | mulsanne  | gray  |
| ford         | T         | black |
+--------------+-----------+-------+

這樣就依照 enum color_t 的順序排列了.

既然具有順序性(Ordering),那就可以查找某個值之前或之後.

select *
  from cars
 where color >= 'gray'
 order by color;

+---------+----------+-------+
|  brand  |  model   | color |
+---------+----------+-------+
| bentley | mulsanne | gray  |
| ford    | T        | black |
+---------+----------+-------+
(2 rows)

若要查找 enum 裡面的內容, 在 Postgresql 中有 Enum Support Functions

https://www.postgresql.org/docs/current/functions-enum.html

計有 enum_first() enum_last() enum_range(), 
enum_range(anyenum, anyenum) <- 找兩個enum 元素之間的值.
可以借助NULL搭配轉型.

select enum_first(null::color_t)
     , enum_first('black'::color_t) 
     , enum_last(null::color_t)
     , enum_range(null::color_t);

+------------+------------+-----------+-----------------------+
| enum_first | enum_first | enum_last |      enum_range       |
+------------+------------+-----------+-----------------------+
| blue       | blue       | black     | {blue,red,gray,black} |
+------------+------------+-----------+-----------------------+

當我們不知 enum 裡面任一元素時,使用 null 搭配轉型,可以取出 first, last, range.
當知道部分,可以使用以下方式取出區間

select enum_range(null, 'gray'::color_t)
     , enum_range('red'::color_t, null);

+-----------------+------------------+
|   enum_range    |    enum_range    |
+-----------------+------------------+
| {blue,red,gray} | {red,gray,black} |
+-----------------+------------------+

觀察 enum_range()  回傳的值,有{},應該是 array.

雖然文件中,沒有寫明是 array.我們可以使用 pg_typeof()

select pg_typeof(enum_range(null::color_t));
+-----------+
| pg_typeof |
+-----------+
| color_t[] |
+-----------+

是由 color_t 組成元素的 array. 

也可以利用系統表,看過之前一系列的介紹以後,應該能猜到是用 pg_enum.

我們先來建立另一個 enum ,方便探討.

create type dd_t as enum ('雪風','初風','天津風','時津風');

可以使用以下 SQL Command

select enumtypid::regtype
     , array_agg(enumsortorder::text || '->' || enumlabel) as elements
  from pg_enum
 group by enumtypid;
 
+-----------+---------------------------------------+
| enumtypid |               elements                |
+-----------+---------------------------------------+
| color_t   | {1->blue,2->red,4->black,3->gray}     |
| dd_t      | {1->雪風,2->初風,3->天津風,4->時津風} |
+-----------+---------------------------------------+
(2 rows)

或是不使用轉型,由 join pg_type 取出 enum_name 

select type.typname as enum_name
     , string_agg(enum.enumlabel, '|') as elem1
     , array_agg(enum.enumsortorder::text || '->' || enum.enumlabel) as elem2
  from pg_enum as enum
  join pg_type as type
    on (type.oid = enum.enumtypid)
 group by type.typname;

+-----------+-------------------------+---------------------------------------+
| enum_name |          elem1          |                 elem2                 |
+-----------+-------------------------+---------------------------------------+
| color_t   | blue|red|black|gray     | {1->blue,2->red,4->black,3->gray}     |
| dd_t      | 雪風|初風|天津風|時津風    | {1->雪風,2->初風,3->天津風,4->時津風}     |
+-----------+-------------------------+---------------------------------------+
(2 rows)

上面有驗證了 enum_range() 回傳了 array.
可以透過 array 的 unnest(), 搭配 enum_range() 

select *
  from unnest(enum_range(null::dd_t)) 
       with ordinality as un(rn, elem);
+--------+------+
|   rn   | elem |
+--------+------+
| 雪風   |    1 |
| 初風   |    2 |
| 天津風 |    3 |
| 時津風 |    4 |
+--------+------+
(4 rows)

當然比較簡單的方式.是直接 unnest()

select unnest(enum_range(null::dd_t));

+--------+
| unnest |
+--------+
| 雪風   |
| 初風   |
| 天津風 |
| 時津風 |
+--------+
(4 rows)

可以觀察到 array 型態在 Postgresql 中,應用廣泛.

查元素是否存在 table 的 enum 型態欄位裡,可以使用轉型

select *
  from cars
 where cars.color::text = 'white';

+-------+-------+-------+
| brand | model | color |
+-------+-------+-------+
+-------+-------+-------+

但是 white 是不能轉型為 color_t 的.

直接查看 enum 的方法
例如 島風 是否有在水雷戰隊裡面.

select '島風' = any(enum_range(null::dd_t)::text[]);

+----------+
| ?column? |
+----------+
| f        |
+----------+

也是轉型成 text[],來比較.來看天津風

select '天津風' = any(enum_range(null::dd_t)::text[]);

+----------+
| ?column? |
+----------+
| t        |
+----------+

接著來看加入元素的方法,來把島風加入水雷戰隊.

alter type dd_t add value '島風' after '天津風';
ERROR:  25001: ALTER TYPE ... ADD cannot run inside a transaction block
LOCATION:  PreventInTransactionBlock, xact.c:3213
Time: 12.060 ms

這樣會發生錯誤,雖然官網文件上寫可以使用 before / after , 但也有 transaction 的限制.
這樣島風就不能在好朋友天津風旁邊了.
我們先來將島風加到最後看看

alter type dd_t add value '島風' after '時津風';
ERROR:  25001: ALTER TYPE ... ADD cannot run inside a transaction block
LOCATION:  PreventInTransactionBlock, xact.c:3213

一樣是不行...

修改 pg_enum 直接加進去看看,這種奇技淫巧,我們先來做select, 然後再 insert

select 'dd_t'::regtype::oid
     , '島風'
     , (select max(enumsortorder) + 1 
          from pg_enum
         where enumtypid = 'dd_t'::regtype
       );
+-------+----------+----------+
|  oid  | ?column? | ?column? |
+-------+----------+----------+
| 34682 | 島風     |        5 |
+-------+----------+----------+

insert into pg_enum(enumtypid, enumlabel, enumsortorder)
select 'dd_t'::regtype::oid
     , '島風'
     , (select max(enumsortorder) + 1 
          from pg_enum
         where enumtypid = 'dd_t'::regtype
       );

select unnest(enum_range(null::dd_t));
+--------+
| unnest |
+--------+
| 雪風   |
| 初風   |
| 天津風 |
| 時津風 |
| 島風   |
+--------+
(5 rows)

島風已經加入了.這種方式適合有愛的提督.

一般還是會選擇 建立新的 enum, 
然後將有使用到 enum 的 table 修改成使用新 enum的方式.


上一篇
Postgresql 的 Domain 型態
下一篇
Postgresql 中的 check
系列文
以Postgresql為主,聊聊資料庫.31

1 則留言

我要留言

立即登入留言