6 tháng với Apache AGE: Tại sao chúng tôi từ bỏ các truy vấn quan hệ thuần túy
Sáu tháng trước, nền tảng logistics của chúng tôi đã chạm đến giới hạn. Chúng tôi đang xây dựng một công cụ gợi ý (recommendation engine) để theo dõi các phụ thuộc đa tầng giữa nhà cung cấp, kho bãi và các tuyến vận chuyển. Dù PostgreSQL là lựa chọn hàng đầu cho dữ liệu có cấu trúc, nó lại gặp khó khăn trong việc ánh xạ các mối quan hệ sâu. Các CTE đệ quy của chúng tôi trở thành những “con quái vật” dài 200 dòng, mất 5 giây để thực thi và còn tốn nhiều thời gian hơn để gỡ lỗi (debug).
Chúng tôi đã cân nhắc Neo4j, nhưng việc quản lý thêm một cụm cơ sở dữ liệu khác giống như một “cái bẫy” bảo trì. Sau đó, tôi tình cờ biết đến Apache AGE (A Graph Extension). Nó tích hợp chức năng đồ thị trực tiếp vào PostgreSQL bằng ngôn ngữ openCypher. Sau nửa năm vận hành thực tế (production), tôi đã thấy nó biến đổi kiến trúc dữ liệu của mình từ một mớ hỗn độn thành một hệ thống tinh gọn.
Thiết lập trong 5 phút
Docker là con đường ít trở ngại nhất để thử nghiệm AGE. Việc biên dịch từ mã nguồn trên một instance hiện có là khả thi, nhưng sự không tương thích giữa các phiên bản thường gây đau đầu trong quá trình xây dựng ban đầu.
docker run --name age-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d apache/age
Khi container đã sẵn sàng, hãy kết nối qua psql và chạy tập lệnh khởi tạo. Các lệnh này sẽ chuẩn bị môi trường và thiết lập các đường dẫn tìm kiếm (search paths) cần thiết.
-- Tải extension AGE
CREATE EXTENSION age;
-- Thiết lập đường dẫn bao gồm namespace age
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
Trong AGE, dữ liệu nằm trong các “graph” (đồ thị), hoạt động như các namespace riêng biệt. Hãy xây dựng một mạng lưới nhỏ để xem nó hoạt động thế nào.
SELECT create_graph('social_network');
-- Tạo một node Person
SELECT * FROM cypher('social_network', $$
CREATE (n:Person {name: 'Alice', age: 30})
$$) as (v agtype);
-- Tạo một Person khác và một mối quan hệ
SELECT * FROM cypher('social_network', $$
MATCH (a:Person {name: 'Alice'})
CREATE (b:Person {name: 'Bob', age: 25}),
(a)-[:FOLLOWS]->(b)
$$) as (v agtype);
Cách Apache AGE lấp đầy khoảng trống
Điều khiến AGE trở nên đặc biệt là công cụ lưu trữ (storage engine) của nó. Nó không chỉ mô phỏng đồ thị; nó ánh xạ các nhãn đồ thị (graph labels) sang các bảng Postgres và các cạnh (edges) sang các cấu trúc đã được tối ưu hóa. Khi bạn thực hiện một truy vấn openCypher, AGE sẽ chuyển đổi nó thành một kế hoạch thực thi (execution plan) gốc của Postgres.
Lợi thế của openCypher
Nếu bạn đã từng sử dụng Neo4j, bạn sẽ thấy cú pháp này rất quen thuộc. Nó được thiết kế cho dữ liệu có nhiều mối quan hệ. Hãy so sánh khả năng đọc của việc tìm “bạn của bạn” dưới đây.
SQL tiêu chuẩn (Cách khó):
WITH RECURSIVE friends AS (
SELECT friend_id FROM user_relations WHERE user_id = 1
UNION
SELECT r.friend_id FROM user_relations r
INNER JOIN friends f ON f.friend_id = r.user_id
)
SELECT * FROM friends;
Apache AGE (Cách tinh gọn):
SELECT * FROM cypher('social_network', $$
MATCH (u:Person {name: 'Alice'})-[:FOLLOWS*2]->(fof)
RETURN fof.name
$$) as (fof_name agtype);
Phiên bản Cypher tập trung vào mục đích (intent-based). Bạn mô tả mẫu dữ liệu (pattern), và AGE sẽ xử lý logic duyệt đồ thị (traversal). Trong môi trường production của chúng tôi, sự thay đổi này đã cắt giảm 40% mã nguồn ở tầng truy cập dữ liệu và cải thiện khả năng đọc truy vấn ngay lập tức.
Sức mạnh của các truy vấn hỗn hợp
Khoảnh khắc “Aha!” của đội ngũ chúng tôi là khi nhận ra mình có thể join các bảng quan hệ tiêu chuẩn với dữ liệu đồ thị. Đây là một lợi thế khổng lồ so với các cơ sở dữ liệu đồ thị độc lập. Bạn có thể giữ dữ liệu giao dịch nặng trong các bảng nghiêm ngặt, trong khi đẩy các logic mạng lưới phức tạp sang công cụ đồ thị.
Hãy tưởng tượng việc join bảng orders tiêu chuẩn với đồ thị customer_connections để tìm ra những người mua hàng có sức ảnh hưởng.
SELECT
u.customer_name,
o.total_amount
FROM orders o
JOIN (
SELECT * FROM cypher('social_network', $$
MATCH (c:Customer)-[:REFERRED]->(other)
RETURN c.name as customer_name, count(other) as referral_count
$$) as (customer_name agtype, referral_count agtype)
) AS u ON o.customer_name = u.customer_name::text
WHERE u.referral_count::int > 5;
Những bài học thực chiến
Việc chuyển dịch khối lượng công việc thực tế sang AGE đã dạy cho chúng tôi một vài điều mà tài liệu hướng dẫn không đề cập. Dưới đây là những gì bạn cần lưu ý.
1. Bắt buộc phải lập chỉ mục (Indexing)
Duyệt đồ thị không phải là phép màu. AGE lưu trữ các thuộc tính dưới định dạng giống JSONB gọi là agtype. Nếu bạn đang khớp các node theo email hoặc sku, hãy tạo chỉ mục GIN trên bảng nhãn tương ứng. Nếu không, các truy vấn của bạn sẽ rơi vào tình trạng quét toàn bộ bảng (full table scan) rất chậm.
2. Làm chủ việc ép kiểu agtype
Kết quả trả về dưới dạng agtype. Các driver ứng dụng của bạn (Node.js hoặc Python) có thể hiểu chúng là chuỗi hoặc đối tượng. Hãy làm quen với việc ép kiểu tường minh trong các SQL wrapper, chẳng hạn như (result).property::int hoặc ::text, để đảm bảo ứng dụng nhận được đúng kiểu dữ liệu.
3. Tối ưu bộ nhớ
Các thao tác duyệt sâu rất tốn RAM. Chúng tôi nhận thấy việc tăng work_mem từ mặc định 4MB lên 256MB là cực kỳ quan trọng cho các hoạt động MATCH sâu 4 tầng. Nếu tỷ lệ hit của buffer cache giảm, tốc độ duyệt đồ thị của bạn sẽ chậm lại đáng kể.
4. Kiểm tra phiên bản
AGE khá kén chọn hệ điều hành máy chủ. Hiện tại nó hỗ trợ PostgreSQL từ phiên bản 11 đến 16, nhưng các bản phát hành AGE cụ thể sẽ nhắm tới các phiên bản PG cụ thể. Chúng tôi đã phải nâng cấp một cụm PG 10 cũ chỉ để bắt đầu, vì vậy hãy kiểm tra bảng tương thích (compatibility matrix) trước khi bắt tay vào làm.
Kết luận
Apache AGE mang lại những gì tốt nhất của cả hai thế giới. Bạn có được tính tuân thủ ACID của PostgreSQL và khả năng mô hình hóa linh hoạt của một cơ sở dữ liệu đồ thị mà không tốn chi phí vận hành cho một hệ thống đa cơ sở dữ liệu. Nếu các câu lệnh join SQL của bạn đang trở nên mất kiểm soát, đã đến lúc ngừng viết CTE và bắt đầu viết Cypher.

