PostgreSQL 這幾年在雲端服務、AI 向量檢索、地理資訊系統的加持下,愈來愈多原本用 MySQL、MariaDB、SQL Server 的團隊開始考慮遷移,或是在新專案直接起手採用。不過雖然都是關聯式資料庫、都能用 SQL 操作,實際上手之後會發現許多細節不太一樣,從識別字怎麼解析、自增主鍵怎麼宣告,到 UPSERT 要怎麼寫,都有各自的習慣。這篇文章整理熟悉 MySQL/MariaDB 或 SQL Server 的開發者轉到 PostgreSQL 最容易卡住的地方,也比較這三類資料庫的設計取捨,幫助有資料庫基礎的開發者建立正確的心智模型。
PostgreSQL 的型別系統與 DDL 語法比較細節的部分,之前有整理過一篇 PostgreSQL 入門學習筆記 資料型態與DDL,本篇就不再重複這些細節,著重在「從其他資料庫遷移過來會遇到什麼」。
三家資料庫的定位差異
在比較細節之前,先大致理解這三類資料庫在工程文化上的走向,之後很多設計差異自然就比較好理解。
| 面向 | PostgreSQL | MySQL/MariaDB | SQL Server |
|---|---|---|---|
| 授權 | PostgreSQL License(寬鬆類 BSD) | MySQL 採 GPLv2 + 商業雙軌;MariaDB 採 GPLv2 | Microsoft 商業授權,Express 版免費但有資源上限 |
| 設計取向 | 規格優先,力求遵循 SQL 標準並補強擴充性 | 實用優先,以簡單快速著稱 | 企業整合優先,與 .NET、Windows Server、Power BI 深度綁定 |
| 擴充方式 | Extension 機制(PostGIS、pgvector、TimescaleDB…) | Storage Engine、Plugin,擴充範圍較固定 | CLR Integration、Linked Server,較偏企業環境 |
| 常見託管 | Amazon RDS/Aurora、Google Cloud SQL、Azure、Supabase、Neon、Crunchy | RDS/Aurora、Google Cloud SQL、Azure Database for MySQL | Azure SQL Database、SQL Managed Instance、AWS RDS for SQL Server |
MySQL/MariaDB 的血緣故事在 資料庫教學1:資料庫系統介紹 有整理過,不熟的讀者可以參考。PostgreSQL 跟前兩者不一樣的地方,除了授權很寬鬆之外,核心團隊的節奏比較像 OpenJDK 那種「規範優先、慢工出細活」,每年九月固定出一個大版本,少有商業版對應,功能上不分社群版與企業版。SQL Server 則是微軟的旗艦資料庫,跟 Visual Studio、SSMS、SSIS、Power BI 這些工具整合非常深,企業導入比較偏整體解決方案。
識別字大小寫與引號
這個是新手第一關最容易撞到的牆。PostgreSQL 的規則簡單一句話:「沒加雙引號的識別字,一律折成小寫」。也就是說 CREATE TABLE Users 實際建立的表名叫 users,之後 SELECT * FROM Users 會找到它,但 SELECT * FROM "Users" 反而會找不到。
-- 以下三個查詢指向同一張表 users
SELECT * FROM users;
SELECT * FROM USERS;
SELECT * FROM Users;
-- 但這個會失敗,因為真正的表名是小寫
SELECT * FROM "Users"; -- ERROR: relation "Users" does not exist
-- 如果當初建立表的時候就加了雙引號,那就反過來
CREATE TABLE "Users" (id int);
SELECT * FROM "Users"; -- OK
SELECT * FROM Users; -- ERROR
MySQL 的行為跟作業系統有關。Linux 上預設表名大小寫敏感(因為底層 ext4/xfs 大小寫敏感),Windows 與 macOS 上預設不敏感,這個差異由 lower_case_table_names 變數控制,習慣在 macOS 開發、上線到 Linux 的團隊很常在這邊出包。SQL Server 則是由 collation 決定,預設通常裝成 SQL_Latin1_General_CP1_CI_AS,CI 代表不敏感,所以 Users 與 users 完全等價。
還有一個陷阱是雙引號的語意。MySQL 預設把雙引號當字串(跟單引號一樣),要啟用 ANSI_QUOTES 模式之後才會跟 PostgreSQL/SQL Server 一致把雙引號當識別字。SQL Server 則是透過 QUOTED_IDENTIFIER 設定,預設已經是 ON。從 MySQL 過來的人如果把字串也用雙引號包,在 PostgreSQL 執行會直接報錯,改用單引號就行。
實務上的建議:表名、欄位名全部用小寫 + 底線命名(例如 order_items、created_at),完全不加雙引號,就不會掉進大小寫的坑。真的要保留大小寫再加引號,並且準備好每次都要加。
自增主鍵的三種做法
自增主鍵三家各有習慣寫法,來源機制也不太一樣。
-- MySQL/MariaDB
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- SQL Server
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100)
);
-- PostgreSQL 早期寫法:SERIAL/BIGSERIAL
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT
);
-- PostgreSQL 10 之後推薦的標準寫法
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT
);
PostgreSQL 的 SERIAL 底層其實是「宣告一個 SEQUENCE 物件,再讓欄位預設值取用這個 SEQUENCE」的語法糖,所以有時會看到表裡除了欄位還有一個 users_id_seq 的物件存在。PostgreSQL 10 之後推出的 GENERATED ALWAYS AS IDENTITY 是 ANSI SQL 標準語法,跟 SQL Server 的 IDENTITY 在語意上更接近,並且可以選擇 BY DEFAULT(允許手動指定值)或 ALWAYS(禁止手動指定)。新專案建議用這個寫法。
從 MySQL 過來的人要注意一個行為差異:PostgreSQL 的 SEQUENCE 在回滾(ROLLBACK)之後不會把號碼吐回去。也就是說如果一個交易 INSERT 失敗或手動 ROLLBACK,那個取走的號碼就永久跳過了,之後 ID 序列會看到空洞。這不是 bug,是為了避免在高併發下 SEQUENCE 本身變成瓶頸,設計就是這樣。SQL Server 的 IDENTITY 也有類似行為,只有 MySQL 的 AUTO_INCREMENT 在部分情況下會回收。所以任何程式邏輯都不該假設 ID 是連續的。
另外「取得最後插入的 ID」的寫法也不同。MySQL 用 LAST_INSERT_ID(),SQL Server 用 SCOPE_IDENTITY(),PostgreSQL 直接在 INSERT 語句後面加 RETURNING 子句就能一次拿到:
INSERT INTO users (name) VALUES ('Kyle') RETURNING id, created_at;
RETURNING 是 PostgreSQL 比較舒服的小功能,INSERT、UPDATE、DELETE 都能用,比起多跑一趟查詢省事很多。
UPSERT 寫法
「不存在就插入、存在就更新」這個需求三家都有,但語法各走各的,算是最常查官方文件的地方之一。
-- MySQL/MariaDB
INSERT INTO page_views (page, count)
VALUES ('/home', 1)
ON DUPLICATE KEY UPDATE count = count + 1;
-- SQL Server(MERGE 語法)
MERGE page_views AS target
USING (VALUES ('/home', 1)) AS src(page, count)
ON target.page = src.page
WHEN MATCHED THEN UPDATE SET count = target.count + 1
WHEN NOT MATCHED THEN INSERT (page, count) VALUES (src.page, src.count);
-- PostgreSQL
INSERT INTO page_views (page, count)
VALUES ('/home', 1)
ON CONFLICT (page) DO UPDATE SET count = page_views.count + 1;
PostgreSQL 的 ON CONFLICT 明確要求指定衝突的欄位或約束名稱,這點比 MySQL 的 ON DUPLICATE KEY UPDATE 清楚,因為 MySQL 會對所有唯一鍵衝突一視同仁地觸發更新,多個唯一鍵的表偶爾會更新到不預期的那一筆。SQL Server 的 MERGE 語法強大,但歷史上有個出名的併發 race 問題(同一個 key 在兩個交易同時 MERGE 可能會重複插入),要特別加 HOLDLOCK 提示才安全,這個坑比較深,官方建議能用 INSERT … WHERE NOT EXISTS 就不要用 MERGE。
PostgreSQL 也有「我不想更新,遇到衝突就直接忽略」的捷徑:
INSERT INTO users (email, name)
VALUES ('[email protected]', 'Kyle')
ON CONFLICT (email) DO NOTHING;
分頁、TOP 與 LIMIT
SQL Server 的 SELECT TOP n 是比較特殊的一家,PostgreSQL 與 MySQL 都是用 LIMIT。PostgreSQL 兩種都支援,ANSI SQL 標準的 FETCH FIRST n ROWS ONLY 跟非標準的 LIMIT n 都可以:
-- MySQL/MariaDB
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 40;
-- SQL Server(2012 之後)
SELECT * FROM posts ORDER BY created_at DESC OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
-- SQL Server 舊寫法
SELECT TOP 20 * FROM posts ORDER BY created_at DESC;
-- PostgreSQL 兩種寫法都吃
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 40;
SELECT * FROM posts ORDER BY created_at DESC OFFSET 40 FETCH NEXT 20 ROWS ONLY;
要注意的是 OFFSET 在任何一家資料庫都是效能陷阱。OFFSET 1000000 的時候資料庫還是得讀過前面一百萬筆才能跳過,大量分頁的 API 建議改用 keyset pagination(拿上一頁最後一筆的 id 或 created_at 當錨點),這個技巧三家都通用。
GROUP BY 嚴格性
PostgreSQL 跟 SQL Server 對 GROUP BY 的檢查都嚴格:SELECT 子句裡面只要出現的非聚合欄位,都必須同時出現在 GROUP BY 清單,不然會直接報錯。MySQL 傳統上寬鬆,允許 SELECT 有沒在 GROUP BY 的欄位、自動取該群組任一筆的值(隨機不可預期),這是多年來 MySQL 被詬病的點之一。
-- 這個在 MySQL 傳統模式下能跑,name 會隨便回傳一筆
SELECT user_id, name, COUNT(*) FROM orders GROUP BY user_id;
-- 在 PostgreSQL/SQL Server 會直接錯:name 必須在 GROUP BY
-- column "orders.name" must appear in the GROUP BY clause
-- or be used in an aggregate function
MySQL 5.7 之後預設開啟了 ONLY_FULL_GROUP_BY,行為跟 PostgreSQL 一致了,但很多舊系統為了相容性還是關掉這個設定。從 MySQL 過來的人如果一直仰賴寬鬆 GROUP BY,到 PostgreSQL 要改寫不少查詢。正確的寫法是用聚合函式(例如 MAX(name)、STRING_AGG(name, ','))或是 window function(ROW_NUMBER() OVER (PARTITION BY ...))。
Boolean、NULL 與空字串
PostgreSQL 有真正的 BOOLEAN 型別,值就是 TRUE、FALSE、NULL 三種,寫起來自然。
SELECT * FROM users WHERE active; -- PostgreSQL 可以直接這樣寫
SELECT * FROM users WHERE active = TRUE; -- 這樣也行
-- 插入時可接受多種形式
INSERT INTO users (active) VALUES (TRUE), (FALSE), ('t'), ('f'), ('yes'), ('no');
MySQL 其實沒有真正的 BOOLEAN,BOOL 跟 BOOLEAN 都是 TINYINT(1) 的別名,TRUE 實際上是 1、FALSE 是 0。SQL Server 則是用 BIT,值是 0/1/NULL,不能寫 WHERE active 直接當條件,要寫 WHERE active = 1。遷移過來的程式碼裡面只要看到跟 1/0 比較的布林欄位,改成 PostgreSQL 時可以順便改成 TRUE/FALSE,讀起來更直覺。
NULL 的比較這三家都一致:NULL = NULL 結果是 NULL(不是 TRUE),要判斷 NULL 只能用 IS NULL、IS NOT NULL、IS DISTINCT FROM。題外話,Oracle 會把空字串 '' 當成 NULL,三家都不會,這是從 Oracle 過來的人要留意的;反過來從 MySQL/SQL Server/PostgreSQL 過去 Oracle 才要小心。
字串比較與 Collation
字串比較的大小寫是否敏感、排序規則怎麼走,由 collation 決定。PostgreSQL 預設採用作業系統的 locale,大多數情況是 en_US.UTF-8,字串比較大小寫敏感、口音敏感。MySQL 預設的 collation 是 utf8mb4_0900_ai_ci(MySQL 8),大小寫不敏感、口音不敏感。SQL Server 則看伺服器與資料庫的 collation,常見的 SQL_Latin1_General_CP1_CI_AS 是大小寫不敏感、口音敏感。
這個差異會直接讓從 MySQL 過來的人踩到雷,因為在 MySQL 裡 WHERE email = '[email protected]' 也能查到小寫的那筆,遷移到 PostgreSQL 之後就找不到了。解法有幾種:
- 把欄位改用
CITEXT型別(需要啟用citext擴充) - 查詢時用
LOWER(email) = LOWER('[email protected]'),並且在LOWER(email)上建立索引 - 應用層統一小寫之後再寫入資料庫
PostgreSQL 15 之後也允許對特定欄位指定 ICU collation 來做不敏感比較,但是設定較繁瑣,大部分情況應用層統一小寫反而最單純。
Schema 命名空間
Schema 是這三家差異最大的概念之一,因為「schema」這個詞在三家裡的意義完全不同。
| 概念 | PostgreSQL | MySQL/MariaDB | SQL Server |
|---|---|---|---|
| Database | 真正的隔離單位,跨 database 要重新連線 | 其實就是 schema,USE db 切換 | 真正的隔離單位,跟 PostgreSQL 類似 |
| Schema | Database 底下的命名空間,public 為預設 | 沒有這層概念,Schema = Database | Database 底下的命名空間,dbo 為預設 |
| 完整路徑 | db.schema.table | db.table | db.schema.table |
SQL Server 過來的人對 PostgreSQL 的 schema 會比較適應,觀念幾乎一樣,只要把 dbo. 換成 public. 或自訂 schema 前綴就可以了。MySQL 過來的人則要建立新的心智模型:原本 MySQL 的一個 database 在 PostgreSQL 的對應物其實是一個 schema,而不是一個 database。PostgreSQL 真正的 database 之間是完全隔離的,甚至跨 database 的外鍵、join 都不能直接做,要透過 FDW(Foreign Data Wrapper)或 dblink 擴充。
PostgreSQL 連線時會依照 search_path 變數搜尋 schema,預設是 "$user", public。也就是先找跟登入帳號同名的 schema,找不到再找 public。這個機制在多租戶系統設計上很有用,每個租戶一個 schema,連線時設定 search_path 就能讓同一份 SQL 跑在不同租戶資料上。
預存程序與函式
PostgreSQL 預設的預存程序語言是 PL/pgSQL,語法類似 Oracle 的 PL/SQL,跟 MySQL 與 SQL Server 的 T-SQL 有蠻多差異。更特別的是 PostgreSQL 可以載入 PL/Python、PL/Perl、PL/v8(JavaScript)等語言來寫預存程序,雖然實務上大家還是 PL/pgSQL 為主。
另外 PostgreSQL 11 才把 function 與 procedure 分家,之前只有 function 的概念,procedure 主要是為了支援交易控制(COMMIT/ROLLBACK)而加進來的。MySQL 與 SQL Server 一直以來都有這兩種區分。
-- PostgreSQL 的 function 範例
CREATE OR REPLACE FUNCTION calc_total(user_id INT)
RETURNS NUMERIC AS $$
DECLARE
total NUMERIC;
BEGIN
SELECT SUM(amount) INTO total FROM orders WHERE orders.user_id = calc_total.user_id;
RETURN COALESCE(total, 0);
END;
$$ LANGUAGE plpgsql;
-- 呼叫
SELECT calc_total(42);
特別注意 PostgreSQL 的函式本體用 $$ ... $$ 或 $tag$ ... $tag$ 包起來,這是 dollar-quoted string,主要是為了避免本體中的單引號跟外層單引號打架,寫起來比 MySQL/SQL Server 的跳脫字元乾淨許多。從其他家過來的人第一次看到這個語法會覺得怪,習慣之後反而覺得舒服。
這邊要務實地反向說一件事:預存程序這件事本身就不建議在新專案大量使用。三家的預存程序都難以版本控制、難以單元測試、難以在 CI/CD 流程中自動化。把業務邏輯寫在應用層(Java/Python/Go/.NET)比較容易管理,資料庫專心做它擅長的事就好。遷移到 PostgreSQL 的時候,其實是一個好機會重新檢視舊系統裡哪些預存程序其實應該搬到應用層。
連線模型與連線池
這是 PostgreSQL 跟其他兩家最大的架構差異,也是上線後最常遇到的雷。PostgreSQL 採用 process-per-connection 模型,每條進來的連線都會 fork 一個獨立的 OS process 來服務,這個 process 大約會佔 10MB 左右的記憶體。MySQL 與 SQL Server 都是 thread-based,每條連線只是一條 thread,成本低很多。
這意味著 PostgreSQL 不太適合讓應用程式直接維持大量長連線。如果有 20 台應用伺服器、每台連線池設 50,那就是 1000 個 process 同時跑,記憶體很快就爆掉。因此 PostgreSQL 實務上幾乎一定要搭配外部連線池中介軟體,常見的有:
- PgBouncer:最主流的選擇,輕量、穩定,支援 transaction pooling 模式可以極大提升連線複用率
- PgCat:Rust 寫的新秀,支援 sharding 與 load balancing
- Supabase Supavisor:Elixir 寫的,Supabase 自家方案
雲端服務像是 AWS RDS Proxy、Azure 的 PostgreSQL 連線池、Supabase 與 Neon 通常都內建處理好了,自架的時候才需要自己部署 PgBouncer。這點在預算規劃、架構圖上都跟 MySQL、SQL Server 不一樣,是遷移時一定要先想好的部分。
MVCC 與 VACUUM
PostgreSQL 與 MySQL InnoDB 都採用 MVCC(Multi-Version Concurrency Control),讀寫不互相阻塞。但兩邊實作不同,PostgreSQL 的 UPDATE 實際上是插入一筆新版本、標記舊版本死亡,之後由 VACUUM 回收空間。MySQL InnoDB 把舊版本放在 undo log 裡,事務結束後 purge thread 回收,使用者通常感覺不到。
PostgreSQL 內建 autovacuum 程序會自動執行 VACUUM,在大部分工作負載下不需要特別管。但是寫入量非常大的表(例如日誌類型、計數器類型)或是大量 UPDATE 的表,常常要調 autovacuum 的觸發門檻(autovacuum_vacuum_scale_factor),不然會累積很多 dead tuples 導致表膨脹、查詢變慢。這是 PostgreSQL DBA 的基本功。
SQL Server 的 Read Committed Snapshot Isolation(RCSI)是選擇啟用的(預設關閉),需要透過 ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ON 開啟,開啟後行為比較接近 PostgreSQL/MySQL 的 MVCC。很多從 SQL Server 過來的 DBA 一開始會驚訝 PostgreSQL 「預設就是 MVCC,不用特別開」,這是三家設計哲學的差異。
交易隔離與鎖定
三家的預設交易隔離級別也不一樣:
| 資料庫 | 預設隔離級別 | 備註 |
|---|---|---|
| PostgreSQL | Read Committed | 也支援 Repeatable Read、Serializable(真 SSI) |
| MySQL InnoDB | Repeatable Read | 預設比 PostgreSQL 嚴格,但實作不完全符合 SQL 標準 |
| SQL Server | Read Committed | 啟用 RCSI 後行為類似 PostgreSQL |
從 MySQL 過來要特別注意:MySQL 的 Repeatable Read 下,同一個交易內重複讀取會看到一樣的快照,PostgreSQL 的 Read Committed 下,每個 statement 看到的快照可能不同。程式碼如果原本仰賴 MySQL 這個特性,搬到 PostgreSQL 要改成主動設 BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ。
PostgreSQL 的 Serializable 用的是 Serializable Snapshot Isolation(SSI)演算法,是真正的可序列化,代價是某些交易會失敗並被要求重試。這個特性對正確性很敏感的系統(金融、會計)很有價值,但應用層要準備好重試邏輯。
JSON 與擴充型別
三家都有 JSON 支援,但成熟度與能力差蠻多。PostgreSQL 的 JSONB 是儲存成二進位格式,支援 GIN 索引可以對任意路徑快速查詢,功能跟 MongoDB 已經很接近。MySQL 5.7 起有 JSON 型別但索引能力較弱(要配合 generated column),SQL Server 的 JSON 其實是存成 NVARCHAR 加上查詢函式,沒有專屬型別。
-- PostgreSQL 的 JSONB 查詢
CREATE TABLE events (id SERIAL, data JSONB);
INSERT INTO events (data) VALUES ('{"user": "kyle", "tags": ["ai", "db"]}');
-- 直接查 JSON 內部欄位
SELECT data->>'user' FROM events WHERE data @> '{"tags": ["ai"]}';
-- 建立 GIN 索引讓 @> 查詢飛快
CREATE INDEX idx_events_data ON events USING GIN (data);
其他 PostgreSQL 特色型別,例如陣列、範圍、網路位址、幾何,在舊文 PostgreSQL 入門學習筆記 資料型態與DDL 有完整介紹,這邊不重複。另外靠擴充機制能加上更多能力,例如 PostGIS 做地理資訊系統、pgvector 做向量檢索(LLM RAG 場景)、TimescaleDB 做時序資料,這些都是 MySQL/SQL Server 沒有或比較弱的地方。
備份、備援與複寫
備份與複寫的名詞與工具三家都不一樣。
| 類別 | PostgreSQL | MySQL/MariaDB | SQL Server |
|---|---|---|---|
| 邏輯備份 | pg_dump、pg_dumpall | mysqldump、mariabackup --sql | BACKUP DATABASE ... TO DISK |
| 實體備份 | pg_basebackup、pg_backrest | xtrabackup、mariabackup | 完整、差異、交易紀錄備份 |
| 即時複寫 | Streaming Replication(基於 WAL) | Binlog Replication、Group Replication | AlwaysOn Availability Groups |
| 邏輯複寫 | Logical Replication(PG 10+) | Row-based Binlog | Transactional Replication |
PostgreSQL 的複寫機制在雲端託管環境下通常不用自己處理,RDS、Aurora、Cloud SQL、Supabase 都會代管。自架環境的話 streaming replication 設定相對直覺,比 MySQL 的 binlog 複寫好理解。但是 PostgreSQL 內建沒有自動 failover 機制,要搭配 Patroni、repmgr、pg_auto_failover 這類工具,這點 MySQL 的 Group Replication 與 SQL Server 的 AlwaysOn 內建支援比較好。
優缺點快速對照
| 面向 | PostgreSQL | MySQL/MariaDB | SQL Server |
|---|---|---|---|
| ANSI SQL 符合度 | 高 | 中等,有不少方言 | 中等,T-SQL 方言 |
| 複雜查詢能力(CTE、window、array、JSON) | 強 | MySQL 8 之後補齊,但仍較弱 | 強 |
| 擴充性(自訂型別、函式、外掛) | 極強 | 有限 | 企業版有 CLR Integration |
| 連線成本 | 高(process-based),需連線池 | 低(thread-based) | 低(thread-based) |
| 寫入吞吐 | 中等偏高 | 高(尤其 InnoDB + 分區) | 高 |
| 授權成本 | 免費 | 免費(或付費 Enterprise) | 高,Standard/Enterprise 都是商業授權 |
| GUI 與管理工具 | pgAdmin、DBeaver、TablePlus | MySQL Workbench、DBeaver | SSMS(企業界最成熟的 GUI) |
| 生態系統 | PostGIS、pgvector、TimescaleDB 等垂直領域 | Web 開發生態齊全 | Power BI、SSIS、Reporting Services |
| 學習曲線 | 起步容易,進階特性(VACUUM、explain)需投入 | 最平緩 | 跟微軟生態熟悉度相關 |
什麼情境該選誰
務實地反向說一件事,不是所有情境都該無腦選 PostgreSQL。
PostgreSQL 比較適合的場景:需要複雜查詢、CTE、window function、JSONB、陣列、範圍型別、地理資訊、向量檢索這些進階能力;需要嚴格的資料正確性(金融、會計);雲端原生部署,團隊習慣搭配 managed service;新專案或中等規模系統。
MySQL/MariaDB 比較適合的場景:超大量簡單讀寫的 Web 應用(社群、電商);想要最簡單的運維、最多的教學資源;需要 Galera Cluster、Vitess 這類橫向擴展方案;LAMP 既有系統維護;寫入壓力非常高且 schema 簡單的情境。
SQL Server 比較適合的場景:企業環境已經有 Windows Server、.NET、Power BI、SSIS 等微軟技術棧;有 DBA 團隊熟悉 T-SQL 與 SSMS;已經買了 Software Assurance 訂閱;報表與 BI 需求重度依賴 Reporting Services。這種情境下硬要遷移到 PostgreSQL,只是把成熟的生態丟掉換來新學習成本,不一定划算。
遷移實務建議
真的決定從 MySQL 或 SQL Server 遷移到 PostgreSQL 的時候,有幾個實務上的重點。
- 先拿一個小系統練手:挑一個流量不高、風險可控的服務先遷,讓團隊熟悉 psql、explain、autovacuum 的操作,之後再處理核心系統。
- 用專業的遷移工具:pgloader 可以一鍵從 MySQL 匯入 PostgreSQL,會自動處理型別轉換、索引、約束。SQL Server 來源可以考慮 AWS 的 Schema Conversion Tool(SCT)或 Azure 的 DMS。手刻 SQL 匯出入很少會比工具快。
- 先解時區問題:MySQL 的
DATETIME是無時區的,遷移到 PostgreSQL 務必用TIMESTAMPTZ(含時區),並在匯入時明確指定原始時區,不然會遇到時區混亂的地獄。SQL Server 的DATETIME2也一樣無時區,DATETIMEOFFSET才有。 - 預存程序重寫或搬到應用層:PL/pgSQL 跟 T-SQL、MySQL stored procedure 的寫法差異大,如果預存程序不多,趁機搬到應用層反而比較乾淨。
- 連線池一定要部署:不管是 PgBouncer 還是雲端託管的 Proxy,都要在遷移第一天就到位,不然上線後很容易因為連線數爆炸崩潰。
- 觀察 autovacuum 行為:新系統上線後前一兩週重點觀察
pg_stat_user_tables、pg_stat_activity,確認 autovacuum 追得上寫入量,必要時調整 scale_factor 或 cost_limit。
PostgreSQL 的學習曲線一開始看似平緩,真正上線運行才會遇到 VACUUM、explain plan 閱讀、連線池調整這些進階題。建議在測試環境做夠久的壓力測試,先把這些課補起來再切正式環境。而且對於長期維護的系統,選擇一個規範清晰、擴充性好、社群活躍的資料庫,短期的學習成本遠低於長期的技術鎖定成本,這也是這幾年 PostgreSQL 熱度持續上升的主要原因。