標題:為什麼我作為一個喜歡 MySQL 的人,現在想推薦 PostgreSQL
已發布:是
描述:為什麼我現在推薦新應用程式使用 PostgreSQL,即使我仍然喜歡 MySQL 並且已經使用它很長時間了。
標籤:MySQL、PostgreSQL、資料庫、SQL
我喜歡MySQL。我使用它已經很久了,也曾在本地環境中執行過它。
然而,自從加入現在的公司以來,我有了更多使用PostgreSQL的機會。說實話,一開始我對它很抗拒。我用MySQL太久了,一部分原因是習慣使然,而且我覺得我對PostgreSQL的顧慮也有些過頭了。
但隨著實際使用,我逐漸開始體會到PostgreSQL的優點。如今,如果有人問我新專案會選擇哪個資料庫,我的答案通常是PostgreSQL。
因為我使用 MySQL 已經很久了,所以我也了解早期 MySQL 的一些缺點。同時,我認為僅憑過去的印象來談論現在的 MySQL 是不準確的。如果正確配置sql_mode ,就可以避免很多危險的行為,而 MySQL 8 也新增了大量功能。
此外,這次我想在假設MySQL和PostgreSQL都將在雲端執行的前提下進行比較,而不是基於本地部署時代的印象。過去被認為是PostgreSQL缺點的一些問題現在已經不再那麼重要了。
這不是一篇關於「MySQL很糟糕」的文章,也不是一篇關於「PostgreSQL的理念很優美」的文章。
如果只寫結論,那就是這兩點:
過去被認為是PostgreSQL缺點的一些問題現在已經變得無關緊要了。功能上的差距已經大大縮小,而且在託管服務的背景下,您無需擔心的事情也更多了。
另一方面,從應用實現的角度來看,PostgreSQL 在某些方面仍然明顯更勝一籌。
本文將從這個角度展開討論。
在早期的比較中,PostgreSQL 的缺點通常被認為是操作繁重以及 DDL 處理起來比較笨拙。
但我認為現在再提出這些觀點有點過時了。
MySQL 在線上 DDL 方面已經非常強大,但至少對於新增列這類日常任務而言,我認為 MySQL 和 PostgreSQL 之間已經沒有明顯的區別了。分區問題也不再像以前那麼重要了。
此外,PostgreSQL 特有的運維問題,例如VACUUM ,在使用託管服務時出現的頻率會大大降低,因為使用者需要直接處理這些問題的場景大大減少。我認為,將過去需要使用者自行管理所有事務的本地部署時代的情況直接應用於當前的雲端時代並不公平。
最近,由於託管服務已成為主流,用戶不再直接接觸更多環節,複製方面的差異也變得不那麼明顯了。我覺得現在能明顯感受到 MySQL 優勢的情況比以前少了很多。
換句話說,過去一些不推薦使用 PostgreSQL 的合理理由現在已經變得不那麼充分了。
這是重點。
MySQL 8 縮小了很大差距。即便如此,從實際編寫應用程式的角度來看,PostgreSQL 仍然更容易被推薦。
首先我想澄清一點。以下這些特性以前被認為是 PostgreSQL 的優勢,但由於 MySQL 8 也加入了這些特性,因此它們不再是決定性因素:
CHECK約束
視窗函數
SKIP LOCKED
目前來看,將這些優勢視為PostgreSQL獨有的優勢是不公平的。
然而,正如我稍後會解釋的,「視窗函數本身是在 MySQL 8 中加入的」和「能夠自然地將視窗函數引入更新處理」是兩回事。我仍然認為,就後者而言,PostgreSQL 要容易得多。
ON CONFLICT DO NOTHING不能取代INSERT IGNORE這是 MySQL 最初不具備的功能,也是我推薦 PostgreSQL 的一個重要原因。
MySQL 有INSERT IGNORE 。
然而,這很難被視為ON CONFLICT DO NOTHING的替代方案。
PostgreSQL 的ON CONFLICT DO NOTHING特性本質上是一個明確指示「僅當發生唯一約束衝突時才不插入資料」的功能。你想要執行的操作會原封不動地轉換為 SQL 語句。
相較之下,MySQL 的INSERT IGNORE並不是專門用來忽略重複項的功能。它會將錯誤轉換為警告並繼續處理,因此對於「我只想忽略重複項」這種使用場景來說,它的範圍太廣了。
這種差異看起來很小,但實際上卻很大。
這樣一來,在審查過程中,行為更容易被理解,並且降低了無意中接受無效輸入的可能性。
RETURNING非常強大這也是 MySQL 最初不具備的功能,也是我推薦 PostgreSQL 的另一個相當重要的原因。
在 PostgreSQL 中,您可以使用INSERT/UPDATE/DELETE ... RETURNING 。因為您可以直接傳回更改後的結果,所以自然可以用一條語句完成「取得更改結果」的操作。
例如,你可以這樣做:
INSERT INTO users(name, email)
VALUES('catatsuy', '[email protected]')
RETURNING id, name, email, created_at;
當你擁有了這一點,以下事情就會變得非常自然:
直接接收插入的ID
直接接收預設值或儲存值
傳回更新後的行,並將其用作 API 回應。
直接接收 upsert 操作的結果
說實話,MySQL 中常見的基於LAST_INSERT_ID()的插入方式相當有限。
您可以取得的資訊範圍很窄,基本上只集中在取得一個數位AUTO_INCREMENT ID 上。
您無法自然地從插入結果中取得任意列,也無法按原樣傳回包含預設值和產生列的完整行。
例如,你想要的是這樣的:
你使用 UUID 作為主鍵,所以你要直接回到它們。
您希望傳回包含產生的列和預設值的完整行。
您希望將插入的整個行直接傳遞到下一步。
你不能使用LAST_INSERT_ID()來實現這個功能。
此外,即使在單一語句中插入多行資料, LAST_INSERT_ID()也不會直接傳回插入的結果。它只能傳回第一個AUTO_INCREMENT值。
因此,如果要在應用程式中直接處理多行INSERT操作的結果,會很不方便。而使用 PostgreSQL 的RETURNING ,可以直接傳回插入的行,這種差異非常顯著。
「能夠直接返回修改後的結果」不僅僅是一個方便的功能,它會影響應用程式實現的整個架構方式。
VALUES有助於實際應用。這並非是說MySQL完全缺少某個功能。相反,我認為PostgreSQL讓這個功能的使用更加自然流暢。
可以輕鬆地即時建立一個小型常量表,將其與現有資料連接,並直接連接到更新處理流程。這樣一來,就無需將不完善的臨時表處理流程推送到應用程式端了。
例如,當您想要將從應用程式接收的少量值合併並基於這些值進行更新時,在 PostgreSQL 中您可以這樣寫:
UPDATE users u
SET plan = v.plan
FROM (
VALUES
(1, 'pro'),
(2, 'free'),
(3, 'team')
) AS v(id, plan)
WHERE u.id = v.id;
這種處理方式在實際應用中非常常見。
例如,您可能希望立即傳遞一小組類似主值的資料並用它們進行更新,或將從 API 接收的一組值直接傳送到 SQL 中。
再舉一個例子,將從應用程式接收的一組值視為連接目標也很自然:
SELECT u.*
FROM users u
JOIN (
VALUES
(1),
(2),
(5)
) AS v(id)
ON u.id = v.id;
並非MySQL完全不能做類似的事情。自MySQL 8.0.19版本起,它就有了VALUES語句,可以將其視為表值建構器。
然而,在 MySQL 中,你需要使用ROW(...)來寫列名,如果保留原有的列名,它們會變成類似column_0和column_1這樣的形式。這與 PostgreSQL 有些不同,在 PostgreSQL 中,你可以直接建立一個小型常量表,賦予它自然的列名,然後直接將其傳遞給JOIN或UPDATE語句。
例如,在 MySQL 中,同樣的思路可以這樣實作:
SELECT u.*
FROM users u
JOIN (
VALUES ROW(1), ROW(2), ROW(5)
) AS v
ON u.id = v.column_0;
這雖然不是什麼引人注目的功能,但這類事情會影響日常使用的便利性。
這部分很重要。
視窗函數本身是在 MySQL 8 中加入的。
因此,將視窗函數本身視為 PostgreSQL 獨有的優勢是錯誤的。
然而,在 PostgreSQL 中,透過結合WITH和UPDATE ... FROM ,可以輕鬆地將視窗函數的結果自然地引入更新處理中。我認為這裡仍然存在差異。
例如,如果您只想為每個使用者的最新行設定一個標誌,您可以這樣寫:
WITH ranked AS (
SELECT
id,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM sessions
)
UPDATE sessions s
SET is_latest = (r.rn = 1)
FROM ranked r
WHERE s.id = r.id;
MySQL 8 中確實存在視窗函數。
然而,PostgreSQL 在將它們與這種更新邏輯連接起來方面要自然得多。
這不僅是一項方便的分析功能,它更是應用程式實施的強大武器。
這我可以明確地將其描述為一項功能差異。
MySQL 最初就沒有這個功能,現在仍然沒有。
PostgreSQL 支援部分索引,您可以只對某些行建立索引,例如使用WHERE deleted_at IS NULL 。這非常適合軟刪除模式,並且對於按狀態管理記錄也很有用。
CREATE INDEX idx_users_active_email
ON users(email)
WHERE deleted_at IS NULL;
例如,如果您有一個使用軟刪除的表,並且您只想加快「按電子郵件地址在活躍用戶中搜尋」的速度,您可以直接編寫該程式碼。
在 MySQL 中,您可以使用生成列或函數索引來實現類似的功能。但是,這並不能取代部分索引。
PostgreSQL 的部分索引僅將滿足類似WHERE deleted_at IS NULL條件的行加入索引中。換句話說,從一開始就將不必要的行排除在索引之外。
相較之下,MySQL 產生的列和函數索引本質上是對所有行計算一個表達式,然後將結果建立索引。如果表達式設計得當,它們也可以用於類似的目的,但它們並不能直接實現「僅包含部分行而保持物理上較小的索引」。
因此,就規模、更新成本和意圖清晰度而言,PostgreSQL 的部分索引更加直接明了。 MySQL 部分索引可以作為一種變通方案,但很難說它具備相同的功能。
這不僅是編寫 SQL 語句感覺上的差異,而是實實在在的功能差異,也是推薦 PostgreSQL 的理由。
這是我的個人印象,但我感覺在 MySQL 世界裡,很多人認為外鍵是不必要的,而在 PostgreSQL 世界裡,很多人認為外鍵是必要的。
我認為這與其說是理念上的差異,不如說是測試的難易度、操作的難易度以及防止漏洞進入的難易程度的差異。
在 PostgreSQL 中,外鍵可以設定DEFERRABLE 。
這非常重要。
CREATE TABLE authors (
id bigint PRIMARY KEY
);
CREATE TABLE books (
id bigint PRIMARY KEY,
author_id bigint NOT NULL,
CONSTRAINT books_author_fk
FOREIGN KEY(author_id)
REFERENCES authors(id)
DEFERRABLE INITIALLY DEFERRED
);
因此,您可以將約束檢查延遲到交易結束。
BEGIN;
INSERT INTO books(id, author_id) VALUES(1, 100);
INSERT INTO authors(id) VALUES(100);
COMMIT;
這段 SQL 語句在 PostgreSQL 中可以正常運作。
父級在中間不存在,但只要在提交時保持一致性就沒問題。
這有什麼用?
載入包含循環引用的資料
建立複雜的測試資料
遷移過程中順序暫時顛倒
批量插入件和更換操作
這類流程在實際工作中很常見。
能否自然而然地寫出這些文章是一件非常重要的事。
MySQL 沒有這種機制。
NO ACTION其實就是RESTRICT 。
換句話說,你不能把處理過程建構成「只要最終結果一致就行」的形式。你始終受到順序規則的約束,即父級必須先於子級。
這看起來似乎是個小問題,但它會使載入測試資料和編寫遷移過程變得更加困難。
例如,在測試程式碼中,如果你想大致載入跨多個表的測試資料,PostgreSQL 可以編寫程式碼,確保在交易結束時資料一致性得到滿足。但在 MySQL 中,你無法做到這一點,因此你必須始終嚴格管理測試資料的插入順序。
如果使用外鍵會使測試更加麻煩,我認為很自然地就會出現一種文化,即人們會說:“那麼我們就停止使用外鍵吧。”
在 MySQL 中,可以使用foreign_key_checks=0來停用約束。
這看起來很方便,但實際上很危險。
SET foreign_key_checks = 0;
INSERT INTO books(id, author_id) VALUES(1, 999);
SET foreign_key_checks = 1;
這樣一來,在停用約束條件時插入的不一致資料可能會保留下來。
即使你再次啟用它們,MySQL 也不會回溯並驗證在此期間插入的所有不一致之處。
如果為了測試或遷移的方便而關閉約束條件,就很容易發生意外,導致不一致的資料直接匯入。
PostgreSQL 有一個工具可以設定「保留約束,但延遲檢查時間」。
MySQL 的發展方嚮往往是「關閉約束本身」。
這個差距相當大。
例如,考慮一對具有父子關係的普通表。
CREATE TABLE users (
id bigint PRIMARY KEY
);
CREATE TABLE orders (
id bigint PRIMARY KEY,
user_id bigint NOT NULL,
CONSTRAINT orders_user_fk
FOREIGN KEY(user_id)
REFERENCES users(id)
DEFERRABLE INITIALLY DEFERRED
);
在 PostgreSQL 中,即使在測試期間,只要在一個事務中,你也可以先向orders中插入資料,然後再向users中插入資料。
BEGIN;
INSERT INTO orders(id, user_id) VALUES(10, 1);
INSERT INTO users(id) VALUES(1);
COMMIT;
這種靈活性對建立測試夾具、資料遷移和簡化測試程式碼大有裨益。
MySQL 沒有這個功能。
因此,在 MySQL 中,人們更容易傾向於這樣想:“外鍵礙事,所以要關閉它們”或“在應用程式中保證它”,而在 PostgreSQL 中,人們更容易傾向於這樣想:“讓我們正確地使用外鍵”。
PostgreSQL 外鍵的優點不只在於其功能較多。
其中很大一部分原因是,它們使得編寫測試、遷移和資料載入更加容易,同時保持約束不變,因此,在生產環境中實際使用外鍵變得更加容易。
最近,我認為這可能是人們選擇採用 PostgreSQL 時最常提到的原因。
PostgreSQL 擁有pgvector ,它不僅允許你儲存向量,還允許你直接在應用程式中使用距離運算和相似度搜尋。它還提供了用於最近鄰搜尋的索引,因此在實際應用中非常容易使用。
相較之下,假設使用開源版(OSS),MySQL 在 MySQL 9.0 中新增了 Vector 類型,該版本已作為創新版本發布,而長期支援版(LTS)尚未發布。然而,距離函數僅在 OCI 上的 MySQL HeatWave 和 MySQL AI 中提供,MySQL 商業版和社群版均不包含。換句話說,開源版無法進行向量運算,因此實際上並不適用於此。這與 PostgreSQL + pgvector有明顯的差異。
這一點也非常重要。
我仍然認為字元集和排序規則在 MySQL 中比在 PostgreSQL 中更容易引發問題。
然而,這不僅是 MySQL 本身的問題,還包括框架、連接器和預設設定等。
日本有一些著名的例子,例如所謂的“哈哈爸爸問題”和“壽司啤酒問題”。
這兩個問題與其說是字元編碼問題,不如說是排序問題。
Haha-Papa 問題是指由於排序規則,看起來不同的字串被視為相同的字串。
壽司啤酒問題是指,當涉及表情符號和類似字符的比較結果與你的直覺預期不符時。
這些問題的棘手之處在於,「我們把它改成utf8mb4就萬事大吉了」是不夠的。實際上,你需要同時理解字元集和排序規則。
而 MySQL 8 並沒有簡化這個問題,反而帶來了更多需要考慮的事情。它新增了排序規則,這些規則與舊系統共存,因此「舊風格」、「MySQL 8 之後的風格」和「框架預設設定」並不總是一致。
換句話說,MySQL 8 的確改進了一些方面,但由於這些改進導致新舊風格並存,整體情況在某些方面變得更加混亂。
我認為這倒不是因為 MySQL 本身不好,而是因為它歷史悠久,並且在發展過程中一直保持著與 MySQL 的兼容性。
然而,從應用程式開發人員的角度來看,這種複雜性直接成為發生事故的入口。
過去,PostgreSQL 也存在一些明顯的弱點。
但現在,其中許多問題的重要性已大大降低。功能差距縮小了,而且在託管服務的背景下,用戶無需直接考慮的事情也更多了。
另一方面,從應用程式實現的角度來看,PostgreSQL 仍然更容易被推薦。
其中特別大的就是這些。
CHECK約束
視窗函數
SKIP LOCKED
ON CONFLICT DO NOTHING
RETURNING
VALUES
能夠將視窗函數引入更新處理
部分索引
外鍵的成熟度
透過pgvector進行向量運算
不太可能在字元集和排序規則方面造成問題
我喜歡MySQL。
我使用它已經很長時間了,我仍然認為它是一個很好的資料庫,很容易就能獲得良好的效能。
即便如此,如果現在讓我為一個新專案選擇哪個資料庫,我會推薦PostgreSQL。
這並非因為 MySQL 不好。
因為即使現在,PostgreSQL 的許多舊缺點都已得到彌補,但我仍然認為 PostgreSQL 在應用程式實現的便利性方面具有優勢。
原文出處:https://dev.to/catatsuy/why-i-as-someone-who-likes-mysql-now-want-to-recommend-postgresql-2a8i