1. 程式人生 > 程式設計 >[譯] 使用 PostgreSQL 的一些建議

[譯] 使用 PostgreSQL 的一些建議

1. 資料庫編碼

1.1 不要使用 SQL_ASCII 編碼

1.1.1 為什麼

雖然這個名稱看起來和 ASCII 有關係,但並非如此。相反,它只是禁止使用空位元組。

更重要的是,SQL_ASCII 對所有的編碼轉換的函式而言意味著“不轉換”。也就是說,原始位元組編碼是什麼就是什麼。除非特別小心,否則 SQL_ASCII 編碼的資料庫可能最終會儲存許多不同編碼的混合資料,可能無法可靠地恢復原始字元。

1.1.2 什麼時候可以使用

如果你的輸入資料已經是編碼的混合的資料,例如 IRC 的日誌或非 MIME 相容的電子郵件,那麼 SQL_ASCII 可能是最後的方法 - 應該首先考慮使用 bytea 編碼,或者可以檢測是否為 UTF8 編碼,如果非 UTF8 編碼,例如 WIN1252 編碼的資料可以假定為 UTF8 編碼。

2. 工具

2.1 不要使用 psql -W 或者 psql --password

不要使用 psql -W 或者 psql --password

2.1.1 為什麼

如果使用 --password 或 -W 標誌連線服務,psql 會提示你需要輸入密碼 - 因此即使伺服器不需要密碼,也會提示你輸入密碼。

這個選項沒有必要,它會讓人誤以為伺服器需要密碼。如果你登入的使用者沒有設定密碼,或者你在提示時輸入了錯誤的密碼,你仍然會成功登入並認為這就是正確的密碼 - 但你無法使用這個密碼從其他客戶端(通過 localhost 連線)或以其他使用者身份登入。

2.1.2 什麼時候可以使用

不要使用。

2.2 不要使用 RULE

不要使用 RULE,(譯者注: CREATE RULE 定義一個適用於特定表或者檢視的新規則)如果想要用,請使用觸發器替代。

2.2.1 為什麼

RULE 非常強大,但並不好理解。它看起來像是一些條件邏輯,但實際上它會重寫查詢或向查詢中新增其他查詢。

這意味著所有 non-trivial 的規則都是不正確的

。(譯者注:關於 non-trivial 的定義參考引用文章)

Depesz 對此有更多話要說

2.2.2 什麼時候可以使用

不要使用。

2.3 不要使用表繼承

不要使用表繼承,如果真的要用,可以使用外來鍵來替代。

2.3.1 為什麼

表繼承是一個時髦的概念,其中資料庫與面向物件的程式碼緊耦合。事實證明,這些耦合的東西實際上並沒有產生預期的結果。

2.3.2 什麼時候可以使用

幾乎不使用……差不多。現在表分割槽是本地完成的,表繼承的常見場景已經被一些特性所取代。

3. SQL 語句

3.1 不要使用 NOT IN

不要使用 NOT IN,或 NOT 和 IN 的任意組合,如 NOT(x IN (select...))。

(如果你認為你想要 NOT IN (select...) 那麼你應該使用 NOT EXISTS 替代。)

3.1.1 為什麼

兩個理由:

  1. 如果存在 NULL 值,則 NOT IN 會以意外的方式執行:
select * from foo where col not in (1,null);
  -- always returns 0 rows

select * from foo where col not in (select x from bar);
  -- returns 0 rows if any value of bar.x is null
複製程式碼

發生這種情況是因為如果 col = 1 則 col IN(1,null) 返回 TRUE,否則返回 NULL(即它永遠不會返回 FALSE)。由於 NOT(TRUE) 為 FALSE,但 NOT(NULL) 仍為 NULL,因此 NOT(col IN(1,null)) (與 col NOT IN(1,null)相同)無法返回 TRUE,也就是說 NOT IN (1,NULL) 這種形式永遠不會返回資料。

  1. 由於上面的第 1 點,NOT IN (SELECT ...) 不能很好地優化。特別是,規劃器(planner 負責生成查詢計劃)無法將其轉換為 anti-join,因此它變為雜湊子規劃或普通子規劃。雜湊子規劃很快,但規劃器只允許該計劃用於小結果集;普通的子計劃非常慢(事實上是 O(N²)時間複雜度)。這意味著在小規模測試中效能看起來不錯,但一旦資料量大,效能就會減慢 5 個或更多個數量級; 我們不希望這種情況發生。

3.1.2 什麼時候可以使用

NOT IN (list,of,values,...)只是在列表中有 null 值(引數或其他方式)時,才會有問題。所以在排除沒有 null 值時,是可以用的。

3.2 不要使用大寫字母命名

不要使用 NamesLikeThis,而是使用 names_like_this 的命名方式。

3.2.1 為什麼

PostgreSQL 將表,列,函式等名稱轉換為小寫,除非它們使用“雙引號”擴起來才不會被轉換。

所以 create table Foo() 將會建立一個表名為 foo 的表,執行 create table "Bar"() 才會建立表名為 Bar 的表。

這些查詢語句將會正常執行:select * from Foo,select * from foo,select * from "Bar"

這些查詢語句會報錯 “no such table”:select * from "Foo",select * from Bar,select * from bar

這意味著如果在表名或列名中使用大寫字母,則查詢時必須使用雙引號。這很煩人,但是當你使用其他工具訪問資料庫時,其中一些名稱使用雙引號,而另一些則不使用,這會讓人感到困惑。

堅持使用 a-z,0-9 和下劃線來表示名稱,就不必再擔心了。

3.2.2 什麼時候可以使用

如果在輸出中顯示好看的名稱很重要,那麼你可能想使用大寫字母。但是你也可以使用列別名,也可以在查詢中輸出好看的名稱:select character_name as "Character Name" from foo

3.3 不要使用 BETWEEN(尤其是時間戳)

3.3.1 為什麼

BETWEEN 使用閉區間比較:範圍兩端的值會包含在結果中。

這是一個查詢的問題

SELECT * FROM blah WHERE timestampcol BETWEEN '2018-06-01' AND '2018-06-08'
複製程式碼

這將包括時間戳恰好為 2018-06-08 00:00:00.000000 的結果。查詢可以工作,但是由於是閉區間,可能在下一次查詢會包含這個時刻值(例如 '2018-06-08' AND '2018-06-09' 就會包含那一刻的值)。

用下面的語句替換

SELECT * FROM blah WHERE timestampcol >= '2018-06-01' AND timestampcol < '2018-06-08'
複製程式碼

3.3.2 什麼時候可以使用

BETWEEN 對於整數或日期等離散型別的資料是安全的,需要記住 BETWEEN 是閉區間。但使用 BETWEEN 可能是一個壞習慣。

4. 日期/時間 的儲存

(譯者注:日期/時間 中文檔案

4.1 不要使用 timestamp(without time zone)

不要使用 timestamp 型別來儲存時間戳,而是使用 timestamptz(也稱為帶時區的時間戳)來儲存。

4.1.1 為什麼

timestamptz 記錄 UTC 的微秒數,你可以插入任何時區的值。預設情況下,它將顯示當前時區的時間,但你可以轉換成其他時區。

因為它儲存了時間戳資訊,可以用演演算法來轉換成不同時區的時間戳。

timestamp(也稱為沒有時區的時間戳)不會執行任何操作,它只會儲存你提供的日期和時間。你可以將其視為日曆和時鐘的圖片,而不是時間點,沒有時區資訊。因此,沒有時區的時間戳沒法轉換時區。

因此,如果你要儲存的是時間點,而不是時鐘圖片,請使用 timestamptz

更多有關 timestamptz 的資訊

4.1.2 什麼時候可以使用

如果你以抽象方式處理時間戳,或者只是為了 app 的儲存和檢索,而不對它們進行時間計算,那麼 timestamp 可能是合適的。

4.2 不要使用 timestamp(without time zone)來儲存 UTC 時間

將 UTC 值儲存在沒有時區的 timestamp ,通常是從其他缺乏可用時區支援的資料庫繼承資料的做法。

改為使用 timestamp with time zonetimestamptz

4.2.1 為什麼

因為資料庫無法知道是否是 UTC 時區。

這讓時間計算變得複雜。例如,“給定時區 u.timezone 的最後一個午夜”的計算語句為:

date_trunc('day',now() AT TIME ZONE u.timezone) AT TIME ZONE u.timezone AT TIME ZONE 'UTC'
複製程式碼

並且“u.timezone 中 x.datecol 日期之前的午夜”的計算語句為:

date_trunc('day',x.datecol AT TIME ZONE 'UTC' AT TIME ZONE u.timezone)
  AT TIME ZONE u.timezone AT TIME ZONE 'UTC'
複製程式碼

4.2.2 什麼時候可以使用

如果與非時區支援資料庫的相容性勝過所有其他考慮因素。

4.3 不要使用 timetz

不要使用 timetz 型別,可以使用 timestamptz 代替。

4.3.1 為什麼

甚至手冊也告訴你它只是為了遵守 SQL 標準而實現的。

帶有時區的時間型別由 SQL 標準定義,但定義顯示的屬性讓人懷疑它的可用性。在大多數情況下,日期,時間,沒有時區的時間戳和帶時區的時間戳的組合應該可以提供任何應用程式所需的日期/時間功能。

4.3.2 什麼時候可以使用

從不使用。

4.4 不要使用 CURRENT_TIME

不要使用 CURRENT_TIME 函式。使用以下是合適的:

  • CURRENT_TIMESTAMP 或者 now() 如果你想要 timestamp with time zone
  • LOCALTIMESTAMP 如果你想要 timestamp without time zone
  • CURRENT_DATE 如果你想要 date
  • LOCALTIME 如果你想要 time

4.4.1 為什麼

它返回 timetz 型別的值,關於 timetz 請看上一條解釋。

4.4.2 什麼時候可以使用

從不使用。

4.5 不要使用 timestamp(0) 或者 timestamptz(0)

不要使用 timestamp() 或者 timestamptz() 進行時間戳的轉換(尤其是 0)。

使用 date_trunc('second',blah) 替換。

4.5.1 為什麼

因為它會將小數部分四捨五入截斷它。這可能會導致意外問題;考慮到當你將 now() 儲存到這樣一個列中時,你可能會在將來儲存一個小數秒的值。

4.5.2 什麼時候可以使用

從不使用。

5. 文字儲存

5.1 不要使用 char(n)

不要使用 char(n),使用 text 可能更適合。

5.1.1 為什麼

使用 char(n) 型別的欄位,如果長度不夠會使用空格填充到宣告的長度。這可能不是你想要的。

名字 描述
character varying(n),varchar(n) 變長,有長度限制
character(n),char(n) 定長,不足補空白
text 變長,無長度限制

手冊上說:

char 型別的數值物理上都用空白填充到指定的長度 n, 並且以這種方式儲存和顯示。不過,在比較兩個 char 型別的值時,尾隨的空白是無關緊要的,不需要理會。 在空白比較重要的排序規則中,這個行為會導致意想不到的結果, 比如 SELECT 'a '::CHAR(2) collate "C" < 'a\n'::CHAR(2)返回真。 在將 char 值轉換成其它字串型別的時候, 它後面的空白會被刪除。請注意, 在 varchar 和 text 數值裡, 結尾的空白是有語意的。 並且當使用模式匹配時,如 LIKE,使用正則表示式。

空格填充確實浪費空間,也不會讓操作變得更快;事實上,很多情況下我們還要去掉空格。

提示: 這三種型別之間沒有效能差別,除了當使用填充空白型別時的增加儲存空間, 和當儲存長度約束的列時一些檢查存入時長度的額外的 CPU 週期。 雖然在某些其它的資料庫系統裡,char(n) 有一定的效能優勢,但在 PostgreSQL 裡沒有。 事實上,char(n) 通常是這三個中最慢的, 因為額外儲存成本。在大多數情況下,應該使用 text 或 varchar。

5.1.2 什麼時候可以使用

當你移植使用了固定寬度欄位的非常非常舊的軟體時。或者當你閱讀上面手冊中的片段並認為“是的,這是完全合理的,並且符合我的要求” 時可以使用。

5.2 即使對於固定長度的識別符號,也不要使用 char(n)

有時候人們用“我的值剛好是 N 個字元”(例如國家程式碼,雜湊值或來自其他系統的識別符號)來回應“為什麼不要使用 char(n)”。其實,即使在這些場景下使用 char(n) 也不是一個好主意。

5.2.1 為什麼

對於太短的值,char(n) 會用空格填充它們。因此,帶有確定長度的 char(n) 比較 text 而言沒有任何實際好處。

5.2.2 什麼時候可以使用

從不使用。

5.3 預設情況下不要使用 varchar(n)

預設情況下不要使用 varchar(n) 型別。考慮使用 varchar(沒有長度限制)或 text 替代。

5.3.1 為什麼

varchar(n) 是一個帶長度的文字欄位,如果你嘗試將長度超過 n 個字元(而不是位元組)的字串插入其中,則會引發錯誤。

varchar(沒有 (n) )或 text 是相似的,沒有長度限制。如果在三種欄位型別中插入相同的字串,它們將佔用完全相同的空間,並且效能基本沒有差異。

如果你想要一個長度限制的文字欄位,那麼 varchar(n) 很不錯,但是如果你定義姓氏欄位為 varchar(20),那麼當 Hubert Blaine Wolfeschlegelsteinhausenbergerdorff 註冊到你的服務時,將會報錯。

有些資料庫沒有長 text 的型別,或者它們沒有像 varchar(n) 那樣被良好支援。那這些資料庫的使用者通常會使用類似 varchar(255) 的表示方法,但他們真正想要的是 text

如果你需要約束欄位中的值,比如說約束最大長度 - 或者是最小長度,或者是一組有限制的字串 - 檢查約束可以做到這些。

5.3.2 什麼時候可以使用

如果你想要一個文字欄位,而且插入太長的字串需要丟擲錯誤,並且不想使用顯式檢查約束,那麼 varchar(n) 是一個非常好的型別。只是使用時需要多考慮。

6. 其他資料型別

6.1 不要使用 money

money 資料型別實際上不太適合儲存貨幣值。數字或整數可能更好。

6.1.1 為什麼

大量理由

money 型別儲存帶有固定小數精度的貨幣金額。 lc_monetary 用來設定格式化數字。但它的四捨五入的行為可能不是你想要的。

名字 儲存容量 描述 範圍
money 8 位元組 貨幣金額 -92233720368547758.08 到 +92233720368547758.07

如果你更改了 lc_monetary 設定,則所有 money 列都將包含錯誤的值。這意味著如果你插入'$ 10.00'而 lc_monetary 設定為 en_US.UTF-8,你檢索的值可能是'10,00 Lei'或'¥1,000'。

6.1.2 什麼時候可以使用

如果你只是用單一貨幣工作,不處理小數美分並且只做加法和減法運算,那麼 money 型別可能是正確的。

6.2 不要使用 serial

對於新的應用程式,應使用 identity

6.2.1 為什麼

serial 型別有一些奇怪的行為,使結構,依賴和許可權管理更繁瑣。

6.2.2 什麼時候可以使用

  • 如果你需要支援 10.0 版之前的 PostgreSQL。
  • 在表繼承的某些組閤中
  • 更一般地說,如果你以某種方式使用來自多個表的相同序列,儘管在這些情況下,顯式宣告可能優於 serial 型別。