Cơ sở dữ liệuPostgreSQL, phần 4/5

WAL, checkpoint và recovery

Commit trong PostgreSQL bền khi commit record của WAL đã xuống pg_wal; checkpoint ghi page sau; recovery chỉ có redo; .NET đổi độ bền lấy tốc độ cho nhật ký truy cập bằng synchronous_commit theo từng giao dịch.

Mục lục
  1. 1. Đường ghi dữ liệu
  2. 2. Mức phục hồi
  3. 3. Cách các tầng này ăn khớp
  4. 4. Áp dụng trong .NET
  5. Những chỗ hay hiểu sai
  6. Kết luận
  7. Đọc tiếp
  8. Nguồn

Khi API BanHang nhận kết quả COMMIT cho đơn 10042, page chứa đơn đó có thể chưa hề được ghi xuống tệp dữ liệu. PostgreSQL vẫn không mất đơn nhờ WAL, và phục hồi sau mất điện khác SQL Server ở một điểm: không có pha undo. Đọc xong, bạn biết commit bền lúc nào, chỉnh nhịp checkpoint theo số đo, và chọn đúng chỗ để đổi độ bền lấy tốc độ ghi từ .NET.

Đọc nhanh

  • Commit bền khi commit record của WAL đã flush xuống pg_wal; tệp dữ liệu được ghi sau, bởi checkpointer hoặc background writer.
  • Recovery sau sự cố chỉ có redo: giao dịch không có commit record tự vô hình nhờ pg_xact.
  • Lần sửa đầu của mỗi page sau checkpoint ghi cả ảnh page vào WAL, nên checkpoint dày làm WAL phình.
  • synchronous_commit = off chỉ có thể mất các giao dịch cuối, tối đa khoảng 600 ms, không làm hỏng database.

Bài dùng đơn 10042 và các ổ của db01 ở bài cluster, tablespace và partition, cùng chuỗi phiên bản dòng ở bài MVCC, mốc PostgreSQL 17.

1. Đường ghi dữ liệu

shared_buffers và page cache

Page được đọc vào shared_buffers, bộ đệm dùng chung của mọi tiến trình. Sửa dòng là sửa page trong bộ đệm, page thành dirty. Khác SQL Server, PostgreSQL đọc ghi tệp qua page cache của hệ điều hành, nên một page có thể nằm ở cả hai nơi.

shared_buffers mặc định thường là 128 MB. Tài liệu gợi ý điểm khởi đầu 25% RAM cho máy chỉ chạy database, và cho rằng trên 40% khó tốt hơn vì PostgreSQL vẫn dựa vào cache của hệ điều hành. Với 64 GB RAM, điểm khởi đầu là 16 GB. Phần còn lại hệ điều hành dùng làm cache tệp, khác với max server memory 56 GB của chương SQL Server.

WAL trước, page sau

Quy tắc giống SQL Server: thay đổi trên tệp dữ liệu chỉ được ghi sau khi bản ghi WAL mô tả nó đã nằm trên đĩa. Mỗi page mang pd_lsn. Trước khi ghi một dirty page, engine flush WAL tới ít nhất pd_lsn đó. Thứ tự của một UPDATE đã commit:

  1. Page nằm trong shared_buffers, hoặc được đọc lên.
  2. Tuple mới được ghi, xmax của tuple cũ được đặt, bản ghi WAL vào WAL buffers, page thành dirty và nhận pd_lsn mới.
  3. Lúc COMMIT, commit record vào WAL, và WAL được flush xuống pg_wal tới hết commit record.
  4. pg_xact đánh dấu giao dịch là committed.
  5. Client nhận kết quả thành công.
  6. Sau đó checkpointer, background writer hoặc chính backend mới ghi dirty page xuống tệp dữ liệu.
sequenceDiagram
  participant API as API .NET
  participant BE as Backend
  participant W as pg_wal
  participant CP as Checkpointer
  participant F as Tệp dữ liệu
  API->>BE: UPDATE đơn 10042
  Note over BE: sửa page trong shared_buffers
  API->>BE: COMMIT
  BE->>W: flush tới hết commit record
  Note over BE: pg_xact ghi committed
  BE-->>API: thành công
  CP->>F: về sau mới ghi dirty page

Với synchronous_commit = on, mặc định, bước 3 chờ flush cục bộ, và chờ thêm standby đồng bộ nếu đã cấu hình.

Sửa đơn 10042, theo từng thời điểm

BEGIN;

SELECT pg_current_xact_id();   -- minh họa: 7341905

UPDATE don_hang
SET tong_tien = 1750000.00
WHERE don_hang_id = 10042
  AND ngay_tao = '2026-10-02 11:58:00+07';

-- Chưa COMMIT. Page 26470 trong shared_buffers có hai tuple:
-- lp 12 (1.500.000, xmax = 7341905) và lp 48 (1.750.000, xmin = 7341905).
-- Bản ghi WAL đang ở WAL buffers. Tệp trên /srv/pg_ssd chưa có lp 48.
-- Phiên khác đọc thấy 1.500.000 vì 7341905 chưa commit.

COMMIT;

-- Commit record đã được flush xuống /srv/pg_wal. pg_xact ghi 7341905 là committed.
-- Phiên mới đọc thấy 1.750.000. Tệp dữ liệu có thể chưa có lp 48 cho tới checkpoint.

Ba cách mất điện ngay sau đó:

Thời điểm mất điện Trên tệp dữ liệu Trong pg_wal Sau khi PostgreSQL mở lại
Sau UPDATE, trước COMMIT, WAL chưa flush Chỉ có lp 12 Không có bản ghi của lần sửa 1.500.000. Giao dịch không để lại dấu vết
Sau UPDATE, trước COMMIT, WAL đã flush vì commit khác hoặc WAL writer Có thể đã có cả lp 48 nếu page được ghi Có bản ghi sửa, không có commit record Redo dựng lại cả hai tuple. 7341905 không commit nên bị coi là aborted, lp 48 vô hình. Vẫn đọc 1.500.000. Không chạy undo
Sau COMMIT, trước checkpoint Có thể chỉ có lp 12 Có bản ghi sửa và commit record Redo dựng lại lp 48 và trạng thái commit. Đọc 1.750.000

Hàng giữa là khác biệt chính với SQL Server, nơi recovery có pha undo đưa TongTien về 1.500.000. Ở đây không cần: tuple của giao dịch chưa commit vẫn nằm trên page nhưng không ai thấy, và VACUUM dọn sau như mọi dead tuple khác.

full_page_writes và WAL sau checkpoint

Đĩa ghi theo sector, thường 512 byte. Mất điện giữa lúc ghi một page 8 KB có thể để lại page nửa cũ nửa mới, và bản ghi WAL chỉ mô tả thay đổi thì không sửa được page rách. Vì vậy, với full_page_writes = on (mặc định), lần sửa đầu tiên của mỗi page sau một checkpoint ghi cả ảnh page vào WAL. Recovery dùng ảnh đó làm điểm xuất phát cho page.

Hệ quả: lượng WAL tăng vọt ngay sau mỗi checkpoint, rồi giảm khi các page nóng đã có ảnh trong chu kỳ. Ước lượng thô cho banhang, chỉ tính page heap:

  • Đơn của khoảng 3 ngày gần nhất nhận phần lớn lần đổi trạng thái: 39.000 đơn / 136 ≈ 290 page nóng.
  • checkpoint_timeout mặc định 5 phút: 288 checkpoint mỗi ngày × 290 ảnh page × 8 KB ≈ 652 MB WAL mỗi ngày chỉ cho ảnh page.
  • checkpoint_timeout = 15min: 96 × 290 × 8 KB ≈ 218 MB.
  • Lệnh DELETE nhầm lúc 12:07 chạm 47.059 page lần đầu trong chu kỳ: riêng ảnh page tới khoảng 47.059 × 8 KB ≈ 368 MB WAL, chưa kể bản ghi xóa cho từng dòng.

Giãn checkpoint từ 5 lên 15 phút cắt ảnh page trong WAL còn khoảng một phần ba

checkpoint_timeout = 5min (mặc định)652 MBcheckpoint_timeout = 15min218 MB
WAL mỗi ngày chỉ cho ảnh page của khoảng 290 page heap nóng. Ước lượng theo phép tính trên, không phải số đo.
Bảng số liệu
Giá trị
checkpoint_timeout = 5min (mặc định)652 MB
checkpoint_timeout = 15min218 MB

Checkpoint thưa hơn làm WAL nhỏ hơn, đổi lại recovery sau sự cố dài hơn vì phải redo nhiều WAL hơn. wal_compression nén ảnh page bằng pglz, hoặc lz4, zstd nếu bản build có hỗ trợ. Chỉ tắt full_page_writes khi hệ thống tệp bảo đảm không ghi rách page; tài liệu lấy ví dụ ZFS. Điểm khởi đầu cho banhang, không phải giá trị mặc định:

# /etc/postgresql/17/main/postgresql.conf
shared_buffers = 16GB          # 25% RAM, cần restart
checkpoint_timeout = 15min     # mặc định 5min
max_wal_size = 4GB             # mặc định 1GB

Đo lượng ảnh page và loại checkpoint. View pg_stat_checkpointer có từ PostgreSQL 17. Trên PostgreSQL 16, xem checkpoints_timed và checkpoints_req trong pg_stat_bgwriter.

SELECT wal_records, wal_fpi, pg_size_pretty(wal_bytes) AS wal FROM pg_stat_wal;

SELECT num_timed, num_requested, buffers_written FROM pg_stat_checkpointer;

num_requested tăng nhanh hơn num_timed nghĩa là WAL chạm max_wal_size trước khi hết checkpoint_timeout. Nâng max_wal_size.

Ai đẩy dữ liệu xuống đĩa

Checkpoint chạy mỗi checkpoint_timeout, hoặc sớm hơn khi WAL sắp vượt max_wal_size.

Tiến trình Việc làm Gần với SQL Server
Backend lúc COMMIT Flush WAL tới hết commit record của chính nó Log flush khi commit
WAL writer Ghi và flush WAL buffers định kỳ, mặc định mỗi 200 ms Log flush nền
Checkpointer Ghi mọi dirty page, rải đều trên khoảng 90% chu kỳ theo checkpoint_completion_target 0,9, ghi bản ghi checkpoint, cập nhật pg_control Checkpoint
Background writer Ghi trước một phần dirty page để backend tìm được buffer sạch Lazy writer
Backend khi cần buffer Tự ghi một dirty page nếu không còn buffer sạch Không có tương ứng gần

Recovery lúc khởi động: chỉ có redo

Sau sự cố, PostgreSQL đọc pg_control, lấy bản ghi checkpoint gần nhất, rồi redo từ vị trí redo ghi trong bản ghi đó tới hết WAL. Mọi thay đổi trước vị trí redo đã chắc chắn nằm trên tệp dữ liệu. Không có pha undo. Giao dịch không có commit record là aborted, tuple của nó vô hình. Thời gian recovery tỷ lệ với lượng WAL phải redo, nên checkpoint_timeout và max_wal_size là hai núm chỉnh RTO cho sự cố mất điện, giống TARGET_RECOVERY_TIME của SQL Server.

Commit không chờ WAL

synchronous_commit = off cho COMMIT trả về trước khi WAL được flush. Nó là bản tương ứng của delayed durability trong chương kỹ thuật SQL Server. Rủi ro là mất các giao dịch cuối, không phải hỏng dữ liệu: tài liệu ghi cửa sổ rủi ro tối đa bằng ba lần wal_writer_delay, tức 600 ms với mặc định. fsync = off thì khác, có thể làm hỏng database tùy ý sau sự cố.

Tham số đặt được cho từng giao dịch bằng SET LOCAL synchronous_commit = off ngay sau BEGIN, hợp với luồng ghi dày như nhật ký truy cập. Đơn đã thu tiền giữ on.

2. Mức phục hồi

Recovery lúc khởi động chỉ cứu được sự cố mất điện. Mất cả máy thì cần bản sao ở nơi khác. PostgreSQL không có recovery model theo database. Mức phục hồi do cấu hình cấp cluster quyết định: wal_level, archive_mode cùng archive_command hoặc archive_library, và lịch base backup.

Cấu hình Về được đâu khi mất cả máy Gần với SQL Server
Chỉ pg_dump hằng đêm Thời điểm bắt đầu bản dump gần nhất SIMPLE với một full mỗi đêm
wal_level = replica, base backup, không archive Thời điểm kết thúc base backup gần nhất SIMPLE với full backup
replica, archive_mode = on, base backup Bất kỳ thời điểm nào từ cuối base backup tới WAL cuối đã archive FULL với chuỗi log backup

Cùng sự cố lúc 14:00 ngày 2026-10-02, ba cách cấu hình cho ba kết cục:

Cách để banhang Còn lại sau sự cố Phần đơn hàng mất
Chỉ pg_dump lúc 22:00 hôm trước Dữ liệu lúc bắt đầu dump Mọi đơn từ 22:00 đến 14:00
Base backup và WAL archive, archive_timeout = 5min, mất cả máy Tới segment WAL cuối đã archive Tối đa khoảng 5 phút, cộng thời gian chạy lệnh archive
Như trên, /srv/pg_wal còn đọc được Chép phần WAL chưa archive vào, redo tới cuối Không mất giao dịch đã commit

wal_level = minimal bỏ thông tin cần cho archive và replica. Nó không đi cùng archive_mode hay base backup trực tuyến, chỉ hợp với máy nạp dữ liệu làm lại được. Lệnh archive chỉ chạy trên segment đã đầy 16 MB. archive_timeout ép chuyển segment sau một khoảng thời gian, nên giới hạn tuổi của phần WAL chưa archive. Đó là RPO khi mất cả máy.

Tài liệu cảnh báo segment chuyển sớm vẫn dài 16 MB, nên archive_timeout quá ngắn làm phình kho archive, và coi mức khoảng một phút là hợp lý. banhang chọn 5 phút: tối đa 288 segment mỗi ngày, khoảng 4,5 GB nếu mọi segment đều bị ép chuyển. Cần RPO vài giây thì dùng pg_receivewal nhận WAL liên tục qua kết nối replication, hoặc một standby.

RTO là thời gian chép lại toàn bộ cluster từ bản backup, cộng thời gian redo WAL từ điểm bắt đầu backup tới mốc cần về. Base backup 22:00 cộng WAL tới 12:06 nghĩa là redo hơn 14 giờ WAL. Incremental lúc 12:00 rút phần đó còn vài phút, giống vai trò differential ở SQL Server. Cách lấy hai bản đó và khôi phục ở bài sao lưu và khôi phục.

3. Cách các tầng này ăn khớp

Lấy đúng lần COMMIT lúc 12:04 của đơn 10042, tong_tien từ 1.500.000 lên 1.750.000:

  1. Tuple 51 byte, chiếm 56 byte cộng 4 byte line pointer, nằm ở page 26470 của heap don_hang_2026, trong tệp dưới /srv/pg_ssd/ts_data/PG_17_<catversion>/<OID banhang>/.
  2. UPDATE ghi tuple mới ở lp 48 trên cùng page và đặt xmax cho lp 12. Không cột nào có chỉ mục bị đổi và page còn chỗ, nên đây là HOT: hai chỉ mục không nhận entry mới.
  3. Bản ghi WAL, kèm ảnh cả page nếu đây là lần sửa đầu của page sau checkpoint, được flush xuống /srv/pg_wal trước khi COMMIT trả về. pg_xact ghi giao dịch là committed. Tệp trên /srv/pg_ssd có thể chưa đổi.
  4. Incremental chạy từ 12:00 đến 12:05 nhận WAL qua -X stream tới lúc nó kết thúc, nên lần sửa nằm trong phần WAL đi kèm bản đó. Ghép bản 22:00 với bản 12:00 rồi redo tới điểm nhất quán là có 1.750.000.
  5. Checkpoint sau đó ghi dirty page xuống tệp dữ liệu.
  6. Segment WAL chứa lần sửa được archive sang /mnt/backup/wal khi đầy hoặc khi archive_timeout ép chuyển. Sau checkpoint kế tiếp và sau khi đã archive, segment được tái sử dụng.
  7. Base backup 22:00 ngày 2026-10-01 là mốc gốc. Không có nó thì bản incremental và toàn bộ WAL archive không dựng được cluster nào.
  8. Khi không snapshot nào còn cần lp 12, pruning hoặc VACUUM dọn dữ liệu của nó, lp 12 thành redirect sang lp 48.

Thiếu bước 6, pg_wal phình tới khi đầy và server dừng. Thiếu bước 8 trên diện rộng, heap phình và index-only scan quay về đọc heap.

4. Áp dụng trong .NET

Với đơn hàng và thanh toán, giữ mặc định synchronous_commit = on: CommitAsync của Npgsql trả về nghĩa là commit record đã nằm trên pg_wal, đơn sống qua mất điện. Nhật ký truy cập thì khác. API ghi một dòng cho mỗi request, khoảng 300 request mỗi giây lúc cao điểm, và mất vài trăm mili giây nhật ký cuối khi sập máy là chấp nhận được. Đó đúng là chỗ tài liệu gợi ý dùng commit không chờ WAL.

Middleware đẩy mỗi lượt truy cập vào một Channel<T> có giới hạn. Một BackgroundService gom tối đa 1.000 lượt, rồi ghi cả lô trong một giao dịch có SET LOCAL synchronous_commit = off, bằng COPY nhị phân:

await using var conn = await db.OpenConnectionAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
// Chỉ giao dịch này không chờ flush WAL. Đơn hàng, thanh toán vẫn dùng mặc định on.
await using (var set = new NpgsqlCommand("SET LOCAL synchronous_commit = off", conn, tx))
    await set.ExecuteNonQueryAsync(ct);
await using (var writer = await conn.BeginBinaryImportAsync(
    "COPY nhat_ky_truy_cap (thoi_diem, thoi_gian_ms, ma_trang_thai, duong_dan) FROM STDIN (FORMAT BINARY)", ct))
{
    foreach (var luot in lo)
    {
        await writer.StartRowAsync(ct);
        await writer.WriteAsync(luot.ThoiDiem, NpgsqlDbType.TimestampTz, ct);
        await writer.WriteAsync(luot.ThoiGianMs, NpgsqlDbType.Integer, ct);
        await writer.WriteAsync(luot.MaTrangThai, NpgsqlDbType.Smallint, ct);
        await writer.WriteAsync(luot.DuongDan, NpgsqlDbType.Text, ct);
    }
    await writer.CompleteAsync(ct);
}
await tx.CommitAsync(ct); // trả về trước khi commit record xuống pg_wal

SET LOCAL chỉ có hiệu lực tới hết giao dịch, nên kết nối trả về pool không mang thiết lập này sang request khác. Cột của nhat_ky_truy_cap xếp 8, 4, 2 byte rồi text, theo quy tắc căn lề ở bài page và tuple. Channel đầy thì bỏ lượt mới (DropWrite), để nhật ký không bao giờ làm chậm request. Bản đầy đủ đã biên dịch bằng dotnet build (.NET SDK 10.0.401, Npgsql 10.0.3) nhưng chưa chạy với PostgreSQL server.

NhatKyTruyCap.cs: middleware, Channel và worker ghi nhật ký theo lôC# · 72 dòng
#:sdk Microsoft.NET.Sdk.Web
#:package Npgsql@10.0.3
// Nhật ký truy cập của API: middleware đẩy vào Channel, worker ghi theo lô
// bằng COPY nhị phân trong giao dịch có SET LOCAL synchronous_commit = off.
// Biên dịch: dotnet build NhatKyTruyCap.cs. Chạy cần PostgreSQL 17 với bảng nhat_ky_truy_cap:
//   CREATE TABLE nhat_ky_truy_cap (thoi_diem timestamptz NOT NULL, thoi_gian_ms int NOT NULL,
//                                  ma_trang_thai smallint NOT NULL, duong_dan text NOT NULL);
using System.Diagnostics;
using System.Threading.Channels;
using Npgsql;
using NpgsqlTypes;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(_ => NpgsqlDataSource.Create(
    builder.Configuration.GetConnectionString("BanHang") ?? "Host=db01;Database=banhang;Username=api"));
builder.Services.AddSingleton(Channel.CreateBounded<LuotTruyCap>(
    new BoundedChannelOptions(50_000) { FullMode = BoundedChannelFullMode.DropWrite }));
builder.Services.AddHostedService<GhiNhatKy>();
var app = builder.Build();

app.Use(async (HttpContext ctx, RequestDelegate next) =>
{
    var batDau = Stopwatch.GetTimestamp();
    await next(ctx);
    var kenh = ctx.RequestServices.GetRequiredService<Channel<LuotTruyCap>>();
    kenh.Writer.TryWrite(new LuotTruyCap(DateTime.UtcNow,
        (int)Stopwatch.GetElapsedTime(batDau).TotalMilliseconds,
        (short)ctx.Response.StatusCode, ctx.Request.Path.Value ?? "/"));
});
app.MapGet("/", () => "BanHang API");
app.Run();

record LuotTruyCap(DateTime ThoiDiem, int ThoiGianMs, short MaTrangThai, string DuongDan);

sealed class GhiNhatKy(Channel<LuotTruyCap> kenh, NpgsqlDataSource db, ILogger<GhiNhatKy> log) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stop)
    {
        var lo = new List<LuotTruyCap>(1_000);
        while (await kenh.Reader.WaitToReadAsync(stop))
        {
            while (lo.Count < 1_000 && kenh.Reader.TryRead(out var luot)) lo.Add(luot);
            try { await GhiLoAsync(lo, stop); }
            catch (NpgsqlException ex) { log.LogWarning(ex, "Bỏ {SoDong} dòng nhật ký", lo.Count); }
            lo.Clear();
        }
    }

    async Task GhiLoAsync(List<LuotTruyCap> lo, CancellationToken ct)
    {
        await using var conn = await db.OpenConnectionAsync(ct);
        await using var tx = await conn.BeginTransactionAsync(ct);
        // Chỉ giao dịch này không chờ flush WAL. Đơn hàng, thanh toán vẫn dùng mặc định on.
        await using (var set = new NpgsqlCommand("SET LOCAL synchronous_commit = off", conn, tx))
            await set.ExecuteNonQueryAsync(ct);
        await using (var writer = await conn.BeginBinaryImportAsync(
            "COPY nhat_ky_truy_cap (thoi_diem, thoi_gian_ms, ma_trang_thai, duong_dan) FROM STDIN (FORMAT BINARY)", ct))
        {
            foreach (var luot in lo)
            {
                await writer.StartRowAsync(ct);
                await writer.WriteAsync(luot.ThoiDiem, NpgsqlDbType.TimestampTz, ct);
                await writer.WriteAsync(luot.ThoiGianMs, NpgsqlDbType.Integer, ct);
                await writer.WriteAsync(luot.MaTrangThai, NpgsqlDbType.Smallint, ct);
                await writer.WriteAsync(luot.DuongDan, NpgsqlDbType.Text, ct);
            }
            await writer.CompleteAsync(ct);
        }
        // Trả về trước khi commit record xuống pg_wal: sập máy có thể mất lô cuối, không hỏng dữ liệu.
        await tx.CommitAsync(ct);
    }
}

Những chỗ hay hiểu sai

  • "COMMIT trả về nghĩa là page đã nằm trên tệp dữ liệu." Chỉ commit record của WAL đã nằm trên pg_wal. Page xuống tệp sau, lúc checkpoint hoặc khi cần buffer.
  • "Recovery hoàn tác giao dịch dở dang." Không có undo. Giao dịch không có commit record là aborted, tuple của nó vô hình.
  • "synchronous_commit = off có thể làm hỏng database." Nó chỉ có thể mất các giao dịch cuối, tối đa khoảng ba lần wal_writer_delay. fsync = off mới có thể làm hỏng database.
  • "Checkpoint càng dày càng tốt." Checkpoint dày rút ngắn recovery, nhưng làm WAL phình vì ảnh page sau mỗi checkpoint.

Kết luận

Ở PostgreSQL, commit bền khi commit record nằm trên pg_wal. Page xuống tệp, checkpoint và archive đều đến sau, và chỉ quyết định recovery dài bao lâu, về được tới đâu.

Trong dự án .NET của bạn:

  • Coi CommitAsync trả về là mốc bền vững của đơn hàng và thanh toán; không đặt synchronous_commit = off trong chuỗi kết nối dùng chung.
  • Luồng ghi dày, mất được vài trăm mili giây cuối như nhật ký truy cập: gom lô qua Channel<T>, SET LOCAL synchronous_commit = off trong giao dịch, ghi bằng BeginBinaryImportAsync.
  • Đo wal_fpi, wal_bytes trong pg_stat_wal và num_requested so với num_timed trong pg_stat_checkpointer trước khi đổi checkpoint_timeout hay max_wal_size.
  • Không bao giờ đặt fsync = off, kể cả trên môi trường thử tải dùng chung dữ liệu thật.

Đọc tiếp

Nguồn

Đọc tiếp

Bài tiếp theo trong series

Sao lưu và khôi phục theo thời điểm

WAL archive, base backup và incremental của PostgreSQL 17; dựng cả cluster về 12:06 ở máy khác để lấy lại đơn bị xóa nhầm mà không mất đơn mới; health check .NET cho WAL archive.

14 phút đọc

Trong PostgreSQL

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.

14 phút đọc

Trong PostgreSQL

Cluster, tablespace và partition

Một instance PostgreSQL là một cluster dùng chung WAL và sao lưu; tablespace là một thư mục của cả cluster; partition đặt đơn mới lên SSD, đơn cũ lên HDD; .NET nạp đơn vào đúng partition bằng COPY nhị phân.

14 phút đọc

Trong PostgreSQL

Page, tuple và TOAST

Một dòng đơn hàng chiếm 60 byte trên page PostgreSQL vì header MVCC 23 byte; giá trị dài được nén hoặc cắt sang bảng TOAST; code .NET đặt cột đúng thứ tự và chỉ đọc cột cần.

14 phút đọc