Skip to content

Cấu trúc lưu trữ ​

Trang này nói về chuyện database thực sự sắp xếp dữ liệu trên đĩa thế nào. Cùng là “lưu một dòng rồi đọc lại”, nhưng có hai trường phái rất khác nhau:

  • Sửa tại chỗ (B+Tree): mỗi dòng có một vị trí cố định; sửa dòng là sửa đúng chỗ đó. MySQL InnoDB, PostgreSQL, SQL Server, Oracle dùng cách này.
  • Chỉ ghi nối vào cuối (log-structured, như LSM-tree): không bao giờ sửa dữ liệu cũ, mà ghi bản mới vào chỗ khác rồi định kỳ dọn. RocksDB, Cassandra, ScyllaDB dùng cách này.

Cách sắp xếp quyết định database ghi nhanh hay đọc nhanh, tốn bao nhiêu đĩa, và vì sao một số thao tác tưởng rẻ lại đắt. Với dev backend, kiến thức này trả lời được những câu như:

  • Vì sao Cassandra chịu được lượng ghi lớn hơn MySQL?
  • Vì sao bảng PostgreSQL cập nhật nhiều thì phình to?
  • Vì sao khoá chính của InnoDB nên ngắn?
  • Vì sao ClickHouse đếm hàng tỉ dòng nhanh như vậy?

Phần B+Tree trong InnoDB đã có chi tiết ở MySQL chương 1 (page, buffer pool) và chương 4 (index). Trang này nói phần chung và so sánh các cấu trúc với nhau. Các kết quả có số liệu được chạy thật ngày 2026-10-16 trên MySQL 8.0.46 và PostgreSQL 17.

Ba thước đo để so sánh ​

Thước đoNghĩaVí dụ
Write amplificationSố byte thực sự ghi xuống đĩa chia cho số byte app ghiApp ghi 1 MB nhưng đĩa nhận 10 MB thì hệ số là 10
Read amplificationSố lần đọc đĩa cho một câu queryTra một key phải đọc 5 page
Space amplificationDung lượng trên đĩa chia cho dung lượng dữ liệu thật10 MB dữ liệu chiếm 15 MB đĩa thì hệ số là 1,5

Định nghĩa theo RocksDB Tuning Guide. Không cấu trúc nào thấp cả ba: giảm cái này thường làm tăng cái kia. Chọn cấu trúc lưu trữ chính là chọn chịu thiệt ở thước đo nào.

B+Tree: sửa tại chỗ ​

Dữ liệu chia thành các page cỡ cố định (InnoDB 16 KB, PostgreSQL 8 KB), xếp thành một cây đã sắp xếp theo key. Tìm một key là đi từ gốc xuống lá, thường chỉ 3–4 page dù bảng có hàng trăm triệu dòng (MySQL chương 1).

  • Đọc ổn định: tra một key hay đọc một khoảng key đều nhanh và đoán trước được.
  • Ghi ngẫu nhiên: sửa một dòng là sửa đúng page chứa nó, mà các page nằm rải rác trên đĩa. Page đầy thì phải tách đôi (page split).
  • Ghi nhiều hơn dữ liệu thật: đổi một dòng 100 byte cũng làm cả page 16 KB thành bẩn và sớm muộn phải ghi lại, cộng thêm một bản trong redo log.

Hai cách giữ dòng của bảng: clustered và heap ​

Cùng dùng B+Tree cho index, nhưng MySQL và PostgreSQL đặt dòng dữ liệu ở hai chỗ khác nhau. Khác biệt này giải thích nhiều hành vi khác nhau giữa hai database.

MySQL InnoDB: clustered indexPostgreSQL: heap
Dòng dữ liệu nằm ởLá của cây khoá chính; bảng chính là cây khoá chínhMột vùng riêng gọi là heap, không theo thứ tự nào; mỗi dòng có địa chỉ ctid (số page, vị trí trong page)
Secondary index chứaGiá trị khoá chínhctid
UPDATE một dòngSửa tại chỗ trong page; bản cũ đưa vào undo logGhi một phiên bản mới ở chỗ khác trong heap; bản cũ ở lại cho tới khi VACUUM dọn

Kết quả chạy thật cho thấy rõ khác biệt:

PostgreSQL: UPDATE tạo dòng mới.

sql
SELECT ctid, id, qty FROM items;            -- id 1 ở (0,1)
UPDATE items SET qty = 11 WHERE id = 1;
SELECT ctid, id, qty FROM items;            -- id 1 chuyển sang (0,4)

Cập nhật mọi dòng của một bảng 1 triệu dòng (autovacuum tắt để thấy rõ):

BướcKích thước heapGhi chú
Sau khi INSERT35 MB
Sau UPDATE toàn bảng69 MB1.000.000 dead tuple (phiên bản cũ)
Sau VACUUM69 MBVACUUM đánh dấu chỗ trống để dùng lại, không trả dung lượng cho hệ điều hành
UPDATE toàn bảng lần 269 MBDòng mới dùng lại chỗ VACUUM vừa dọn
Sau VACUUM FULL35 MBViết lại toàn bộ bảng, khoá bảng trong lúc chạy

MySQL InnoDB: sửa tại chỗ. Cùng bảng 1 triệu dòng, cập nhật toàn bảng hai lần, file của bảng vẫn 36 MB. Phiên bản cũ nằm trong undo tablespace riêng, và purge thread dọn khi không còn transaction nào cần (MySQL chương 6).

Hệ quả cho dev:

  • PostgreSQL: bảng cập nhật nhiều cần VACUUM chạy đều. Đây là việc của autovacuum. Nếu autovacuum chạy không kịp, hoặc bị một transaction mở lâu chặn lại, bảng phình to và chậm dần (Hàng đợi bằng database).
  • PostgreSQL: HOT update giảm bớt chi phí. Nếu UPDATE không đổi cột nào có index và page còn chỗ, PostgreSQL đặt phiên bản mới cùng page và không phải cập nhật index (heap-only tuple). Chạy thử: UPDATE cột qty khi chưa có index trên qty là HOT; tạo index trên qty rồi UPDATE lại thì không còn HOT (PostgreSQL: HOT Updates). Vì vậy đừng thêm index lên cột cập nhật liên tục nếu không thật cần.
  • InnoDB: secondary index chứa sẵn khoá chính. Với index chỉ trên email, câu SELECT id FROM users WHERE email = ? vẫn chỉ đọc index (EXPLAIN ghi Using index), vì mỗi mục của index đã có id. Đổi lại, khoá chính dài làm mọi secondary index to ra. Đây là một lý do khoá chính nên ngắn (MySQL: Clustered and Secondary Indexes, Sinh ID).
  • PostgreSQL: index-only scan cần VACUUM. Index không lưu thông tin dòng nào còn hiệu lực, nên PostgreSQL phải hỏi visibility map, và nếu page chưa được đánh dấu thì phải đọc heap (PostgreSQL: Index-Only Scans). Chạy thử: index-only scan lấy 50.000 dòng phải đọc heap 50.000 lần (Heap Fetches: 50000); sau VACUUM thì còn 0.

LSM-tree: chỉ ghi nối, dọn sau ​

LSM-tree (log-structured merge-tree) không bao giờ sửa dữ liệu đã ghi trên đĩa. Theo tài liệu RocksDB và Cassandra:

  1. Ghi: lệnh ghi được nối vào cuối WAL (Cassandra gọi là commit log) để không mất khi sập, rồi ghi vào memtable, một cấu trúc đã sắp xếp nằm trong RAM. Vì chỉ có một lần ghi nối tuần tự và một lần ghi vào RAM, ghi rất nhanh.
  2. Flush: memtable đầy thì được ghi xuống đĩa thành một file đã sắp xếp gọi là SSTable. SSTable không bao giờ bị sửa nữa.
  3. Compaction: theo thời gian có nhiều SSTable chứa các bản khác nhau của cùng một key. Một tiến trình nền gộp chúng lại, giữ bản mới nhất, bỏ bản cũ và bản đã xoá. Ở RocksDB, các file ở tầng L0 có thể trùng key với nhau; từ L1 trở xuống mỗi tầng là một dãy đã sắp xếp, và tầng sau thường lớn gấp khoảng 10 lần tầng trước (RocksDB: Leveled Compaction).
  4. Đọc: tìm trong memtable trước, rồi tới các SSTable từ mới tới cũ. Để không phải mở từng file, mỗi SSTable có một Bloom filter: cấu trúc nhỏ trả lời được “chắc chắn không có key này” hoặc “có thể có”. Gặp “chắc chắn không” thì bỏ qua file đó (Cassandra: Storage Engine).
  5. Xoá: không xoá ngay mà ghi một tombstone (dấu xoá). Bản cũ và tombstone chỉ biến mất khi compaction chạy tới chúng.
Thử: LSM-tree thu nhỏ (memtable 4 key, L0 tối đa 3 SSTable)
Memtable (RAM, đã sắp xếp)
trống
L0 trên đĩa (mới nhất bên trái; các SSTable có thể trùng key)
trống
L1 trên đĩa (một dãy đã sắp xếp, mỗi key một bản)
trống
0lệnh ghi của app
0dòng ghi xuống đĩa (WAL 0 + flush 0 + compaction 0)
–write amplification

Gợi ý: bấm “Ghi 8 key” vài lần, rồi đọc một key vừa ghi và một key cũ. Ký hiệu ✝ là tombstone (dấu xoá). Mô hình đơn giản hoá: chỉ có hai tầng, mỗi dòng tính là một đơn vị ghi, Bloom filter báo nhầm khoảng 1/8 lần; RocksDB thật có nhiều tầng, mỗi tầng lớn gấp khoảng 10 lần tầng trên.

Cái giá: dữ liệu bị ghi lại nhiều lần qua các tầng compaction. RocksDB ghi rằng write amplification của leveled compaction “thường lớn hơn 10”. Ví dụ tính trong tài liệu của họ, một database 500 GB có write amplification khoảng 33 (RocksDB Tuning Guide). Các kiểu compaction khác nhau là các cách đổi chác giữa ba thước đo: leveled ưu tiên ít tốn đĩa, universal ưu tiên ít ghi lại nhưng tốn đĩa và đọc nhiều hơn (RocksDB Overview).

Dùng LSM-tree: RocksDB, LevelDB, Cassandra, ScyllaDB, HBase. MySQL cũng chạy được trên RocksDB qua storage engine MyRocks.

So sánh B+Tree và LSM-tree ​

B+TreeLSM-tree
GhiGhi ngẫu nhiên vào page; chậm hơn khi bảng lớn hơn RAMGhi tuần tự vào WAL và memtable; rất nhanh
Đọc một keyỔn định: một đường từ gốc xuống láCó thể phải xem nhiều SSTable; Bloom filter giảm bớt
Đọc theo khoảngTốt: lá đã sắp xếp và nối với nhauPhải gộp kết quả từ nhiều SSTable
Write amplificationGhi cả page cho thay đổi nhỏGhi lại nhiều lần qua compaction
Space amplificationPage không đầy, phân mảnh (PostgreSQL thêm dead tuple)Bản cũ và tombstone nằm lại tới khi compaction; nén tốt vì file không đổi
Hiệu năng theo thời gianỔn địnhCó thể dao động khi compaction chạy nền chiếm I/O
Hợp vớiOLTP đọc ghi lẫn lộn, cần transaction và đọc theo khoảngGhi rất nhiều: log, sự kiện, IoT, time series, tin nhắn

Hash index: tra theo key nhanh nhất, không đọc được khoảng ​

Hash index tính hash của key để nhảy thẳng tới vị trí dữ liệu. Tra đúng một key rất nhanh, nhưng các key gần nhau không nằm gần nhau, nên không đọc được theo khoảng (BETWEEN, >, ORDER BY).

  • PostgreSQL có hash index, chỉ dùng cho phép so sánh =; mặc định CREATE INDEX tạo B-tree (PostgreSQL: Index Types). Trên thực tế ít khi cần dùng.
  • MySQL chỉ có hash index cho bảng engine MEMORY (mất dữ liệu khi khởi động lại). Bản thân tài liệu MySQL cũng cảnh báo hay gặp vấn đề hiệu năng khi dùng hash index mặc định cho các query mà B-tree hợp hơn (MySQL: MEMORY Storage Engine). InnoDB không cho tạo hash index.
  • Bitcask (storage engine của Riak) ghép hash với log. Dữ liệu chỉ được ghi nối vào cuối file. Một bảng hash trong RAM tên keydir lưu với mỗi key vị trí của bản mới nhất. Nhờ vậy mỗi lần đọc không cần quá một lần seek đĩa. Xoá là ghi tombstone, và tombstone được dọn khi gộp file (Bitcask, Basho 2010). Vì keydir chứa mọi key nên số key bị giới hạn bởi RAM, dù value thì lớn hơn RAM được.
  • Redis về bản chất là một bảng hash lớn trong RAM (Redis).

Lưu theo dòng và lưu theo cột ​

Mọi thứ ở trên là lưu theo dòng: các cột của một dòng nằm cạnh nhau. Hợp với OLTP, nơi mỗi lần đọc hay ghi thường đụng tới cả dòng (một đơn hàng, một user).

Lưu theo cột đặt mọi giá trị của một cột cạnh nhau. Câu SELECT SUM(amount) FROM orders trên bảng 50 cột chỉ phải đọc đúng cột amount, thay vì đọc cả 50 cột của mọi dòng. Các giá trị cùng kiểu nằm cạnh nhau cũng nén tốt hơn nhiều. Đổi lại, thao tác đụng tới cả dòng (đọc một đơn hàng, sửa một dòng) đắt hơn tương ứng (ClickHouse: What is a columnar database?).

Dùng lưu theo cột: ClickHouse, BigQuery, Snowflake, Redshift, DuckDB, và định dạng file Parquet. Khi nào tách báo cáo sang hệ lưu theo cột: OLTP vs OLAP.

WAL: ghi log trước khi ghi dữ liệu ​

Cấu trúc nào cũng cần một thứ chung: write-ahead log. Trước khi sửa dữ liệu thật, database nối mô tả thay đổi vào cuối một file log rồi fsync. Lý do:

  • Ghi nối tuần tự rất nhanh, nhanh hơn nhiều so với ghi ngẫu nhiên vào các page rải rác. Commit chỉ cần chờ log xuống đĩa; các page dữ liệu được ghi sau, gom theo lô.
  • Khôi phục sau khi sập: chạy lại log để đưa dữ liệu về đúng trạng thái đã commit.
  • Replication và CDC đọc chính dòng log này để gửi thay đổi sang máy khác (Replication, MySQL chương 9).
Hệ thốngWAL của nó
MySQL InnoDBRedo log; binlog là log riêng cho replication (MySQL chương 8)
PostgreSQLWAL, cũng là nguồn cho replication và logical decoding
RocksDBWAL đi kèm memtable
CassandraCommit log, dùng chung cho các bảng
BitcaskFile dữ liệu chính là log, nên khôi phục không cần chạy lại gì

Ý nghĩa với dev backend ​

Câu hỏiTrả lời
Vì sao khoá chính InnoDB nên ngắn và tăng dần?Mọi secondary index chứa khoá chính; khoá ngẫu nhiên làm chèn rải rác khắp cây và gây page split (Sinh ID)
Vì sao bảng PostgreSQL cập nhật nhiều bị phình?Mỗi UPDATE tạo phiên bản mới; cần autovacuum chạy kịp, tránh transaction mở lâu, dùng HOT update
Ghi rất nhiều, ít cập nhật, đọc theo key (log, sự kiện, tin nhắn)Cân nhắc hệ dùng LSM-tree (Cassandra, ScyllaDB), hoặc partition bảng theo thời gian trên MySQL/PostgreSQL (Dữ liệu chuỗi thời gian)
Báo cáo quét hàng trăm triệu dòngHệ lưu theo cột (ClickHouse, BigQuery), không phải thêm index trên OLTP (OLTP vs OLAP)
Xoá hàng loạt trên Cassandra hay RocksDB rồi đọc chậm hẳnTombstone chưa được compaction dọn, mỗi lần đọc phải đi qua chúng

Ghi nhớ

  1. Hai trường phái: sửa tại chỗ (B+Tree) và chỉ ghi nối rồi dọn sau (LSM-tree). So sánh bằng write, read, space amplification.
  2. InnoDB giữ dòng trong cây khoá chính và secondary index chứa khoá chính; PostgreSQL giữ dòng trong heap, mỗi UPDATE tạo phiên bản mới và cần VACUUM.
  3. Chạy thử: UPDATE toàn bảng 1 triệu dòng làm heap PostgreSQL tăng từ 35 MB lên 69 MB, còn file InnoDB giữ nguyên 36 MB.
  4. LSM-tree: WAL và memtable, flush thành SSTable bất biến, compaction gộp dần, Bloom filter giúp đọc, xoá bằng tombstone. Ghi rất nhanh, đổi lại write amplification thường trên 10.
  5. Hash index chỉ tra được bằng =; lưu theo cột hợp với báo cáo quét nhiều dòng ít cột.
  6. Mọi database đều ghi WAL trước: ghi tuần tự nhanh, khôi phục được khi sập, và là nguồn cho replication.

Nguồn tham khảo ​

Đã chạy thử ngày 2026-10-16 trên MySQL 8.0.46 và PostgreSQL 17 (Docker; PostgreSQL chạy với autovacuum=off để thấy rõ phiên bản cũ):

  • ctid đổi khi UPDATE;
  • số HOT update trước và sau khi tạo index;
  • kích thước heap PostgreSQL và file InnoDB khi cập nhật toàn bảng 1 triệu dòng;
  • Heap Fetches trước và sau VACUUM;
  • EXPLAIN Using index của InnoDB với secondary index.

Mô phỏng LSM-tree là mô hình thu nhỏ để minh hoạ cơ chế, không phải số liệu đo.

Tài liệu đã đối chiếu (ngày 2026-10-16):

NguồnDùng cho phần
RocksDB Tuning GuideĐịnh nghĩa ba loại amplification; ví dụ write amplification khoảng 33
RocksDB: Leveled CompactionL0 trùng key, các tầng sau là dãy đã sắp xếp, hệ số 10 giữa các tầng; write amplification thường lớn hơn 10
RocksDB OverviewWAL, memtable, flush thành SST; compaction bỏ bản cũ và bản đã xoá; leveled và universal
Cassandra: Storage EngineCommit log, memtable, SSTable bất biến, Bloom filter mỗi SSTable, compaction
PostgreSQL, HOT UpdatesĐiều kiện và lợi ích của HOT update
PostgreSQL, Index-Only ScansIndex không có thông tin hiệu lực; visibility map
PostgreSQL, Index TypesHash index chỉ dùng cho =; B-tree là mặc định
MySQL 8.4, Clustered and Secondary IndexesClustered index chứa dòng; secondary index chứa khoá chính; khoá chính dài làm index to
MySQL 8.4, MEMORY Storage EngineHASH là index mặc định của MEMORY; mất dữ liệu khi khởi động lại
Basho, Bitcask: A Log-Structured Hash Table (2010)Ghi nối vào cuối file, keydir trong RAM, đọc tối đa một lần seek, tombstone, file dữ liệu cũng là log
ClickHouse, What is a columnar database?Chỉ đọc cột cần dùng, nén theo cột, thao tác cả dòng đắt hơn

Chưa đối chiếu, từ kiến thức chung:

  • Kích thước page: InnoDB 16 KB, PostgreSQL 8 KB (chương 1 đã nhắc phần InnoDB).
  • Danh sách hệ thống dùng LSM-tree (LevelDB, ScyllaDB, HBase, MyRocks) và dùng lưu theo cột (BigQuery, Snowflake, Redshift, DuckDB, Parquet).
  • InnoDB không cho tạo hash index do người dùng định nghĩa (chỉ có adaptive hash index nội bộ).
  • Bitcask: “số key bị giới hạn bởi RAM” là suy ra từ việc keydir chứa mọi key, bài viết không nói thẳng.
  • Tombstone làm chậm đọc trên Cassandra và RocksDB.

Code đã dùng để chạy thử ​

Script nằm trong thư mục lab/cau-truc-luu-tru/ của repo. Tự chạy lại bằng npm run lab:up rồi npm run lab -- cau-truc-luu-tru (cách chạy). Lần chạy lại ngày 2026-10-19 cho cùng kết quả. Lần này autovacuum chỉ tắt cho các bảng thử nghiệm (WITH (autovacuum_enabled = false)), không tắt cho cả server như lần đầu.

heap-pg.sql: ctid, HOT update, heap phình khi UPDATE toàn bảng, Heap Fetches
sql
-- PostgreSQL heap: UPDATE tạo phiên bản mới, HOT update, bảng phình khi UPDATE toàn bảng, Heap Fetches.
-- Tắt autovacuum cho riêng các bảng thử nghiệm để thấy rõ phiên bản cũ (không tắt cho cả server).
DROP TABLE IF EXISTS items, big, ios;

\echo '--- ctid đổi khi UPDATE'
CREATE TABLE items (id INT PRIMARY KEY, name TEXT NOT NULL, qty INT NOT NULL) WITH (autovacuum_enabled = false);
INSERT INTO items VALUES (1,'a',10),(2,'b',20),(3,'c',30);
SELECT ctid, id, qty FROM items ORDER BY id;
UPDATE items SET qty = 11 WHERE id = 1;
SELECT ctid, id, qty FROM items ORDER BY id;

\echo '--- HOT update: chưa có index trên qty thì là HOT; có index rồi thì không'
SELECT pg_stat_force_next_flush();
SELECT n_tup_upd, n_tup_hot_upd FROM pg_stat_user_tables WHERE relname = 'items';
CREATE INDEX ON items (qty);
UPDATE items SET qty = 12 WHERE id = 1;
SELECT pg_stat_force_next_flush();
SELECT n_tup_upd, n_tup_hot_upd FROM pg_stat_user_tables WHERE relname = 'items';

\echo '--- Kích thước heap khi UPDATE toàn bảng 1 triệu dòng'
CREATE TABLE big (id INT PRIMARY KEY, v INT NOT NULL) WITH (autovacuum_enabled = false);
INSERT INTO big SELECT g, 0 FROM generate_series(1, 1000000) g;
VACUUM big;
SELECT 'sau insert' AS step, pg_size_pretty(pg_relation_size('big')) AS heap;
UPDATE big SET v = v + 1;
SELECT pg_stat_force_next_flush();
SELECT 'sau update toàn bảng' AS step, pg_size_pretty(pg_relation_size('big')) AS heap, (SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname = 'big') AS dead_tuples;
VACUUM big;
SELECT 'sau VACUUM' AS step, pg_size_pretty(pg_relation_size('big')) AS heap;
UPDATE big SET v = v + 1;
SELECT 'update lần 2 sau VACUUM' AS step, pg_size_pretty(pg_relation_size('big')) AS heap;
VACUUM FULL big;
SELECT 'sau VACUUM FULL' AS step, pg_size_pretty(pg_relation_size('big')) AS heap;

\echo '--- Index-only scan: Heap Fetches trước và sau VACUUM'
CREATE TABLE ios (id INT PRIMARY KEY, pad TEXT) WITH (autovacuum_enabled = false);
INSERT INTO ios SELECT g, repeat('x', 50) FROM generate_series(1, 100000) g;
SET enable_seqscan = off; SET enable_bitmapscan = off;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) SELECT id FROM ios WHERE id <= 50000;
VACUUM ios;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) SELECT id FROM ios WHERE id <= 50000;
innodb.sql: secondary index chứa khoá chính, UPDATE tại chỗ
sql
-- MySQL InnoDB: secondary index chứa sẵn khoá chính; UPDATE toàn bảng không làm file bảng phình.
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS big;

SELECT '--- SELECT id … WHERE email = ? chỉ đọc index (Using index); SELECT name thì phải đọc bảng' AS step;
CREATE TABLE users (id INT PRIMARY KEY, email VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL, INDEX idx_email (email));
INSERT INTO users VALUES (1,'an@x.vn','An'),(2,'binh@x.vn','Bình');
EXPLAIN SELECT id FROM users WHERE email = 'an@x.vn';
EXPLAIN SELECT name FROM users WHERE email = 'an@x.vn';

SELECT '--- Kích thước file bảng khi UPDATE toàn bảng 1 triệu dòng' AS step;
SET SESSION cte_max_recursion_depth = 2000000;
CREATE TABLE big (id INT PRIMARY KEY, v INT NOT NULL);
INSERT INTO big WITH RECURSIVE s AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM s WHERE n < 1000000) SELECT n, 0 FROM s;
SELECT 'sau insert' AS step, FILE_SIZE DIV 1048576 AS file_mb FROM information_schema.INNODB_TABLESPACES WHERE NAME = CONCAT(DATABASE(), '/big');
UPDATE big SET v = v + 1;
SELECT 'sau update toàn bảng' AS step, FILE_SIZE DIV 1048576 AS file_mb FROM information_schema.INNODB_TABLESPACES WHERE NAME = CONCAT(DATABASE(), '/big');
UPDATE big SET v = v + 1;
SELECT 'update lần 2' AS step, FILE_SIZE DIV 1048576 AS file_mb FROM information_schema.INNODB_TABLESPACES WHERE NAME = CONCAT(DATABASE(), '/big');
SELECT NAME, FILE_SIZE DIV 1048576 AS undo_file_mb FROM information_schema.INNODB_TABLESPACES WHERE NAME LIKE 'innodb_undo%';