Cơ sở dữ liệuPostgreSQL, phần 3/5
MVCC, HOT update và VACUUM
UPDATE trong PostgreSQL ghi phiên bản dòng mới ngay trong heap; HOT giữ chỉ mục khỏi bị chạm, VACUUM dọn phiên bản cũ; code .NET dùng xmin làm concurrency token và không để giao dịch mở lâu.
Ở BanHang, mỗi lần đơn đổi trạng thái là một lần UPDATE, và PostgreSQL không sửa dòng tại chỗ mà ghi thêm một phiên bản mới ngay trong bảng. Nếu VACUUM không theo kịp, hoặc một giao dịch bỏ quên giữ phiên bản cũ lại, bảng phình và truy vấn đọc nhiều page hơn mỗi ngày. Đọc xong, bạn biết khi nào UPDATE không chạm chỉ mục, chỉnh autovacuum cho partition nóng, và dùng chính xmin làm concurrency token trong EF Core.
Đọc nhanh
UPDATEghi tuple mới và đặtxmaxcho tuple cũ; tuple cũ thành dead tuple cho tới khi pruning hoặc VACUUM dọn.- Không có clustered index: mọi chỉ mục trỏ tới
ctid, nên lần sửa không phải HOT thêm entry vào mọi chỉ mục. - Với ngưỡng mặc định, autovacuum chỉ dọn
don_hang_2026sau khoảng 18 ngày; hạ scale factor riêng cho partition nóng. - Giao dịch mở lâu từ code .NET giữ dead tuple lại;
xminđổi mỗi lần sửa nên dùng được làm concurrency token.
Bài dùng bảng don_hang chia partition theo năm ở bài cluster, tablespace và partition và cấu trúc tuple ở bài page, tuple và TOAST, mốc PostgreSQL 17.
1. Không có clustered index
SQL Server lưu bảng có clustered index ngay trong cây B-tree: page lá của chỉ mục chính là page dữ liệu. PostgreSQL không có cấu trúc này. Mỗi bảng là một heap, dòng nằm ở page nào còn chỗ. Mọi chỉ mục, kể cả khóa chính, là cấu trúc riêng mà mỗi entry trỏ tới một ctid.
| Việc | SQL Server với PK_DonHang clustered |
PostgreSQL |
|---|---|---|
| Tìm đơn 10042 theo khóa chính | Seek B-tree, page lá chứa dòng | Seek chỉ mục khóa chính, lấy ctid, đọc page heap |
Tìm theo khach_hang_id |
Seek chỉ mục phụ, lấy khóa clustered, key lookup | Seek chỉ mục phụ, lấy ctid, đọc page heap. Cùng chi phí như khóa chính |
| Dòng có phiên bản mới ở chỗ khác | Chỉ mục phụ không đổi vì nó lưu khóa clustered | Mọi chỉ mục cần entry mới, trừ khi UPDATE là HOT |
| Kiểm tra dòng có thấy được không | Khóa hoặc version store | Đọc xmin và xmax trên tuple ở heap |
Chỉ mục PostgreSQL không chứa thông tin hiển thị. Tìm qua chỉ mục vẫn phải đọc heap để xem tuple có thấy được không, trừ khi visibility map cho phép index-only scan (mục 2).
CLUSTER sắp lại heap theo một chỉ mục, nhưng chỉ một lần: tài liệu ghi rõ dòng thêm hoặc sửa sau đó không được đặt theo thứ tự chỉ mục. Lệnh giữ khóa ACCESS EXCLUSIVE suốt thời gian chạy, chặn cả đọc lẫn ghi, và cần chỗ trống ít nhất bằng bảng cộng các chỉ mục. Trên PostgreSQL 17, CLUSTER một bảng partition sắp từng partition và không chạy được trong transaction block.
Chỗ dùng hợp lý là partition không còn đổi: sắp don_hang_2025 theo chỉ mục (khach_hang_id, ngay_tao) của nó một lần, báo cáo theo khách năm 2025 đọc ít page hơn mãi về sau. Đơn mới của don_hang_2026 thường ghi vào cuối heap, nên thứ tự vật lý gần đúng theo ngay_tao mà không cần CLUSTER, cho tới khi VACUUM mở lại chỗ trống ở page cũ.
2. MVCC trên đĩa
UPDATE ghi phiên bản mới
UPDATE trong PostgreSQL ghi một tuple mới chứa toàn bộ dòng sau khi sửa, rồi đặt xmax của tuple cũ bằng ID giao dịch đang sửa. Tuple cũ vẫn nằm trên page. Giao dịch nào thấy phiên bản nào được quyết định bằng xmin, xmax, snapshot của giao dịch, và trạng thái commit trong pg_xact.
Đơn 10042 tạo lúc 11:58, tong_tien 1.500.000, nằm ở page 26470 của don_hang_2026. Lúc 12:04 ứng dụng sửa thành 1.750.000. ID giao dịch và số line pointer là minh họa.
| Thời điểm | lp | t_xmin |
t_xmax |
t_ctid |
tong_tien |
Ai thấy |
|---|---|---|---|---|---|---|
| 11:58, insert đã commit | 12 | 7340112 | 0 | (26470,12) | 1.500.000 | Mọi phiên |
12:04, sau UPDATE, chưa commit |
12 | 7340112 | 7341905 | (26470,48) | 1.500.000 | Các phiên khác |
| 48 | 7341905 | 0 | (26470,48) | 1.750.000 | Chỉ phiên đang sửa | |
Sau COMMIT |
12 | 7340112 | 7341905 | (26470,48) | 1.500.000 | Snapshot lấy trước lúc commit |
| 48 | 7341905 | 0 | (26470,48) | 1.750.000 | Snapshot lấy sau lúc commit | |
| Sau pruning hoặc VACUUM | 12 | Line pointer thành redirect sang 48 | ||||
| 48 | 7341905 | 0 | (26470,48) | 1.750.000 | Mọi phiên |
t_ctid của tuple cũ trỏ sang tuple mới, tạo thành chuỗi phiên bản. Phiên khác đọc lúc 12:04 thấy xmax = 7341905 trên tuple cũ, nhưng giao dịch đó chưa commit nên vẫn trả về 1.500.000. Không ai bị chặn: đọc không chờ ghi, ghi không chờ đọc. Xem qua bảng cha:
SELECT tableoid::regclass AS partition, ctid, xmin, xmax, tong_tien
FROM don_hang
WHERE don_hang_id = 10042
AND ngay_tao = '2026-10-02 11:58:00+07';
Truy vấn thường chỉ trả phiên bản thấy được, nên luôn ra một dòng. heap_page_items của pageinspect trả mọi tuple trên page bất kể hiển thị, nên thấy cả lp 12 lẫn lp 48.
HOT update và fillfactor
Lần sửa lúc 12:04 không chạm chỉ mục, nhờ heap-only tuple (HOT). Theo tài liệu, HOT xảy ra khi UPDATE không đổi cột nào mà chỉ mục của bảng tham chiếu (chỉ mục BRIN không tính), và page chứa tuple cũ còn đủ chỗ cho tuple mới. tong_tien không nằm trong khóa chính hay ix_don_hang_khach_hang. Page 26470 là page cuối của partition, còn nhiều chỗ.
Hai chỉ mục vẫn trỏ tới (26470,12). Engine tới lp 12 rồi đi theo t_ctid sang lp 48. Khi không snapshot nào còn cần tuple cũ, pruning xóa dữ liệu của nó và biến lp 12 thành redirect. Pruning chạy ngay trong lúc đọc page, kể cả bởi SELECT, không chờ VACUUM.
Lần sửa không phải HOT thì mọi chỉ mục đều nhận entry mới cho tuple mới, kể cả chỉ mục mà cột của nó không đổi. Tuple mới thường sang page khác vì page cũ hết chỗ. Chỉ mục một phần kiểu WHERE trang_thai = 1, tương ứng IX_DonHang_DangMo của chương kỹ thuật SQL Server, tham chiếu trang_thai trong điều kiện lọc. Có chỉ mục đó thì mọi lần đổi trạng thái đơn mất HOT.
Đơn năm 2026 đi qua các trạng thái 1, 2, 3, 4 trong vài ngày. Khoảng 13.000 đơn mỗi ngày lấp một page 136 dòng trong khoảng 15 phút, nên lần đổi trạng thái sau đó chỉ còn HOT nếu pruning đã mở chỗ. fillfactor 90 chừa 10% page cho phiên bản mới. Đặt cho partition 2027 ngay từ đầu, vì tham số chỉ áp cho lần insert sau, không viết lại page đã có:
ALTER TABLE don_hang_tu_2027 SET (fillfactor = 90);
Cái giá là thêm khoảng 11% page:
Dòng mới mỗi page = (8.192 − 819 − 24) / 60 ≈ 122, chừa 848 byte, đủ cho 14 phiên bản
3,6 triệu đơn ≈ 29.509 page thay vì 26.471
Đo trước và sau bằng tỷ lệ n_tup_hot_upd / n_tup_upd ở cuối mục này.
DELETE lúc 12:07 trên page
Lệnh xóa nhầm ở bài sao lưu và khôi phục xóa 6.400.000 đơn trước năm 2026. Nó không giải phóng page nào. Nó đặt xmax trên 6,4 triệu tuple trong 47.059 page của hai partition lịch sử, và ghi WAL cho từng thay đổi. Sau khi commit, các tuple đó thành dead tuple: vẫn nằm trên đĩa, nhưng không lệnh SQL nào đọc lại được. Đường lấy lại dữ liệu vẫn là khôi phục theo thời điểm.
Không có undo cũng có nghĩa ROLLBACK rẻ: giao dịch hủy chỉ được đánh dấu aborted trong pg_xact, tuple nó đã ghi thành dead tuple cho VACUUM dọn. SQL Server, khi chưa bật ADR, phải hoàn tác từng thay đổi lúc rollback.
VACUUM và VACUUM FULL
VACUUM thường xóa dead tuple khỏi bảng và chỉ mục khi không giao dịch nào còn cần chúng, rồi ghi chỗ trống vào free space map để insert và update sau dùng lại. Nó chạy song song với SELECT, INSERT, UPDATE, DELETE. Nó không trả chỗ cho hệ điều hành, trừ khi các page cuối bảng trống hẳn và lấy được khóa độc quyền ngắn để cắt tệp. VACUUM FULL viết lại toàn bộ bảng sang tệp mới không còn chỗ trống, nên bảng nhỏ lại thật.
VACUUM FULL khóa cả bảng
VACUUM FULL giữ khóa ACCESS EXCLUSIVE đến khi xong, chặn cả đọc, và cần thêm chỗ đĩa bằng bản sao mới của bảng. Đừng xếp nó vào lịch bảo trì đêm như rebuild index. Tài liệu khuyên dùng VACUUM thường và để autovacuum chạy đủ nhịp.
Autovacuum với partition 2026
Autovacuum chạy VACUUM trên một bảng khi số dead tuple vượt:
ngưỡng vacuum = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples
Mặc định 50 và 0,2. Với don_hang_2026, reltuples = 3.600.000. Ước lượng thô, chưa trừ phần pruning đã dọn:
Ngưỡng = 50 + 0,2 × 3.600.000 = 720.050 dead tuple
Dead tuple/ngày ≈ 13.000 đơn × 3 lần đổi trạng thái = 39.000
Chạm ngưỡng sau ≈ 720.050 / 39.000 ≈ 18 ngày
Trong 18 ngày đó heap phình và visibility map ít page được đánh dấu. Hạ hệ số cho riêng partition đang nóng:
ALTER TABLE don_hang_2026 SET (autovacuum_vacuum_scale_factor = 0.02);
Ngưỡng mới = 50 + 0,02 × 3.600.000 = 72.050, khoảng 2 ngày.
Đặt tham số trên partition. Bảng partition không chứa tuple nên autovacuum không xử lý bảng cha, kể cả ANALYZE. Chạy ANALYZE don_hang theo lịch nếu truy vấn cần thống kê cấp bảng cha.
Bảng chỉ nhận insert cũng được vacuum theo ngưỡng riêng: autovacuum_vacuum_insert_threshold (1.000) + autovacuum_vacuum_insert_scale_factor (0,2) × reltuples, tức 721.000 dòng mới với partition 2026. Từ PostgreSQL 18, ngưỡng insert nhân thêm tỷ lệ phần bảng chưa freeze, và autovacuum_vacuum_max_threshold (mặc định 100.000.000) làm trần cho ngưỡng vacuum của bảng rất lớn.
Sau 12:07, don_hang_truoc_2025 có 3.000.000 dead tuple, vượt xa ngưỡng 600.050 của nó, nên autovacuum sẽ dọn trong vài phút. Việc đó không cản khôi phục theo thời điểm, vì khôi phục dựng lại từ base backup và WAL, không đọc page hiện tại.
Visibility map, free space map, index-only scan
Visibility map giữ hai bit cho mỗi page heap. Bit all-visible nghĩa là mọi tuple trên page thấy được với mọi giao dịch. Bit all-frozen nghĩa là mọi tuple đã freeze. Chỉ VACUUM bật các bit này. Mọi thao tác sửa page đều tắt chúng.
Index-only scan dùng bit all-visible: page đã all-visible thì không cần đọc heap để kiểm tra hiển thị. Partition lịch sử sau VACUUM gần như toàn page all-visible. Báo cáo các ngày đặt hàng của khách 42 trong năm 2025 chỉ cần ngay_tao, nên đọc thẳng từ chỉ mục (khach_hang_id, ngay_tao). Trong EXPLAIN (ANALYZE), nút Index Only Scan với Heap Fetches gần 0 nghĩa là visibility map đang làm việc. Số lớn nghĩa là partition cần VACUUM.
Free space map ghi mức chỗ trống của từng page để insert tìm page còn chỗ mà không quét bảng, phần việc PFS làm cho heap ở SQL Server. VACUUM cập nhật map này.
XID wraparound và freeze
ID giao dịch dài 32 bit và được so sánh theo vòng tròn: với mỗi XID, khoảng hai tỷ giá trị là quá khứ và hai tỷ là tương lai. Tuple có xmin cũ quá khoảng hai tỷ giao dịch sẽ bỗng trông như ở tương lai và biến mất. VACUUM ngăn việc đó bằng cách freeze: đánh dấu tuple đủ cũ là thấy được với mọi giao dịch, không cần so xmin nữa.
Khi age(relfrozenxid) của một bảng vượt autovacuum_freeze_max_age (mặc định 200 triệu), autovacuum chạy VACUUM chống wraparound trên bảng đó, kể cả khi autovacuum bị tắt. Ở mức 2.000 giao dịch mỗi giây, 200 triệu XID đến sau 100.000 giây, khoảng 28 giờ. Với banhang vài chục nghìn giao dịch mỗi ngày, mốc đó cách nhiều năm. Khi số XID còn lại quá ít, PostgreSQL cảnh báo, rồi từ chối cấp XID mới để bảo vệ dữ liệu.
Partition lịch sử nên freeze một lần sau khi nạp. Page all-frozen được các lần VACUUM chống wraparound sau bỏ qua. Lần freeze đầu ghi lại mọi page của hai partition, chạy ngoài giờ cao điểm.
VACUUM (FREEZE, ANALYZE) don_hang_truoc_2025, don_hang_2025;
SELECT c.relname, age(c.relfrozenxid) AS tuoi_xid
FROM pg_class AS c
WHERE c.relkind = 'r'
ORDER BY age(c.relfrozenxid) DESC
LIMIT 5;
Đo dead tuple và HOT
Truy vấn dưới lấy số dead tuple, tỷ lệ dead và số lần update HOT của từng partition đơn hàng từ pg_stat_user_tables.
Dead tuple, HOT update và lần autovacuum gần nhất của từng partition
SELECT relname, n_live_tup, n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS pct_dead,
n_tup_upd, n_tup_hot_upd, last_autovacuum, autovacuum_count
FROM pg_stat_user_tables
WHERE relname LIKE 'don\_hang%'
ORDER BY n_dead_tup DESC;
n_dead_tup là số ước lượng. last_autovacuum trống hoặc rất cũ trên partition có n_dead_tup cao nghĩa là autovacuum không theo kịp, hoặc một giao dịch mở lâu đang giữ dead tuple lại. Cần số chính xác thì dùng extension pgstattuple, chấp nhận nó đọc cả bảng.
3. Áp dụng trong .NET
xmin làm concurrency token. Mỗi UPDATE ghi tuple mới với xmin là ID giao dịch đang sửa, kể cả khi là HOT, nên xmin đổi mỗi lần dòng đổi. Từ Npgsql.EntityFrameworkCore.PostgreSQL 7.0, property uint gắn [Timestamp] được ánh xạ vào cột hệ thống xmin. EF Core thêm điều kiện trên token đã đọc vào UPDATE; không dòng nào khớp thì SaveChangesAsync ném DbUpdateConcurrencyException. Hai nhân viên cùng đổi trạng thái đơn 10042 thì người lưu sau nhận lỗi thay vì ghi đè.
[Table("don_hang"), PrimaryKey(nameof(DonHangId), nameof(NgayTao))]
class DonHang
{
[Column("don_hang_id")] public long DonHangId { get; set; }
[Column("ngay_tao"), Precision(0)] public DateTime NgayTao { get; set; }
[Column("trang_thai")] public short TrangThai { get; set; }
[Timestamp] public uint Version { get; set; } // ánh xạ vào cột hệ thống xmin
}
don.TrangThai = 2;
try { await db.SaveChangesAsync(); }
catch (DbUpdateConcurrencyException) { /* đơn vừa bị sửa ở nơi khác: đọc lại rồi quyết định */ }
Giao dịch mở lâu giữ dead tuple. Tài liệu ghi: giao dịch đang mở ngăn VACUUM dọn những dead tuple mà có thể chỉ nó còn thấy. Lỗi hay gặp trong code .NET là gọi BeginTransactionAsync, rồi gọi cổng thanh toán đối tác hoặc chờ người dùng khi giao dịch còn mở. Giữ giao dịch quanh đúng các lệnh ghi.
Thêm lưới an toàn: tham số Options của Npgsql gửi tham số runtime lúc mở kết nối; với idle_in_transaction_session_timeout=30s, phiên ngồi không trong giao dịch quá 30 giây bị server cắt. PostgreSQL 17 có thêm transaction_timeout giới hạn cả thời gian giao dịch. Application Name cho biết service nào đang giữ giao dịch trong pg_stat_activity:
const string cs = "Host=db01;Database=banhang;Username=api;Application Name=banhang-api;"
+ "Options=-c idle_in_transaction_session_timeout=30s";
SELECT pid, application_name, state, now() - xact_start AS da_mo, age(backend_xmin) AS tuoi_xmin
FROM pg_stat_activity
WHERE xact_start < now() - interval '5 minutes'
ORDER BY xact_start;
Phần in mô hình của chương trình dưới chạy được không cần server. Trên .NET 10.0.12 với Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3, nó in đúng ba dòng:
.NET 10.0.12
Version -> cột xmin, kiểu xid, concurrency token: True, sinh lúc: OnAddOrUpdate
Options gửi lúc mở kết nối: -c idle_in_transaction_session_timeout=30s
Nhánh --sua cần server nên mới chỉ được biên dịch bằng dotnet build. File có #:property PublishAot=false vì file-based app bật AOT mặc định, và khi đó EF Core không dựng mô hình lúc chạy.
XminToken.cs: entity don_hang với xmin, chuỗi kết nối có Options, nhánh đổi trạng thái
#:package Npgsql.EntityFrameworkCore.PostgreSQL@10.0.3
#:property PublishAot=false
// xmin làm concurrency token cho don_hang, và chuỗi kết nối tự cắt giao dịch bỏ quên.
// Không có tham số: in cách EF Core ánh xạ Version, không cần server.
// "--sua": đổi trạng thái đơn 10042, cần PostgreSQL 17 với database banhang.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Npgsql;
const string cs = "Host=db01;Database=banhang;Username=api;Application Name=banhang-api;"
+ "Options=-c idle_in_transaction_session_timeout=30s";
await using var db = new BanHangDb(cs);
var version = db.Model.FindEntityType(typeof(DonHang))!.FindProperty(nameof(DonHang.Version))!;
Console.WriteLine($".NET {Environment.Version}");
Console.WriteLine($"Version -> cột {version.GetColumnName()}, kiểu {version.GetColumnType()}, " +
$"concurrency token: {version.IsConcurrencyToken}, sinh lúc: {version.ValueGenerated}");
Console.WriteLine($"Options gửi lúc mở kết nối: {new NpgsqlConnectionStringBuilder(cs).Options}");
if (args is ["--sua"])
Console.WriteLine(await DoiTrangThaiAsync(db, 10042,
new DateTimeOffset(2026, 10, 2, 11, 58, 0, TimeSpan.FromHours(7)).UtcDateTime, trangThaiMoi: 2));
static async Task<string> DoiTrangThaiAsync(BanHangDb db, long id, DateTime ngayTao, short trangThaiMoi)
{
var don = await db.DonHang.SingleAsync(d => d.DonHangId == id && d.NgayTao == ngayTao);
don.TrangThai = trangThaiMoi;
try
{
// UPDATE ... WHERE don_hang_id = @id AND ngay_tao = @ngay AND xmin = @version_da_doc
await db.SaveChangesAsync();
return $"Đơn {id}: trạng thái {trangThaiMoi}, xmin mới {don.Version}";
}
catch (DbUpdateConcurrencyException)
{
// Phiên khác đã ghi phiên bản mới của dòng sau lúc ta đọc: xmin đã khác.
return $"Đơn {id} vừa bị sửa ở nơi khác, đọc lại rồi quyết định";
}
}
class BanHangDb(string cs) : DbContext
{
public DbSet<DonHang> DonHang => Set<DonHang>();
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseNpgsql(cs);
}
[Table("don_hang"), PrimaryKey(nameof(DonHangId), nameof(NgayTao))]
class DonHang
{
[Column("don_hang_id")] public long DonHangId { get; set; }
[Column("ngay_tao"), Precision(0)] public DateTime NgayTao { get; set; }
[Column("khach_hang_id")] public int KhachHangId { get; set; }
[Column("trang_thai")] public short TrangThai { get; set; }
[Column("tong_tien"), Precision(18, 2)] public decimal TongTien { get; set; }
[Timestamp] public uint Version { get; set; } // ánh xạ vào cột hệ thống xmin
}
Những chỗ hay hiểu sai
- "
UPDATEsửa dòng tại chỗ." Nó ghi phiên bản mới. Phiên bản cũ ở lại trên page cho tới pruning hoặc VACUUM. - "
DELETEtrả chỗ ngay." Nó chỉ đặtxmax. VACUUM đánh dấu chỗ dùng lại được, và thường không trả chỗ cho hệ điều hành. - "
CLUSTERgiữ bảng luôn sắp theo chỉ mục." Chỉ một lần. Dòng mới và dòng sửa không theo thứ tự đó. - "
SELECTkhông ghi gì." Đọc có thể đặt hint bit và chạy pruning, làm page thành dirty. Khi bật checksum, lần đặt hint bit đầu tiên sau checkpoint còn sinh ảnh page trong WAL.
Kết luận
Trong PostgreSQL, mỗi lần sửa là một phiên bản dòng mới nằm ngay trong heap. Chi phí của MVCC trả bằng dọn dẹp: HOT và pruning giữ nó rẻ, autovacuum phải được chỉnh theo nhịp sửa của từng partition, và không giao dịch nào được mở lâu hơn cần thiết.
Trong dự án .NET của bạn:
- Thay cột
RowVersiontự quản bằng[Timestamp] public uint Versionánh xạ vàoxmin, và bắtDbUpdateConcurrencyExceptionở chỗ đổi trạng thái đơn. - Không gọi
HttpClienthay chờ người dùng giữaBeginTransactionAsyncvàCommitAsync. - Đặt
Options=-c idle_in_transaction_session_timeout=30svàApplication Nametrong chuỗi kết nối của từng service. - Theo dõi
n_dead_tup,last_autovacuumvà tỷ lện_tup_hot_upd / n_tup_updcủa partition nóng; cân nhắcautovacuum_vacuum_scale_factor = 0.02vàfillfactor = 90cho partition năm mới.
Đọc tiếp
- Bài trước trong series: PostgreSQL — Page, tuple và TOAST.
- Bài sau trong series: PostgreSQL — WAL, checkpoint và recovery: lần
COMMITlúc 12:04 bền vững thế nào khi page chưa xuống đĩa. - SQL Server — Khóa và leo thang khóa: khóa phía SQL Server.
- SQL Server — Mức isolation và SNAPSHOT: versioning phía SQL Server, đối chiếu với
xminvàxmaxở đây.
Nguồn
- Introduction to MVCC, Heap-Only Tuples (HOT), Visibility Map
- Routine Vacuuming, Automatic Vacuuming (PostgreSQL 17), Vacuuming (PostgreSQL 18)
- CLUSTER, The Cumulative Statistics System, Client Connection Defaults
- Npgsql: Concurrency Tokens, Release notes 7.0, Connection String Parameters
- EF Core: Handling Concurrency Conflicts