本文是 MySQL overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 8.0 modern SQL 特性 — 5 個關鍵能力 + 跟 PostgreSQL 對應特性的對比。


「MySQL 是 SQL 簡單版」是個過時觀念。

這個觀念的來源很合理:MySQL 5.x 時代沒 CTE、window function 要嗑 hack、recursive query 寫不出來、JSON 處理是字串 substring 拼接、複雜分析 query 只能丟去 PostgreSQL 或 Snowflake。整整 10 年 SQL 進階特性 MySQL 全缺、PostgreSQL 全有。

MySQL 8.0(2018 推出)改變這件事。CTE / window function / lateral derived table / JSON_TABLE / hash join / atomic DDL / role-based authentication / common table expression 全部進來。這不是「終於跟上 PG」、是 MySQL 第一次有資格進入 SQL 工程深度討論。但有 caveats:每個特性的 行為實現 跟 PostgreSQL 對應特性都有 微妙差異、不能假設 PG 經驗直接套用。

對從 PostgreSQL 過來評估 MySQL 的讀者:本文是 特性對等驗證 — 哪些 8.0 特性真的可以 production 用、哪些是 marketing 但實作有 gap。對既有 MySQL 5.7 user:本文是 upgrade 5.7 → 8.0 的具體 ROI — 從 SQL feature 角度看升級值不值得。

5 個關鍵特性 + PG 對比

特性 1:CTE(Common Table Expression)

MySQL 8.0 / PG 8.4+ 都支援。

 1-- MySQL 8.0 + PG 都 OK
 2WITH order_summary AS (
 3    SELECT user_id, SUM(amount) AS total
 4    FROM orders
 5    WHERE created_at > '2026-01-01'
 6    GROUP BY user_id
 7)
 8SELECT u.name, os.total
 9FROM users u JOIN order_summary os ON u.id = os.user_id
10WHERE os.total > 1000;

行為差異

  • MySQL 8.0:CTE 不 materialize 為預設、optimizer 把 CTE 視為 inlined subquery、CTE 引用兩次以上會 重複計算
  • PostgreSQL(< 12):CTE fence by default(materialize barrier)、optimizer 不 push predicate 進 CTE
  • PostgreSQL(12+):CTE 行為跟 MySQL 接近、有 MATERIALIZED / NOT MATERIALIZED keyword 明示

對 PG 12+ user:可以套 MySQL 經驗。對 PG 11 以下 user:CTE 行為跟 MySQL 不一樣、要重看 query plan。

Recursive CTE

1WITH RECURSIVE org_chart AS (
2    SELECT id, name, manager_id, 0 AS depth
3    FROM employees WHERE manager_id IS NULL
4    UNION ALL
5    SELECT e.id, e.name, e.manager_id, oc.depth + 1
6    FROM employees e JOIN org_chart oc ON e.manager_id = oc.id
7)
8SELECT * FROM org_chart WHERE depth <= 10;

兩家都支援、但 MySQL 8.0 有 深度上限cte_max_recursion_depth=1000、預設 1000、PG 預設 unlimited)。複雜 hierarchical query(深度 > 1000)MySQL 需要顯式提高 limit。

特性 2:Window Function

MySQL 8.0 / PG 8.4+ 都支援、語法同 SQL standard。

1SELECT
2    order_id,
3    user_id,
4    amount,
5    SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS running_total,
6    RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rank_in_user
7FROM orders;

行為差異

  • 執行 plan:MySQL 8.0 用 window iterator、單 partition 內 sort、外加 in-memory window buffer。PostgreSQL 有更成熟的 WindowAgg node、複雜 frame spec 處理更好
  • Frame spec 支援度:兩家都支援 ROWS / RANGE / GROUPS、但 GROUPS frame MySQL 是 8.0.16+ 才補進、PG 11+ 才補
  • 大資料量 spill behavior:MySQL window function 超過 sort_buffer_size(預設 256K)會 spill 到 disk、Performance 雪崩。PG 用 work_mem(預設 4MB)、寬裕些但也會 spill

對長期用 PG window function 寫複雜 reporting query 的 user:MySQL 8.0 可以做、但 效能 tune 工作量大、不是 drop-in。

特性 3:JSON_TABLE(PG 主要賣點對比)

這是 user 點到的對比重點。

MySQL 8.0 的 JSON_TABLE

 1SELECT t.id, j.name, j.price
 2FROM products t,
 3     JSON_TABLE(
 4         t.metadata,
 5         '$.variants[*]' COLUMNS (
 6             name VARCHAR(50) PATH '$.name',
 7             price DECIMAL(10,2) PATH '$.price'
 8         )
 9     ) AS j
10WHERE t.category = 'shoes';

JSON_TABLE 把 JSON document 內的 array element 展開成 relational rows、然後可以 JOIN / WHERE / GROUP BY。SQL:2016 standard 規範。

PostgreSQL 對應

PG 17+ 有 JSON_TABLE(SQL:2016 standard、跟 MySQL 同語法)、但歷史上 PG user 用兩條不同路線:

  1. JSONB operator(PG 9.4+):

    1SELECT id, metadata->'variants' AS variants
    2FROM products
    3WHERE metadata @> '{"category": "shoes"}';
  2. jsonb_path_query(PG 12+):

    1SELECT t.id, v.name, v.price
    2FROM products t,
    3     jsonb_path_query(t.metadata, '$.variants[*]') AS v;

核心差異

維度MySQL JSON_TABLEPG JSONB operatorPG jsonb_path_query
Index必須對 JSON column 建 generated column + 一般 index、不能直接 GIN index JSON pathGIN index 直接 over JSONB(業界唯一)可以走 GIN expression index
StorageJSON column = LONGTEXT 包裝JSONB = binary、壓縮、index 友善同左
Query 效率(複雜 path)中等(需要 generated column 加速)高(GIN index 直接)
SQL standard 對齊高(JSON_TABLE 是 standard)低(JSONB operator 是 PG 專有)中(jsonpath 是 standard)
大 JSON(> 1 MB)LONGTEXT 仍可、但 query 慢JSONB 壓縮 + 部分 read同左

選型結論

  • MySQL 是 JSON-storage 角色(document 順手存進關聯 DB):JSON_TABLE 夠用、配 generated column + index、production-ready
  • MySQL 是 document-heavy workload(大量 JSON-driven query / 複雜 path / 高 selectivity):PG JSONB GIN index 仍是 clearly winner、或直接用 MongoDB
  • MySQL 8.0 JSON 不是 PG JSONB 替代:JSON_TABLE 是 SQL standard 對齊、好 portable、但 index 跟 storage 仍弱

對「JSON 是 PG 主要賣點」的判斷:JSONB binary storage + GIN index 是 PG 在 JSON workload 的 結構性優勢、MySQL 8.0 補了 SQL_TABLE 但 index 那層沒補。8.0 後 JSON 議題 不是 deal-breaker for MySQL(不像 5.7 時代直接 disqualify)、但仍不是 MySQL 主場。

特性 4:Lateral Derived Table

MySQL 8.0.14+ / PG 9.3+ 都支援。

1-- 對每個 user、找他最近 5 個 order
2SELECT u.id, recent.*
3FROM users u
4LEFT JOIN LATERAL (
5    SELECT order_id, amount
6    FROM orders o
7    WHERE o.user_id = u.id
8    ORDER BY created_at DESC LIMIT 5
9) recent ON true;

Lateral 讓 subquery 可以 引用外部 reference columnu.id)、不可能用 plain subquery 寫出來。

行為差異

  • MySQL 8.0:lateral 後加、optimizer plan 仍在演進、複雜 lateral query 可能 plan 次優
  • PostgreSQL:lateral 早就成熟、plan 跟 join 直接 fuse、效率高

對 PG-experienced 使用 lateral 寫 reporting query 的 user:MySQL 8.0 可以、但有時候要 hint optimizer 達到最佳 plan。

特性 5:Hash Join

MySQL 8.0.18+ / PG 早已有。

MySQL 8.0 之前:只有 nested loop join、大表 JOIN 完全失控(n × m row scan)。8.0.18 加 hash join、optimizer 在預估 row count 大時自動切。

注意:MySQL 8.0 hash join 預設 不對所有 join 開、只在 optimizer_switch='hash_join=on' 且 join condition 是 equality on indexed column 時觸發。常見錯估:複雜 join 條件不觸發 hash join、optimizer fallback nested loop、query 永遠跑不完。

PG 對應:PG 一直有 hash join、optimizer 預設 cover 廣、且有 parallel hash join(PG 11+)大表 JOIN 並行加速。

MySQL hash join 是 補洞、不是 並肩特性。複雜 OLAP query MySQL 仍弱於 PG。

其他 8.0 特性(一句話帶過)

  • Atomic DDL:CREATE TABLE / DROP / ALTER 變 transactional、crash recovery 不會留 orphan table(PG 早就 atomic)
  • Role-based authentication:role 取代 group-level grant、user 可繼承 role(PG 早就 role 系統)
  • CHECK constraint enforcement:5.7 可寫但不執行、8.0 真的 enforce(PG 一直執行)
  • invisible index:建 index 但 optimizer 暫不用、適合 staging query plan 測試(PG 沒原生對應)
  • Resource Group:query 跑時可分配 CPU thread 給特定 user group(PG 沒原生對應)
  • Generated column:MySQL 5.7 已有、8.0 強化、可作為 JSON path 加速的 workaround

配置 step-by-step(從 5.7 → 8.0 SQL feature 升級)

如果已經是 8.0、所有特性都可以用、不必額外配置。如果是 5.7 → 8.0、需要:

  1. character_set_server=utf8mb4:8.0 預設 utf8mb4(5.7 預設 latin1)、character set 不一致導致 query 行為微差
  2. default_authentication_plugin=mysql_native_password:8.0 預設 caching_sha2_password、舊 client 連不上、cluster upgrade 期間用 native_password 保兼容
  3. optimizer_switch='hash_join=on':確認 hash join 啟用、預設應該已 ON
  4. cte_max_recursion_depth=10000:複雜 recursive CTE 需要時提高
  5. 重新 review 所有 ORM-generated SQL:8.0 keywords 變多(WINDOW、RANK、LATERAL 等變成 reserved word)、5.7 識別碼可能變 syntax error

5 個 Production 踩雷

1. CTE 引用兩次 = 跑兩次

1WITH expensive AS (SELECT ... heavy aggregation ...)
2SELECT * FROM expensive WHERE ...
3UNION ALL
4SELECT * FROM expensive WHERE other_condition;

預期 CTE 跑一次、實際 MySQL 跑兩次。Query 時間 doubled。

修法:

  • 把 CTE 結果先 INSERT 進 temporary table、SELECT 兩次走 temp table(手動 materialize)
  • 或 PG 用 MATERIALIZED keyword(MySQL 沒對應 hint、要手動 temp table)

2. Window function 大 partition spill 到 disk

1SELECT order_id,
2       SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at)
3FROM orders;  -- 1 億 row

sort_buffer_size=256K 預設、單 partition > 256K row 開始 spill disk、執行從秒級變分鐘級。

修法:

  • 提高 sort_buffer_size(per-connection、不要設太大、connection × buffer 會吃 RAM)
  • 加 INDEX 包含 user_id, created_at、optimizer 可直接用 sorted index、不必額外 sort

3. JSON_TABLE 跟 generated column 取捨錯誤

直接 JSON_TABLE on every query:

1SELECT * FROM products,
2JSON_TABLE(metadata, '$.variants[*]' COLUMNS (...));

每次 query 跑 JSON parse、無 index 加速、大表 query 慢。

修法:

  • 常 query 的 JSON path 建 generated column:

    1ALTER TABLE products
    2ADD COLUMN category VARCHAR(50)
    3GENERATED ALWAYS AS (JSON_UNQUOTE(metadata->'$.category')) STORED,
    4ADD INDEX idx_category (category);
  • JSON_TABLE 用於 ad-hoc query、不要當熱 path

  • 跟 PG JSONB GIN 對比:PG 不必預先建 generated column、GIN index 直接 over JSONB

4. Hash join 沒觸發 — Optimizer 預估錯 row count

JOIN 大表預期 hash join、實際 MySQL 跑 nested loop、query 跑不完。常見原因:

  • Table statistics 過時(沒跑 ANALYZE TABLE
  • Join condition 不是 pure equality(a.id = b.id + 1 等)
  • 一邊有 LIMIT、optimizer 估 small set、選 nested loop

修法:

  • ANALYZE TABLE 更新 statistics
  • EXPLAIN ANALYZE 看實際 row count vs 估計
  • optimizer_hint(如 /*+ HASH_JOIN(t1 t2) */)強制

5. Recursive CTE 深度上限 — Production query 突然 fail

cte_max_recursion_depth=1000 預設、organization hierarchy / tree query 超過 1000 層直接 fail(ER_CTE_MAX_RECURSION_DEPTH_EXCEEDED)。

修法:

  • 評估真實 hierarchy 深度、設 cte_max_recursion_depth=10000 或更高
  • 或 query 加 WHERE depth < N 提前停(不依賴 implicit limit)
  • 對極大 hierarchy(社群 follow graph 等)改用 graph DB(Neo4j)— MySQL recursive CTE 不是 graph workload 主場

MySQL 8.0 vs PG SQL 特性 cross-reference

特性MySQL 8.0PostgreSQL差異
CTE8.0+8.4+PG 2009 即支援、MySQL 2018 才支援、約晚 9 年
Recursive CTE8.0+(depth 限)8.4+(unlimited)PG 無深度上限
Window function8.0+8.4+Frame spec 兩家略不同(GROUPS frame 推出時點)
Lateral8.0.14+9.3+PG plan 較成熟
JSON_TABLE8.0+17+MySQL 早 6 年(SQL:2016 standard)
JSONB index無原生GIN index over JSONBPG 結構優勢
Hash join8.0.18+PG parallel hash join
Atomic DDL8.0+PG 一直 atomic
Common keyword補齊完整-
Role-based auth8.0+-
Materialized view無原生9.3+PG 結構優勢(MySQL 用 trigger / scheduled refresh 模擬)
Partial indexPG 結構優勢
Expression index8.0.13+MySQL 後加
Full-text search內建(InnoDB 5.6+)內建(tsvector)PG full-text 更成熟
Foreign data wrapper無原生早(FDW)PG 結構優勢

8.0 補了 語法層 大部分缺漏、storage / index / extensibility 層 仍是 PG 結構優勢。對「先選 SQL 工程深度」的 org、PG 仍領先;對「先選 ecosystem / replication / sharding」的 org、MySQL 已不是 disqualifier。

跟其他模組整合

跟 InnoDB Tuning

JSON column 在 InnoDB 是 LONGTEXT 包裝、大 JSON 進 off-page storage(innodb_default_row_format=DYNAMIC 才行、Antelope format 不支援)。Buffer pool 對 LONGTEXT 較不友善、大 JSON workload 可能要更大 buffer pool。詳見 InnoDB Tuning

跟 Query Optimization

8.0 新 hash join + lateral derived 讓 EXPLAIN ANALYZE 結果更複雜。優化複雜 query 需要熟 新 plan node 類型。詳見 Query Optimization deep dive 篇(待寫)。

跟 Online Schema Change

JSON column 跟 generated column 的 schema change 走 gh-ost / pt-osc 沒問題、但 JSON 大表 ALTER 速度比一般 column 慢(每 row 重 serialize)。詳見 Online Schema Change Tools

跟 Replication

Window function / CTE / JSON_TABLE 的 query 結果 replicate(row-level binlog 紀錄結果)、不 replicate query 本身。所以 replica apply 不會重新跑 window function、效率 OK。詳見 Replication Topology

何時 SQL 特性是 MySQL 選型 driver

  • 想要 SQL standard 對齊跨 vendor portable:MySQL 8.0 JSON_TABLE / window 都對齊 standard、PG 部分能力(JSONB operator)是 PG-only、portability MySQL 略好
  • JSON workload < 20% query:MySQL 8.0 + generated column 夠用、不必為 JSON 換 PG
  • JSON workload > 50% query + 複雜 path / aggregation:PG JSONB GIN 仍 winner、考慮 PG 或 MongoDB
  • 需要 materialized view / FDW / partial index:PG 仍領先、不要因為 SQL feature parity 假設 MySQL 全 cover
  • 既有 MySQL 投資 + SQL 工程深度上升:升 8.0 + 訓練團隊用新特性、不是換 vendor

相關連結