Mọi bài "vì sao chọn PostgreSQL" đều kể ba chuyện: nó chặt chẽ về kiểu, nó có DDL trong giao dịch, nó chuẩn SQL hơn. Tôi dựng PostgreSQL 16.14, MySQL 8.0.46 và SQLite cạnh nhau rồi thử từng cái.

Một trong ba đã hết đúng.

Ba CSDL trước cùng một phép thử

Lý lẽ đã hết đúng: chặt chẽ về kiểu

Phép thử kinh điển — chèn chuỗi vào cột số:

CREATE TABLE kieu(so int, chu varchar(3));
INSERT INTO kieu(so) VALUES ('abc');
Kết quả
PostgreSQL 16 ERROR: invalid input syntax for type integer: "abc"
MySQL 8.0 ERROR 1366 (HY000): Incorrect integer value: 'abc'
SQLite chấp nhận — lưu vào cột INTEGER với typeof = text

Và chuỗi quá dài:

Kết quả
PostgreSQL ERROR: value too long for type character varying(3)
MySQL 8.0 ERROR 1406 (22001): Data too long for column 'chu'

MySQL từ chối y hệt PostgreSQL. Lời đồn "MySQL âm thầm cắt cụt dữ liệu của bạn" là chuyện của MySQL 5.6 trở về trước. Từ 5.7, sql_mode mặc định đã có STRICT_TRANS_TABLES:

ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE,
NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, NO_ENGINE_SUBSTITUTION

Nhưng nó chỉ là một câu lệnh:

SET SESSION sql_mode = '';
INSERT INTO k2 VALUES ('abc', 'abcde');
so   chu
0    abc

'abc' thành 0, 'abcde' bị cắt thành 'abc', không lỗi nào. Khác biệt thật giữa hai CSDL không phải "chặt chẽ hay không", mà là PostgreSQL không có công tắc để tắt nó đi. Với MySQL, một thư viện ORM cẩu thả hay một tệp cấu hình kế thừa từ 2015 là đủ để bạn quay lại 2015.

SQLite thì là chuyện khác hẳn: nó dùng kiểu động, cột INTEGER chỉ là gợi ý. Đấy là thiết kế có chủ đích, không phải lỗi — chỉ cần biết trước khi dùng nó làm CSDL cho ứng dụng nhiều người ghi.

Lý lẽ vẫn đúng, và đúng ở mức đáng kể: DDL trong giao dịch

BEGIN;
CREATE TABLE thu_ddl(a int);
ROLLBACK;
Số bảng còn lại sau ROLLBACK
PostgreSQL 0
MySQL 1

Trong MySQL, CREATE TABLE tự động commit giao dịch đang mở. ROLLBACK sau đó không có gì để quay lại.

Nghe là chuyện nhỏ cho tới lần đầu một migration hỏng giữa chừng. Migration của bạn có tám câu lệnh; câu thứ năm gặp lỗi. Với PostgreSQL, cả tám biến mất và CSDL trở về đúng trạng thái trước đó — bạn sửa rồi chạy lại. Với MySQL, bốn câu đầu đã nằm đó vĩnh viễn, và bạn phải tự viết đường lùi cho từng câu.

Đây là lý do các công cụ migration cho PostgreSQL bọc mỗi tệp trong một giao dịch, còn tài liệu của công cụ tương đương cho MySQL luôn có một mục dài về "migration không nguyên tử".

Một khác biệt ít được nhắc: mức cô lập mặc định

PostgreSQL: read committed
MySQL     : REPEATABLE-READ

Hai CSDL mặc định khác nhau, và đây là loại khác biệt gây ra lỗi mà bạn không tái hiện được trên máy mình. Cùng một đoạn mã, chạy trên hai CSDL, cho hai kết quả khác nhau khi có hai giao dịch chạy chồng lên nhau.

Phần 25 của sê-ri này sẽ đo cụ thể hiện tượng nào xảy ra ở mức nào. Ở đây chỉ cần nhớ: đừng giả định mức cô lập, hãy khai tường minh nếu ứng dụng của bạn phụ thuộc vào nó.

Chỗ PostgreSQL thua: mỗi kết nối là một tiến trình

Đây là điều các bài giới thiệu hiếm khi nói. PostgreSQL sinh một tiến trình hệ điều hành riêng cho mỗi kết nối. Tôi mở 50 kết nối rồi đếm:

ket noi client (pg_stat_activity): 51
tien trinh backend (ps)          : 51
tong PSS                         : 68 MB
trung binh moi ket noi           : 1.368 KB

Năm mươi mốt kết nối, năm mươi mốt tiến trình, 68 MB bộ nhớ thật sự tốn thêm — khoảng 1,34 MB mỗi kết nối, và đó là với các kết nối đang nhàn rỗi. Kết nối đang chạy truy vấn còn tốn thêm work_mem.

Đối chiếu:

Mô hình max_connections mặc định
PostgreSQL một tiến trình mỗi kết nối 100
MySQL một luồng mỗi kết nối, chung một tiến trình 151

Đây là lý do gần như mọi hệ thống PostgreSQL nghiêm túc đều có một connection pool đứng trước — PgBouncer, hoặc pool trong ứng dụng. Một ứng dụng web mở 500 kết nối trực tiếp tới PostgreSQL không chỉ vượt max_connections, nó còn tạo 500 tiến trình cho một máy chủ.

MySQL không miễn nhiễm với chuyện này, nhưng ngưỡng đau của nó cao hơn nhiều.

(Tôi phải dựng lại container với --cap-add SYS_PTRACE mới đọc được /proc/*/smaps_rollup. Con số PSS quan trọng hơn RSS ở đây: 51 tiến trình dùng chung phần lớn bộ nhớ, và cộng RSS sẽ đếm phần dùng chung đó 51 lần.)

Vậy chọn thế nào

Ba câu hỏi có ích hơn là so bảng tính năng:

Bạn có cần DDL nguyên tử không? Nếu đội của bạn chạy migration thường xuyên và không muốn viết đường lùi cho từng câu lệnh, đó là lý lẽ mạnh nhất còn lại cho PostgreSQL.

Ai kiểm soát cấu hình CSDL? Nếu chỉ bạn, sql_mode của MySQL không phải rủi ro. Nếu là một đội đông người, hoặc một nhà cung cấp dịch vụ, PostgreSQL loại bỏ hẳn một loại lỗi.

Bạn mở bao nhiêu kết nối? Nhiều kết nối ngắn không có pool là chỗ PostgreSQL đau trước.

Và một câu không nên hỏi: "cái nào nhanh hơn". Cả hai đều đủ nhanh cho gần như mọi ứng dụng, và chênh lệch bạn đo được sẽ đến từ chỉ mục, kế hoạch thực thi và số vòng đi lại — những thứ sê-ri này dành phần lớn thời lượng để đo.

Thử ba mươi giây

Chạy đúng phép thử DDL trên CSDL của bạn:

BEGIN;
CREATE TABLE thu_ddl_tam(a int);
ROLLBACK;
SELECT count(*) FROM information_schema.tables WHERE table_name = 'thu_ddl_tam';

Ra 0 là DDL của bạn nằm trong giao dịch. Ra 1 thì mọi migration nhiều bước của bạn đều có thể dừng lại ở giữa.

Và kiểm mức cô lập bạn đang thật sự chạy:

SHOW default_transaction_isolation;   -- PostgreSQL
SELECT @@transaction_isolation;       -- MySQL

Phần sau đo việc cài PostgreSQL bằng Docker: initdb làm gì trong lần khởi động đầu, và vì sao lần thứ hai nhanh hơn nhiều.