使用 BIGINT 和兩倍的儲存空間或 INTEGER 但有額外的操作?
假設我有一個
agency
用一些列命名的表:internal_id(integer, unique) , external_id(bigint, unique) , name, location, created_at, ...
internal_id
並且external_id
每個都是唯一的並且可以作為主鍵列。還有一些其他表(說
A, B, C, D, E
)引用了這個表。假設這些表中的每一個都可能包含數百萬或數十億行。通常我有
external_id
需要過濾表A, B, C, D, E
數據的時候。考慮到性能和儲存空間,以下哪種方案是最好的選擇:
- 在中用
internal_id
作主鍵agency
,在其他表中用作外鍵。因為這個欄位佔用了 4 個字節的儲存空間,我們可以節省數十億字節。但是,正如我通常擁有的那樣external_id
,我必須為每個查詢做一個額外JOIN
的懲罰:SELECT A.* FROM A INNER JOIN agency ON A.internal_id=agency.internal_id WHERE agency.external_id=5;
- 在中用
internal_id
作主鍵agency
,在其他表中用作外鍵。但是為了擺脫額外的JOIN
,在我的應用程序中,我可以首先使用一個簡單的查詢 ( ) 映射external_id
到,然後將獲取的用於另一個簡單的查詢:internal_id``SELECT internal_id FROM agency WHERE external_id=5``internal_id
SELECT * FROM A WHERE internal_id=59; -- 59 is the fetched internal_id from the other query
它是否比
JOIN
考慮應用程序和數據庫之間的額外往返具有更好的性能?
- 忘記
internal_id
並用external_id
作主鍵和外鍵,每條記錄在其他表中增加 4 個額外字節A, B, C, D, E
(SELECT * FROM A WHERE external_id=5
更新:
agency
表可能包含成千上萬或最多幾百萬行。internal_id
並且external_id
不會隨著時間的推移而改變,但其他非身份列可能很少改變。- 大約有 5 到 7 個相關表
A, B, C, D, E, ...
(
假設
agency
行數少於您為其他表提到的“數百萬和數十億”。遠低於範圍integer
:-2147483648 到 +2147483647。否則我們需要bigint
開始internal_id
。- 但是
agency
還是很大的。否則,不要理會下面的索引優化。- 兩者都
internal_id
幾乎external_id
沒有改變。- ID 值大致均勻分佈。不少非常常見的機構和許多非常罕見的機構。(這可能有利於沒有鍵轉換的查詢優化。)
我會考慮使用這種查詢樣式的方案 1 和 2的組合:
SELECT * FROM A WHERE internal_id = (SELECT internal_id FROM agency WHERE external_id=5);
子查詢封裝了鍵翻譯,可以用作提供文字的替代品
internal_id
。當涉及許多連接時,也使查詢計劃器的工作更簡單一些。除非您
internal_id
為許多後續查詢重複使用,否則單獨的查找會不必要地增加單獨往返伺服器的成本。您可以將關鍵翻譯封裝在一個簡單的 SQL 函式中:
CREATE FUNCTION public.f_ext2int(_external_id bigint) RETURNS int LANGUAGE sql STABLE PARALLEL SAFE AS '(SELECT internal_id FROM public.agency WHERE external_id = _external_id)';
那麼上面的查詢就變成了:
SELECT * FROM A WHERE internal_id = f_ext2int(5);
該函式可以由查詢計劃器“內聯”。看:
我建議這個表定義:
CREATE TABLE agency ( internal_id integer -- PK is NOT NULL implicitly , external_id bigint NOT NULL -- NOT NULL, right? -- , name, location, created_at, ... , PRIMARY KEY (internal_id) INCLUDE (external_id) , UNIQUE (external_id) INCLUDE (internal_id) );
這提供了關鍵索引
(internal_id, external_id)
並強制執行您提到的約束,而沒有冗餘索引(external_id, internal_id)
。第二個 (
UNIQUE (external_id) INCLUDE (internal_id)
) 用於反向查找。似乎你也需要那個。否則,您可以跳過INCLUDE
那裡的子句。為什麼我們需要兩個索引?看:它大量使用覆蓋索引(Postgres 11 或更高版本)。看:
除其他外,覆蓋索引否定了附加列的壓載,以
agency
實現鍵轉換。有了這些索引,鍵轉換就可以快速進行僅索引掃描以進行鍵轉換。在查詢大型表的上下文中,成本幾乎可以忽略不計。
這為每個額外的表和索引節省了“數百萬和數十億”乘以 4 個字節(這可能更重要)。誠然,儲存一直在變得更便宜,但 RAM(和快速記憶體!)通常仍然有限。更大的表和索引意味著更少的數據可以保留在記憶體中。這對性能至關重要。
更寬的行總是或多或少地對數據庫的整體性能產生負面影響,即使使用便宜的儲存也是如此。相關討論:
integer
在許多表(和日誌文件,以及調試……)中使用較小的數字進行操作通常在人眼上更容易。甚至可能是最重要的實際好處。