Giới Thiệu

Techtalk 20 của Grokking với phần trình bày của anh Huy Nguyễn (CTO Holistic) đã giới thiệu đến mọi người về cấu trúc bên trong Postgres và những cơ chế vận hành của Postgres. Đồng thời bài trình bày cũng đã so sánh giữa Postgres và Mysql trong các trường hợp như thêm một data mới vào Postgres và MySQL, index scan, primary key index scan, ... hay cấu trúc lưu trữ index của hai cơ sở dữ liệu quan hệ này. Bài viết này sẽ tóm tắt những nội dung đã được trình bày về cấu trúc bên trong Postgres. Bạn đọc có thể tìm hiểu chi tiết ở mỗi phần với tài liệu tham khảo của tác gỉa ở phía cuối bài viết.

Cấu trúc logic của Postgres

  • Một database trong Postgres thuộc về một Cluster và một Cluster chứa nhiều database. Mỗi cluster là riêng biệt và được quản lý bởi một instance của Postgres Server. Khi một cluster được tạo ra, một database tên "postgres" cũng sẽ mặc định tồn tại trong cluster đó.
  • Mỗi database cluster sẽ có từng database khác nhau, và trong mỗi database sẽ có những thành phần như table, index, view, function, etc. Mỗi database tách biệt với nhau.
  • Mỗi object trong database sẽ được định danh bằng OID (Object Identifiers), một unsigned-int 4 bytes. Khái niệm object của Postgres bao hàm các tables, index, view, function, ...
  • Mỗi OID object trong database được lưu trữ trong từng thư mục khác nhau, tùy vào kiểu của object, VD: OID của database, heap tables được lưu trong pg_database và pg_class.

Cấu trúc vật lý của Postgres

  • Về bản chất, một database cluster là một thư mục chính (base directory) chứa những thư mục con. Trong từng thư mục có thể chứa nhiều file hoặc thư mục con khác nhau.
  • Ngoài ra, Postgres có sử dụng Tablespace, khái niệm tablespace khác với RDBMS khác. Tablespace Postgres lưu trữ data nằm ngoài thư mục chính (base directory).
    Ví dụ cấu trúc vật lý một database cluster
  • Các file cấu hình hệ thống postgres gồm:
    • PG_VERSION: version của postgres
    • pg_hba.conf: thiết lập cấu hình client
    • pg_ident.conf: username mapping
    • postgresql.conf: tham số cấu hình.
    • postgresql.auto.conf: tương tự như postgres.conf và được sử dụng khi set ALTER SYSTEM.
    • postmaster.opts: lưu trữ command line mà server đã khởi động lần gần nhất.
  • Một số thư mục con lưu trữ các thông số trong qúa trình của một database trong postgres:
    • base: thư mục con chứa các thưa mục khác.
    • global: thư mục con chứa cluster-wide tables.
    • pg_commit_ts: các transaction đã commit.
    • Ngoài ra còn những thư mục khác, bạn đọc có thể đọc thêm chi tiết tại đây
  • Ngoài ra, mỗi thư mục con trong database còn chứa thêm hai thư mục mới hậu tố là '_fsm' và '_vm' để lưu trữ thông tin về free space trong database và visibilty.

Cấu trúc bên trong data file Postgres

Data file của Postgres ( heap table, index, fsm, vm) chứa những cấu trúc page (còn gọi là block) . Mỗi page có kích thước bằng nhau và được mặc định là 8KB (có thể thay đổi). Mỗi page trong một data file được gán một số thứ tự bắt đầu từ 0. Mỗi page sẽ thuộc về một table.
Heap table/Page
Cấu trúc của một page gồm các thành phần:

  • Heap tuple: đây là thành phần chứa data thật sự. Khi một record (ở mức logic) được thêm vào database, Postgre sẽ tạo một heap tuple chứa data được thêm vào và đưa heap tuple đó vào page.
  • Line pointer: Những con trỏ "trỏ" đến vùng nhớ chứa heap tuple.
    -Header Info: Chứa thông tin về page đó
    - pd_lsn: gía trị này lưu trữ thông tin LSN và XLOG về lần cuối cùng page này được thay đổi.
    - pd_checksum: gía trị checksum của page này
    - pd_lower: con trỏ "trỏ" đến vị trí cuối cùng của line pointer.
    - pd_upper: con trỏ "trỏ" đến vị trí bắt đầu của heap tuple mới nhất của page.
    - pd_special: gía trị đặc biệt dành cho index.
  • Khi một record mới được thêm vào database, Postgres sẽ tạo một con trỏ line pointer và một tuple chứa data mới được thêm vào. Line pointer đó sẽ trỏ đến tuple mới. Đồng thời pd_lower sẽ được gán cho line pointer mới, pd_upper gán cho tuple mới.
    Khi một tuple mới được tạo
  • Khi một record mới được thêm vào database, nhưng page hiện tại đã đầy (vùng dữ liệu trống không đủ để lưu trữ tuple mới tạo) thì Postgres sẽ tạo một page mới và lưu tuple đó vào page mới.
  • Khi một record được thay đổi thì postgres sẽ:
  • Tạo một tuple mới với dữ liệu đã được thay đổi.
  • Line pointer trỏ đến tuple cũ sẽ trỏ đến tuple mới tạo.
  • Đánh dấu tuple là dead tuple.

Liên quan đến trường hợp thay đổi dữ liệu trong Postgres. Nếu dữ liệu được thay đổi với tần suất cao thì sẽ gây ra hiện tượng phình to dữ liệu ( vì gia tăng dead tuple), làm cho hiệu suất hoạt động của Postgres giảm xuống, gọi là Table bloat. Postgres có một cơ chế để xóa bỏ hoàn toàn những dead tuple này, gọi là VACCUUM (tác gỉa không trình bày trong bài viết này, bạn đọc có thể tự tìm hiểu).

Khi một record bị xóa, Postgres sẽ không thật sự xóa record đó trên đĩa cứng mà sẽ đánh dấu tuple chứa record đó là dead tuple và sẽ không còn đươc nhìn thấy với người dùng.

Mỗi tuple được gán một tuple_id, gọi là TID có cấu trúc gồm một cắp hai số: số thứ tự của page mà tuple này thuộc vềsố offset của tuple đó trong page.

TOAST

Kích thước của một page là cố định, cho nên bất kỳ tuple nào có kích thước lớn hơn kích thước của page sẽ không thể nào lưu trữ trong page đó, đồng thời không thể lưu tuple này bằng nhiều page. Do vậy Postgres sử dụng TOAST (The Oversized-Attribute Storage Technique) để lưu trữ những tuple này.

Khi một tuple có kích thước lớn hơn 2KB, Postgres sẽ lưu trữ tuple này vào TOAST table. Cụ thể, Postgres sẽ nén từng field dữ liệu để gỉam kích thước tuple. Tuy nhiên, nếu sau khi nén mà kích thước dữ liệu vẫn không dưới 2KB, Postgres sẽ tách các field kích thước lớn trong row ra thành từng chunk và lưu vào trong TOAST, đồng thời mỗi field được thay đổi thành những con trỏ trỏ đến từng field của row trong TOAST.

Chi tiết về TOAST bạn đọc có thể tìm hiểu chi tiết tại đâyđây

Cấu trúc của một tuple

Trong một tuple của Postgres gồm các field t_xmin, t_xmax, t_cid, t_ctid, và một số field khác.
t_xmin - ID của transaction insert tuple dữ liệu này.
t_xmax - ID của transaction thay đổi tuple dữ liệu này.
t_cid - Trong một transaction có n câu lệnh (gồm INSERT, SELECT, UPDATE). t_cid sẽ lưu gía trị x là số thứ tự thực thi của câu lệnh insert tuple này vào database (tính từ 0). VD: Transaction A có 3 câu lệnh

BEGIN
	SELECT * FROM TABLE_X;
	INSERT INTO B(X,Z,Y) VALUES (1,3,-7)
	SELECT H FROM TABLE_K;
END

Câu lệnh thứ 2 trong transaction A insert tuple vào table B nên tuple này sẽ có t_cid = 1 ( vì tính từ 0)
t_ctid: lưu hai gía trị là t_cidoffset của tuple trong page. Nó là con trỏ đế trỏ đến tuple hiện tại. Nếu tuple bị thay đổi dữ liệu, nó sẽ trỏ đến tuple mới sau khi được thay đổi.

X
Insert tuple trong Postgres


Update tuple trong Postgres


Delete tuple trong Postgres

MVCC Postgres

Một vấn đề mà bất cứ RDBMS cần phải giải quyết đó là xử lý các transaction thực thi đồng thời. Mỗi transaction khi cùng thay đổi hoặc đọc một record đều phải thỏa mãn tính chất read-consistency. Postgres sử dụng hai gía trị xminxmax.

  • Khi một record mới được thêm vào database bởi một transaction, gía trị xmin của record sẽ là transaction ID của transaction đó.
  • Khi một transaction thay đổi một record hiện tại, transaction ID có transaction đó sẽ gán cho xmax.
  • Để đảm bảo tính chất read-consistency, Postgres sử dụng transaction snapshot ( một dataset chứa thống tin về tất cả các transaction đang active, tại một thời điểm nào đó đối với một transaction). Các transaction đang active là những transaction đang được thực thi hoặc chưa được bắt đầu thực thi.

Postgres tạo ra một format của transaction snapshot là 'xmin:xmax:xip_list' . Ví dụ ta có format là '100:100' của một transaction bất kỳ. Điều này có nghĩa là tất cả những transaction có txid nhỏ hơn 100 ( từ 99 về trước) sẽ là inactive đối với transacion này. Đồng nghĩa là transaction này chỉ có thể thấy được những sự thay đổi tạo ra bởi những transaction có txid nhỏ hơn 100. Những transaction có txid lớn 100 sẽ là active đối với transaction này và sẽ là "vô hình" đối với transaction này. Ngoài ra một transaction có thể nhìn thấy những transaction khác bắt đầu sau nó nhưng đã được commit hoặc bị roll back. Như hình vẽ bên dưới, transaction ID = 101 và ID = 102 được xem là active đối với transaction ID = 100.

Sequential Scan vs B-Tree Index Scan

Cấu trúc index B-Tree là cấu trúc lữu trữ dữ liệu được index trong database phổ biến nhất trong bất kỳ một RDBMS nào. B-Tree là một Balanced Search Tree với mỗi node chứa nhiều key khác nhau, bạn đọc có thể tìm hiểu về B-Tree tại đây

Xét 2 câu lệnh query sau:
SELECT * FROM table_XSELECT * FROM table_X WHERE column_Y = Z

Với câu query đầu tiên, Postgres thực hiện qúa trình Sequential Scan - Đi qua từng page (block) và đọc từng tuple trong page.

Với câu query thứ hai, điều kiện cho trước là column_Y được index, Postgres sẽ sử dụng Index Scan. Nội dung của một file index trong Postgres: chứa các index tuple theo cấu trúc B-Tree. Mỗi index tuple có hai phần: index keyTID trỏ đến heap tuple chứa data. Như vậy, khi Postgres sử dụng Index Scan, thao tác tìm kiếm sẽ phải duyệt B-tree để tìm ra OID, sau đó Postgres dùng thông tin về block và offset tìm được từ index để tìm ra dữ liệu.

Tài liệu và thông tin tham khảo của bài viết: