สร้างเอนจิน Datalog แบบโต้ตอบได้ด้วย Rust
(github.com/frankmcsherry)- การทดลอง datatoad ซึ่งเป็น เชลล์ Datalog แบบโต้ตอบได้ ที่พัฒนาด้วย Rust เริ่มต้นขึ้นจากข้อจำกัดด้านการใช้งานและประสิทธิภาพของเครื่องมือ Datalog ที่ปรากฏในเวิร์กช็อปด้าน logic programming
- datatoad ตั้งเป้าสถาปัตยกรรมที่สามารถเพิ่มกฎได้ระหว่างรันและอนุมานข้อเท็จจริงใหม่ต่อเนื่อง โดยใช้การจัดเก็บแบบ
columnarและ ชั้น LSM เพื่อลดต้นทุนการจัดการข้อมูลซ้ำในชุดข้อเท็จจริง - การประเมินกฎแปลงส่วน body ของ Datalog ให้เป็นปัญหา join แล้วประมวลผล และแยกการประเมินทั้งชุดกับ การประเมินแบบเพิ่มพูน ตามสถานะ
stableเพื่อหลีกเลี่ยงการ join แบบ stable-stable ที่คำนวณไปแล้ว - ในการทดลองกับชุดข้อมูล Graspan การวิเคราะห์ aliasing ลดลงจาก 736.34 วินาที·50.13GB เหลือ 119.34 วินาที·5.32GB ได้เพียงด้วยการเขียนกฎใหม่ด้วยมือและเพิ่มความสัมพันธ์กลาง
- งานต่อจากนี้ครอบคลุมการปรับเหมาะแผน join ด้วย e-graph, layered trie, การแทนข้อมูลแบบไบต์ความกว้างคงที่, disk spill, การประเมินแบบกระจาย, streaming join และ demand transform
ปัญหาที่ datatoad ต้องการแก้
- ในเวิร์กช็อป logic programming ช่วงสุดสัปดาห์ Memorial Day ความไม่สะดวกของเครื่องมือ Datalog สำหรับการวิเคราะห์โปรแกรมปรากฏชัด จึงเป็นจุดเริ่มต้นของความพยายามสร้าง Datalog implementation ที่ เรียบง่าย ใช้งานได้จริง และเร็ว
- สิ่งที่จะสร้างไม่ใช่ตัวรันตัวอย่างแบบคงที่ แต่เป็นเชลล์ Datalog แบบโต้ตอบได้
- สามารถโหลดข้อเท็จจริงจำนวนมากได้
- สามารถเพิ่มกฎใหม่ได้ระหว่างรัน
- นำผลของกฎที่เพิ่มเข้ามาไปสะท้อนกับสถานะเดิมต่อเนื่อง
- สามารถติดตามโค้ดได้ใน datatoad repository
- datafrog เดิมมีอัลกอริทึมแกนหลักของ Datalog engine อยู่แล้ว แต่ผู้ใช้ต้องเดินสายประกอบเอง จึงนำแนวคิดเดียวกันมาจัดใหม่ใน datatoad ให้อยู่ในรูปแบบที่ใช้งานง่ายกว่า
- ในตัวอย่าง nullability ของกราฟ dataflow
httpdนั้น datatoad ใช้เวลา 8.3 วินาทีกับข้อมูลVec<String>และคิวรีที่ยังไม่คอมไพล์ ซึ่งช้ากว่าตัวอย่าง datafrog ที่ใช้ข้อมูล(u32, u32)และใช้เวลาราว 2 วินาทีประมาณ 4 เท่า - สำหรับปัญหา reachability นั้นให้จำนวนเอาต์พุตทูเพิลเท่ากับ implementation ของ datafrog แต่ยังไม่ถือว่าผ่านการตรวจสอบความถูกต้องทั่วไปอย่างสมบูรณ์
โมเดล Datalog และโครงสร้างเชลล์
- Datalog เป็นภาษาที่เมื่อเขียนกฎตรรกะแบบง่าย ๆ แล้ว จะอนุมานข้อเท็จจริงทั้งหมดที่เข้าถึงได้จากกฎเหล่านั้น
- กฎประกอบด้วย head และ body
- ตัวอย่าง:
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c). tri,edgeคือความสัมพันธ์ และa,b,cคือตัวแปร- ตัวแปรที่ปรากฏใน head ต้องมีอยู่ใน body ด้วย
- ตัวอย่าง:
- ข้อเท็จจริงถูกจัดการเป็นกฎที่มี body ว่าง
- ตัวอย่าง:
edge(1, 2) :- . - สามารถใช้หลาย head เพื่อเขียนหลายข้อเท็จจริงพร้อมกันได้
- ตัวอย่าง:
- ด้วยคุณสมบัติ monotonicity ของ Datalog ต่อให้เพิ่มกฎหรือข้อเท็จจริง ชุดข้อเท็จจริงที่เป็นจริงก็จะไม่ลดลง และเมื่อใช้ชุดกฎอินพุตเดียวกันก็จะได้ผลลัพธ์เดียวกันโดยไม่ขึ้นกับลำดับของกฎ
- การแทนใน Rust มี 3 โครงสร้างหลักคือ
Rule,Atom,TermRule { head: Vec<Atom>, body: Vec<Atom> }Atom { name: String, terms: Vec<Term> }Term::Var(String)หรือTerm::Lit(String)
- การเก็บ literal ใช้
Vec<u8>แทนString- คุณสมบัติที่ต้องการคือความเท่ากันของ literal และลำดับการเรียงใด ๆ ก็ได้
- ว่าไบต์เหล่านั้นจะมีความหมายเป็น
String,(u32, u32)หรืออย่างอื่น เปิดให้ผู้ใช้กำหนดเอง
- สถานะของ interpreter เก็บทั้งกฎและข้อเท็จจริงไว้ด้วยกัน
rules: Vec<Rule>facts: facts::Facts
- เมื่อเชลล์พาร์สแต่ละบรรทัดอินพุตเป็น Datalog แล้ว จะเรียก
State::extendและState::updateและใช้คำสั่ง.listเพื่อพิมพ์ชื่อแต่ละความสัมพันธ์กับจำนวนข้อเท็จจริง
การพาร์สและการเก็บข้อเท็จจริง
- parser อยู่ใน
parse.rsและใช้รูปแบบที่นำมาจากไวยากรณ์ของ Soufflé - ตัวแปรขึ้นต้นด้วย
? - โทเคนจำกัดอยู่ที่
.,,,(,),:-,?ส่วนข้อความที่เหลือจะถูกจัดเป็นชื่อ atom หรือ term - tokenizer จะลบช่องว่างและแทน
:-ด้วย←เพื่อสแกนเหมือนเป็นสัญลักษณ์เดี่ยว - การพาร์สกฎใช้วิธีอ่าน head atom ไปจนถึง turnstile และอ่าน body atom ไปจนถึง period
- atom ประกอบด้วยชื่อ วงเล็บซ้าย รายการ term และวงเล็บขวา
- term ที่มี
?จะเป็นตัวแปร ถ้าไม่มีจะเป็น literal
- กฎที่ไม่ถูกต้องจะคืนค่า
Noneและตอนนี้ยังไม่บอกอย่างละเอียดว่าส่วนใดผิด - หากจะเพิ่มกฎปฏิเสธ ต้องมีโทเคน
Exclamationแต่ยังไม่รองรับ
วงจรชีวิตของชุดข้อเท็จจริง
- การเก็บแบบ
Vec<Vec<String>>อย่างง่ายมีการจัดสรรหน่วยความจำซ้อนกัน จึง ไม่เป็นมิตรต่อการจัดการหน่วยความจำ - datatoad ใช้
columnarเพื่อแปลงชนิดข้อมูล Rust ให้เป็นเลย์เอาต์แบบแบนด้วยการจัดสรรเชิงเส้นจำนวนน้อย- เก็บไบต์ของสตริง ขอบเขตของสตริง และขอบเขตของข้อเท็จจริงไว้ในอาร์เรย์แยกกัน
FactContainerห่อรายการข้อเท็จจริงที่ถูกจัดเรียงและลบข้อมูลซ้ำแล้ว โดยใช้ wrapper type เพื่อสื่อ invariant ว่าถูกจัดเรียงและไม่ซ้ำ- เนื่องจากคอนเทนเนอร์แบบ columnar แทบจะเป็น append-only จึงไม่เหมาะกับการแก้ไขระหว่างทาง และการเพิ่มข้อเท็จจริงใหม่จึงใช้รูปแบบ log-structured merge-tree (LSM)
FactLSM { layers: Vec<FactContainer> }- จัดการให้ขนาดของแต่ละชั้นโตแบบเรขาคณิต
- รวมชั้นที่มีขนาดต่างกันไม่เกิน 2 เท่าเพื่อคงสถานะเรียงลำดับและลบซ้ำ
FactBuilderมีทั้งพื้นที่activeที่ยังไม่เรียงและอาจซ้ำ และlayersที่เรียงและลบซ้ำแล้ว- ข้อเท็จจริงของแต่ละความสัมพันธ์จะเคลื่อนผ่าน 3 ระยะ
to_add: ข้อเท็จจริงที่เพิ่งเข้ามาใหม่แต่ยังไม่ยืนยันว่า novel หรือไม่recent: ข้อเท็จจริงที่ distinct แล้วแต่ยังต้องประมวลผลstable: ข้อเท็จจริงที่ distinct และประมวลผลครบแล้ว
FactSet::advanceจะย้ายrecentไปยังstableและลบข้อเท็จจริงในto_addที่มีอยู่ในstableแล้ว เพื่อสร้างrecentชุดใหม่
การประเมินกฎคือปัญหา join
- ส่วน body ของกฎ Datalog สามารถมองเป็น equi-join ในฐานข้อมูลเชิงสัมพันธ์ได้
- ตัวอย่างกฎสามเหลี่ยมมีดังนี้
tri(?a, ?b, ?c) :- edge(?a, ?b), edge(?b, ?c), edge(?a, ?c).
- หากไล่ enumerates การกำหนดค่าตัวแปรทั้งหมดโดยตรง แม้จะเป็นจำนวนจำกัดก็ยังมากเกินไป จึงใช้การเรียงตามคอลัมน์คีย์ของตัวแปรร่วมแล้วทำการ merge
- implementation ลดรูป body จากขวาไปซ้าย
- join ความสัมพันธ์สองตัวสุดท้ายเพื่อสร้างความสัมพันธ์กลาง แล้วจึง join กับความสัมพันธ์ทางซ้ายอีกครั้ง
- ถ้า body atom มีเพียงตัวเดียว ก็แค่แปลงรูปไปเป็นรูปของ head
JoinPlanเก็บข้อมูลดังนี้bodysสำหรับจัดเรียงใหม่และกรอง body atom ให้เหมาะกับการ joinjoinsที่เก็บ key arity และ output projection ของแต่ละ join กลางheadsที่ระบุตำแหน่งพิกัดหรือลิเทอรัลที่จะใส่ใน head atom- arity ของ join สุดท้ายที่ใช้สร้าง head
- ตอนนี้แผนที่ใช้เป็นเพียง right-linear join plan แบบง่าย
- ตอนสร้าง
JoinPlanจะใช้ตำแหน่งปรากฏซ้ายสุดและขวาสุดของตัวแปรแต่ละตัวเพื่อตัดสินว่าต้องเก็บค่าตัวแปรใดไว้ถึงเมื่อไร และแบ่งคอลัมน์ของ body atom ออกเป็นคอลัมน์ dead, key และ value - ฟังก์ชันแกนหลักคือ
implement_plan(rule, plan, pos, stable, facts)- เมื่อมีการเพิ่มกฎใหม่ จะเริ่มจากข้อเท็จจริงทั้งหมดด้วย
stable = true - เมื่อวนใช้กฎเดิมซ้ำ จะคำนวณเฉพาะสิ่งที่อนุมานใหม่ด้วย
stable = false
- เมื่อมีการเพิ่มกฎใหม่ จะเริ่มจากข้อเท็จจริงทั้งหมดด้วย
incremental join และ merge join
- join เป็นการดำเนินการแบบ bilinear จึงแยกได้ดังนี้
(A + a) ⋈ (B + b) = A ⋈ B + A ⋈ b + a ⋈ B + a ⋈ b
A ⋈ Bที่สร้างจาก stable ทั้งคู่แล้ว ไม่จำเป็นต้องคำนวณซ้ำ- ถ้าต้องการเฉพาะอนุพันธ์ใหม่ ก็ทำเพียงสาม join นี้
A ⋈ ba ⋈ Ba ⋈ b
join_withจะรวมหรือตัด stable-stable join ออกตามแฟลกstablejoinจริงเป็น merge join ที่ไล่อ่านอินพุตที่เรียงลำดับแล้วสองชุดตามลำดับ- ถ้าคีย์เท่ากัน จะเรียก
actionกับทุกคู่ผสมของคีย์นั้น - ถ้าคีย์ไม่เท่ากัน จะใช้
gallopกระโดดข้ามไปยังจุดที่มีโอกาสแมตช์ถัดไปอย่างรวดเร็ว
- ถ้าคีย์เท่ากัน จะเรียก
gallopเป็นแนวคิดที่นำมาจากฝั่ง EmptyHeaded โดยจะเดินหน้าแบบยกกำลังตราบใดที่เงื่อนไขเชิงโมโนโทนิกยังเป็นจริง แล้วค่อยบีบช่วงด้วยวิธีแบบไบนารี
การทดลองวิเคราะห์ Nullability
- ข้อมูลทดลองมาจากโครงการ Graspan และยังมีสำเนาอยู่ใน Google Drive
- อินพุตของการวิเคราะห์ dataflow มีสอง relation คือ
eและnn(?a, ?b): ค่า?aอาจถูกเขียนลงที่ตำแหน่ง?be(?a, ?b): ค่าที่ตำแหน่งหนึ่ง?aอาจเคลื่อนไปยังอีกตำแหน่ง?b
- กฎ reachability มีดังนี้
n(?a, ?c) :- n(?a, ?b), e(?b, ?c) .
- ในอินพุต
httpdค่าเริ่มต้น.listแสดงดังนี้e: 9,905,624n: 138,331
- ถ้ารันกฎนี้ตรงๆ จะใช้เวลาประมาณ 15 วินาที และ
nจะเพิ่มเป็น 9,393,283 รายการ - สาเหตุหนึ่งที่ช้าคือ relation ชั่วคราว
.temp-0-0-inซึ่งจัดเรียงnใหม่ให้ตรงกับ join key โตขึ้นถึง 9,393,283 รายการ - หากผู้ใช้เขียนกฎใหม่ ประสิทธิภาพจะดีขึ้น
m(?loc, ?val) :- n(?val, ?loc) .m(?loc, ?val) :- m(?mid, ?val), e(?mid, ?loc) .
- กับอินพุต
httpdชุดเดิม หลังเขียนใหม่ กฎข้อที่สองรันได้ราว 8.43 วินาที
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| datatoad | 8.43s | 24.33s | 55.01s |
| datafrog | 1.30s | 4.06s | 8.03s |
- ตัวเลขของ Graspan สำหรับ
lnx_kernelมีเครื่องหมาย*กำกับอยู่ โดยในงานวิจัยรายงานเวลารวมทั้งหมด และเพราะมีการชนกันของตัวระบุอินพุต จึงดูเป็นการรันแบบเดียวกันตรงๆ ได้ยาก - หากจะเทียบกับเครื่องมือที่ใช้ในงานจริง Soufflé อาจเป็นเป้าหมายที่เหมาะสมกว่า
การวิเคราะห์ Aliasing และการปรับให้เหมาะด้วยมือ
- การวิเคราะห์ชุดที่สองคือการวิเคราะห์ aliasing ของ Zheng และ Rugina ที่ Graspan นำมาใช้
- relation อินพุตมีสองชนิด
A(?val, ?loc):?loc <- ?valD(?val, ?loc):?locถูกใช้งานในรูป*?val
- เป้าหมายคือหาทั้ง memory alias และ value alias
- memory alias: นิพจน์ lvalue สองตัวอาจชี้ไปยังตำแหน่งหน่วยความจำเดียวกัน
- value alias: นิพจน์สองตัวอาจประเมินได้เป็นค่าพอยน์เตอร์เดียวกัน
- ในสัญลักษณ์ของงานวิจัยจะมี
^T,^?,^*^T: transpose ของ relation^?: เทอมแบบเลือกได้^*: ทำซ้ำ 0 ครั้งขึ้นไป
- ใน Datalog นั้น
^?แทนได้ด้วยการแยกเป็นสองกฎ และ^*จัดการโดยใส่ relation identity อย่างชัดเจน - การรันกฎเริ่มต้นใช้เวลานาน
- หลังป้อนกฎเริ่มต้น identity สองข้อสุดท้าย ใช้เวลา 686.57 วินาที และ 736.34 วินาที
- โปรเซสใช้หน่วยความจำ 50.13GB
V: 361,947,256M: 92,806,768F: 2,669,647
- มีการใช้ relation transpose
-V,-M,-a,-dอย่างชัดเจนเพื่อตัด relation ชั่วคราว-inออก- รวม 815.92 วินาที หรือราว 13.6 นาที
- หน่วยความจำ 31.96GB
- แนวทางของ Zheng และ Rugina เป็นแบบ demand-driven และในทางปฏิบัติต้องใช้เพียง
Mจึง inlineVเข้าไปในM- ไม่ต้องสร้าง
-Vจำนวน 361,947,256 รายการ - หน่วยความจำลดลงเหลือ 18.96GB
- ไม่ต้องสร้าง
- ตั้งชื่อชิ้น join ที่เกิดซ้ำว่า
Fdเพื่อใช้ซ้ำ และหลังจากนั้นคำนวณFdโดยตรงแทนFเพื่อลดปัญหา identity ด้วย - รูปแบบสุดท้ายที่เพิ่ม relation กลาง
MFdให้ผลลัพธ์ดังนี้- เวลาในการรัน: 119.34 วินาที
- หน่วยความจำ: 5.32GB
-M: 92,806,768Fd: 1,858,986MFd: 73,474,947
- เพียงแค่เขียนใหม่ด้วยมือ ก็ปรับปรุงทั้งหน่วยความจำและเวลาในการรันได้เกือบ 10 เท่าเมื่อเทียบกับความพยายามครั้งแรก
- การสร้างผลลัพธ์กลางที่มีชื่อ ทำให้สามารถประกอบ bushy-tree join plan ที่ต้องการได้โดยตรงในระดับหนึ่ง แต่ถ้าตั้งชื่อ relation ที่จริงแล้วไม่จำเป็นอย่าง
Vก็จะต้องเสียต้นทุนมากเพื่อสร้างมันขึ้นมา
คิวรีแบบ Demand-driven และ magic sets
- คิวรีแบบ demand-driven คือวิธีที่สำรวจเฉพาะส่วนที่จำเป็นต่อ fact เป้าหมายที่กำหนด
- magic sets อาจใช้เป็นคำตอบแบบประมาณได้
- เป็นการแปลงที่ฝัง target literal ลงไปในคิวรี
- อาจนึกถึงการเริ่มจาก
dที่สนใจแทนทุกdแต่ถ้าใช้แบบตรงๆ อย่างง่ายอาจผิดได้
- magic sets ไม่ใช่คำตอบที่เหมาะที่สุดเสมอไป และมีแผนจะอ่านงานที่เกี่ยวข้องเพิ่มเพื่อหาแนวทางที่อาจมีประสิทธิภาพกว่านี้
- ลิงก์ที่เกี่ยวข้องมีดังนี้
การปรับ join plan ให้เหมาะ และ IR สำหรับ data parallel
- เสน่ห์ของ Datalog ไม่ได้อยู่ที่ Horn clause เองเท่านั้น แต่อยู่ที่มันเปิดเผยปัญหาแกนหลักของการคำนวณแบบ data parallel อย่าง data rendezvous ได้อย่างบริสุทธิ์
- กฎ
h(x, y, z) :- b1(x, y), b2(y, z) .มองได้ว่าเป็นปัญหาการรวบรวมxและzที่เกี่ยวข้องมาไว้ที่เดียวกันสำหรับแต่ละy - การทำงานพื้นฐานของการคำนวณแบบ data parallel คือการรวบรวมเรกคอร์ดตามคีย์แล้วส่งต่อให้ตรรกะของผู้ใช้ โดย join เป็นการแสดงการกำหนดเส้นทางแบบเลือกเฉพาะกรณีชนิดหนึ่งในนั้น
- IR แบบง่ายใช้ opcode ต่อไปนี้
Var(String): คอลเลกชันที่มีชื่อMap(Action): filter, permutation, projectionKey(usize): ทำเครื่องหมายว่ากี่คอลัมน์ด้านหน้าเป็นคีย์Mul(usize): รวมหลายคอลเลกชันที่มีความยาวคีย์เท่ากัน
- ต่อมา
MapและKeyถูกรวมกันเป็นAction.key_arity Actionเก็บ literal filter, variable equality filter, projection และ key arity- จุดเริ่มต้นที่ง่ายที่สุดคือ cross join body atom ทั้งหมดก่อน แล้วค่อยวาง filter และ projection ตามแต่ละ head ซึ่งแม้จะได้คำตอบที่ถูกต้อง แต่ประสิทธิภาพจะแย่มาก
การเพิ่มประสิทธิภาพบนพื้นฐาน e-graph
- การเพิ่มประสิทธิภาพใช้ e-graph และ equality saturation
- ใช้ egg webpage, บทความก่อนหน้า และ
eggเป็นแหล่งอ้างอิง - term graph แทนด้วยแมปของ
ENode<T>ที่มีIdกำกับ และใช้โหนดเดียวกันร่วมกันเพื่อแทนโปรแกรมให้กระชับขึ้น - กฎ e-graph ที่นำมาใช้มีสามแบบ
MulPermute: ทำให้การเรียงลำดับอินพุตของMul(k)เป็นสมมูลกันMulPartition: แบ่งMul(k)ได้หลายแบบและทำให้เป็นสมมูลกันMapPushdown: ดันMapลงไปใต้Mul(2)เพื่อสร้างรูปแบบ join ที่มีคีย์
- กฎตัวอย่างมีดังนี้
head(?a, ?b) :- a(?x, ?a), b(?y, ?x), b(?y, ?z), a(?z, ?b) .
- หลัง equality saturation จะกำหนดต้นทุนในขั้น extraction
- ต้นทุนของ
Mapคือจำนวนคอลัมน์เอาต์พุต - ต้นทุนของ
Mulคือจำนวนคอลัมน์คีย์บวกกับผลรวมของจำนวนคอลัมน์ที่ไม่ใช่คีย์ของอินพุต - ต้นทุนของ
Varคือ 0 - ถ้าเสมอกัน จะลดจำนวน
Mapก่อน แล้วจึงลดจำนวนMul
- ต้นทุนของ
- ในตัวอย่าง แผนที่ถูกเลือกถูกพบใน wave two ซึ่งมีคอลัมน์ที่ไม่สัมพันธ์กันได้มากสุดสองคอลัมน์
- map สำหรับอินพุต
a,b - join หนึ่งครั้ง
- projection ขั้นกลาง
- join กับตัวเอง
- projection สุดท้าย
- map สำหรับอินพุต
- การสำรวจแผนนี้ใช้เวลาราว 40ms แม้ใน release build และเวลาส่วนใหญ่ถูกใช้ไปกับ equivalence saturation
อัปเดตการรันแผนเพิ่มประสิทธิภาพ
- ในอัปเดต 2025-06-29 ได้มีการทำให้การรันแผนที่เพิ่มประสิทธิภาพแล้วใช้งานได้จริง
- แผนออกมาเป็น
Vec<ENode<Op>>แต่ในการรันจริงจะไม่ได้รันแต่ละโหนดอย่างอิสระ - วิธีรันที่ตั้งใจไว้เป็นดังนี้
- สำหรับแต่ละ
Varจะใช้Mapหลายตัวที่พึ่งพามันภายในการสแกนคอลเลกชันภายนอกเพียงครั้งเดียว - สำหรับแต่ละ
Mulจะใช้Mapหลายตัวที่พึ่งพามันภายในการสแกน join เพียงครั้งเดียว
- สำหรับแต่ละ
Op::Map(action)ไม่ใช่โอเปอเรชันที่ถูกทำโดยตรง แต่เป็นงานที่ถูกคิวไว้ให้โอเปอเรชันที่มันพึ่งพา- เพื่อรองรับสิ่งนี้จึงเพิ่ม
TempAction- literal filter
- ตัวกรองความเท่ากันของตัวแปร
- projection ที่เก็บได้ทั้งการอ้างอิงคอลัมน์หรือสตริงลิเทอรัล
- ขั้นเตรียม execution plan แยก body และ head แล้วรวบรวม
Mapaction ตามโหนดอินพุต - การจัดการชื่อในระหว่างรัน
Varแบ่งเป็นสามกรณี- การสร้าง head จะเขียนลงในชื่อ relation ของ head
- การแปลงแบบ identity จะใช้ชื่ออินพุตเดิมซ้ำ
- การแปลงที่ไม่ trivial จะเก็บลงชื่อชั่วคราวแบบ
.temp-*
- การรัน
Mul(2)จะตรวจสอบ key arity และชื่อของอินพุตทั้งสอง แล้วเรียกjoin_withเพื่อสะสมผลลัพธ์ลงใน builder หลายตัว - ในตัวอย่าง aliasing ที่ซับซ้อน การรันแผนที่เพิ่มประสิทธิภาพแล้วใช้เวลา 114.28 วินาที เร็วกว่าก่อนหน้าที่อยู่ระดับ 119 วินาทีราว 5 วินาที แต่ยังไม่ชัดเจนว่าเพราะเหตุใด
- การเพิ่มประสิทธิภาพแบบ multi-rule ยังไม่ได้ทำ และอาจต้องกลับไปคิดใหม่แม้แต่แนวทางของแผนเอง
การเพิ่มประสิทธิภาพการแทน facts
- แม้จะลดจาก 50GB เหลือ 5GB ได้แล้ว แต่ก็ดูเหมือนว่ายังใช้หน่วยความจำมากกว่าที่จำเป็นราว 10 เท่า
- largest layer ของ relation ขนาดใหญ่
-Mเก็บ facts 57,289,225 รายการ และใช้ 2,098,253,766 ไบต์โดยประมาณ- fact boundaries: 458,313,800 ไบต์
- term boundaries: 916,627,600 ไบต์
- ข้อมูลไบต์จริง: 723,312,366 ไบต์
- การเพิ่มประสิทธิภาพอย่างแรกอาศัยข้อเท็จจริงที่ว่า arity คงที่
- หากทุก fact มี 2 คอลัมน์ fact boundaries สามารถแทนได้ด้วยแค่ stride และ length
- จึงแทบจะตัดข้อมูลขอบเขตขนาด 458MB นี้ทิ้งได้
- การเพิ่มประสิทธิภาพอย่างที่สองคือทำให้ความยาวของ term เท่ากัน
- หากแปลงตัวเลขเป็นสตริงความกว้างคงที่ 7 หลัก term boundaries ก็สามารถแทนด้วย stride และ length ได้
- แต่จำนวนไบต์จริงอาจเพิ่มขึ้นแทน
- การเพิ่มประสิทธิภาพอย่างที่สามคือแทนตัวเลขด้วย binary แทนข้อความ
- ตัวเลข 7 หลักเก็บใน
u32ขนาด 4 ไบต์ได้ - และยังเก็บใน 3 ไบต์ได้ด้วย จึงเหลือ
57,289,225 × 2 × 3 = 343,735,350ไบต์ - ลดจากราว 2GB เหลือราว 350MB หรือประมาณ 6.10 เท่า
- ตัวเลข 7 หลักเก็บใน
- หากบีบอัดการซ้ำของ term ตัวแรก ก็ลดลงได้อีก
- แม้มี facts 57,289,225 รายการ แต่ first term ที่ไม่ซ้ำมีเพียง 1,147,612 รายการ
- หากเก็บเป็นรูป
(Term, [Term])แทน(Term, Term)largest layer จะลดเหลือราว 184,491,407 ไบต์ - เทียบกับ 2GB แรกเริ่มแล้วลดลงราว 11.37 เท่า
- ในอัปเดต 2025-07-02 ได้ใช้การเพิ่มประสิทธิภาพชุดแรกแล้ว
- largest batch มีขนาดเป็น 343,735,382 ไบต์ ซึ่งคือค่าเชิงทฤษฎี 343,735,350 ไบต์บวกเพิ่ม 32 ไบต์
- เวลารันลดจากราว 115 วินาทีเหลือราว 95 วินาที หรือดีขึ้นประมาณ 20%
การแทนแบบ layered trie
- ในอัปเดต 2025-07-20 layered trie ใช้งานได้แล้ว
- มีการเปรียบเทียบระหว่างการแทนแบบ row-oriented กับแบบ column-oriented layered trie
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 เป็นวิธีลดการซ้ำของคอลัมน์ต้น ๆ ในการแทนแบบ row ที่เรียงลำดับแล้ว
- แต่ละคอลัมน์คือรายการของรายการค่า
- แต่ละรายการคือชุดค่าที่ไม่ซ้ำและเรียงลำดับแล้ว ซึ่งสอดคล้องกับ prefix ของคอลัมน์ก่อนหน้า
- จำนวนรายการของคอลัมน์หนึ่งจะเท่ากับจำนวนไอเท็มรวมทั้งหมดของคอลัมน์ก่อนหน้า
- แม้จะมองเป็นต้นไม้ได้ แต่การติดตั้งใช้งานจริงยังคงเก็บเป็นคอลัมน์
- ข้อดีคือสามารถทำการค้นหา, join, ผลต่างเซต, และ merge ได้ในระดับ prefix
- ในกรณีที่แทบไม่มีค่าที่ไม่ซ้ำ และการมองทั้งแถวพร้อมกันเหมาะกว่า วิธีแบบ row-oriented อาจได้เปรียบกว่า
- ได้เพิ่ม
FactContainertrait เป็นนามธรรมร่วมformlenapplyjoinexceptmerge
applyติดตาม range ของแต่ละ layer ด้วย stack แบบชัดเจนแทนการเรียกซ้ำ แล้วสร้างแถวเพื่อส่งให้ actionalignเป็นเฮลเปอร์ร่วมสำหรับจัด prefix ของ layered trie สองตัวให้ตรงกันOrdering::Less: ช่วงที่มีเฉพาะใน selfOrdering::Greater: ช่วงที่มีเฉพาะใน otherOrdering::Equal: prefix ความยาว arity ที่มีร่วมกันทั้งสองฝั่ง
join,except,mergeถูกสร้างบนalignทั้งหมดjoinจะคลี่ extension ที่เหลือบน prefix ร่วมแล้วทำ cross joinexceptจะ graft ช่วงที่มีเฉพาะใน self ด้วยTrieBuildermergeจะ graft ช่วงแบบ self-only, other-only และ equal อย่างเหมาะสมอย่างละหนึ่งครั้ง
การเพิ่มประสิทธิภาพแบบความกว้างคงที่และประสิทธิภาพ
- เมื่ออัปเกรดเป็น
[u8; 4]แบบความกว้างคงที่ได้ ประสิทธิภาพการเปรียบเทียบจะดีขึ้นมาก - layered trie สามารถใช้การเพิ่มประสิทธิภาพแบบความกว้างคงที่แยกตามคอลัมน์ได้ จึงมีโอกาสได้เปรียบกว่า row ในระยะยาว
- กับ
exceptและmergeสามารถใช้upgrade·downgradeได้ง่าย แต่การนำไปใช้กับjoinยากกว่าเพราะปัญหาเรื่องชนิดข้อมูลของ 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 |
- ขณะนี้เวลาราว 2/3 ถูกใช้ไปใต้
join - มองว่ายังมีช่องให้ปรับแต่ง join ได้เพิ่มอีกราว 2 เท่าเป็นอย่างน้อย
- การลองเพียงสลับลำดับ inner loop ไม่ได้ให้การปรับปรุงที่วัดผลได้
ปรับให้เฉพาะทางถึงระดับโค้ดที่คอมไพล์แล้ว
- หากตรวจจับได้ว่า term มีความยาวเท่ากันและ fact มี arity เท่ากัน ก็สามารถมอง
Vec<u8>เป็นVec<[[u8; B]; T]>ได้ - รูปแบบนี้ทำให้ Rust เข้าใจรูปร่างข้อมูลได้ดีขึ้น ลดต้นทุนการตรวจ bounds·length และทำให้การเปรียบเทียบถูกลงมากเป็นพิเศษ
- การเปรียบเทียบถูกใช้ในหลายจุดของ datatoad
- การ sort·dedup ของ fact batch
- การ merge batch
- การ merge คีย์ของ join
- การกรองโดยเปรียบเทียบ fact ใหม่กับ fact เดิมที่มีอยู่
- ประสิทธิภาพอ้างอิงเป็นดังนี้
| 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 |
- การปรับแต่ง sort ถูกทดลองด้วยวิธี
unsafetransmuteVec<u8>เป็นVec<[u8; 8]>แล้วทำ 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 |
- การปรับแต่ง merge ถูกทำแบบเรียบง่าย โดยนำอินพุตทั้งสองมาต่อกันแล้ว 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 |
- ยังไปไม่ถึงประสิทธิภาพแบบคอมไพล์แล้วของ datafrog
- ต้นทุนการเปรียบเทียบที่เหลือยังอยู่ใน join และ antijoin ด้วย และมีแผนจะกลับมาดูโอกาสปรับแต่งแบบเดียวกันอีกครั้งระหว่างย้ายไปใช้โครงสร้างแบบ trie
- กำลังหาวิธีตัด
unsafeออก และสรุปเพิ่มเติมว่าควรหลีกเลี่ยงโค้ด unsafe
งานที่ยังเหลือ
-
spill ลงดิสก์
- สตอเรจแบบ
columnarประกอบด้วยการจัดสรรขนาดใหญ่จำนวนน้อย - ตอนสร้างสามารถเขียนลงไฟล์แทนหน่วยความจำ แล้วนำกลับมาใช้ผ่าน memory map ได้
- สตอเรจแบบ
-
การประเมินผลแบบกระจาย
- join, การกำจัดข้อมูลซ้ำ, และ distinctness check อาศัยความเท่ากันของคีย์
- สามารถกระจายคีย์และข้อมูลที่เกี่ยวข้องไปยัง worker หลายตัวได้
- สามารถขยายไปถึงหลายโปรเซสด้วย
timely_communication
-
การประเมินกฎแบบสตรีมมิง
- ปัจจุบัน join ใช้ binary join และ materialized output
- หากมีอินเด็กซ์ที่ต้องการ ก็สามารถสร้างแผนที่ไม่ materialize สถานะภายในได้
- worst-case optimal join ก็จะพูดถึงในส่วนนี้ด้วย
-
ปรับให้เฉพาะทางสำหรับ custom representation
- มีแนวคิดในการตรวจจับ transitive closure แล้วปรับให้เฉพาะกับ strongly connected component decomposition
- สำหรับ equivalence relation สามารถใช้โครงสร้างข้อมูล union-find ได้
bddbddbและ factorized databases ก็กล่าวถึงหัวข้อที่เกี่ยวข้องเช่นกัน
-
การค้นหา fact ที่เกี่ยวข้อง
- ต้องทำความเข้าใจและนำ demand transform มาปรับใช้
- มองว่าเป็นการแปลงที่จำเป็นสำหรับการสำรวจ Datalog แบบโต้ตอบ
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เห็นบทความนี้ขึ้นอันดับ 1 แล้วน่าสนใจดี
ตอนนี้กำลังสร้างเกมวางแผนแบบเรียลไทม์ด้วย Differential Datalog และ Rust โดยให้ DDL จัดการตรรกะของเกม
ส่วนใหญ่ก็เป็นข้ออ้างในการลองไอเดียใหม่ ๆ แล้วได้โกนขนจามรีกันไม่รู้จบมากกว่า
https://github.com/vmware-archive/differential-datalog
อนึ่ง ทีม ddlog ตอนนี้ไปก่อตั้ง Feldera แล้ว และก็น่าพิจารณาการใช้ DBSP โดยตรงจาก Rust ด้วย
https://github.com/feldera/feldera
https://github.com/salsa-rs/salsa
ยิ่งน่าสนใจเพราะ DDLog ไม่ได้ถูกดูแลอย่างแข็งขันแล้ว
“ฉัน ผู้ร้ายฉาวโฉ่ ได้รับคำเชิญ โดยครึ่งหนึ่งเชื่อว่ากำลังจะไปรับการลงทัณฑ์ที่ควรได้รับมานานแล้ว” — เป็น ประโยคเปิดที่ดีที่สุด ของบล็อกเทคนิคที่อ่านมาในปีนี้
คำแทรกของผู้เล่าเรื่องก็ยอดเยี่ยม และบทความที่ลึกทางเทคนิคแต่ยังอ่านสนุกแบบนี้หาได้ยาก
การเดินทางเพื่อ optimize query เรื่อง alias ให้ความรู้สึกเหมือนนิยายสืบสวน และผู้อ่านก็ได้คร่ำครวญไปด้วยกันกับ การใช้หน่วยความจำ 50GB ก่อนจะโห่ร้องยินดีเมื่อลดลงเหลือ 5GB
ทั้งโค้ดและงานเขียนยอดเยี่ยมมาก
เคยทำงาน port mangle datalog ไปเป็น Rust ไปบ้างเล็กน้อย
อยู่ที่ https://github.com/google/mangle/tree/main/rust และอยู่ใน repository เดียวกับ implementation ภาษา Go
ความคืบหน้าช้า เพราะไม่ใช่ priority สูง แถมยังมีอาการ second-system syndrome ด้วย
Mangle Rust มุ่งไปทางการจัดการข้อมูลขนาดใดก็ได้ โดยอ่านและเขียน fact จากดิสก์ด้วย memory mapping ส่วน implementation ภาษา Go เป็นแบบ in-memory
บทความนี้ดีตรงที่พูดถึงการ parse Datalog และยังแตะ LSM tree ด้วย แถมตามได้ง่ายกว่าเนื้อหาเกี่ยวกับ datafrog มาก
ใน Rust มี implementation ของ Datalog จำนวนมากที่ใช้ procedural macro เช่น ascent, crepe แต่มีข้อเสียคือรับ query ตอน runtime ได้ยาก
ถ้าใช้สำหรับ static analysis ที่ query และ program ถูกกำหนดตายตัว วิธีแบบ procedural macro อาจเหมาะกว่า
แม้กระแส การฟื้นคืนของ Datalog ในตอนนี้ดูเหมือนจะแผ่วลง แต่ก็ดีใจที่เห็นแฟนตัวยงแกนหลักยังคงยืนหยัดอยู่
งานประชุม Datalog 2.0 ล่าสุดเล็กลงกว่าก่อนพอสมควร และงาน HYTRADBOI ครั้งที่สองก็มีสัดส่วน Datalog ต่ำลง
ใน HYTRADBOI ครั้งแรก หนึ่งในสี่ของงานที่ส่งเข้ามาเกี่ยวข้องกับ Datalog
การที่คนอื่นแชร์โปรเจกต์ Datalog ล่าสุดก็เป็นเรื่องน่าชื่นใจ
ตอนนี้กำลังเตรียม migration ซอฟต์แวร์ขนาดใหญ่ และสร้าง data quality pipeline สำหรับฐานข้อมูล SQL legacy
ถ้า structuring query ดี ๆ จะอ่านง่ายมาก จึงคิดว่า Datalog มีประโยชน์กว่า SQL มากในการระบุและค้นหาปัญหาคุณภาพข้อมูล
Datalog 2.0 เป็น workshop satellite ของ LPNMR ซึ่งเป็นการประชุมวิชาการในยุโรปที่ค่อนข้างไม่เป็นที่รู้จักมากนัก และบังเอิญว่าการประชุมนั้นจัดที่ Dallas
ตอนที่ไปเข้าร่วมเองก็รู้สึกว่างานค่อนข้างเงียบ และถึงจะส่ง paper เข้า workshop ไปด้วย ก็ไม่ค่อยเห็นคนในสาขานั้นมากนัก
ข้อยกเว้นที่เด่นชัดคือกลุ่มจากยุโรปที่มาแนะนำตัวแก้ปัญหา Nemo
ผมคิดว่าการที่ปีนี้ผู้เข้าร่วมน้อย สะท้อนมากกว่าว่านี่เป็น workshop satellite ของการประชุมที่ชื่อเสียงไม่ได้ใหญ่อยู่แล้ว และงานหลักก็เป็น ICLP มากกว่าจะสะท้อนว่าความสนใจต่อ implementation ของ Datalog ลดลง
แน่นอนว่าไม่ได้พยายามโต้แย้งข้ออ้างใหญ่ที่ว่าแทบไม่เหลือความใหม่ใน implementation ของ engine Datalog แบบบริสุทธิ์แล้ว
พื้นที่วิจัยก้าวไปไกลกว่านั้นมากแล้ว ไปสู่ปัญหาที่แปลกใหม่กว่า เช่น streaming (HydroFlow), choice (Dusa), และสิ่งที่ใกล้กับ general chase (chase engine ของ Egglog)
โดยทั่วไปแทบไม่มีใครขัดแย้งว่า vanilla Datalog น่าเบื่อ แต่ monotone forward saturation และ Horn clause เป็น baseline ที่มั่งคั่งซึ่งเข้าใจภูมิทัศน์ด้าน performance engineering ดีแล้ว จึงเหมาะสำหรับต่อยอดทฤษฎีที่น่าสนใจกว่าอย่าง semiring หรือ Z-set
ถ้าสนใจส่วน state machine และ parsing ขอแนะนำ presentation เก่าของ Rob Pike เรื่อง Lexical Scanning in Go ด้วย
https://www.youtube.com/watch?v=HxaD_trXwRE
แม้จะเป็น Go แต่ส่วนใหญ่ก็เอาไปประยุกต์กับภาษาอื่นได้ง่าย
ดีที่ภาษา modern อย่าง Rust, Zig, Go รองรับ Unicode/rune/grapheme แบบ native
เมื่อเทียบกับ Java, .NET, C++ หรือภาษา scripting แล้ว ปัญหาหลายอย่างหายไปเฉย ๆ
โดยรวมชอบงาน Datalog ของผู้เขียน แต่ไม่อยากให้สื่อสอนสำหรับผู้เริ่มต้นสอน binary join
พอออกนอกกรณีอุดมคติ ภายในจะรกขึ้นอย่างรวดเร็ว และ method แบบ general join ทำให้ generalize ในหัวได้ง่ายกว่ามาก
https://en.wikipedia.org/wiki/Worst-case_optimal_join_algorithm
https://github.com/frankmcsherry/blog/blob/master/posts/2025-05-29.md
สมัยเรียนเมื่อนานมาแล้วเคยแตะ Prolog นิดหน่อย และพอรู้คร่าว ๆ ว่าใช้ทำอะไร มีประโยชน์ตรงไหน แต่ไม่ได้เข้าใจลึกซึ้ง
หลังจากนั้นก็ได้ยินเรื่อยมาว่า Datalog ยอดเยี่ยมมาก แต่ก็ยังจับประเด็นไม่ได้ว่า Datalog ปรับปรุงอะไรจาก Prolog กันแน่
เมื่อกี้ลองกวาดดูหน้า Datalog ใน Wikipedia แล้ว ดูเหมือนว่า Prolog มีประสิทธิภาพค่อนข้างแย่ ส่วน Datalog ลดพลังในการแสดงออกและฟีเจอร์ลง แลกกับการปรับปรุงประสิทธิภาพอย่างมาก ทำให้รองรับชุดข้อมูลที่ใหญ่ขึ้นและการประมวลผลแบบขนานได้มากขึ้น
ดูเหมือนจะรวมถึงการสูญเสียความเป็น Turing-complete ด้วย เลยสงสัยว่านี่คือประเด็นหลัก หรือผมเข้าใจผิดไปไกลกันแน่
cut ไม่ได้แค่ป้องกันการคำนวณที่สูญเปล่า แต่ยังอาจมีผลต่อผลลัพธ์ด้วย
ส่วน Datalog โดยทั่วไปมี syntax ใกล้เคียงฐานข้อมูลเชิงสัมพันธ์อีกแบบหนึ่งมากกว่า
พื้นที่ค้นหาขนาดมหึมาที่ดูเล็กน้อยใน Prolog อาจกินหน่วยความจำมากเกินไปใน Datalog จนไม่สามารถแสดงออกได้เลย
Datalog เหมือนรถ commuter ที่ใช้เกียร์ CVT ส่วน Prolog ใกล้เคียงรถ F1
มันไม่เชิงเป็นการปรับปรุง แต่ใกล้เคียงกับการตัดทอน Prolog บางส่วนเพื่อไม่ให้ผู้ใช้ยิงเท้าตัวเองทิ้ง และยังนำไป implement/ฝังในแอปพลิเคชันอื่นได้ง่ายกว่ามาก
ถ้าคุ้นกับ Prolog แล้ว Datalog โดยมากจะรู้สึกอึดอัด
ไม่มี
call/3ไม่มี term/goal expansion และโดยพื้นฐานแล้ว Datalog เหมือนถูกออกแบบโดยดึงชุดความสามารถร่วมขั้นต่ำของ Prolog มาใช้กับการค้นหาฐานข้อมูลแบบโต้ตอบโค้ด Datalog ที่เร็วเขียนได้ง่าย แต่เพดานก็ต่ำกว่ามากเช่นกัน
Prolog ก็สามารถเขียนในแบบที่เปิดให้ทำงานพร้อมกันได้ แต่เป็นงานระดับกลางที่ต้องเข้าใจ implementation
Guarded Horn Clauses และภาษาที่ต่อยอดมาจากมันถูกพัฒนาขึ้นเพื่อทำให้ส่วนนี้เป็นรูปแบบเชิงทางการ แต่พัฒนาการฝั่งญี่ปุ่นหลังยุค Prolog นั้นเข้าใจยากมาก
ประสิทธิภาพของ Prolog ขึ้นอยู่กับโปรแกรมเมอร์ implementation ที่ใช้ และตำแหน่งที่นำไปใช้เป็นอย่างมาก
เช่นเดียวกับ Lisp, Prolog ก็สามารถใช้สร้าง native machine code จาก DSL ณ เวลา compile ได้
ถ้าเข้าใจว่า implementation พื้นฐานทำงานอย่างไร และเขียนโค้ดให้เข้าทางกับมัน ก็เร็วพอได้
แต่การจะทำแบบนั้นต้องเขียนโค้ด Prolog กับ implementation เดียวกันเป็นเวลาหลายปี
งานวิจัยด้านการ optimize compiler ของ Prolog ก็มีมาก และมีกรณี implementation เชิง proprietary ด้วย
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/
ถ้าอยากใช้ Datalog กับ Rust, cozodb เขียนด้วย Rust และมี syntax สำหรับ query แบบ Datalog ให้ด้วย
ช่วงประมาณพฤศจิกายน 2024 เคยลองดู และพบจุดที่ปรับปรุงได้ง่าย ๆ อยู่หลายอย่างใน storage backend ของ SQLite
https://github.com/cozodb/cozo/issues/285
เคยใช้กับการวิเคราะห์สถิตของโปรแกรมด้วย และภายในใช้ sorted tree กับเทคนิคด้าน type
เอกสารเพียงพอที่จะเทียบกับคำอธิบายแบบทีละขั้นในบล็อกช่วงแรก ๆ ได้ และงานด้าน query optimization น่าสนใจเป็นพิเศษ
แต่ถ้าไม่ได้ทำงานในหน่วยความจำจาก Rust ค่าใช้จ่ายในการ serialize ข้อมูลจะสูง และตัวโปรเจกต์เอง ต่อให้มองในแง่ดี ก็อยู่ในสภาพค่อนข้างเงียบ
ก่อนหน้านี้แฟน ๆ Clojure เคยบอกว่า Datalog ดีกว่า SQL และน่าเสียดายที่ฐานข้อมูลเชิงสัมพันธ์ทั้งหมดใช้ SQL
ผมยังไม่ได้เจาะลึกพอว่าทำไมพวกเขาถึงคิดแบบนั้น
http://canonical.org/~kragen/binary-relations เริ่มจากคิวรีแบบไม่ recursive ที่เรียบง่าย แต่การแปลเป็น SQL ก็เข้าขั้นอาชญากรรมแล้ว ส่วนวิธีแก้ด้วย SQL ที่แยกส่วนอย่างเหมาะสมนั้นถึงขั้นสมควรถูกประหาร
ช่วงหลัง ANSI SQL เพิ่มความสามารถแบบ recursive เข้ามาแล้ว จึงไม่ใช่ว่าเป็นไปไม่ได้โดยสิ้นเชิงอีกต่อไป แต่มีข้อเสียใหญ่สามอย่าง
อย่างแรก เผลอทำให้ SQL เป็น Turing-complete ไปเสียแล้ว ในทางกลับกันคิวรี Datalog รับประกันว่าจะสิ้นสุด
อย่างที่สอง ยังใช้งานได้เทอะทะมาก
อย่างที่สาม เพราะเหตุผลข้อแรก จึงมักไม่ได้ถูก implement อย่างสมบูรณ์ ทำให้ไว้ใจใช้งานได้ยาก
ถ้าอยากลองจับ Datalog ในสภาพแวดล้อม notebook ที่เป็นมิตรบนออนไลน์ แนะนำ Percival
https://percival.ink/
ในบรรดา implementation ของ Datalog ไม่มีมาตรฐานเทียบเท่า “ANSI SQL” แต่ถ้าเข้าใจแนวคิดหลักแล้ว Datalog แบบอื่น ๆ ก็ไม่ยากนัก
ยังมี fork ของ Percival ที่คอมไพล์ Datalog เป็น SQLite ด้วย ดังนั้นถ้าอยากดูว่าทั้งสองอย่างแสดงสิ่งเดียวกันอย่างไร ก็ลองดูได้
https://percival.jake.tl/
aggregation และ join ขั้นสูงกว่านี้ยังไม่เสร็จ แต่รูปแบบพื้นฐานทำงานได้ดี
Logica เป็นคอมไพเลอร์ Datalog→SQL ที่จริงจังและสมบูรณ์กว่ามาก สร้างโดยนักวิจัยของ Google โดยคอมไพล์ไปเป็น BigTable, DuckDB และ SQL dialect บางแบบ
https://logica.dev/
จุดที่ Datalog ง่ายขึ้นมากกว่าหนึ่งลำดับขั้นคือเวลาจัดการ คิวรี/กฎแบบ recursive
ใน SQL ก็ทำได้ แต่ความรู้สึกใกล้เคียงกับการดูด Play-Doh ผ่านหลอด
Materialize.com ของ Frank มีรูปแบบ SQL “WITH MUTUALLY RECURSIVE” ที่ดีกว่าวิธี recursive แบบ ANSI SQL เดิมมาก และ Notion กำลังประเมินเพื่อใช้กับคิวรีโหลดหน้าและการซิงก์ข้อมูล
https://materialize.com/blog/recursion-in-materialize/
Feldera ก็มีรูปแบบคล้ายกันสำหรับ recursive view
https://www.feldera.com/blog/recursive-sql-queries-in-feldera
ผมชอบที่ Feldera สามารถทำให้แต่ละ “rule” หรือ sub-view เป็น statement แยกกันได้ โดยไม่ต้องยัดทุกอย่างไว้ใน statement ยักษ์เพียงอันเดียว
ข้อเสียหลักที่เห็นระหว่างทดสอบคือ SQL dialect ของ Feldera มีข้อจำกัดที่สืบทอดมาจาก Apache Calcite อยู่พอสมควร ส่วน SQL dialect ของ Materialize พยายามอย่างหนักมากที่จะให้เข้ากันได้กับ PostgreSQL
เป็นบทความใหม่ของ McSharry ยอดเยี่ยมเลย
ครั้งสุดท้ายที่ตรวจดู ดูเหมือนว่า VMWare จะถอยห่างจาก differential datalog ไปแล้ว
https://www.feldera.com/
ดูเหมือนพวกเขาเปลี่ยนจาก differential Datalog ไปเป็น differential SQL ซึ่งผมคิดว่าอาจเป็นเพราะตระหนักแล้วว่า Datalog ขายยากจริง ๆ