Xây dựng một engine Datalog tương tác bằng Rust
(github.com/frankmcsherry)- Từ những giới hạn về khả năng sử dụng và hiệu năng của các công cụ Datalog được bộc lộ tại workshop về lập trình logic, thử nghiệm datatoad — một shell Datalog tương tác dựa trên Rust — đã bắt đầu
- datatoad hướng tới cấu trúc cho phép thêm quy tắc trong lúc chạy và liên tục suy dẫn các fact mới; nó giảm chi phí xử lý trùng lặp trong tập fact bằng lưu trữ
columnarvà tầng LSM - Việc đánh giá quy tắc chuyển phần thân Datalog thành bài toán join để xử lý, và tùy theo trạng thái
stablemà tách thành đánh giá toàn bộ và đánh giá tăng dần, tránh các join stable-stable đã tính trước đó - Trong thử nghiệm với bộ dữ liệu Graspan, chỉ bằng cách viết lại quy tắc thủ công và đưa thêm các quan hệ trung gian, phân tích aliasing đã giảm từ 736,34 giây·50,13GB xuống 119,34 giây·5,32GB
- Các công việc tiếp theo mở rộng sang tối ưu hóa kế hoạch join dựa trên e-graph, layered trie, biểu diễn byte độ rộng cố định, spill ra đĩa, đánh giá phân tán, streaming join và demand transform
Vấn đề datatoad muốn giải quyết
- Vào cuối tuần Memorial Day, tại workshop về lập trình logic, sự bất tiện của các công cụ Datalog dùng cho phân tích chương trình trở nên nổi bật; từ đó nảy sinh thử nghiệm xây dựng một triển khai Datalog đơn giản, dùng được và nhanh
- Đối tượng triển khai không phải là trình chạy ví dụ tĩnh, mà là một shell Datalog tương tác
- Có thể nạp hàng loạt fact
- Có thể thêm quy tắc mới trong lúc chạy
- Liên tục phản ánh kết quả của các quy tắc được thêm vào trạng thái hiện có
- Có thể theo dõi mã tại datatoad repository
- datafrog hiện có cung cấp thuật toán cốt lõi của một engine Datalog, nhưng ở dạng người dùng phải tự nối dây; datatoad tổ chức lại cùng ý tưởng đó theo cách dễ dùng hơn
- Trong ví dụ nullability của đồ thị dataflow
httpd, datatoad mất 8,3 giây với dữ liệuVec<String>và truy vấn chưa được biên dịch, chậm hơn khoảng 4 lần so với khoảng 2 giây của ví dụ datafrog dùng dữ liệu(u32, u32) - Với bài toán reachability, nó tạo ra cùng số tuple đầu ra như triển khai datafrog, nhưng chưa phải là trạng thái đã hoàn tất kiểm chứng tính đúng đắn tổng quát
Mô hình Datalog và cấu trúc shell
- Datalog là ngôn ngữ trong đó khi viết các quy tắc logic đơn giản, nó sẽ suy dẫn mọi fact có thể đạt tới từ các quy tắc đó
- Quy tắc gồm head và body
- Ví dụ:
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c). tri,edgelà các quan hệ, còna,b,clà biến- Biến xuất hiện trong head cũng phải có trong body
- Ví dụ:
- Fact được xử lý như quy tắc có body rỗng
- Ví dụ:
edge(1, 2) :- . - Có thể dùng nhiều head để viết nhiều fact cùng lúc
- Ví dụ:
- Nhờ tính đơn điệu của Datalog, ngay cả khi thêm quy tắc hoặc fact, tập fact đúng không bị thu hẹp; cùng một tập quy tắc đầu vào sẽ đạt tới cùng một kết quả bất kể thứ tự quy tắc
- Biểu diễn trong Rust xoay quanh ba cấu trúc
Rule,Atom,TermRule { head: Vec<Atom>, body: Vec<Atom> }Atom { name: String, terms: Vec<Term> }Term::Var(String)hoặcTerm::Lit(String)
- Việc lưu literal dùng
Vec<u8>thay vìString- Các tính chất cần thiết là tính bằng nhau của literal và một thứ tự sắp xếp tùy ý
- Việc các byte mang ý nghĩa
String,(u32, u32)hay ý nghĩa khác được để mở cho người dùng
- Trạng thái interpreter lưu cả quy tắc và fact
rules: Vec<Rule>facts: facts::Facts
- Khi shell parse dòng nhập thành Datalog, nó gọi
State::extendvàState::update; lệnh.listxuất ra tên từng quan hệ và số fact
Parse và lưu fact
- Parser nằm trong
parse.rs, dùng một dạng lấy từ cú pháp Soufflé - Biến bắt đầu bằng
? - Token được giới hạn ở
.,,,(,),:-,?; phần văn bản còn lại được xử lý như tên atom hoặc term - Tokenizer loại bỏ khoảng trắng và thay
:-bằng←để quét như một ký hiệu đơn - Việc parse quy tắc đọc các atom head cho tới turnstile, rồi đọc các atom body cho tới dấu chấm
- Atom gồm tên, ngoặc trái, danh sách term và ngoặc phải
- Term là biến nếu có
?, nếu không thì là literal
- Quy tắc sai trả về
None, và hiện chưa cho biết chi tiết phần nào bị sai - Để thêm quy tắc phủ định cần token
Exclamation, nhưng hiện chưa xử lý
Vòng đời của tập fact
- Cách lưu
Vec<Vec<String>>đơn giản có các cấp cấp phát lồng nhau nên bất lợi cho quản lý bộ nhớ - datatoad dùng
columnarđể chuyển kiểu Rust thành một số ít cấp phát tuyến tính với layout phẳng- Lưu byte của chuỗi, ranh giới chuỗi và ranh giới fact trong các mảng riêng
FactContainerbọc danh sách fact đã được sắp xếp và loại trùng, đồng thời dùng wrapper type để hàm ý bất biến sắp xếp/loại trùng- Vì container dạng cột trên thực tế là append-only và không phù hợp để thay đổi ở giữa, việc thêm fact mới dùng dạng log-structured merge-tree (LSM)
FactLSM { layers: Vec<FactContainer> }- Quản lý sao cho kích thước các tầng tăng theo cấp số nhân
- Duy trì trạng thái đã sắp xếp và loại trùng bằng cách hợp nhất các tầng có kích thước trong phạm vi gấp đôi nhau
FactBuildercó cả vùngactivechưa sắp xếp và có thể trùng lặp, cùng cáclayersđã sắp xếp và loại trùng- Fact của mỗi quan hệ di chuyển qua ba giai đoạn
to_add: fact mới đến nhưng chưa được kiểm tra có novel hay khôngrecent: distinct và vẫn cần xử lýstable: distinct fact đã được xử lý hoàn toàn
FactSet::advancechuyểnrecentsangstable, rồi loại khỏito_addcác fact đã có trongstableđể tạorecentmới
Đánh giá quy tắc là bài toán join
- Phần thân của quy tắc Datalog có thể xem là equi-join trong cơ sở dữ liệu quan hệ
- Ví dụ quy tắc tam giác như sau
tri(?a, ?b, ?c) :- edge(?a, ?b), edge(?b, ?c), edge(?a, ?c).
- Nếu liệt kê trực tiếp mọi phép gán biến thì dù là hữu hạn cũng sẽ quá nhiều, nên cần sắp xếp theo các cột khóa của biến chung rồi merge
- Triển khai rút gọn body từ phải sang trái
- Join hai quan hệ cuối để tạo quan hệ trung gian, rồi join tiếp với quan hệ bên trái
- Nếu chỉ có một body atom thì chỉ chuyển đổi sang dạng head
JoinPlanchứa các thông tin saubodysđể sắp xếp lại và lọc các body atom cho phù hợp với joinjoinschứa key arity và projection đầu ra của từng join trung gianheadsbiểu thị tọa độ hoặc literal sẽ chèn vào head atom- arity của join tạo head cuối cùng
- Kế hoạch hiện tại là right-linear join plan đơn giản
- Khi tạo
JoinPlan, dùng vị trí xuất hiện leftmost và rightmost của từng biến để xác định cần giữ giá trị biến đến lúc nào, rồi chia các cột body atom thành cột dead, key và value - Hàm cốt lõi là
implement_plan(rule, plan, pos, stable, facts)- Khi thêm quy tắc mới, bắt đầu với
stable = truetrên toàn bộ fact - Trong quá trình áp dụng lặp lại các quy tắc hiện có, dùng
stable = falseđể chỉ tính các suy dẫn mới
- Khi thêm quy tắc mới, bắt đầu với
Join tăng dần và merge join
- Join là song tuyến tính nên được phân rã như sau
(A + a) ⋈ (B + b) = A ⋈ B + A ⋈ b + a ⋈ B + a ⋈ b
A ⋈ Bđã được tạo giữa các phần ổn định thì không cần tính lại- Nếu chỉ cần các suy diễn mới, chỉ thực hiện ba phép join
A ⋈ ba ⋈ Ba ⋈ b
join_withbao gồm hoặc loại trừ join stable-stable tùy theo cờstablejointhực tế là merge join, quét tuần tự hai đầu vào đã được sắp xếp- Nếu khóa giống nhau, gọi
actioncho mọi tổ hợp ứng với khóa đó - Nếu khóa khác nhau, dùng
gallopđể nhanh chóng nhảy tới điểm có khả năng khớp tiếp theo
- Nếu khóa giống nhau, gọi
galloplà ý tưởng lấy từ EmptyHeaded: tiến theo cấp số nhân trong khi điều kiện đơn điệu còn đúng, rồi thu hẹp lại theo kiểu tìm kiếm nhị phân
Thử nghiệm phân tích Nullability
- Dữ liệu thử nghiệm là dữ liệu của dự án Graspan, và vẫn còn trên Google Drive
- Đầu vào phân tích dataflow có hai quan hệ
evànn(?a, ?b): giá trị?acó thể được dùng tại vị trí?be(?a, ?b): giá trị ở một vị trí?acó thể di chuyển sang vị trí khác?b
- Quy tắc reachability như sau
n(?a, ?c) :- n(?a, ?b), e(?b, ?c) .
- Với đầu vào
httpd,.listban đầu cho thấye: 9,905,624n: 138,331
- Chạy trực tiếp quy tắc mất khoảng 15 giây, và
ntrở thành 9,393,283 mục - Một lý do khiến chậm là quan hệ tạm
.temp-0-0-in, nơinđược sắp xếp lại theo khóa join, đã phình lên tới 9,393,283 mục - Khi người dùng viết lại quy tắc, hiệu năng được cải thiện
m(?loc, ?val) :- n(?val, ?loc) .m(?loc, ?val) :- m(?mid, ?val), e(?mid, ?loc) .
- Trên cùng đầu vào
httpd, sau khi viết lại, quy tắc thứ hai chạy ở mức 8,43 giây
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| datatoad | 8.43s | 24.33s | 55.01s |
| datafrog | 1.30s | 4.06s | 8.03s |
- Số liệu Graspan của
lnx_kernelcó dấu*; bài báo báo cáo cả tổng thời gian và do xung đột định danh đầu vào nên khó xem là đúng cùng một lần chạy - Nếu muốn so sánh với công cụ mà người thực hành sử dụng, Soufflé có thể là đối tượng phù hợp hơn
Phân tích aliasing và tối ưu thủ công
- Phân tích thứ hai là phân tích aliasing của Zheng và Rugina được Graspan đưa vào
- Có hai quan hệ đầu vào
A(?val, ?loc):?loc <- ?valD(?val, ?loc):?locđược dùng dưới dạng*?val
- Mục tiêu là tìm memory alias và value alias
- memory alias: hai biểu thức lvalue có thể trỏ tới cùng một vị trí bộ nhớ
- value alias: hai biểu thức có thể đánh giá ra cùng một giá trị con trỏ
- Ký hiệu trong bài báo có
^T,^?,^*^T: chuyển vị quan hệ^?: hạng mục tùy chọn^*: lặp 0 lần trở lên
- Trong Datalog,
^?được biểu diễn bằng cách tách thành hai quy tắc, còn^*được xử lý bằng cách thêm tường minh quan hệ identity - Lần chạy quy tắc ban đầu mất nhiều thời gian
- Sau khi nhập hai quy tắc khởi tạo identity cuối cùng, lần lượt mất 686,57 giây và 736,34 giây
- Tiến trình dùng 50,13GB
V: 361,947,256M: 92,806,768F: 2,669,647
- Dùng tường minh các quan hệ chuyển vị
-V,-M,-a,-dđể loại bỏ quan hệ tạm-in- Tổng 815,92 giây, khoảng 13,6 phút
- Bộ nhớ 31,96GB
- Cách tiếp cận của Zheng và Rugina là demand-driven và thực tế chỉ cần
M, nên inlineVvàoM- Không tạo 361,947,256 mục
-V - Bộ nhớ giảm xuống 18,96GB
- Không tạo 361,947,256 mục
- Đặt tên và tái sử dụng đoạn join lặp lại là
Fd, sau đó tính trực tiếpFdthay choFđể giảm cả vấn đề identity - Dạng cuối cùng đưa vào quan hệ trung gian
MFdcho kết quả sau- Thời gian chạy: 119,34 giây
- Bộ nhớ: 5,32GB
-M: 92,806,768Fd: 1,858,986MFd: 73,474,947
- Chỉ bằng viết lại thủ công, bộ nhớ và thời gian chạy được cải thiện gần 10 lần so với thử nghiệm ban đầu
- Tạo các kết quả trung gian được đặt tên cho phép phần nào trực tiếp cấu thành join plan kiểu bushy-tree mong muốn, nhưng nếu đặt tên cho một quan hệ thực ra không cần thiết như
V, hệ thống sẽ tạo nó với chi phí lớn
Truy vấn demand-driven và magic sets
- Truy vấn demand-driven là cách chỉ khám phá phần cần thiết cho một fact mục tiêu cụ thể
- Magic sets có thể được dùng như một lời giải xấp xỉ
- Đây là phép biến đổi nhúng target literal vào truy vấn
- Có thể nghĩ tới biểu thức bắt đầu không phải từ mọi
dmà chỉ từdquan tâm, nhưng nếu áp dụng đơn giản thì có thể sai
- Magic sets không phải đáp án tối ưu; tác giả dự định đọc thêm các bài báo liên quan để tìm cách tiếp cận có thể hiệu quả hơn
- Các liên kết liên quan như sau
Tối ưu join plan và IR song song dữ liệu
- Sức hấp dẫn của Datalog nằm ở chỗ nó bộc lộ một cách thuần túy vấn đề cốt lõi của tính toán song song dữ liệu là data rendezvous, hơn là bản thân Horn clause
- Quy tắc
h(x, y, z) :- b1(x, y), b2(y, z) .có thể được xem là bài toán gom cácxvàzliên quan về một chỗ cho từngy - Thao tác cơ bản của tính toán song song dữ liệu là gom record theo khóa và chuyển cho logic người dùng; join biểu diễn phần định tuyến chọn lọc trong đó
- IR đơn giản dùng các opcode sau
Var(String): collection có tênMap(Action): filter, hoán vị, projectionKey(usize): đánh dấu vài cột phía trước là khóaMul(usize): kết hợp nhiều collection có cùng độ dài khóa
- Sau đó
MapvàKeyđược gộp vàoAction.key_arity Actionchứa literal filter, bộ lọc bằng nhau của biến, projection và key arity- Điểm khởi đầu đơn giản nhất là cross join tất cả body atom rồi đặt filter và projection cho từng head; cách này tạo ra kết quả đúng nhưng hiệu năng rất tệ
Tối ưu hóa dựa trên e-graph
- Quá trình tối ưu hóa sử dụng e-graph và equality saturation
- Tài liệu tham khảo gồm trang web egg, bài viết trước đó và
egg - term graph được biểu diễn dưới dạng map các
ENode<T>có gắnId, chia sẻ các node giống nhau để biểu diễn chương trình gọn hơn - Có ba quy tắc e-graph đã áp dụng
MulPermute: biến các hoán vị đầu vào củaMul(k)thành tương đươngMulPartition: chiaMul(k)theo nhiều cách để tạo các dạng tương đươngMapPushdown: đẩyMapxuống dướiMul(2)để tạo dạng join có khóa
- Quy tắc ví dụ như sau
head(?a, ?b) :- a(?x, ?a), b(?y, ?x), b(?y, ?z), a(?z, ?b) .
- Sau equality saturation, chi phí được gán ở bước extraction
- Chi phí
Maplà số cột đầu ra - Chi phí
Mullà tổng số cột khóa và số cột không phải khóa của các đầu vào - Chi phí
Varlà 0 - Nếu hòa, tối thiểu hóa số lượng
Map, rồi đến số lượngMul
- Chi phí
- Trong ví dụ, kế hoạch được chọn được tìm thấy ở wave two với tối đa hai cột không tương quan
- map cho đầu vào
a,b - một lần join
- projection trung gian
- join với chính nó
- projection cuối cùng
- map cho đầu vào
- Việc tìm kiếm kế hoạch này mất khoảng 40ms ngay cả trong release build, và phần lớn thời gian được dùng cho equivalence saturation
Cập nhật thực thi kế hoạch tối ưu hóa
- Trong bản cập nhật 2025-06-29, phần thực thi kế hoạch đã tối ưu hóa được triển khai
- Kế hoạch được xuất ra dưới dạng
Vec<ENode<Op>>, nhưng khi thực thi thực tế không chạy từng node một cách độc lập - Cách thực thi dự định như sau
- Với mỗi
Var, áp dụng nhiềuMapphụ thuộc vào nó trong một lần quét collection bên ngoài - Với mỗi
Mul, áp dụng nhiềuMapphụ thuộc vào nó trong một lần quét join
- Với mỗi
Op::Map(action)không hẳn là một phép toán được thực hiện trực tiếp, mà là tác vụ được đưa vào hàng đợi cho phép toán phụ thuộc- Để làm việc này,
TempActionđược đưa vào- bộ lọc literal
- bộ lọc đẳng thức biến
- projection có thể chứa tham chiếu cột hoặc string literal
- Bước chuẩn bị kế hoạch thực thi tách body và head, rồi gom các action
Maptheo từng node đầu vào - Khi thực thi
Var, việc xử lý tên được chia thành ba trường hợp- tạo head thì dùng tên quan hệ head
- biến đổi identity thì tái sử dụng tên đầu vào hiện có
- biến đổi không trivial thì lưu vào tên tạm
.temp-*
- Thực thi
Mul(2)kiểm tra key arity và tên của hai đầu vào, rồi gọijoin_withđể tích lũy kết quả vào nhiều builder - Trong ví dụ aliasing phức tạp, thực thi kế hoạch đã tối ưu hóa mất 114,28 giây, nhanh hơn khoảng 5 giây so với mức 119 giây trước đó, nhưng lý do chưa rõ ràng
- Tối ưu hóa multi-rule chưa được triển khai, và có thể cần suy nghĩ lại chính cách tiếp cận bằng kế hoạch
Tối ưu hóa biểu diễn fact
- Đã giảm 50GB xuống 5GB, nhưng vẫn được xem là dùng nhiều bộ nhớ hơn cần thiết khoảng 10 lần
- largest layer của quan hệ lớn
-Mchứa 57.289.225 fact và dùng khoảng 2.098.253.766 byte- ranh giới fact: 458.313.800 byte
- ranh giới term: 916.627.600 byte
- dữ liệu byte thực tế: 723.312.366 byte
- Tối ưu hóa đầu tiên tận dụng việc arity là cố định
- Nếu mọi fact đều có 2 cột, ranh giới fact có thể được biểu diễn chỉ bằng stride và length
- Về thực chất loại bỏ dữ liệu ranh giới ở mức 458MB
- Tối ưu hóa thứ hai là làm cho độ dài term bằng nhau
- Nếu biến số thành chuỗi fixed-width 7 chữ số, ranh giới term cũng có thể được biểu diễn bằng stride và length
- Đổi lại, số byte thực tế có thể tăng
- Tối ưu hóa thứ ba là biểu diễn số bằng binary thay vì text
- Số 7 chữ số nằm gọn trong
u324 byte - Vì cũng nằm gọn trong 3 byte, kết quả là
57,289,225 × 2 × 3 = 343,735,350byte - Giảm từ khoảng 2GB xuống khoảng 350MB, tức giảm khoảng 6,10 lần
- Số 7 chữ số nằm gọn trong
- Nếu nén sự lặp lại của term đầu tiên thì còn giảm thêm
- Có 57.289.225 fact, nhưng distinct first term chỉ có 1.147.612
- Nếu lưu dưới dạng
(Term, [Term])thay vì(Term, Term), largest layer giảm xuống mức 184.491.407 byte - Giảm khoảng 11,37 lần so với 2GB ban đầu
- Trong bản cập nhật 2025-07-02, tối ưu hóa bước 1 đã được áp dụng
- largest batch trở thành 343.735.382 byte, nhiều hơn giá trị lý thuyết 343.735.350 byte đúng 32 byte
- Thời gian thực thi giảm từ khoảng 115 giây xuống khoảng 95 giây, cải thiện khoảng 20%
Biểu diễn layered trie
- Trong bản cập nhật 2025-07-20, layered trie đã có thể hoạt động
- So sánh biểu diễn row-oriented và biểu diễn layered trie column-oriented
toad-rowtoad-col
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| toad-row | 3.88s | 11.30s | 25.67s |
| toad-col | 3.47s | 11.94s | 23.09s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 8.4h | 6.0h* | 1.7h* |
| toad-row | 28.21s | 28.25s | 7.62s |
| toad-col | 19.39s | 21.96s | 9.48s |
| datafrog | UNK | UNK | UNK |
- layered trie là phương thức hạn chế sự lặp lại của các cột phía trước trong biểu diễn row đã sắp xếp
- Mỗi cột là một danh sách các danh sách giá trị
- Mỗi danh sách là danh sách các giá trị distinct đã sắp xếp, tương ứng với prefix của cột trước đó
- Số danh sách trong một cột bằng tổng số item của cột trước đó
- Có thể xem như một cây, nhưng triển khai thực tế vẫn giữ theo dạng cột
- Ưu điểm là có thể thực hiện tìm kiếm, join, difference và merge theo từng đơn vị prefix
- Khi hầu như không có giá trị distinct và xem toàn bộ hàng một lần sẽ tốt hơn, cách tiếp cận row-oriented có thể có lợi hơn
- Đưa vào trait
FactContainerlàm abstraction chungformlenapplyjoinexceptmerge
applydùng stack tường minh thay vì đệ quy để theo dõi range của từng layer, tạo hàng và chuyển cho actionalignlà helper chung để khớp prefix của hai layered trieOrdering::Less: range chỉ có trong selfOrdering::Greater: range chỉ có trong otherOrdering::Equal: prefix có độ dài arity chung ở cả hai phía
join,except,mergeđều được triển khai trênalignjoinbung phần extension còn lại tại prefix chung để cross joinexceptgraft range chỉ có trong self vàoTrieBuildermergegraft các range self-only, other-only và equal đúng một lần tùy trường hợp
Tối ưu hóa fixed-width và hiệu năng
- Khi có thể nâng cấp lên fixed-width
[u8; 4], hiệu năng so sánh được cải thiện đáng kể - layered trie có thể áp dụng tối ưu hóa fixed-width theo từng cột, nên về dài hạn có khả năng lợi thế hơn row
upgrade·downgradeđã được áp dụng dễ dàng choexceptvàmerge, nhưng áp dụng chojoinkhó hơn do vấn đề kiểu của Rust
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| toad-row | 3.88s | 11.30s | 25.67s |
| ^-- +opt | 3.11s | 9.49s | 19.83s |
| toad-col | 3.47s | 11.94s | 23.09s |
| ^-- +opt | 2.55s | 9.13s | 15.95s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 8.4h | 6.0h* | 1.7h* |
| toad-row | 28.21s | 28.25s | 7.62s |
| ^-- +opt | 23.31s | 23.08s | 6.73s |
| toad-col | 19.39s | 21.96s | 9.48s |
| ^-- +opt | 14.26s | 16.45s | 8.33s |
| datafrog | UNK | UNK | UNK |
- Hiện khoảng 2/3 thời gian được tiêu tốn dưới
join - Cho rằng vẫn còn dư địa tối ưu hóa join thêm ít nhất khoảng 2 lần nữa
- Thử nghiệm chỉ đơn giản sắp xếp lại inner loop không tạo ra cải thiện đo được
Chuyên biệt hóa xuống mức mã đã biên dịch
- Khi phát hiện các term có cùng độ dài và các fact có cùng arity, có thể xem
Vec<u8>nhưVec<[[u8; B]; T]> - Dạng này giúp Rust hiểu rõ hơn hình dạng dữ liệu, giảm chi phí kiểm tra bounds·length, và khiến phép so sánh đặc biệt rẻ
- Phép so sánh được dùng ở nhiều điểm trong datatoad
- Sắp xếp và loại trùng batch fact
- Hợp nhất batch
- Hợp nhất khóa join
- So sánh fact mới với fact hiện có để lọc
- Hiệu năng chuẩn như sau
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| datatoad | 7.44s | 17.26s | 42.25s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 8.4h | 6.0h* | 1.7h* |
| datatoad | 101.24s | 96.36s | 20.20s |
| datafrog | UNK | UNK | UNK |
- Tối ưu hóa sắp xếp được thử nghiệm bằng cách
unsafetransmuteVec<u8>thànhVec<[u8; 8]>rồi sort·dedup
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 7.44s | 17.26s | 42.25s |
| dt-sort | 4.99s | 13.55s | 32.15s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 101.24s | 96.36s | 20.20s |
| dt-sort | 52.99s | 53.19s | 11.20s |
| datafrog | UNK | UNK | UNK |
- Tối ưu hóa hợp nhất được triển khai theo cách đơn giản: nối hai input rồi sort·dedup
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 7.44s | 17.26s | 42.25s |
| dt-sort | 4.99s | 13.55s | 32.15s |
| dt-both | 3.71s | 11.23s | 23.58s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 101.24s | 96.36s | 20.20s |
| dt-sort | 52.99s | 53.19s | 11.20s |
| dt-both | 31.32s | 30.08s | 8.56s |
| datafrog | UNK | UNK | UNK |
- Vẫn chưa đạt tới hiệu năng đã biên dịch của datafrog
- Chi phí so sánh còn lại cũng nằm trong join và antijoin; dự định sẽ xem xét lại các cơ hội tối ưu hóa tương tự khi chuyển sang cấu trúc dựa trên trie
- Đang tìm cách loại bỏ
unsafe, đồng thời bổ sung kết luận rằng nên tránh mã unsafe
Những việc còn lại
-
Spill ra đĩa
- Kho lưu trữ
columnargồm một số ít vùng cấp phát lớn - Khi tạo, có thể ghi ra file thay vì bộ nhớ và dùng lại bằng memory map
- Kho lưu trữ
-
Đánh giá phân tán
- Join, loại trùng và kiểm tra distinctness dựa trên tính bằng nhau của khóa
- Có thể phân tán khóa và dữ liệu tương ứng cho nhiều worker
- Có thể mở rộng tới nhiều process bằng
timely_communication
-
Đánh giá quy tắc dạng streaming
- Join hiện tại dùng binary join và materialized output
- Nếu có các index cần thiết, có thể tạo kế hoạch không materialize trạng thái nội bộ
- Worst-case optimal join cũng sẽ được xử lý ở đây
-
Chuyên biệt hóa custom representation
- Có ý tưởng phát hiện transitive closure và chuyên biệt hóa cho strongly connected component decomposition
- Equivalence relation có thể dùng cấu trúc dữ liệu union-find
bddbddbvà factorized databases cũng được đề cập như các chủ đề liên quan
-
Tìm kiếm các fact liên quan
- Cần hiểu và áp dụng demand transform
- Được xem là phép biến đổi cần thiết cho khám phá Datalog tương tác
1 bình luận
Ý kiến trên Hacker News
Thấy bài này leo lên vị trí số 1 cũng thú vị
Hiện tôi đang làm một game chiến thuật thời gian thực bằng Differential Datalog và Rust, trong đó logic game do DDL quản lý
Phần lớn chỉ là cái cớ để tiếp xúc với ý tưởng mới và thử một màn yak shaving bất tận
https://github.com/vmware-archive/differential-datalog
Nhân tiện, nhóm ddlog giờ đã lập startup Feldera, và việc dùng trực tiếp DBSP trong Rust cũng đáng cân nhắc
https://github.com/feldera/feldera
https://github.com/salsa-rs/salsa
DDLog không còn được bảo trì tích cực nữa nên càng thú vị hơn
“Ta, một kẻ phản diện khét tiếng, được mời đến, nửa tin rằng mình sắp nhận sự trừng phạt lẽ ra đã phải đến từ lâu.” — đây là câu mở đầu hay nhất trong các bài blog kỹ thuật tôi đọc năm nay
Những đoạn chen ngang của người kể chuyện cũng rất tuyệt, và hiếm có bài nào vừa sâu về kỹ thuật vừa đọc thú vị như vậy
Hành trình tối ưu hóa truy vấn alias có cảm giác như tiểu thuyết trinh thám; độc giả cũng cùng rên rỉ trước mức dùng bộ nhớ 50GB, rồi reo lên khi nó giảm xuống 5GB
Cả code lẫn bài viết đều xuất sắc
Tôi đã làm được một ít việc trong quá trình port mangle datalog sang Rust
Nó nằm ở https://github.com/google/mangle/tree/main/rust, trong cùng repository với bản triển khai Go
Vì mức ưu tiên không cao và còn có hội chứng hệ thống thứ hai, tiến độ khá chậm
Mangle Rust đi theo hướng xử lý dữ liệu kích thước tùy ý bằng cách đọc/ghi các fact trên đĩa qua memory mapping, còn bản Go là kiểu in-memory
Bài này hay vì vừa parse Datalog vừa đụng tới LSM tree, và dễ theo dõi hơn nhiều so với các tài liệu liên quan đến datafrog
Trong Rust có nhiều triển khai Datalog dùng procedural macro như ascent, crepe, nhưng nhược điểm là khó nhận truy vấn lúc runtime
Nếu dùng cho phân tích tĩnh với truy vấn và chương trình cố định, cách tiếp cận procedural macro có thể tốt hơn
Dù làn sóng phục hưng Datalog hiện tại có vẻ đang yếu đi, tôi vẫn vui khi thấy những người đam mê cốt lõi tiếp tục bám trụ
Hội nghị Datalog 2.0 gần đây nhỏ hơn trước khá nhiều, và hội nghị HYTRADBOI lần thứ hai cũng có tỷ trọng Datalog thấp
Ở HYTRADBOI đầu tiên, một phần tư bài gửi có liên quan đến Datalog
Việc những người khác chia sẻ các dự án Datalog gần đây cũng rất đáng khích lệ
Hiện tôi đang chuẩn bị cho một đợt migration phần mềm quy mô lớn và xây dựng pipeline chất lượng dữ liệu cho cơ sở dữ liệu SQL legacy
Nếu cấu trúc truy vấn tốt thì rất dễ đọc, nên tôi cho rằng Datalog hữu ích hơn SQL nhiều trong việc xác định và tìm các vấn đề chất lượng dữ liệu
Datalog 2.0 là một workshop vệ tinh của LPNMR, một hội nghị châu Âu tương đối ít được biết đến, và năm đó hội nghị tình cờ được tổ chức ở Dallas
Khi trực tiếp tham dự, tôi cũng thấy sự kiện hơi vắng; tôi có nộp bài cho workshop, nhưng không thấy nhiều người trong lĩnh vực đó
Ngoại lệ là một số người phía châu Âu giới thiệu solver Nemo khá nổi bật
Tôi nghĩ việc năm nay ít người tham dự phản ánh rõ hơn rằng đây là workshop vệ tinh của một hội nghị vốn không quá danh tiếng, và sự kiện chính lại là ICLP, hơn là phản ánh sự thiếu quan tâm đến triển khai Datalog
Tất nhiên tôi không định phản bác luận điểm lớn rằng gần như không còn nhiều cái mới trong việc triển khai engine Datalog thuần túy
Không gian nghiên cứu đã vượt xa mức đó và chuyển sang các vấn đề lạ hơn như streaming (HydroFlow), choice (Dusa), và những thứ gần với general chase (chase engine của Egglog)
Nhìn chung không mấy ai phản đối rằng Datalog vanilla khá nhàm chán, nhưng forward saturation đơn điệu và mệnh đề Horn là một baseline phong phú với địa hình kỹ thuật hiệu năng đã được hiểu rõ, nên rất phù hợp để xây các lý thuyết thú vị hơn như semiring hay Z-set
Nếu bạn thấy phần state machine và parsing thú vị, tôi cũng khuyên xem bài thuyết trình cũ của Rob Pike, Lexical Scanning in Go
https://www.youtube.com/watch?v=HxaD_trXwRE
Tuy viết bằng Go, phần lớn nội dung có thể áp dụng dễ dàng cho các ngôn ngữ khác
Thật tốt khi các ngôn ngữ hiện đại như Rust, Zig, Go hỗ trợ Unicode/rune/grapheme một cách native
So với Java, .NET, C++ hay các ngôn ngữ script, nhiều vấn đề đơn giản là biến mất
Nhìn chung tôi thích các công trình Datalog của tác giả, nhưng mong các tài liệu nhập môn đừng dạy binary join
Khi ra ngoài trường hợp lý tưởng, bên trong nhanh chóng trở nên lộn xộn, còn phương thức kiểu general join thì dễ tổng quát hóa trong đầu hơn nhiều
https://en.wikipedia.org/wiki/Worst-case_optimal_join_algorithm
https://github.com/frankmcsherry/blog/blob/master/posts/2025-05-29.md
Từ lâu khi còn đi học tôi có tiếp xúc sơ qua với Prolog, và đại khái biết nó dùng vào đâu, hữu ích cho việc gì, nhưng không phải là hiểu sâu
Sau đó tôi cứ nghe nói Datalog rất tuyệt, nhưng thật ra vẫn chưa nắm được Datalog cải thiện điều gì so với Prolog
Vừa lướt qua trang Datalog trên Wikipedia thì có vẻ Prolog có hiệu năng tương đối kém, còn Datalog thì giảm bớt sức biểu đạt và tính năng để đổi lấy cải thiện hiệu năng lớn, cho phép xử lý các tập dữ liệu lớn hơn và song song hóa nhiều hơn
Có vẻ điều này cũng bao gồm việc mất tính Turing-complete, nhưng tôi tò mò không biết đó có phải điểm cốt lõi hay tôi đang hiểu sai hoàn toàn
cut không chỉ ngăn các tính toán lãng phí mà còn có thể ảnh hưởng đến kết quả
Ngược lại, Datalog nhìn chung gần với một cơ sở dữ liệu quan hệ có cú pháp khác hơn
Một không gian tìm kiếm khổng lồ nhưng tầm thường trong Prolog có thể ngốn quá nhiều bộ nhớ trong Datalog đến mức không thể biểu diễn được
Datalog giống một chiếc xe đi làm gắn hộp số CVT, còn Prolog gần với xe F1 hơn
Thay vì là một cải tiến, nó giống như cắt bớt một phần Prolog để người ta không tự bắn vào chân mình, và cũng dễ triển khai, nhúng vào các ứng dụng khác hơn nhiều
Nếu đã quen với Prolog thì Datalog nhìn chung sẽ gây cảm giác tù túng
Không có
call/3, không có term/goal expansion, và về cơ bản Datalog được thiết kế bằng cách rút ra mẫu số chung tối thiểu của Prolog để dùng cho tìm kiếm cơ sở dữ liệu tương tácMã Datalog nhanh thì dễ viết, nhưng trần khả năng cũng thấp hơn nhiều
Prolog cũng có thể được viết theo cách cho phép đồng thời, nhưng đó là một bài toán trung cấp đòi hỏi phải hiểu cách triển khai
Guarded Horn Clauses và các ngôn ngữ phái sinh được phát triển để hình thức hóa những phần như vậy, nhưng các phát triển ở Nhật sau Prolog thì rất khó hiểu
Hiệu năng Prolog phụ thuộc rất nhiều vào lập trình viên, bản triển khai được dùng và nơi nó được dùng
Giống Lisp, Prolog cũng có thể được dùng để sinh mã máy native từ DSL tại thời điểm biên dịch
Nếu hiểu cách bản triển khai nền tảng hoạt động và viết mã thuận theo nó thì đủ nhanh
Tuy nhiên để làm vậy cần viết mã Prolog với một bản triển khai trong vài năm
Cũng có nhiều nghiên cứu về tối ưu hóa trình biên dịch Prolog, và có cả các bản triển khai độc quyền
http://logicprogramming.stanford.edu/readings/ullman.pdf
https://www.ueda.info.waseda.ac.jp/AITEC_ICOT_ARCHIVES/ICOT/Museum/IFS/abst/078.html
https://www.sciencedirect.com/science/article/pii/S0743106696889813
https://link.springer.com/content/pdf/10.1007/3-540-18024-9_26.pdf
https://sicstus.sics.se/
Nếu muốn dùng Datalog và Rust, cozodb được viết bằng Rust và cũng cung cấp cú pháp truy vấn Datalog
Khoảng tháng 11/2024 tôi xem thử và đã tìm thấy vài điểm có thể cải thiện dễ dàng trong backend lưu trữ SQLite
https://github.com/cozodb/cozo/issues/285
Tôi cũng đã dùng nó cho phân tích tĩnh chương trình, và nội bộ nó sử dụng cây sắp xếp cùng các kỹ thuật kiểu
Tài liệu đủ tốt để đem so sánh với phần giải thích từng bước trên blog, và đặc biệt công việc tối ưu hóa truy vấn rất thú vị
Tuy nhiên nếu không làm việc trong bộ nhớ từ Rust thì chi phí tuần tự hóa dữ liệu lớn, và dự án này, nói tích cực nhất, cũng đang khá im ắng
Trước đây, các fan Clojure từng nói rằng Datalog tốt hơn SQL, và thật đáng tiếc khi cơ sở dữ liệu quan hệ hầu hết đều dùng SQL.
Tôi chưa đào sâu đủ để hiểu vì sao họ nghĩ như vậy.
http://canonical.org/~kragen/binary-relations bắt đầu bằng một truy vấn phi đệ quy đơn giản, nhưng bản dịch sang SQL đã gần như “phạm tội”, còn lời giải SQL được tách bạch tử tế thì đáng bị xử tử.
Gần đây ANSI SQL đã bổ sung tính năng đệ quy nên không còn hoàn toàn bất khả thi nữa, nhưng có ba nhược điểm lớn.
Thứ nhất, nó vô tình biến SQL thành Turing-complete, trong khi truy vấn Datalog thì được đảm bảo kết thúc.
Thứ hai, nó vẫn cực kỳ cồng kềnh khi dùng.
Thứ ba, vì lý do thứ nhất, tính năng này thường không được triển khai đầy đủ, nên khó có thể tin cậy để dùng.
Nếu muốn thử Datalog trong một môi trường notebook thân thiện trên web, tôi khuyên dùng Percival.
https://percival.ink/
Trong các triển khai Datalog nói chung không có một chuẩn tương đương “ANSI SQL”, nhưng một khi nắm được ý tưởng cốt lõi thì các Datalog khác cũng không quá khó.
Cũng đã có một fork của Percival biên dịch Datalog sang SQLite, nên nếu muốn xem hai bên biểu diễn cùng một thứ như thế nào thì có thể xem thử.
https://percival.jake.tl/
Aggregate và các join nâng cao hơn vẫn chưa hoàn thiện, nhưng dạng cơ bản hoạt động tốt.
Logica là một trình biên dịch Datalog→SQL nghiêm túc và hoàn thiện hơn nhiều do một nhà nghiên cứu của Google tạo ra; nó biên dịch sang BigTable, DuckDB và một số phương ngữ SQL.
https://logica.dev/
Lĩnh vực mà Datalog dễ hơn từ một bậc độ lớn trở lên là khi xử lý truy vấn/quy tắc đệ quy.
SQL cũng làm được, nhưng cảm giác gần như hút Play-Doh qua ống hút.
Materialize.com của Frank có một dạng SQL “WITH MUTUALLY RECURSIVE” tốt hơn nhiều so với cách đệ quy ANSI SQL cũ, và Notion đang đánh giá nó cho truy vấn tải trang và đồng bộ dữ liệu.
https://materialize.com/blog/recursion-in-materialize/
Feldera cũng có một dạng tương tự cho view đệ quy.
https://www.feldera.com/blog/recursive-sql-queries-in-feldera
Tôi thích Feldera ở chỗ có thể viết mỗi “quy tắc” hoặc subview thành một câu lệnh riêng, thay vì nhét tất cả vào một câu lệnh khổng lồ.
Nhược điểm chính tôi thấy khi thử nghiệm là phương ngữ SQL của Feldera có khá nhiều ràng buộc kế thừa từ Apache Calcite, còn phương ngữ SQL của Materialize thì rất nỗ lực bám sát khả năng tương thích PostgreSQL.
Lại có bài mới của McSharry, tuyệt vời.
Lần cuối tôi kiểm tra thì có vẻ VMWare đã rời xa differential datalog.
https://www.feldera.com/
Có vẻ họ đã chuyển từ differential Datalog sang differential SQL, có lẽ vì nhận ra Datalog thực sự rất khó bán.