回到關聯式資料庫管理系統,它能為應用程式提供的是:
- 存取資料、執行交易的服務
- 在多個應用程式基座之間保證一致性的共同 API
- 與資料庫服務交換資料的傳輸機制
本章的焦點是 ACID 中的 C——資料一致性(consistency)。應用程式一旦成長,就會拆成多個部分:管理面板、客服後台、對外前台、會計與財務報表,也許還有業務後台等;其中一些可能採用第三方方案,就算全部自製,不同部分也常用不同技術棧(Go 或 Java 後端、Python/Django 或 Ruby on Rails、PHP、Node.js…)。要讓這一群應用程式協同運作、遵守同一套商業規則,需要一個能保證整體一致性的核心系統——這正是 RDBMS 要解決的主要問題,也是關聯模型如此通用的原因。下一章〈資料塑模〉會把 schemaless 與關聯式塑模拿來比較;在那之前,先要理解 PostgreSQL 是怎麼保證一致性的。
屬性值、資料域與資料型別#
- 資料域(data domain)就像數學裡的概念:一組被賦予共同名稱的值的集合,例如自然數域、有理數域。
- 在關聯理論中,基本資料域可以組合成元組(tuple):元組是一串屬性的列表;關聯(relation)則是一串元組的列表,所有元組共享同一份屬性域清單——名稱與資料型別。
- 關聯模型的基礎,就是在資料集合內建立一致性:把資料結構化到「我們知道自己在處理什麼」,並且能夠強制執行商業約束。
第一個被強制執行的商業約束是「資料必須正確」。例如 PostgreSQL 的 timestamp 型別實作了格里曆(Gregorian Calendar)——沒有零年、零月、零日。其他系統可能接受任何「長得像 timestamp」的文字,PostgreSQL 則真的檢查值在格里曆中是否成立:
select date '2010-02-29';ERROR: date/time field value out of range: "2010-02-29"2010 年不是閏年,所以 2010-02-29 不是合法日期。順帶一提,這種輸入語法稱為修飾字面值(decorated literal):用資料型別修飾字面值,PostgreSQL 就不必猜它是什麼。
再試試惡名昭彰的零時戳:
select timestamp '0000-00-00 00:00:00';ERROR: date/time field value out of range: "0000-00-00 00:00:00"格里曆沒有零年——西元前 1 年之後直接是西元 1 年:
select date(date '0001-01-01' + x * interval '1 day')
from generate_series (-2, 1) as t(x); date
═══════════════
0001-12-30 BC
0001-12-31 BC
0001-01-01
0001-01-02
(4 rows)實作格里曆不是必須忍受的限制,而是可以善加利用的強大選擇:PostgreSQL 懂閏年、懂時區,日期時間型別還支援有意義的字面值:
select date 'today' + time 'allballs' as midnight; midnight
═════════════════════
2017-08-14 00:00:00
(1 row)一致性與資料型別行為#
PostgreSQL 資料型別的關鍵之處在於行為(behavior)。與物件導向系統類似,PostgreSQL 實作了函式與運算子的多型(polymorphism),在執行期依參數型別派發程式碼。看一個極簡單的查詢:
select code from drivers where driverid = 1;driverid = 1 這個運算式在欄位與字面值之間使用了 = 運算子。PostgreSQL 從系統目錄(catalog)得知 driverid 是 bigint,而字面值 1 被解析為 integer:
select pg_typeof(driverid), pg_typeof(1) from drivers limit 1; pg_typeof │ pg_typeof
═══════════╪═══════════
bigint │ integer
(1 row)那 8 位元組整數與 4 位元組整數之間的 = 是怎麼實作的?這個決定是動態的:= 運算子依左右運算元的型別派發到一個既定的函式。我們可以直接查 PostgreSQL 的目錄:
select oprname, oprleft::regtype, oprcode::regproc
from pg_operator
where oprname = '='
and oprleft::regtype::text ~ 'int|time|text|circle|ip'
order by oprleft;查詢輸出:24 個 = 運算子實例
oprname │ oprleft │ oprcode
═════════╪═════════════════════════════╪══════════════════════════
= │ bigint │ int84eq
= │ bigint │ int8eq
= │ bigint │ int82eq
= │ smallint │ int28eq
= │ smallint │ int2eq
= │ smallint │ int24eq
= │ int2vector │ int2vectoreq
= │ integer │ int48eq
= │ integer │ int42eq
= │ integer │ int4eq
= │ text │ texteq
= │ abstime │ abstimeeq
= │ reltime │ reltimeeq
= │ tinterval │ tintervaleq
= │ circle │ circle_eq
= │ time without time zone │ time_eq
= │ timestamp without time zone │ timestamp_eq
= │ timestamp without time zone │ timestamp_eq_date
= │ timestamp without time zone │ timestamp_eq_timestamptz
= │ timestamp with time zone │ timestamptz_eq_timestamp
= │ timestamp with time zone │ timestamptz_eq
= │ timestamp with time zone │ timestamptz_eq_date
= │ interval │ interval_eq
= │ time with time zone │ timetz_eq
(24 rows)- 這個查詢只依運算子左側期望的型別過濾;目錄裡當然也存了右側期望的型別與結果型別(等值比較的結果型別是 Boolean)。
- 輸出中的
oprcode欄位就是運算子被使用時實際執行的 PostgreSQL 函式名稱。 - 以
driverid = 1來說,PostgreSQL 會用int84eq函式來實作這個查詢——除非driverid上有 index:那時 PostgreSQL 會改走索引找出符合的資料列,只比較索引內容而不碰資料表內容。
使用 PostgreSQL 時,資料型別提供的是:
- 輸入的資料表示法(輸入字面值的預期格式)
- 輸出的資料表示法
- 一組操作該型別的函式
- 既有函式針對新型別的特化實作
- 運算子針對該型別的特化實作
- 該型別的索引支援
索引支援涵蓋多種索引:B-tree、GiST、GIN、SP-GiST、hash 與 BRIN(本書不深入各索引類型的細節)。以「型別、支援函式、運算子與索引之間的關係」為例,可以看 ip4r 擴充套件型別的 GiST 支援:
select amopopr::regoperator
from pg_opclass c
join pg_am am on am.oid = c.opcmethod
join pg_amop amop on amop.amopfamily = c.opcfamily
where opcintype = 'ip4r'::regtype
and am.amname = 'gist'; amopopr
════════════════
>>=(ip4r,ip4r)
<<=(ip4r,ip4r)
>>(ip4r,ip4r)
<<(ip4r,ip4r)
&&(ip4r,ip4r)
=(ip4r,ip4r)
(6 rows)pg_opclass是運算子類別(operator class)的清單,每個類別屬於pg_opfamily中的一個運算子家族;每種索引實作一個pg_am中的存取方法;可搭配某索引存取方法使用的運算子則列在pg_amop。- 這些目錄查詢屬於相當進階的素材,日常應用開發用不到;但理解 PostgreSQL 的運作方式,能讓你更聰明地使用這個你已經依賴的系統。
PostgreSQL 的資料型別實作是一套完全動態的系統:函式與運算子在執行期派發,擴充套件作者還有 API 可以在執行期(也就是你輸入
create extension時)註冊新的索引支援。理解這一點,才能體會「資料型別」這個核心概念在 PostgreSQL 裡能做到多少事。