SingleStore 5 — Thực thi & biên dịch truy vấn

15 thg 7, 2026 5 lượt xem
#sql
#distributed
#mpp
#singlestore
#htap
#query-optimization

SingleStore 5 — Thực thi & biên dịch truy vấn

bài kiến trúc ta biết một truy vấn đi từ aggregator xuống các leaf; ở bài sharding & distributed join ta thấy dữ liệu chia thành partition và một join có thể là collocated, reshuffle hay broadcast. Bài này trả lời câu hỏi bên trong mỗi node chuyện gì xảy ra khi câu SQL chạy — và đây là chỗ SingleStore khác hẳn phần lớn CSDL quan hệ: nó không diễn giải (interpret) truy vấn từng toán tử, từng dòng, mà biên dịch (compile) truy vấn ra mã máy rồi thực thi khối mã đó song song trên các leaf theo mô hình MPP (Massively Parallel Processing).

Mô hình tinh thần cần nắm ngay: trong Postgres/MySQL cổ điển, engine dựng một cây toán tử rồi chạy một vòng lặp diễn giải — mỗi dòng đi qua cây phải qua nhiều lệnh nhánh (switch/virtual call) để biết "toán tử này làm gì với dòng này". Với hàng tỷ dòng, chi phí diễn giải đó cộng dồn rất lớn. SingleStore sinh ra mã riêng cho đúng câu truy vấn đó (code generation) — vòng lặp quét, so sánh, cộng dồn được nội tuyến (inline) thành native code, chạy nhanh gần như code viết tay. Bù lại có một chi phí một lần: biên dịch. Vì thế SingleStore cache lại plan đã biên dịch để lần sau khỏi làm lại — đó là plan cache.

Lưu ý: SingleStore tương thích giao thức MySQL nên các block SQL dưới đây kết nối bằng client MySQL, nhưng cú pháp DDL phân tán (SHARD KEY, SORT KEY, columnstore) và các lệnh EXPLAIN/PROFILE/SHOW PLANCACHE là của riêng SingleStore. Sandbox của app là PostgreSQL read-only nên không block nào được đánh dấu "chạy được".

Vòng đời một truy vấn: từ SQL tới mã máy chạy trên leaf

Khi aggregator nhận một câu SQL, nó không nhảy thẳng vào chạy. Chuỗi bước là:

Vài điểm phải nhớ ở luồng này:

  • Tham số hoá (parameterization): trước khi tra cache, SingleStore tách các hằng số (literal) khỏi câu truy vấn và thay bằng tham số. Nhờ đó WHERE amount > 100WHERE amount > 5000 được coi là cùng một hình dạng (query shape)dùng chung một plan biên dịch. Đây là lý do plan cache hiệu quả: bạn không phải biên dịch lại cho từng giá trị khác nhau.
  • Optimizer chạy trước codegen: SingleStore là optimizer dựa trên chi phí (cost-based), dùng thống kê (số dòng, phân bố) để chọn thứ tự join và — quan trọng với hệ phân tán — chọn cách di chuyển dữ liệu: giữ cục bộ (collocated), reshuffle (repartition theo khoá join), hay broadcast (phát một bảng nhỏ tới mọi partition). Quyết định này được "đóng băng" vào plan.
  • Code generation: plan được hạ xuống một biểu diễn trung gian cấp thấp rồi biên dịch thành mã máy native (SingleStore từ thời MemSQL dùng kỹ thuật này, sinh code rồi biên dịch qua LLVM). Kết quả là một khối thực thi không còn overhead diễn giải.
  • Pushdown + MPP: aggregator đẩy phần plan thực thi được xuống leaf; mỗi leaf chạy song song trên các partition của nó (mỗi partition thường là một luồng), rồi aggregator gather và gộp phần còn lại (ví dụ tổng hợp cuối của GROUP BY).

Chi phí biên dịch một lần và plan cache

Vì biên dịch tốn thời gian (thường vài chục đến vài trăm mili-giây, có thể hơn với truy vấn phức tạp), lần chạy đầu tiên của một hình dạng truy vấn mới sẽ chậm hơn các lần sau. Từ lần thứ hai trở đi, cùng hình dạng → cache hit → bỏ qua cả optimize lẫn codegen, chạy thẳng mã đã biên dịch. Plan cache của SingleStore được giữ trong bộ nhớ và ghi cả trên đĩa, nên khối biên dịch sống sót qua khởi động lại node — sau restart không phải biên dịch lại từ đầu cho các truy vấn quen thuộc.

Hệ quả thực tế: SingleStore rất hợp với workload lặp lại (dashboard, API ngân hàng gọi cùng vài chục câu truy vấn tham số hoá hàng triệu lần/ngày). Ngược lại, workload sinh vô số câu truy vấn hình dạng khác nhau (ví dụ ghép chuỗi literal thay vì dùng tham số) sẽ khiến cache miss liên tục và trả giá biên dịch — đây là một anti-pattern nên tránh; hãy dùng truy vấn tham số hoá / prepared statement.

Xem các plan đã biên dịch đang nằm trong cache:

-- Liệt kê plan trong plan cache: hình dạng truy vấn, số lần chạy,
-- thời gian biên dịch/thực thi tích luỹ...
SHOW PLANCACHE;

-- (khi cần) xoá plan đã biên dịch để buộc biên dịch lại
DROP ... FROM PLANCACHE;   -- ví dụ khi đổi thống kê/muốn tối ưu lại

SHOW PLANCACHE cho biết mỗi hình dạng truy vấn đã chạy bao nhiêu lần, tốn bao lâu để biên dịch và thực thi — rất hữu ích để phát hiện truy vấn "nóng" hoặc phát hiện cache đang bị phá vì thiếu tham số hoá.

Đọc kế hoạch: EXPLAIN

EXPLAIN <query> cho ta kế hoạch phân tán mà optimizer chọn — không chạy truy vấn. Đây là công cụ đầu tiên để hiểu dữ liệu sẽ di chuyển thế nào giữa aggregator và leaf. Xét một câu ngân hàng: gộp doanh số giao dịch theo chi nhánh, join bảng transactions (lớn, shard theo account_id) với accounts:

EXPLAIN
SELECT a.branch_id,
       COUNT(*)      AS n_txn,
       SUM(t.amount) AS total_amount
FROM transactions t
JOIN accounts a ON t.account_id = a.account_id
WHERE t.txn_date >= '2026-07-01'
GROUP BY a.branch_id;

Kế hoạch (rút gọn, minh hoạ) đọc từ dưới lên — phần thụt sâu chạy trên leaf, phần trên cùng chạy trên aggregator:

Project [branch_id, n_txn, total_amount]
Gather partitions:all                         <- aggregator GOM kết qutmi partition leaf
  Project [branch_id, n_txn, total_amount]
  HashGroupBy [SUM(t.amount), COUNT(*)] groups:[a.branch_id]
  HashJoin [t.account_id = a.account_id]
  |
  |---Broadcast [db.accounts]                  <- accounts NHỎ: phát ti mi partition (broadcast)
  |
  ColumnStoreFilter [t.txn_date >= '2026-07-01']
  ColumnStoreScan db.transactions, SORT KEY (txn_date)   <- quét columnstore, ct bt theo SORT KEY

Cách đọc:

  • Gather là ranh giới: mọi thứ bên dưới được pushdown xuống leaf chạy song song; Gather là bước aggregator thu kết quả cục bộ về. Có GROUP BY thì leaf làm tổng hợp cục bộ (partial aggregate) trước, aggregator chỉ gộp phần cuối → ít dữ liệu qua mạng.
  • ColumnStoreScan + SORT KEY: xác nhận truy vấn quét columnstore và tận dụng SORT KEY (txn_date) để bỏ qua (segment elimination) các segment ngoài khoảng ngày — liên quan trực tiếp tới bài index & tối ưu.
  • Broadcast [db.accounts]: optimizer thấy accounts nhỏ nên phát bản sao tới mọi partition để join cục bộ, thay vì xáo transactions khổng lồ. Nếu accountsreference table thì bản sao đã có sẵn mọi node — không cần broadcast qua mạng.

Nhận diện reshuffle (repartition) trong plan

Điều phải cảnh giác là khi khoá join không trùng SHARD KEY của cả hai bảng và không bên nào đủ nhỏ để broadcast. Khi đó optimizer chèn một toán tử Repartition (còn gọi là reshuffle) — nó băm lại và bắn dữ liệu qua mạng giữa các leaf để những dòng cùng khoá join gặp nhau trên cùng một partition:

Gather partitions:all
  HashGroupBy ...
  HashJoin [t.account_id = c.customer_id]
  |
  |---Repartition [c.customer_id]              <- RESHUFFLE: xáo dliu qua mng gia các leaf
  |   ColumnStoreScan db.customers
  |
  ColumnStoreScan db.transactions

Thấy Repartition/Broadcast trong plan không phải lúc nào cũng xấu, nhưng với bảng lớn nó là chi phí mạng đắt nhất của truy vấn phân tán. Cách chữa gốc thường là chọn lại SHARD KEY để join trở thành collocated (không xáo) — chi tiết ở bài sharding & distributed join. EXPLAIN là nơi bạn phát hiện điều này trước khi chạy.

Đo lường thật: PROFILE

EXPLAIN chỉ nói optimizer định làm gì và ước lượng số dòng. Để biết thực tế truy vấn tốn bao nhiêu, dùng PROFILE — nó chạy thật câu truy vấn và đính kèm số liệu runtime cho từng toán tử. Quy trình hai bước:

-- 1) Chạy truy vấn có thu thập profile
PROFILE
SELECT a.branch_id, COUNT(*) AS n_txn, SUM(t.amount) AS total_amount
FROM transactions t
JOIN accounts a ON t.account_id = a.account_id
WHERE t.txn_date >= '2026-07-01'
GROUP BY a.branch_id;

-- 2) Xem kế hoạch đã được chú thích bằng số liệu thực thi
SHOW PROFILE;          -- hoặc SHOW PROFILE JSON; để lấy dạng máy đọc

SHOW PROFILE trả về đúng cây plan của EXPLAIN nhưng mỗi toán tử nay được gắn số đo thật: số dòng thực sự đi qua, thời gian thực thi, bộ nhớ dùng, và lượng dữ liệu qua mạng. Mô hình đọc:

Các trường số liệu quan trọng trong profile và ý nghĩa:

Trường (đại ý)Đọc gì từ nó
actual_row_countSố dòng thật qua toán tử. So với est_rows của EXPLAIN: lệch lớn ⇒ thống kê sai ⇒ optimizer chọn plan kém.
exec_timeThời gian toán tử đó tốn. Tìm toán tử ngốn thời gian nhất — đó là nút cổ chai.
memory_useBộ nhớ toán tử dùng (đặc biệt HashJoin/HashGroupBy build bảng băm). Cao quá có thể chạm giới hạn resource pool.
network_trafficByte qua mạng ở Repartition/Broadcast/Gather. Con số lớn ở Repartition = reshuffle đắt → cân nhắc lại shard key.
start_time / độ lệch giữa partitionNếu một partition xong muộn hẳn ⇒ data skew (dữ liệu lệch), thường do shard key phân bố không đều.

Nói cách khác: EXPLAIN để đọc chiến lược, PROFILE để đo thực tế và tìm nút cổ chai. Quy trình tối ưu điển hình là chạy PROFILE, tìm toán tử có exec_time/network_traffic lớn nhất, đối chiếu actual_rows với ước lượng; nếu lệch nhiều thì cập nhật thống kê (ANALYZE TABLE), nếu nghẽn ở Repartition thì xử lý ở tầng thiết kế shard key/index.

Vì sao biên dịch + MPP nhanh (và khi nào không)

Gộp lại, tốc độ của SingleStore đến từ ba tầng cộng hưởng:

  1. Bỏ overhead diễn giải nhờ code generation — vòng lặp xử lý dòng thành native code.
  2. Song song hai mức: nhiều leaf cùng chạy, và trong mỗi leaf nhiều partition chạy đồng thời (MPP).
  3. Pushdown: đẩy lọc + tổng hợp cục bộ xuống sát dữ liệu để giảm dữ liệu qua mạng trước khi gather.

Nhưng cùng cơ chế đó tạo ra các cạm bẫy đã nêu: cache miss vì không tham số hoá (trả giá biên dịch lặp lại), và reshuffle/broadcast bảng lớn (trả giá mạng). Cả hai đều lộ ra trong EXPLAIN/PROFILE/SHOW PLANCACHE — biết đọc ba công cụ này là kỹ năng cốt lõi khi vận hành SingleStore.

Use case thực tế

Bối cảnh (minh hoạ): API chấm điểm rủi ro của NCB gọi một truy vấn tổng hợp lịch sử giao dịch 90 ngày cho mỗi tài khoản, chạy ~2 triệu lần/ngày. transactions (~5 tỷ dòng, columnstore) shard theo account_id.

  • Ban đầu: đội backend ghép thẳng account_id và ngày vào chuỗi SQL (không tham số hoá). SHOW PLANCACHE cho thấy hàng trăm nghìn hình dạng khác nhau, mỗi cái biên dịch ~60ms rồi gần như không dùng lại → CPU aggregator phí vào biên dịch, p99 latency ~140ms.
  • Sửa 1 — tham số hoá: đổi sang prepared statement với tham số. Giờ chỉ còn một hình dạng trong plan cache, cache hit ~100%, cắt hẳn chi phí biên dịch. p99 xuống ~35ms.
  • Sửa 2 — bỏ reshuffle: một truy vấn báo cáo join transactions với customers (shard theo customer_id) hiện Repartition [customer_id] với network_traffic ~220MB/lần trong PROFILE. Vì transactions cũng có cột customer_id, đội chuyển bảng phụ trợ/khoá để join trở thành collocated theo account_id, hoặc dùng customers làm reference table — plan mới không còn Repartition, thời gian truy vấn báo cáo giảm ~60%.

Các con số trên là minh hoạ, nhưng phản ánh đúng hai đòn bẩy tối ưu điển hình của SingleStore: plan cache (tham số hoá)loại bỏ reshuffle (shard key/reference).

Ghi nhớ

  • SingleStore biên dịch truy vấn ra mã máy (code generation / compiled query execution) rồi chạy MPP song song trên leaf — không diễn giải từng dòng như engine truyền thống.
  • Plan cache: biên dịch một lần, dùng lại cho các lần sau cùng hình dạng; plan giữ cả trên đĩa nên sống qua restart. Xem bằng SHOW PLANCACHE.
  • Tham số hoá literal → nhiều giá trị khác nhau dùng chung một plan. Không tham số hoá ⇒ cache miss liên tục ⇒ trả giá biên dịch — anti-pattern.
  • Query pushdown: aggregator đẩy lọc + tổng hợp cục bộ xuống leaf; Gather là ranh giới leaf↔aggregator, gộp partial aggregate để giảm dữ liệu qua mạng.
  • EXPLAIN đọc chiến lược (không chạy): nhận diện Repartition (reshuffle), Broadcast, Gather, ColumnStoreScan + SORT KEY.
  • PROFILE + SHOW PROFILE đo thực tế: actual_rows, exec_time, memory_use, network_traffic — tìm nút cổ chai.
  • actual_rows lệch nhiều so với ước lượng ⇒ thống kê sai (chạy ANALYZE TABLE); network_traffic lớn ở Repartition ⇒ reshuffle đắt, cân nhắc lại shard key (sharding & distributed join).
  • Nhanh nhờ: bỏ overhead diễn giải + song song leaf/partition + pushdown. Chậm khi: cache miss hoặc reshuffle/broadcast bảng lớn.

Nguồn tham khảo

  • SingleStore Documentation — "Query Compilation" / Code Generation (docs.singlestore.com)
  • SingleStore Documentation — "Distributed SQL" & Query Execution (Aggregator, Leaf, Gather, Repartition, Broadcast)
  • SingleStore Documentation — "EXPLAIN and PROFILE" (đọc kế hoạch phân tán và số liệu runtime)
  • SingleStore Documentation — "Plan Cache" / SHOW PLANCACHE (Management & Query Tuning)
  • SingleStore Documentation — "Optimizing Table Data Structures" (SHARD KEY, SORT KEY, columnstore scan)
  • SingleStore Engineering Blog — bài về code generation / compiled query execution (nền tảng từ thời MemSQL)
  • MySQL Documentation — dev.mysql.com (phần cú pháp tương thích: prepared statement / tham số hoá)

Bài viết liên quan

Khoá chính/ngoại/tổng hợp, ràng buộc (NOT NULL, UNIQUE, CHECK, FK) và cách mô hình hoá quan hệ 1:1, 1:n, n:n cho hệ khách hàng — tài khoản — giao dịch. Đi qua chuẩn hoá 1NF/2NF/3NF bằng ví dụ trước/sau cụ thể, rồi bàn khi nào nên cố tình phi chuẩn hoá để đọc nhanh — giúp thiết kế lược đồ đúng ngay từ đầu.

13 thg 7, 2026 5
SQL & Databases
Nổi bật

Index (B-Tree) giúp database tìm dữ liệu theo O(log n) thay vì quét tuần tự O(n). Bài giải thích cấu trúc B-Tree, các loại index (hash, composite, partial, covering), khi nào optimizer bỏ index, cách đọc EXPLAIN/EXPLAIN ANALYZE (seq vs index scan, cost, rows, kiểu join), selectivity, leftmost prefix và các mẫu tối ưu: SARGable, keyset pagination, diệt N+1.

13 thg 7, 2026 5

Kiến trúc shared-nothing của SingleStore: Master Aggregator giữ metadata và điều phối, Child Aggregator scale kết nối, Leaf node chứa dữ liệu chia thành partition. Bài mổ xẻ luồng một query (aggregator nhận → pushdown xuống leaf → gộp kết quả) và cơ chế High Availability master/replica, failover, redundancy level.

15 thg 7, 2026 5

Nhập môn dữ liệu không gian (spatial/geospatial): dữ liệu gắn vị trí trên Trái Đất, các loại hình học điểm/đường/vùng, hệ toạ độ CRS/SRID (WGS84, UTM/VN2000), vector vs raster, quan hệ và phép đo không gian. Đặt nền cho cả series GIS và giá trị của nó với ngân hàng NCB: mạng lưới chi nhánh/ATM, phân tích khách hàng theo địa bàn, rủi ro và gian lận theo vị trí.

13 thg 7, 2026 5

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!