PostgreSQL vs MariaDB cho ứng dụng CRUD giao dịch
PostgreSQL vs MariaDB: cái nhìn thực tế về chỉ mục, migration, JSON và tính năng truy vấn trở nên quan trọng khi ứng dụng CRUD vượt qua mức prototype.

Khi một ứng dụng CRUD vượt khỏi mức prototype
Một ứng dụng prototype thường cảm thấy nhanh vì dữ liệu nhỏ, đội ngũ ít và lưu lượng dự đoán được. Bạn có thể chạy qua với các truy vấn đơn giản, vài chỉ mục và chỉnh schema thủ công. Rồi ứng dụng có người dùng thật, quy trình thật và deadline thật.
Tăng trưởng thay đổi tải công việc. Các danh sách và dashboard được mở suốt ngày. Nhiều người sửa cùng một bản ghi. Các job nền bắt đầu ghi theo lô. Đó là lúc “hôm qua vẫn chạy được” biến thành trang chậm, timeout ngẫu nhiên và chờ khoá vào giờ cao điểm.
Bạn có thể đã vượt ngưỡng nếu gặp các dấu hiệu như trang danh sách chậm sau trang 20, release bao gồm backfill dữ liệu (không chỉ cột mới), nhiều trường “flex” cho metadata và payload tích hợp, hoặc ticket hỗ trợ nói “lưu lâu lắm” vào giờ bận.
Đó là lúc so sánh PostgreSQL và MariaDB không còn là sở thích thương hiệu mà trở thành câu hỏi thực tế. Với workloads CRUD giao dịch, các chi tiết quyết định thường là tùy chọn chỉ mục khi truy vấn phức tạp hơn, an toàn khi migrate khi bảng lớn, lưu trữ và truy vấn JSON, và các tính năng truy vấn giúp giảm công việc phía ứng dụng.
Bài viết này tập trung vào các hành vi của cơ sở dữ liệu đó. Nó không đi sâu vào kích thước máy chủ, giá cloud, hay hợp đồng nhà cung cấp. Những thứ đó quan trọng, nhưng thường dễ thay đổi sau hơn là một schema và phong cách truy vấn mà sản phẩm của bạn phụ thuộc vào.
Bắt đầu từ yêu cầu ứng dụng, không phải thương hiệu cơ sở dữ liệu
Điểm bắt đầu đúng không phải là “PostgreSQL hay MariaDB”. Mà là hành vi hàng ngày của ứng dụng: tạo bản ghi, cập nhật vài trường, liệt kê kết quả lọc và giữ tính đúng đắn khi nhiều người bấm cùng lúc.
Ghi lại những màn hình bận nhất của bạn. Mỗi thao tác ghi có bao nhiêu lượt đọc đi kèm? Các đợt tăng xảy ra khi nào (đăng nhập buổi sáng, báo cáo cuối tháng, import lớn)? Ghi chính xác bộ lọc và sắp xếp bạn phụ thuộc, vì chúng quyết định thiết kế chỉ mục và mẫu truy vấn về sau.
Rồi định nghĩa những điều không thể mặc cả. Với nhiều đội, điều đó là nhất quán nghiêm ngặt cho tiền hoặc tồn kho, lịch sử audit cho “ai đã thay đổi gì”, và các truy vấn báo cáo không vỡ vụn mỗi khi schema thay đổi.
Thực tế vận hành quan trọng không kém tính năng. Quyết định bạn sẽ chạy database được quản lý hay tự host, khôi phục từ backup nhanh thế nào, và chấp nhận bao nhiêu thời gian bảo trì.
Cuối cùng, định nghĩa “đủ nhanh” bằng vài mục tiêu rõ ràng. Ví dụ: p95 độ trễ API ở tải bình thường (200–400 ms), p95 ở lúc đỉnh (có thể gấp 2 lần bình thường), chờ khoá tối đa chấp nhận được khi cập nhật (dưới 100 ms), và giới hạn thời gian backup/restore.
Nguyên lý chỉ mục quyết định tốc độ CRUD
Hầu hết ứng dụng CRUD cảm thấy nhanh cho tới khi bảng chạm hàng triệu hàng và mọi màn hình trở thành “một danh sách lọc có sắp xếp”. Lúc đó, chỉ mục là khác biệt giữa truy vấn 50 ms và timeout 5 giây.
B-tree là chỉ mục mặc định ở cả PostgreSQL và MariaDB. Chúng hữu ích khi bạn lọc trên một cột, join theo khoá, và khi ORDER BY khớp với thứ tự trong chỉ mục. Sự khác biệt hiệu năng thực sự thường phụ thuộc vào tính chọn lọc (bao nhiêu hàng khớp) và liệu chỉ mục có thoả mãn cả lọc lẫn sắp xếp mà không phải quét thêm hàng hay không.
Khi ứng dụng trưởng thành, chỉ mục composite quan trọng hơn chỉ mục một cột. Mẫu phổ biến là lọc đa tenant cộng với trạng thái và sắp xếp theo thời gian, ví dụ (tenant_id, status, created_at). Đặt bộ lọc ổn định nhất trước (thường là tenant_id), rồi bộ lọc tiếp theo, rồi cột bạn sắp xếp. Cách này thường tốt hơn nhiều chỉ mục rời rạc mà optimizer không thể kết hợp hiệu quả.
Khác biệt hiện ra với các “chỉ mục thông minh” hơn. PostgreSQL hỗ trợ partial và expression indexes, rất hữu ích cho các màn hình tập trung (ví dụ chỉ mục cho ticket “open” thôi). Chúng mạnh, nhưng có thể làm team bất ngờ nếu truy vấn không khớp đúng predicate.
Chỉ mục không miễn phí. Mỗi insert và update phải cập nhật mọi chỉ mục, nên dễ cải thiện một màn hình nhưng âm thầm làm chậm mọi ghi.
Cách đơn giản để kỷ luật:
- Thêm chỉ mục chỉ cho một đường dẫn truy vấn thực sự (một màn hình hoặc API bạn có thể đặt tên).
- Ưu tiên một chỉ mục composite tốt thay vì nhiều chỉ mục chồng chéo.
- Kiểm tra lại chỉ mục sau khi thay đổi tính năng và loại bỏ phần thừa.
- Lên kế hoạch bảo trì: PostgreSQL cần vacuum/analyze định kỳ để tránh bloat; MariaDB cũng dựa vào thống kê tốt và dọn dẹp thỉnh thoảng.
- Đo lường trước và sau, thay vì tin vào trực giác.
Chỉ mục cho các màn hình thực tế: danh sách, tìm kiếm và phân trang
Hầu hết ứng dụng CRUD dùng thời gian cho vài màn hình: một danh sách có bộ lọc, một ô tìm kiếm, và trang chi tiết. Lựa chọn cơ sở dữ liệu ít quan trọng hơn việc chỉ mục có khớp với các màn hình đó không, nhưng hai engine cung cấp công cụ khác nhau khi bảng lớn.
Với trang danh sách, nghĩ theo thứ tự: lọc trước, sau đó sắp xếp, rồi phân trang. Mẫu phổ biến là “tất cả ticket cho account X, status in (open, pending), mới nhất trước.” Một chỉ mục composite bắt đầu bằng các cột lọc và kết thúc bằng cột sắp xếp thường thắng.
Phân trang cần chú ý. Phân trang bằng OFFSET (ví dụ OFFSET 380) chậm hơn khi bạn cuộn vì cơ sở dữ liệu vẫn phải đi qua các hàng trước đó. Phân trang theo keyset ổn định hơn: bạn truyền giá trị cuối cùng đã thấy (như created_at và id) và yêu cầu “20 bản ghi cũ hơn đó”. Nó cũng giảm trùng và khoảng trống khi hàng mới xuất hiện giữa chừng.
PostgreSQL có một tuỳ chọn hữu ích cho màn hình danh sách: chỉ mục “covering” với INCLUDE, có thể cho phép index-only scans khi visibility map cho phép. MariaDB cũng có thể thực hiện covering reads, nhưng thường bạn làm điều đó bằng cách đặt các cột cần thiết trực tiếp vào định nghĩa chỉ mục. Điều này làm chỉ mục rộng hơn và tốn chi phí duy trì hơn.
Bạn cần chỉ mục tốt hơn nếu endpoint danh sách chậm dần khi bảng lớn mặc dù trả về chỉ 20–50 hàng, sắp xếp chậm trừ khi bạn bỏ ORDER BY, hoặc I/O tăng trong các bộ lọc đơn giản. Truy vấn dài cũng dễ làm tăng chờ khoá trong giờ bận.
Ví dụ: màn hình orders lọc theo customer_id và status và sắp xếp theo created_at thường được hưởng lợi từ chỉ mục bắt đầu (customer_id, status, created_at). Nếu sau này bạn thêm “tìm theo order number”, đó thường là một chỉ mục riêng, không phải ghép vào chỉ mục danh sách.
Migrations: giữ release an toàn khi dữ liệu lớn
Migrations nhanh chóng không còn là “thay đổi bảng” nữa. Một khi có người dùng thật và lịch sử thật, bạn cần xử lý backfill dữ liệu, siết ràng buộc, và dọn các hình dạng dữ liệu cũ mà không làm vỡ ứng dụng.
Mặc định an toàn là expand, backfill, contract. Thêm những gì bạn cần theo cách không gây gián đoạn mã hiện tại, sao chép hoặc tính toán dữ liệu theo bước nhỏ, rồi chỉ xóa đường dẫn cũ khi chắc chắn.
Trên thực tế thường là thêm cột nullable hoặc bảng mới, backfill theo lô trong khi giữ ghi nhất quán, validate sau bằng ràng buộc như NOT NULL, foreign keys và quy tắc unique, rồi mới gỡ cột, chỉ mục và code cũ.
Không phải mọi thay đổi schema đều như nhau. Thêm một cột thường rủi ro thấp. Thêm chỉ mục có thể tốn kém trên bảng lớn, nên lên kế hoạch vào giờ ít traffic và đo lường. Thay đổi kiểu cột thường nguy hiểm nhất vì có thể viết lại dữ liệu hoặc chặn ghi. Mẫu an toàn thường là: tạo cột mới với kiểu mới, backfill, rồi chuyển đọc/ghi sang cột mới.
Rollback cũng khác nghĩa khi ở quy mô lớn. Rollback schema đôi khi dễ; rollback dữ liệu thường không. Hãy rõ ràng về những gì bạn có thể hoàn tác, nhất là khi migration bao gồm xóa dữ liệu hoặc biến đổi mất mát.
Hỗ trợ JSON: trường linh hoạt mà không gây đau sau này
Trường JSON hấp dẫn vì cho phép bạn phát hành nhanh: trường bổ sung, payload tích hợp, tuỳ chọn người dùng, và ghi chú từ hệ thống ngoại có thể lưu mà không cần thay đổi schema. Bài toán là quyết định cái gì nên vào JSON và cái gì xứng đáng là cột thực.
Ở cả PostgreSQL và MariaDB, JSON thường phù hợp nhất khi nó hiếm khi bị lọc và chủ yếu để hiển thị, lưu để debug, là blob “cấu hình” cho user hoặc tenant, hoặc dùng cho thuộc tính nhỏ tùy chọn mà không ảnh hưởng báo cáo.
Chỉ mục JSON là nơi các team bị bất ngờ. Truy vấn một key JSON một lần thì dễ. Lọc và sắp xếp trên key đó qua bảng lớn là nơi hiệu năng có thể sụt giảm. PostgreSQL có tuỳ chọn mạnh để chỉ mục đường dẫn JSON, nhưng bạn vẫn cần kỷ luật: chọn vài key bạn thực sự lọc trên và chỉ mục chúng, còn lại giữ payload không chỉ mục. MariaDB cũng có thể truy vấn JSON, nhưng các pattern “tìm sâu trong JSON” phức tạp thường trở nên mong manh và khó giữ tốc độ.
JSON cũng làm suy yếu ràng buộc. Khó hơn để bắt buộc “phải là một trong các giá trị này” hoặc “luôn tồn tại” bên trong blob không có cấu trúc, và công cụ báo cáo nói chung thích cột kiểu rõ ràng.
Quy tắc mở rộng: bắt đầu với JSON cho phần chưa biết, nhưng chuẩn hoá thành cột hoặc bảng con khi bạn (1) lọc hoặc sắp xếp trên nó, (2) cần ràng buộc, hoặc (3) thấy nó xuất hiện trên dashboard hàng tuần. Lưu nguyên response API shipping đầy đủ trong JSON thường ổn. Các trường như delivery_status và carrier thường xứng đáng là cột thực khi hỗ trợ và báo cáo phụ thuộc vào chúng.
Các tính năng truy vấn xuất hiện trong ứng dụng trưởng thành
Ban đầu, hầu hết CRUD chạy trên SELECT, INSERT, UPDATE, DELETE đơn giản. Sau đó bạn thêm feed hoạt động, view audit, báo cáo admin, và tìm kiếm cần cảm giác tức thì. Đó là lúc lựa chọn trông giống như đánh đổi tính năng.
CTE và subquery giúp giữ truy vấn phức tạp dễ đọc. Hữu ích khi bạn xây một kết quả theo bước (lọc orders, join payments, tính tổng). Nhưng đọc được có thể che giấu chi phí. Khi truy vấn chậm, bạn có thể phải viết lại CTE thành subquery hoặc join và kiểm tra lại execution plan.
Window functions quan trọng khi ai đó yêu cầu “xếp hạng khách hàng theo chi tiêu”, “hiển thị tổng chạy”, hoặc “trạng thái mới nhất cho mỗi ticket”. Chúng thường thay cho vòng lặp phức tạp phía ứng dụng và giảm số truy vấn.
Viết idempotent là một yêu cầu trưởng thành. Khi retry xảy ra (mạng di động, job nền), upsert cho phép bạn ghi an toàn mà không tạo bản ghi trùng:
- PostgreSQL:
INSERT ... ON CONFLICT - MariaDB:
INSERT ... ON DUPLICATE KEY UPDATE
Tìm kiếm là tính năng lén lút xuất hiện. Full-text built-in có thể đủ cho catalog sản phẩm, knowledge base và ghi chú hỗ trợ. Tìm kiếm theo trigram hữu ích cho gợi ý gõ và sai chính tả. Nếu tìm kiếm trở thành lõi (ranking phức tạp, nhiều bộ lọc, lưu lượng lớn), công cụ tìm kiếm ngoài có thể đáng công vận hành thêm.
Ví dụ: một portal đơn giản bắt đầu với “liệt kê orders”. Một năm sau cần “hiển thị đơn gần nhất của mỗi khách, xếp hạng theo chi tiêu hàng tháng, và tìm theo tên bị gõ sai”. Đó là các khả năng cơ sở dữ liệu, không chỉ công việc UI.
Giao dịch, khoá và đồng thời khi tải cao
Khi lưu lượng thấp, hầu hết DB đều ổn. Dưới tải, khác biệt thường là bạn xử lý tốt các thay đổi đồng thời trên cùng dữ liệu hay không, chứ không phải tốc độ thô. Cả PostgreSQL và MariaDB đều chạy workloads CRUD giao dịch, nhưng bạn vẫn phải thiết kế để tránh contention.
Cách diễn đạt isolation
Một transaction là một nhóm bước cần thành công cùng nhau. Isolation kiểm soát những gì session khác thấy trong khi các bước đó chạy. Isolation cao hơn tránh đọc bất ngờ, nhưng có thể tăng chờ. Nhiều app khởi đầu với mặc định và thắt chặt isolation chỉ cho các luồng cần thật sự (như charge thẻ và cập nhật order).
Nguyên nhân thực sự gây đau khoá
Vấn đề khoá trong CRUD thường đến từ vài nguyên nhân lặp lại: hot rows ai cũng cập nhật, bộ đếm thay đổi mỗi hành động, hàng đợi job nơi nhiều worker tranh giành cùng “job tiếp theo”, và các transaction dài giữ khoá trong khi công việc khác (hoặc thời gian người dùng) diễn ra.
Để giảm contention, giữ transaction ngắn, chỉ cập nhật cột cần thiết, và tránh gọi mạng trong transaction.
Thói quen hữu ích là retry khi xung đột. Nếu hai agent hỗ trợ lưu sửa cùng ticket cùng lúc, đừng để thất bại âm thầm. Phát hiện xung đột, load lại hàng mới nhất, và yêu cầu người dùng áp dụng lại thay đổi.
Để phát hiện sớm, theo dõi deadlock, transaction chạy lâu, và truy vấn dành thời gian chờ thay vì chạy. Kích hoạt slow query logs là thói quen tốt, nhất là sau release thêm màn hình hoặc job nền.
Vận hành quan trọng sau khi ra mắt
Sau ra mắt, bạn không chỉ tối ưu tốc độ truy vấn. Bạn tối ưu phục hồi, thay đổi an toàn và hiệu năng dự đoán được.
Bước tiếp theo phổ biến là thêm replica. Primary xử lý ghi, replica phục vụ các trang đọc nặng như dashboard hoặc báo cáo. Điều này thay đổi cách bạn nghĩ về tính mới: một số đọc có thể lùi vài giây, nên ứng dụng cần biết màn hình nào phải đọc từ primary (ví dụ “vừa đặt hàng xong”) và màn hình nào chấp nhận dữ liệu hơi cũ.
Backup chỉ là một nửa công việc. Quan trọng là bạn có thể restore nhanh và đúng hay không. Lên lịch test restore định kỳ vào môi trường riêng, rồi xác thực cơ bản: app có kết nối được không, các bảng chính có tồn tại không, và các truy vấn quan trọng trả kết quả mong đợi. Nhiều đội phát hiện quá muộn rằng họ backup sai thứ, hoặc thời gian restore vượt xa ngân sách downtime.
Upgrade cũng không còn là “bấm và hy vọng”. Lên kế hoạch cửa sổ bảo trì, đọc ghi chú tương thích, và test đường nâng cấp với bản sao dữ liệu production. Ngay cả các bản vá nhỏ cũng có thể thay đổi query plan hoặc hành vi quanh chỉ mục và hàm JSON.
Quan sát đơn giản đem lại nhiều lợi ích sớm. Bắt đầu với slow query logs và các truy vấn hàng đầu theo tổng thời gian, độ bão hòa kết nối, replication lag (nếu có replica), tỷ lệ cache hit và áp lực I/O, cùng chờ khoá và sự kiện deadlock.
Cách chọn: quy trình đánh giá thực tế
Nếu bạn bế tắc, dừng đọc danh sách tính năng và chạy thử nhỏ với workload của chính bạn. Mục tiêu không phải benchmark hoàn hảo mà là tránh bất ngờ khi bảng chạm hàng triệu hàng và chu kỳ release tăng tốc.
1) Xây thử nghiệm nhỏ giống production
Chọn một lát cắt ứng dụng đại diện cho điểm đau thật: một hai bảng chính, vài màn hình, và các đường ghi phía sau. Thu thập các truy vấn hàng đầu (các truy vấn cho danh sách, trang chi tiết, và job nền). Tạo số hàng thực tế (ít nhất gấp 100 lần dữ liệu prototype, với hình dạng tương tự). Thêm chỉ mục bạn nghĩ cần, rồi chạy cùng truy vấn với cùng bộ lọc và sắp xếp và ghi lại thời gian. Lặp lại khi có ghi xảy ra (một script đơn giản insert và update là đủ).
Ví dụ nhanh là màn hình “Khách hàng” lọc theo trạng thái, tìm theo tên, sắp xếp theo hoạt động gần nhất, và phân trang. Màn hình này thường tiết lộ liệu thiết kế chỉ mục và hành vi planner có bền vững không.
2) Diễn tập migrations như một release thực
Tạo bản staging của dataset và luyện những thay đổi bạn biết sẽ tới: thêm cột, đổi kiểu, backfill, thêm chỉ mục. Đo thời gian, xem nó có chặn ghi không, và rollback thực sự nghĩa là gì khi dữ liệu đã thay đổi.
3) Dùng bảng điểm đơn giản
Sau khi test, chấm điểm mỗi lựa chọn theo hiệu năng cho truy vấn thực của bạn, đúng đắn và an toàn (ràng buộc, transaction, các trường hợp biên), rủi ro migration (khoá, downtime, phương án phục hồi), công sức vận hành (backup/restore, replication, monitoring), và độ thoải mái của team.
Chọn cơ sở dữ liệu giảm rủi ro cho 12 tháng tới, không phải cái thắng một micro-test.
Sai lầm và bẫy thường gặp
Các vấn đề cơ sở dữ liệu tốn kém nhất thường bắt đầu như “giải pháp nhanh”. Cả hai DB đều chạy ứng dụng CRUD, nhưng thói quen sai sẽ tổn hại cả hai khi traffic và dữ liệu tăng.
Bẫy phổ biến là coi JSON là lối tắt cho mọi thứ. Một trường “extras” linh hoạt tốt cho dữ liệu thực sự tùy chọn, nhưng các trường lõi như status, timestamps và foreign keys nên là cột thực. Nếu không bạn sẽ có bộ lọc chậm, validate bất tiện, và refactor đau khi báo cáo trở thành ưu tiên.
Chỉ mục có bẫy riêng: thêm chỉ mục cho mọi bộ lọc trên màn hình. Chỉ mục tăng tốc đọc nhưng làm chậm ghi và khiến migrations nặng hơn. Chỉ mục những gì người dùng thực sự dùng, rồi xác thực bằng tải đo.
Migrations có thể cắn bạn khi chúng khoá bảng. Thay đổi “big-bang” như viết lại cột lớn, thêm NOT NULL với default, hoặc tạo chỉ mục lớn có thể chặn ghi trong vài phút. Phân tách thay đổi rủi ro thành các bước và lên lịch khi app yên tĩnh.
Cũng đừng phụ thuộc mãi vào mặc định ORM. Khi danh sách từ 1.000 lên 10 triệu hàng, bạn cần đọc execution plan, phát hiện chỉ mục thiếu, và sửa các join chậm.
Dấu hiệu cảnh báo nhanh: trường JSON dùng cho lọc và sắp xếp chính, số lượng chỉ mục tăng mà không đo hiệu năng ghi, migrations viết lại bảng lớn trong một deploy, và phân trang không có ordering ổn định (dẫn đến thiếu và trùng hàng).
Checklist nhanh trước khi quyết định
Trước khi bạn chọn bên nào, làm một kiểm tra thực tế dựa trên các màn hình bận nhất và quy trình release.
- Các màn hình quan trọng có giữ nhanh ở tải đỉnh không? Test trang danh sách chậm nhất với bộ lọc, sắp xếp và phân trang thực tế, và xác nhận chỉ mục khớp chính xác truy vấn đó.
- Bạn có thể deploy thay đổi schema an toàn không? Viết kế hoạch expand-backfill-contract cho thay đổi lớn tiếp theo.
- Bạn có quy tắc rõ ràng cho JSON vs cột không? Quyết định key JSON nào phải searchable hoặc sortable và key nào thật sự linh hoạt.
- Bạn có phụ thuộc vào tính năng truy vấn cụ thể không? Kiểm tra upsert, window functions, hành vi CTE, và liệu bạn cần functional hay partial indexes.
- Bạn có thể vận hành sau khi ra mắt không? Chứng minh bạn có thể restore từ backup, đo các truy vấn chậm, và baseline độ trễ cùng chờ khoá.
Ví dụ: từ tracking đơn hàng đơn giản tới portal khách hàng bận rộn
Hình dung một portal khách hàng bắt đầu đơn giản: khách đăng nhập, xem đơn, tải hoá đơn và mở ticket hỗ trợ. Tuần đầu tiên, hầu như cơ sở dữ liệu nào cũng ổn. Trang load nhanh và schema nhỏ.
Vài tháng sau, các khoảnh khắc tăng trưởng xuất hiện. Khách yêu cầu bộ lọc như “đơn giao trong 30 ngày, trả bằng thẻ, có hoàn tiền một phần”. Hỗ trợ muốn xuất CSV nhanh cho reviews tuần. Tài chính muốn audit trail: ai đổi trạng thái hoá đơn, khi nào, từ đâu tới đâu. Mẫu truy vấn trở nên rộng và đa dạng hơn màn hình ban đầu.
Đó là lúc quyết định chuyển thành quan tâm các tính năng cụ thể và cách chúng cư xử dưới tải thực.
Nếu bạn thêm các trường linh hoạt (hướng dẫn giao hàng, thuộc tính tuỳ chỉnh, metadata ticket), hỗ trợ JSON quan trọng vì bạn sẽ muốn truy vấn bên trong các trường đó sau này. Thành thật về liệu team của bạn sẽ chỉ mục đường dẫn JSON, validate hình dạng, và giữ hiệu năng có thể đoán khi JSON lớn lên không.
Báo cáo là một điểm áp lực khác. Ngay khi bạn join orders, invoices, payments và tickets với nhiều bộ lọc, bạn sẽ quan tâm tới chỉ mục composite, lập kế hoạch truy vấn, và việc dễ dàng tiến hoá chỉ mục mà không downtime. Migrations cũng không còn là “chạy script vào thứ Sáu” mà trở thành một phần của mọi release, vì một thay đổi nhỏ có thể chạm tới triệu hàng.
Cách thực tế tiến lên là viết ra năm màn hình và các export bạn mong đợi trong sáu tháng tới, thêm bảng lịch sử audit sớm, benchmark với kích thước dữ liệu thực tế bằng các truy vấn chậm nhất của bạn (không phải hello-world CRUD), và tài liệu hoá quy tắc team về dùng JSON, chỉ mục và migrations.
Nếu bạn muốn đi nhanh mà không tự tay dựng mọi lớp, AppMaster (appmaster.io) có thể sinh backend production-ready, web app và native mobile app từ mô hình trực quan. Nó cũng khuyến khích bạn coi màn hình, bộ lọc và quy trình nghiệp vụ như tải truy vấn thực tế từ sớm, giúp phát hiện rủi ro chỉ mục và migration trước khi vào production.
Câu hỏi thường gặp
Bắt đầu bằng việc ghi lại tải thực tế của bạn: các màn hình danh sách bận nhất, bộ lọc, sắp xếp và các đường dẫn ghi dữ liệu vào giờ cao điểm. Cả hai đều có thể chạy CRUD tốt, nhưng lựa chọn an toàn hơn là cái phù hợp với cách bạn thiết kế chỉ mục, migrate và truy vấn dữ liệu trong 12 tháng tới, chứ không phải cái tên nào quen thuộc hơn.
Nếu các trang danh sách chậm dần khi bạn lướt sâu vào các trang, đó có thể là chi phí của các truy vấn OFFSET. Nếu việc lưu thỉnh thoảng bị treo vào giờ cao điểm, bạn có thể gặp tranh chấp khoá hoặc transaction kéo dài. Nếu các release bắt đầu bao gồm backfill và các chỉ mục lớn, migrations đã trở thành vấn đề độ tin cậy chứ không chỉ là thay đổi schema.
Mặc định là một chỉ mục composite cho mỗi truy vấn màn hình quan trọng, sắp xếp theo bộ lọc ổn định nhất trước và cột sắp xếp ở cuối. Ví dụ, danh sách đa tenant thường ổn với (tenant_id, status, created_at) vì nó hỗ trợ lọc và sắp xếp mà không phải quét nhiều.
Phân trang bằng OFFSET chậm dần khi bạn vào các trang cao vì cơ sở dữ liệu vẫn phải bỏ qua các hàng trước đó. Phân trang theo keyset (dùng giá trị cuối cùng đã thấy như created_at và id) giữ hiệu năng ổn định hơn và giảm trùng hoặc mất hàng khi có hàng mới xuất hiện trong lúc cuộn.
Chỉ thêm chỉ mục khi bạn có thể xác định chính xác màn hình hoặc API cần nó, và kiểm tra lại sau mỗi release. Quá nhiều chỉ mục chồng chéo sẽ âm thầm làm chậm mọi thao tác insert và update, khiến ứng dụng cảm thấy chậm vào giờ cao điểm ghi.
Áp dụng cách làm mở rộng — backfill — thu hẹp: thêm cấu trúc mới theo cách không phá vỡ mã hiện có, backfill theo lô nhỏ, kiểm chứng bằng ràng buộc sau, rồi chỉ xóa đường dẫn cũ khi bạn chắc chắn đã chuyển đọc và ghi. Điều này giữ cho release an toàn khi bảng lớn và lưu lượng ổn định.
Giữ JSON cho dữ liệu dạng payload chủ yếu để hiển thị hoặc lưu cho debug, và đưa các trường vào cột thực khi bạn bắt đầu lọc, sắp xếp hoặc báo cáo trên chúng thường xuyên. Việc này tránh các truy vấn nặng với JSON và giúp áp dụng ràng buộc như giá trị bắt buộc dễ hơn.
Upsert là cần thiết khi retry trở nên phổ biến (mạng di động, job nền, timeout). PostgreSQL dùng INSERT ... ON CONFLICT, còn MariaDB dùng INSERT ... ON DUPLICATE KEY UPDATE; trong cả hai trường hợp, hãy định nghĩa khóa duy nhất cẩn thận để retry không tạo bản ghi trùng.
Giữ transaction ngắn, tránh gọi mạng trong khi transaction mở, và giảm các “hot row” mà ai cũng cập nhật (như bộ đếm chia sẻ). Khi xung đột xảy ra, hãy retry hoặc hiển thị xung đột rõ ràng cho người dùng để các sửa đổi không bị mất thầm lặng.
Có, nếu bạn chấp nhận độ trễ nhẹ cho các trang đọc nặng như dashboard và báo cáo. Giữ các đọc “vừa thay đổi xong” trên primary (ví dụ ngay sau khi đặt hàng), và theo dõi replication lag để không hiển thị dữ liệu cũ gây nhầm lẫn.


