- Dựa trên các vấn đề Hatchet gặp phải trong 2 năm vận hành production, bài viết tổng hợp các nguyên tắc vận hành theo từng giai đoạn, từ thiết kế schema và truy vấn ban đầu đến ghi khối lượng lớn và migration bảng
- Để đọc nhanh, hãy căn chỉnh index với
ORDER BY, nhưng vì query planner có thể chọn sequential scan dựa trên thống kê và chi phí, cần dùng EXPLAIN ANALYZE để so sánh ước lượng với thực thi thực tế
- Hiệu năng và độ ổn định khi ghi phụ thuộc vào transaction ngắn, chỉ khóa những hàng cần thiết,
CREATE INDEX CONCURRENTLY, và connection pooling; xử lý theo lô đã tăng throughput khoảng 10 lần trong đo đạc của Hatchet
- Trong môi trường ghi tần suất cao, cấu hình autovacuum mặc định có thể không kịp thu hồi dead tuple và transaction ID, và khi chạm tới transaction ID wraparound thì downtime có thể xảy ra ở mức nghiêm trọng
- Khi quy mô tăng lên, có thể tận dụng hàng đợi công việc dựa trên
FOR UPDATE SKIP LOCKED, partitioning, trigger và batch backfill, nhưng cần có khả năng điều khiển SQL trực tiếp ngoài lớp trừu tượng ORM
Đối tượng độc giả và giới hạn của ORM
- Đây là hướng dẫn dành cho các lập trình viên đã biết các khái niệm cơ bản về SQL, hàng, bảng và index, để có thể xử lý các vấn đề Postgres trong production
- Tài liệu Postgres rất toàn diện, nhưng khó tra cứu nhanh khi xảy ra sự cố, nên bài viết cô đọng lại quanh kinh nghiệm vận hành mà Hatchet đã trải qua trong 2 năm
- Các nguyên tắc vẫn áp dụng khi dùng ORM, nhưng khi quy mô tăng lên, sẽ có nhiều tối ưu chỉ làm được khi viết SQL trực tiếp thay vì chỉ dựa vào lớp trừu tượng
- Có thể dùng ORM cùng SQL trực tiếp qua các tính năng như Prisma TypedSQL
- Hatchet viết bằng Go sử dụng
sqlc, công cụ cung cấp hành vi tương tự
- Trong môi trường để Claude viết truy vấn, họ khuyến nghị supabase/agent-skills
Thiết kế schema khó thay đổi
- Sau khi triển khai, thay đổi schema là việc khó nhất, nên cần tạo bản nháp bảng và khóa chính trước, rồi lặp lại thiết kế trong khi viết các truy vấn mà ứng dụng cần
- Trong quá trình thiết kế, hãy dùng các câu hỏi sau để xác định cách bảng sẽ được sử dụng
- Tần suất đọc hay ghi cao hơn
- Bộ lọc được dùng thường xuyên nhất khi đọc là gì
- Cột nào được cập nhật thường xuyên nhất
- Có thể áp dụng 1NF, 2NF, 3NF của chuẩn hóa cơ sở dữ liệu, nhưng đôi khi dạng chuẩn lại xung đột với hiệu quả truy vấn hoặc sự tiện dụng cần cho phát triển nhanh
- Trong một số trường hợp, đưa dữ liệu vào cột
jsonb sẽ đơn giản hơn
- Các quy tắc kinh nghiệm được áp dụng trong thiết kế schema như sau
- Dùng cột identity là số nguyên tự tăng hoặc UUID tích hợp sẵn của Postgres cho khóa chính
- Cột identity hơi nhanh hơn
bigserial
- Luôn dùng
timestamptz cho dữ liệu thời gian
- Mọi bảng đều nên có khóa chính
- Với các bảng dung lượng nhỏ mà tính nhất quán và chính xác là quan trọng, có thể dùng foreign key kèm cascade delete, nhưng cần cẩn trọng trong môi trường dung lượng lớn
Truy vấn đọc và index
- Mô hình đơn giản để hiểu
SELECT nhanh là Postgres либо tìm nhanh một hàng bằng index, hoặc đọc toàn bộ hàng trong bảng bằng sequential scan (seq scan)
- Để tìm nhanh một hàng đơn lẻ, hãy dùng các cấu trúc sau
- Index tường minh
- Unique constraint, vốn là một dạng đặc biệt của index
- Khóa chính được Postgres tự động đánh index
- Index mặc định dùng
btree, có thể hiểu như một bảng riêng lưu dữ liệu theo cách tối ưu cho việc tra cứu
- Thời gian tìm hàng xấp xỉ
log(n), với n là số hàng trong bảng
- Nếu không thể dùng index thì sequential scan sẽ được thực hiện, nhưng cơ sở dữ liệu hiện đại có thể nạp hàng vào bộ nhớ rất nhanh, nên với bảng dưới 20 nghìn hàng thì thường gần như xong ngay lập tức
Join và index tổng hợp
- Với inner join, thông thường nên join vào khóa chính; nếu không, có thể schema hoặc quá trình chuẩn hóa đang có vấn đề
- Hãy xử lý mệnh đề
ON giống như WHERE, và dùng index phù hợp cho điều kiện join
- Truy vấn liệt kê trên bảng lớn thường là truy vấn đầu tiên trở nên chậm trong ứng dụng
- Nếu vừa lọc vừa sắp xếp theo tổ chức và thời điểm tạo, có thể dùng index tổng hợp
CREATE INDEX CONCURRENTLY idx_documents_org_created
ON documents (organization_id, created_at DESC);
- Với truy vấn phức tạp, một quy tắc kinh nghiệm là đặt cột
ORDER BY ở cuối index và khớp luôn chiều sắp xếp
- Postgres có thể quét
btree theo cả hai chiều, nên với cột đơn thì DESC có thể không có nhiều ý nghĩa, nhưng với index tổng hợp thì vẫn nên khớp
- Có thể xem chi tiết cách hoạt động của index giảm dần trong tài liệu liên quan
Ghi, khóa và migration
- Điều kiện đầu tiên để ghi thành công là giữ transaction ngắn
- Nếu không có lý do đặc biệt, đừng gọi dịch vụ bên ngoài trong lúc transaction đang mở
- Điều kiện thứ hai là chỉ khóa những hàng cần thiết
- Khi cập nhật một hàng, hàng đó sẽ bị khóa cho đến khi transaction commit
- Tác động của khóa sẽ rõ rệt hơn khi tải hệ thống tăng lên
- Nếu chạy
CREATE INDEX thông thường trên một bảng lớn hiện có, bảng sẽ bị khóa và insert/update sẽ bị chặn, vì vậy luôn dùng CREATE INDEX CONCURRENTLY
- Năng lực schema migration tốt giúp tăng tốc độ phát triển lặp và kéo dài uptime
- Càng nhiều càng tốt, hãy tránh xóa hoặc loại bỏ cột mà thay vào đó dùng cách bổ sung dần
- Nếu có thể, chạy trong transaction để dễ rollback và xử lý áp dụng dở dang
- Ở mức nâng cao hơn, có thể dùng migration expand and contract
- Trước tiên cần xác định liệu migration có chặn toàn bộ ghi hay không
- Tạo index mà không có
CONCURRENTLY có thể chặn mọi ghi và gây downtime
- Các thao tác
ALTER TABLE cần được xem xét lại, và việc thêm check constraint vào bảng lớn cũng có thể chặn ghi
- Có thể tránh việc chặn đó bằng cách thêm check constraint với
NOT VALID
Quản lý kết nối
- Mọi truy vấn và transaction đều dùng kết nối tới cơ sở dữ liệu, và kết nối có chi phí CPU và bộ nhớ cao nên cần được giữ lâu dài
- Tạo rồi hủy kết nối quá thường xuyên sẽ lãng phí tài nguyên
- Connection storm, khi rất nhiều kết nối mới phát sinh cùng lúc, có thể gây ra các vấn đề khó debug liên quan đến khóa nội bộ của Postgres
- Hãy ưu tiên connection pooler bên ngoài như
pgbouncer, và nếu không thể dùng thì dùng connection pool trong bộ nhớ như một phương án thay thế
- Vì Hatchet không thể giả định cơ sở dữ liệu của người dùng có dùng pooler ngoài, họ dùng pgxpool cho Go
Query planner và thống kê
- Với các truy vấn phức tạp có nhiều join hoặc trộn nhiều kiểu join, chỉ thêm index thường không đủ để giải quyết
- Bản thân index cũng có overhead, nên không nên thêm vô hạn
- Query planner chuyển SQL thành các phép toán nội bộ của cơ sở dữ liệu và quyết định có dùng index hay không, nhưng vì thông tin hạn chế nên đôi khi không chọn được kế hoạch tối ưu
- Thông tin mà planner dùng là thống kê của bảng, có thể xem trong
pg_stats
SELECT *
FROM pg_stats
WHERE tablename = 'mytable';
- Thống kê được thu thập khi
ANALYZE, và cũng được cập nhật khi autovacuum chạy
- Tăng tần suất autovacuum cũng giúp thống kê truy vấn luôn mới
- Một trong những nguyên nhân phổ biến khiến truy vấn chạy sai là tần suất phân tích không đủ
- Nếu đơn giản hóa việc đánh giá truy vấn thành có sequential scan hay không, bạn sẽ giảm được việc tối ưu vi mô làm tăng tính khó đoán của planner
- Khi truy vấn xoay quanh khóa chính và index, planner sẽ dễ chọn kế hoạch hơn
Phân tích execution plan và sequential scan
- Một số nhà cung cấp như Google CloudSQL lưu truy vấn chậm bằng cách lấy mẫu, nhưng không phải dịch vụ nào cũng hỗ trợ
EXPLAIN ANALYZE thực sự chạy truy vấn và so sánh số hàng ước tính theo thống kê bảng với số hàng quét thực tế
- Cần cẩn thận trong production vì truy vấn sẽ thực sự được chạy
- Nếu chỉ muốn xem kế hoạch mà không thực thi, hãy dùng
EXPLAIN bỏ ANALYZE
- Có thể lưu kế hoạch chi tiết ở dạng JSON rồi trực quan hóa tại explain.dalibo.com
psql -XqAt -f explain.sql -d $DATABASE_URL > analyze.json
- Nếu thống kê và index đều ổn mà vẫn quét tuần tự, có thể planner đã tính rằng chi phí sequential scan thấp hơn
- Index được lưu tách biệt với heap chứa dữ liệu bảng thực tế, nên sẽ phát sinh chi phí đọc lại nhiều hàng từ heap sau khi tìm thấy chúng trong index
- Nếu không thể tái cấu trúc truy vấn lớn, hãy chấp nhận sequential scan hoặc cân nhắc partitioning
Ghi khối lượng lớn và xử lý theo lô
- Mỗi truy vấn đều có overhead gồm thời gian round-trip tới cơ sở dữ liệu, thời gian lấy kết nối từ connection pool của ứng dụng, và thời gian Postgres xử lý
- Khóa nội bộ của Postgres cũng có thể trở thành nút thắt trong môi trường throughput cao
- Gộp nhiều hàng vào một truy vấn sẽ giảm các chi phí đó
- Cách đơn giản nhất là gửi nhiều truy vấn lên server cùng lúc trong một implicit transaction
- Trong Go có thể dùng
pgx SendBatch
- Tại Hatchet, xử lý theo lô đã tăng throughput khoảng 10 lần, và các tối ưu thêm cho insert được tổng hợp trong hướng dẫn tăng tốc insert Postgres
autovacuum và transaction ID wraparound
- autovacuum phụ trách dọn dẹp dead tuple và quản lý transaction ID; trong môi trường ghi tần suất cao, có thể cần điều chỉnh cấu hình
- Tuple là một phiên bản của hàng được lưu trên hệ thống tệp
- Dù cập nhật hay xóa hàng, phiên bản cũ vẫn còn cho đến khi mọi transaction đã bắt đầu trước đó commit hoặc rollback
- Phiên bản mà không transaction nào còn có thể đọc được nữa chính là dead tuple
- Nếu tốc độ ghi quá cao, autovacuum có thể không theo kịp tốc độ tạo dead tuple, khiến tình trạng cơ sở dữ liệu xấu đi nhanh chóng
- Nếu kiểm tra tiến trình đang hoạt động trong
pg_stat_activity và thấy truy vấn autovacuum chạy khoảng hơn 1 giờ, hãy cân nhắc đổi cấu hình
- Nếu mọi transaction ID bị dùng hết trước khi autovacuum kịp thu hồi, transaction ID wraparound sẽ xảy ra và dẫn tới downtime nghiêm trọng
Phình bảng và index
- Postgres lưu hàng trong các trang 8KB trên đĩa, và nếu không thể đặt hàng mới vào trang hiện có thì sẽ tạo trang mới
- Khi dead tuple đã được thu hồi nhưng trang vẫn còn trống một phần, hiện tượng phình bảng (table bloat) có thể xảy ra và làm tăng mạnh dung lượng đĩa sử dụng
- Cách phòng tránh tốt nhất là điều chỉnh autovacuum trước khi phình xảy ra
- Với bảng đã bị phình, có thể dùng extension như
pg_repack
VACUUM FULL tích hợp sẵn hầu như không phải lựa chọn tốt
- Postgres 19 dự kiến sẽ thêm
REPACK...CONCURRENTLY để repack bảng đồng thời, nhưng Hatchet vẫn chưa thử
- Phình index cũng là một dạng đặc biệt của phình bảng, và có thể giảm nhờ cấu hình autovacuum phù hợp
- Với index đã bị phình, có thể dùng lệnh tích hợp
REINDEX INDEX CONCURRENTLY
Xử lý đồng thời với FOR UPDATE SKIP LOCKED
FOR UPDATE SKIP LOCKED cho phép giữ chỗ các hàng đã chọn cho transaction hiện tại mà không cản trở các truy vấn khác
- Hatchet dùng nó cho hàng đợi công việc, nơi một truy vấn có thể khóa các công việc đang chờ rồi đổi trạng thái sang
RUNNING
WITH eligible_tasks AS (
SELECT *
FROM tasks
WHERE status = 'QUEUED'
ORDER BY id ASC
FOR UPDATE SKIP LOCKED
LIMIT 100
)
UPDATE tasks
SET status = 'RUNNING'
FROM eligible_tasks
WHERE tasks.id = eligible_tasks.id
RETURNING tasks.*;
- Cũng hữu ích khi cập nhật đồng thời các hàng độc lập với nhau hoặc khi nhiều instance ứng dụng quản lý lease của một đối tượng
- Hatchet dùng nó để phân phối tenant lease giữa nhiều engine
Partitioning
- Partitioning tích hợp sẵn của Postgres chia bảng theo giá trị hàng như timestamp hoặc hash
- Với dữ liệu chuỗi thời gian và dữ liệu tác vụ lịch sử của Hatchet, nó mang lại các lợi ích sau
- Có thể chạy autovacuum độc lập cho từng partition để tăng quy mô xử lý autovacuum của bảng
- Có thể gần như xóa ngay dữ liệu cũ bằng cách drop bảng partition thay vì xóa từng hàng
- Nếu ở giai đoạn lập kế hoạch, Postgres không loại được các partition không cần thiết thì truy vấn đọc có thể bị thêm overhead
Di chuyển dữ liệu giữa các bảng lớn
- Migration bảng lớn ở đây không phải là thay đổi schema, mà là di chuyển lượng lớn dữ liệu từ bảng này sang bảng khác
- Nếu sao chép một bảng rất lớn trong một transaction duy nhất, quá trình đó có thể mất hàng giờ
- Transaction kéo dài sẽ cản trở autovacuum hoạt động bình thường và làm dead tuple phình lên
- Nếu bảng cũ vẫn tiếp tục nhận ghi, dữ liệu đó sẽ không được phản ánh sang bảng mới
- Hatchet chạy batch backfill lớn ngoài transaction, và các lần ghi mới sau khi migration bắt đầu sẽ được sao chép sang bảng mới bằng trigger của Postgres
- Họ dùng unique constraint của khóa chính để ngăn ghi trùng lặp
1 bình luận
Ý kiến trên Hacker News
Nếu là cơ sở dữ liệu vận hành thì chẳng phải việc đầu tiên nên làm là lập kế hoạch sao lưu và khôi phục sao. Tính sẵn sàng cao có thể là tùy chọn ở giai đoạn đầu, nhưng một hướng dẫn sinh tồn mà thiếu sao lưu và khôi phục thì khá khó hiểu
Không biết dạo này người ta còn dùng nhiều Barman(https://pgbarman.org/) cho sao lưu PostgreSQL không
pg_dump_alltừ cron, nén bằngzstd, rồi chép sang S3 hay FTP là đủ. Khi dữ liệu lớn lên thì thời gian và chi phí sao lưu toàn bộ sẽ thành gánh nặng, nhưng cách đơn giản này vẫn có thể trụ được khá lâuTôi từng sao lưu MongoDB cỡ vài TB trên AWS bằng snapshot EBS để triển khai sao lưu gia tăng và khôi phục nhanh. Dù không hỗ trợ khôi phục theo thời điểm, nó có thể chụp thường xuyên theo giờ, nên phù hợp làm chiến lược bổ trợ đi cùng các công cụ chuyên dụng cho PostgreSQL
Có vài điểm cần bổ sung. Nên dùng UUIDv7 thay vì UUIDv4 thông thường, và để tránh deadlock thì không chỉ số lượng hàng bị khóa mà cả thứ tự khóa trong mọi truy vấn cũng phải được cố định một cách xác định, chẳng hạn
id ASCDùng
EXPLAIN (GENERIC_PLAN)thì có thể sao chép truy vấn mà vẫn giữ nguyên placeholder tham số, đồng thời xem được kế hoạch tối ưu hóa của PostgreSQL khi nó không biết giá trị thực tế. Với bảng rỗng hoặc nhỏ, có thể dùngSET enable_seqscan = offđể kiểm tra khả năng sử dụng chỉ mụcChỉ mục B-tree mà ai cũng dùng mặc định khá nặng và dễ phình to, nên nếu chỉ cần tra cứu đơn giản mà không có sắp xếp hay tìm kiếm theo khoảng thì chỉ mục hash cũng đáng cân nhắc. Không thể tạo chỉ mục hash duy nhất, nhưng có thể đạt hiệu quả tương tự bằng ràng buộc loại trừ hash, và nó không hỗ trợ chỉ mục duy nhất nhiều cột
Cũng nên làm quen với chỉ mục GIN·GiST. Với người dùng MySQL thì điều này có thể khá bất ngờ, nhưng ngay cả khi không chuyển sang full-text search, vẫn có thể tăng tốc truy vấn
LIKE '%foo%'bình thườngORDER BYnhất quán, mà còn khi thứ tự khóa bảng khác nhau. Nếu một transaction khóa theo thứ tựtable_a,table_bcòn transaction khác làm ngược lại, thì dù có dùngORDER BYvàFOR UPDATEtrong từng bảng cũng vẫn deadlockVề lý thuyết thì điều này khá rõ, nhưng trong thực tế lại khó debug hơn nhiều vì phải nắm được toàn cục các bảng mà mọi thao tác ghi chạm tới; tôi từng dính vụ này trong một tính năng mở rộng cụ thể. Tôi đang thử GIN cho tra cứu key-value trên JSONB và mức cải thiện hiệu năng là rất lớn, chênh lệch hiệu năng giữa
ANDvàORcũng đáng kểLời khuyên này cũng hay, nhưng các startup tôi từng làm cùng thường vấp phải vấn đề tổ chức còn ở mức thấp hơn cả khả năng mở rộng. Nên tránh dùng ORM, dùng khóa chính tăng tuần tự thay vì các trường có ý nghĩa, và chỉ dùng JSONB một cách hạn chế khi thật sự cần
Dữ liệu gốc nên để ở dạng chỉ thêm mới, chỉ append, không sửa hay xóa. Các bảng phụ phi chuẩn hóa để phục vụ hiệu năng và tiện lợi thì có thể thay đổi, nhưng không nên coi đó là nguồn chân lý
Nên dùng connection pool nhưng chú ý số lượng kết nối; nếu chưa có vấn đề thì có thể chưa cần đến PgBouncer. Nếu không có lý do rõ ràng thì nên tránh transaction tường minh, không nên mở transaction rồi thực hiện tác vụ dài như RPC, và cũng hầu như không nên dùng
SERIALIZABLENếu cần khóa tường minh như
SELECT FOR UPDATEthì có thể thiết kế đang có vấn đề. Đừng tái phát minh hệ thống kiểu bằng cách để các hàng trong một bảng mang nhiều ý nghĩa theo giá trịtype int, cũng đừng bắt chước cơ sở dữ liệu đồ thị bằng các bảngnode·edgetự tham chiếu. Phần lớn trường hợp đều có thể giải quyết bằng các bảng chuẩn hóa thông thườngỞ giai đoạn đầu dự án, tôi thà dành thời gian cho phát triển sản phẩm hơn là suy nghĩ quá mức về schema cơ sở dữ liệu và tối ưu hóa quá sớm
SELECT FOR UPDATEkhá hữu ích ở nhiều nơi, nên muốn biết vấn đề nằm ở đâu. Tôi cũng muốn biết liệu dùng nguồn chân lý chỉ append có làm những kiểu khóa này trở nên không cần thiết hay khôngNgược lại, tôi muốn biết cách giữ các bảng quan hệ truyền thống có thể thay đổi làm nguồn chân lý và ghi log thay đổi bằng trigger thì sao
Tôi không thích cascade delete. Phần lớn lập trình viên sống ở tầng ứng dụng như Python, Node, Go hơn là ở cơ sở dữ liệu, nên việc xóa một hàng ở bảng A mà dữ liệu ở bảng B cũng biến mất khiến cascade delete dễ trông như phép thuật. Nếu cấu hình sai thì còn nguy hiểm hơn, nên về bảo trì lâu dài, các câu lệnh xóa tường minh sẽ tốt hơn; chỉ cần dùng đúng foreign key cũng đủ giữ tính nhất quán
Những cạm bẫy và cách lách khi migrate bảng lớn là đúng, nhưng đã có các công cụ như pg-osc. Nó nên đơn giản đến mức chỉ cần chạy một lệnh rồi căng thẳng quan sát trong 24 giờ khi dữ liệu được sao chép
Việc deploy ứng dụng và cơ sở dữ liệu nên được tách ra từ sớm. Vì không thể deploy thay đổi schema và thay đổi ứng dụng hoàn toàn đồng thời trong một transaction, nên khi vào production cần hình thành thói quen chỉ thực hiện thay đổi schema tương thích ngược, như tạo cột mới ở dạng nullable hoặc có giá trị mặc định, và không đổi tên bảng hay cột
Cũng nên quyết định sớm chiến lược quản lý schema. Cần tránh quy trình deploy kiểu senior developer chạy DDL thủ công lên DB production từ máy cá nhân; có thể dùng các công cụ quen thuộc như Liquibase hoặc Flyway
Query planner tối ưu cho trường hợp trung bình, nhưng với ứng dụng thì đôi khi tối ưu cho trường hợp xấu nhất lại hữu ích hơn. Người dùng trung bình có ít hàng nên với một index nhất định kết quả ra trong 10ms, nhưng với người dùng dùng nhiều thì cùng truy vấn đó có thể mất hơn 1 giây tùy tham số
Tôi đã buộc một đường đi index khác bằng truy vấn phức tạp hơn; hiệu năng trung bình chậm đi một chút nhưng ngay cả trường hợp xấu nhất cũng giảm xuống dưới 100ms. Với công ty, tránh timeout quan trọng hơn nhiều so với tiết kiệm trung bình 10ms
SKIP LOCKEDhữu ích cho hàng đợi công việc dựa trên transaction tương tác, nơi ứng dụng mở transaction và khóa hàng trong lúc xử lý công việc. Trong ứng dụng hiệu năng cao, nên tránh chính các transaction kiểu này và chỉ cần cập nhật ngay hàng sangpending, nên không cầnSKIP LOCKEDCàng mở rộng quy mô, càng phải giảm lượng trạng thái được giữ trong bộ nhớ của cơ sở dữ liệu, và transaction tương tác cũng là một phần trạng thái như vậy. Trong môi trường mở rộng, tính idempotent có lợi hơn tính atomic
Transaction kéo dài có thể làm hại trạng thái cơ sở dữ liệu, nên chỉ dùng khi có lý do rất mạnh. Hãy dùng
idle_in_transaction_session_timeoutđể transaction nhàn rỗi không giữ lock hay tuple quá lâu, và đặtlock_timeoutcho migration để một câu DDL không làm cả hệ thống đứng lạiCũng nên đặt
statement_timeoutđể một truy vấn đắt đỏ không làm tê liệt cả hệ thốngSau khi vận hành PostgreSQL ở giai đoạn đầu của startup, tôi thấy bài này không nhấn mạnh đủ vào giám sát và cảnh báo. PostgreSQL có một vài dạng sự cố cốt lõi nhất định phải tránh, và cảnh báo có thể giúp phát hiện rủi ro sớm
Dù AWS có gửi email nói rằng đang tiến gần đến vòng quay transaction ID, ở startup vẫn rất dễ bỏ lỡ, đặc biệt vào những ngày như Boxing Day. Những tín hiệu mà AWS theo dõi cần được nối với pager chứ không phải email
Có những khác biệt lớn nhưng ít người biết trong cách triển khai connection pool. Phần lớn connection pool của ứng dụng tối ưu độ trễ thấp và khả năng sẵn sàng của kết nối bằng FIFO (vào trước ra trước), nhưng vì luôn giữ kết nối ở trạng thái nóng nên khó giảm các kết nối không cần thiết
PgBouncer và một số pooler bên ngoài dùng LIFO (vào sau ra trước) để tối ưu số kết nối chạm tới PostgreSQL và throughput. Khi tái sử dụng kết nối mới nhất trước, các kết nối dư thừa sẽ tự nhiên nguội đi và bị đóng
Với ứng dụng mới, FIFO là đủ, nhưng khi quy mô tăng lên thì nên dùng công cụ như PgBouncer để cắt giảm hàng trăm kết nối, có thể tới khoảng 90%. Cấu trúc PostgreSQL tạo process cho mỗi kết nối sẽ hoạt động tốt hơn khi số kết nối càng ít
Trong những tình huống rất đặc thù, việc join trong bộ nhớ ứng dụng đã cho kết quả tốt. Khi cố giảm số lần qua lại với cơ sở dữ liệu, đôi khi người ta tạo ra một truy vấn đơn lẻ với
JOIN,UNION,CASEphức tạp chồng chéo lên nhauThay vào đó, nếu chạy độc lập nhiều truy vấn đơn giản rồi duyệt kết quả để liên kết các hàng liên quan bằng map, thì dù có thêm chi phí qua lại và lặp xử lý, kế hoạch truy vấn có thể trở nên dễ dự đoán hơn và ngược lại còn có lợi hơn. Chỉ nên dùng một cách hạn chế, và cũng không phải là khuyến nghị vô điều kiện chỉ vì một số ORM hoạt động nội bộ theo cách này
Nhưng inner join có tính chọn lọc lại tạo ra kết quả nhỏ hơn nhiều so với dữ liệu gốc, nên việc lấy mọi bản ghi về rồi thực hiện giao và lọc cục bộ sẽ tốn kém hơn rất nhiều. Với index join, bộ lập kế hoạch truy vấn có thể tận dụng chỉ mục để tránh quét toàn bộ bảng, sắp xếp và lọc một cách brute-force