Bài toán data silo: Khi dữ liệu nằm rải rác khắp nơi
Sáu tháng trước, tôi nhận lại một mớ hỗn độn. Dữ liệu của công ty bị phân tán trên ba hệ thống hoàn toàn tách biệt, không có cầu nối nào giữa chúng. Thông tin người dùng nằm trong cơ sở dữ liệu MySQL do team web quản lý. Giao dịch tài chính được lưu trong cluster PostgreSQL — được chọn vì tuân thủ chuẩn ACID. Còn khoảng 200GB log clickstream thô thì đang nằm yên trong bucket MinIO dưới dạng file Parquet, không ai đụng tới và vẫn tăng lên mỗi ngày.
Rồi team marketing ném lên bàn tôi câu hỏi này: “Tổng chi tiêu của những người dùng đã click vào banner chiến dịch hè là bao nhiêu?”
Một câu hỏi đơn giản. Ba nguồn dữ liệu riêng biệt. Không có hạ tầng chung.
Tôi có ba lựa chọn, và cả ba đều tệ:
- Viết script Python để lấy dữ liệu từ cả ba nguồn, join chúng trong bộ nhớ bằng Pandas, rồi cầu trời server 16GB RAM không bị nghẹt.
- Xây dựng pipeline ETL bằng Airflow để tập trung tất cả vào một kho dữ liệu duy nhất — giải pháp tốt về lâu dài, nhưng mất hai đến ba ngày setup chỉ cho một câu hỏi ad-hoc.
- Xuất CSV rồi ghép lại trong Excel. Tôi đã thấy cách này kết thúc sự nghiệp người khác như thế nào.
Tại sao truy vấn liên cơ sở dữ liệu lại khó đến vậy
Các cơ sở dữ liệu truyền thống được thiết kế để hoạt động độc lập. MySQL không biết PostgreSQL tồn tại. Cả hai đều không thể đọc trực tiếp file Parquet đang nằm trong object storage.
Điểm nghẽn thực sự nằm ở tầng tính toán. Mỗi engine nói một phương ngữ riêng, dùng định dạng lưu trữ riêng và xây dựng execution plan riêng. Join dữ liệu qua ranh giới này hầu như luôn đồng nghĩa với việc phải di chuyển dữ liệu đến nơi có compute — và đó là lúc sự chậm chạp và chi phí bắt đầu tích lũy.
ETL hay Data Federation: Chọn đúng công cụ
Tôi đã đánh giá ba cách tiếp cận phổ biến trước khi quyết định:
- ETL (Extract, Transform, Load): Phù hợp cho báo cáo định kỳ. Nhưng nó tạo ra độ trễ — bạn luôn truy vấn trên snapshot của ngày hôm qua, không phải dữ liệu thực. Với phân tích one-off, xây dựng pipeline là quá cồng kềnh.
- PostgreSQL Foreign Data Wrappers (FDW): Hữu ích khi Postgres đã là hub trung tâm, nhưng hiệu năng giảm rõ rệt khi join qua nhiều nguồn remote cùng lúc.
- Trino (trước đây là PrestoSQL): Một distributed SQL query engine không lưu trữ bất cứ thứ gì. Nó kết nối trực tiếp đến từng nguồn dữ liệu, chỉ lấy những hàng cần thiết, và thực hiện join ngay trong bộ nhớ của worker.
Trino thắng. Với các truy vấn ad-hoc, đa nguồn trên dữ liệu thực, không có lựa chọn nào sánh được. Nó coi mỗi nguồn — MySQL, Postgres, MinIO — như một “catalog”, để bạn có thể truy vấn tất cả bằng một câu SQL duy nhất.
Cài đặt môi trường Trino với Docker
Docker là cách nhanh nhất để khởi động. Toàn bộ stack — Trino, MySQL, Postgres, MinIO và Hive Metastore — có thể chạy trong vòng chưa đến năm phút.
Cấu hình của Trino nằm trong các file .properties bên trong thư mục /etc/trino/catalog/. Mỗi nguồn dữ liệu tương ứng với một file.
1. Các file cấu hình Catalog
PostgreSQL Connector (postgres.properties):
connector.name=postgresql
connection-url=jdbc:postgresql://postgres-server:5432/finance_db
connection-user=admin
connection-password=secret_pass
MySQL Connector (mysql.properties):
connector.name=mysql
connection-url=jdbc:mysql://mysql-server:3306/user_db
connection-user=analyst
connection-password=another_secret
MinIO/S3 Connector (minio.properties):
Trino đọc object storage thông qua Hive connector. Điều này yêu cầu một Metastore — có thể là Hive Metastore (HMS) hoặc AWS Glue — để theo dõi schema bảng và vị trí file. Với môi trường local, một container HMS standalone là đủ.
connector.name=hive
hive.s3.endpoint=http://minio-server:9000
hive.s3.aws-access-key=minio_admin
hive.s3.aws-secret-key=minio_password
hive.s3.path-style-access=true
hive.metastore.uri=thrift://metastore:9083
2. Chuẩn bị dữ liệu thô
Không phải tất cả dữ liệu của bạn đều đã nằm sẵn trong database. Để chuyển đổi nhanh CSV sang JSON trước khi upload file lên MinIO, tôi hay dùng toolcraft.app/vi/tools/data/csv-to-json. Công cụ này chạy hoàn toàn trên trình duyệt — không có gì được gửi lên server — điều này quan trọng khi xử lý dữ liệu nhạy cảm. Rất tiện để làm sạch các dataset nhỏ trước khi đưa vào MinIO cho Trino truy vấn.
Câu truy vấn trả lời cả ba cơ sở dữ liệu cùng lúc
Sau khi cấu hình catalog xong và Trino đã chạy, câu truy vấn chỉ là SQL thuần túy. Không có cú pháp mới nào cần học. Điểm khác biệt duy nhất là quy ước đặt tên ba phần catalog.schema.table.
Đây là câu query tôi đã chạy cho yêu cầu của team marketing:
SELECT
u.username,
u.email,
SUM(t.amount) as total_spent,
l.campaign_id
FROM mysql.user_db.users u
JOIN postgres.finance_db.transactions t ON u.id = t.user_id
JOIN minio.logs.campaign_clicks l ON u.id = l.user_id
WHERE l.campaign_name = 'Summer_2024'
GROUP BY u.username, u.email, l.campaign_id
HAVING SUM(t.amount) > 100
ORDER BY total_spent DESC;
Một câu query duy nhất đó đã đụng vào ba giao thức mạng khác nhau, xử lý ba định dạng lưu trữ khác nhau, và hợp nhất tất cả trong bộ nhớ phân tán của Trino. Với setup của chúng tôi, nó trả về khoảng 4.200 hàng trong vòng khoảng 8 giây. Không cần ETL. Không có bảng trung gian.
Hiệu năng: Những bài học tôi rút ra theo cách khó nhất
Trino rất mạnh, nhưng các federated query có những điểm thất bại không xuất hiện trong môi trường single-source. Đây là những thứ đã làm khó tôi lúc đầu.
Predicate Pushdown: Đảm bảo nó thực sự đang hoạt động
Trino cố gắng đẩy các bộ lọc WHERE của bạn xuống cơ sở dữ liệu nguồn. Khi hoạt động, MySQL sẽ tự lọc dữ liệu ngay tại chỗ và chỉ trả về các hàng khớp. Khi không hoạt động, Trino sẽ kéo toàn bộ bảng qua mạng trước. Với bảng 50 triệu hàng, đó là sự khác biệt giữa query 3 giây và query 45 phút.
Hãy chạy EXPLAIN ANALYZE cho mọi query chậm. Xem Input rows so với Output rows ở giai đoạn quét connector. Nếu input rows bằng với kích thước toàn bộ bảng, pushdown đã thất bại và bạn cần xem lại điều kiện lọc.
Join lớn giữa các catalog làm giảm hiệu năng nhanh chóng
Join một bảng Postgres 100 triệu hàng với một bảng MySQL 100 triệu hàng đồng nghĩa với việc Trino phải shuffle lượng dữ liệu khổng lồ đến các worker node qua mạng. Nó vẫn chạy được — nhưng rất chậm.
Giải pháp thực tế: giữ bảng lớn nhất trong columnar storage nhanh (Parquet trên MinIO), và áp dụng filter chặt trên các bảng nhỏ hơn được kết nối qua JDBC. Để columnar scan gánh phần nặng, đừng để mạng gánh.
Độ trễ mạng là hệ số nhân thầm lặng
Trino lấy dữ liệu theo thời gian thực. Mỗi mili giây độ trễ giữa Trino và các nguồn dữ liệu sẽ nhân lên theo hàng triệu hàng. Hãy deploy Trino trong cùng VPC với các database mà nó truy vấn. Một cluster Trino ở AWS us-east-1 nói chuyện với MySQL server tại colo Singapore sẽ rất chậm — dù bạn có thêm bao nhiêu worker node đi nữa.
Kết quả: Phân tích thay vì vận chuyển dữ liệu
Trước khi có Trino, phần lớn thời gian của tôi dành cho việc di chuyển dữ liệu — viết script, debug pipeline, chờ ETL job chạy xong. Bản thân việc phân tích gần như là phần phụ.
Tỷ lệ đó đã đảo ngược. PostgreSQL, MySQL và MinIO giờ là các catalog ngang hàng trong một giao diện SQL duy nhất. Những câu hỏi mà trước đây mất cả ngày để setup nay chỉ mất vài phút để trả lời. Dữ liệu vẫn nằm đúng chỗ của nó. Trino mang compute đến với dữ liệu.

