Ba hướng tiếp cận khi xử lý dữ liệu lớn trong Node.js
Giả sử bạn cần tính tổng doanh thu và giá trị đơn trung bình theo từng danh mục từ file xuất 5 triệu bản ghi (khoảng 1.2 GB). Viết bằng Node.js thế nào để server không sập?
Thực tế khi làm backend, bạn thường có 3 lựa chọn:
- Xử lý thuần bằng JavaScript (Streams / Chunks): Đọc từng dòng CSV hoặc parse JSON rồi tính bằng
map,reduce. Cách này không cần thêm phụ thuộc nào. Đổi lại, CPU bị vắt kiệt và code xử lý stream khá dài dòng. - Đẩy sang database chuyên dụng (PostgreSQL, ClickHouse): Node.js chỉ đóng vai trò trung gian gửi câu lệnh SQL qua mạng rồi chờ kết quả. Cách này ổn định nhưng tốn chi phí hạ tầng. Bạn phải duy trì thêm cụm database và chịu độ trễ mạng từ 20ms – 50ms cho mỗi lượt truy vấn.
- Nhúng DuckDB trực tiếp vào Node.js: DuckDB chạy ngay trong cùng tiến trình (in-process) thông qua C++ binding. Engine này đọc thẳng file CSV, Parquet trên đĩa với cú pháp SQL quen thuộc. Không qua network. Không cần dựng database server.
So sánh chi tiết ưu nhược điểm từng giải pháp
Mỗi phương án đều đánh đổi giữa tốc độ, bộ nhớ và chi phí vận hành.
1. Xử lý bằng JavaScript thuần
- Ưu điểm: Triển khai nhanh cho tệp dữ liệu dưới 50.000 dòng. Không phát sinh dependency bên thứ ba.
- Hạn chế: JavaScript chạy đơn luồng. Khi tính toán trên mảng hàng triệu object, V8 engine dễ chạm ngưỡng 1.4 GB heap memory và gây lỗi Out of Memory (OOM). Nếu không tách worker thread, Event Loop sẽ bị nghẽn hoàn toàn, khiến toàn bộ API khác đứng hình.
2. Dùng cơ sở dữ liệu quan hệ (PostgreSQL, MySQL)
- Ưu điểm: Quen thuộc, hỗ trợ giao dịch ACID chặt chẽ, bảo mật và phân quyền bài bản.
- Hạn chế: RDBMS truyền thống lưu trữ dạng dòng (row-oriented). Khi chạy truy vấn quét hàng triệu bản ghi để
SUMhayAVG, đĩa phải đọc toàn bộ các cột không liên quan. Với server cơ bản (2 vCPU, 4GB RAM), câu lệnhGROUP BYtrên 5 triệu dòng có thể mất từ 15 đến 30 giây.
3. Nhúng DuckDB cục bộ
- Ưu điểm:
- Lưu trữ dạng cột (Columnar): Chỉ đọc đúng các cột được chọn trong mệnh đề
SELECT. Lượng I/O đĩa giảm tới 80%. - Vectorized Execution: Xử lý dữ liệu theo từng lô (chunk) trên thanh ghi CPU thay vì duyệt từng dòng đơn lẻ.
- Zero Network Latency: Dữ liệu chuyển trực tiếp giữa DuckDB và Node.js qua bộ nhớ chung.
- Đọc file trực tiếp: Hỗ trợ truy vấn SQL thẳng vào file Parquet, CSV, Arrow mà không cần tạo bảng trung gian.
- Lưu trữ dạng cột (Columnar): Chỉ đọc đúng các cột được chọn trong mệnh đề
- Hạn chế: Không thiết kế cho tác vụ ghi đồng thời cao (concurrency writes). Nếu hàng nghìn request cùng ghi vào một file
.duckdb, ứng dụng sẽ gặp lock tranh chấp.
Khi nào nên chọn công cụ nào?
Hãy chọn công cụ dựa vào bản chất tải công việc:
- JavaScript thuần: Phù hợp khi file nhỏ dưới 10MB, logic xử lý chuỗi phức tạp và tần suất chạy không thường xuyên.
- PostgreSQL / MySQL: Lựa chọn chuẩn cho nghiệp vụ OLTP: lưu giỏ hàng, đặt đơn, xác thực người dùng và cập nhật số dư.
- DuckDB: Tối ưu cho tác vụ OLAP cục bộ: tạo báo cáo theo kỳ, tổng hợp log hệ thống, dashboard phân tích nội bộ hoặc xuất dữ liệu định kỳ.
Trong thực tế, một hệ thống thương mại điện tử có thể lưu đơn hàng vào PostgreSQL. Hàng đêm, script worker trích xuất dữ liệu ra file snapshot Parquet. Khi admin mở dashboard xem biểu đồ xu hướng 12 tháng, API Node.js gọi DuckDB đọc trực tiếp file Parquet 2GB này. Thời gian phản hồi giảm từ 42 giây trên database chính xuống còn 350ms, đồng thời tránh ảnh hưởng đến luồng thanh toán của khách.
Thực hành: Tích hợp DuckDB vào ứng dụng Node.js
Các bước dưới đây hướng dẫn bạn cài đặt và truy vấn dữ liệu từ file thực tế.
Bước 1: Cài đặt thư viện
Tạo thư mục dự án và cài đặt package duckdb:
mkdir node-duckdb-demo
cd node-duckdb-demo
npm init -y
npm install duckdb express
Bước 2: Khởi tạo kết nối DuckDB
DuckDB có thể chạy trên RAM (:memory:) hoặc lưu trữ vào file cục bộ. Tạo file analytics.js:
const duckdb = require('duckdb');
// Khởi tạo database in-memory
const db = new duckdb.Database(':memory:');
// Bọc db.all thành Promise để dùng async/await
const query = (sql) => {
return new Promise((resolve, reject) => {
db.all(sql, (err, rows) => {
if (err) return reject(err);
resolve(rows);
});
});
};
async function main() {
const result = await query('SELECT 42 AS answer');
console.log('Kết quả kết nối:', result);
}
main();
Bước 3: Truy vấn trực tiếp trên file CSV
Tạo file mẫu orders.csv mô phỏng lịch sử bán hàng:
order_id,customer_id,category,amount,created_at
1,101,Electronics,250.50,2024-01-01
2,102,Books,15.20,2024-01-01
3,101,Electronics,120.00,2024-01-02
4,103,Clothing,89.90,2024-01-02
Bạn có thể viết SQL trực tiếp trên đường dẫn file mà không cần chạy lệnh import:
async function analyzeSales() {
const sql = `
SELECT
category,
COUNT(*) AS total_orders,
ROUND(SUM(amount), 2) AS total_revenue,
ROUND(AVG(amount), 2) AS average_order_value
FROM 'orders.csv'
GROUP BY category
ORDER BY total_revenue DESC;
`;
console.time('Query CSV');
const summary = await query(sql);
console.timeEnd('Query CSV');
console.table(summary);
}
analyzeSales();
Bước 4: Nén và đọc với định dạng Parquet
File CSV tốn dung lượng và phải parse chuỗi mỗi lần đọc. Với tập dữ liệu trên 1 triệu dòng, bạn nên đổi sang định dạng Parquet để đọc theo cột và nén tốt hơn:
async function convertAndAnalyze() {
// 1. Chuyển CSV sang Parquet nén Snappy
await query(`
COPY (SELECT * FROM 'orders.csv')
TO 'orders.parquet' (FORMAT PARQUET, COMPRESSION SNAPPY);
`);
console.log('Đã tạo orders.parquet thành công.');
// 2. Query trực tiếp trên Parquet
const parquetSql = `
SELECT
category,
ROUND(SUM(amount), 2) AS revenue
FROM 'orders.parquet'
WHERE amount > 50
GROUP BY category;
`;
const result = await query(parquetSql);
console.table(result);
}
convertAndAnalyze();
Bước 5: Tạo API báo cáo với Express
Ghép nối câu truy vấn DuckDB vào một route API để trả dữ liệu về frontend:
const express = require('express');
const app = express();
app.get('/api/reports/monthly', async (req, res) => {
try {
const data = await query(`
SELECT
strftime('%Y-%m', CAST(created_at AS DATE)) AS month,
ROUND(SUM(amount), 2) AS monthly_revenue
FROM 'orders.parquet'
GROUP BY month
ORDER BY month ASC
`);
res.json({ success: true, count: data.length, data });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
app.listen(3000, () => {
console.log('API analytics sẵn sàng tại port 3000');
});
DuckDB giải quyết tốt bài toán phân tích dữ liệu cục bộ trong Node.js. Bạn không cần cấu hình cụm OLAP cồng kềnh, giảm thiểu chi phí máy chủ mà vẫn đảm bảo thời gian phản hồi cho các báo cáo nội bộ.

