Chào mừng bạn đến với bài học cuối cùng trong series SQL trong Trình duyệt. Suốt 16 bài học qua, chúng ta đã đi từ những câu lệnh truy vấn cơ bản, kỹ thuật JOIN phức tạp, tối ưu hóa Index, đọc cấu trúc file nhị phân, cho tới các cơ chế lưu trữ bền vững cấp trang. Trong bài học thực chiến tổng kết này, chúng ta sẽ kết hợp tất cả các kiến thức đó để xây dựng một Mini Analytics Dashboard chạy 100% offline ở client. Hệ thống cho phép nạp dữ liệu bán hàng, lưu trữ tự động vào IndexedDB, kết nối kết quả SQL với biểu đồ SVG động, và xuất cơ sở dữ liệu dưới dạng file .sqlite.

1. Kiến Trúc Ứng Dụng Analytics Dashboard Client-Side

Một hệ thống Dashboard phân tích dữ liệu tại client được cấu thành từ 3 lớp kiến trúc chính:

  1. Tầng Dữ Liệu (Data Layer): Quản lý bởi thư viện WebAssembly sql.js. Nó chịu trách nhiệm khởi chạy cơ sở dữ liệu trong bộ nhớ RAM ảo và thực thi trực tiếp các truy vấn phân tích SQL.
  2. Tầng Lưu Trữ (Persistence Layer): Kết nối thông qua API IndexedDB của trình duyệt. Mỗi khi cơ sở dữ liệu có sự thay đổi (như thêm đơn hàng hoặc nạp dữ liệu mới), mảng byte nhị phân của cơ sở dữ liệu sẽ được xuất ra bằng db.export() và lưu đè vào IndexedDB theo cơ chế Async. Khi tải lại trang, hệ thống tự động đọc mảng byte này lên và khởi tạo lại database để đảm bảo tính liên tục của dữ liệu.
  3. Tầng Trực Quan Hóa (Visualization Layer): Chuyển đổi mảng kết quả của câu lệnh SQL (dạng bảng hàng/cột) thành các cấu trúc thẻ đồ họa SVG (Scalable Vector Graphics) động. Việc này giúp chúng ta vẽ biểu đồ cột (Bar Chart) hay biểu đồ đường (Line Chart) mà không cần nạp thêm bất kỳ thư viện vẽ biểu đồ nặng nề nào khác từ bên ngoài.

2. Các Truy Vấn Phân Tích Thực Chiến (Analytics Queries)

Để cung cấp số liệu cho Dashboard, chúng ta cần chuẩn bị sẵn 3 câu truy vấn SQL phân tích chính trên dataset kinh doanh **TechMart** đã dùng xuyên suốt khóa học:

Truy vấn 1: Tổng Doanh Thu Theo Tháng (Monthly Revenue)

Câu lệnh này trích xuất năm-tháng từ trường order_date, gom nhóm và tính tổng tiền đơn hàng thành công để vẽ biểu đồ đường xu hướng doanh thu qua các tháng:

SELECT strftime('%Y-%m', order_date) AS month_period,
       ROUND(SUM(total_amount), 2) AS revenue
FROM orders
WHERE status != 'cancelled'
GROUP BY month_period
ORDER BY month_period ASC;

Truy vấn 2: Top 5 Sản Phẩm Bán Chạy Nhất (Top 5 Best Sellers)

Truy vấn thực hiện JOIN 3 bảng order_items, productsorders để thống kê tổng số lượng bán ra của từng sản phẩm, phục vụ việc vẽ biểu đồ cột đứng:

SELECT p.product_name,
       SUM(oi.quantity) AS total_sold
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'delivered'
GROUP BY p.product_id
ORDER BY total_sold DESC
LIMIT 5;

Truy vấn 3: Tỷ Lệ Doanh Thu Theo Danh Mục Sản Phẩm (Revenue Share by Category)

Câu query gom nhóm theo danh mục sản phẩm, tính toán tổng doanh số của từng nhóm để giúp nhà quản lý phân tích cơ cấu ngành hàng đóng góp chính cho TechMart:

SELECT p.category,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS category_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status != 'cancelled'
GROUP BY p.category
ORDER BY category_revenue DESC;

3. Import và Export File Cơ Sở Dữ Liệu Nhị Phân .sqlite

Một điểm thú vị của SQLite là toàn bộ cơ sở dữ liệu được gói gọn trong một file duy nhất. Nhờ đó ở client, chúng ta có thể dễ dàng hiện thực hóa hai chức năng:

  • Nhập file (Import): Sử dụng thẻ HTML <input type="file"> để đọc file database từ máy người dùng dưới dạng ArrayBuffer bằng FileReader, rồi truyền thẳng vào hàm khởi tạo new SQL.Database(new Uint8Array(arrayBuffer)).
  • Xuất file (Export): Gọi hàm db.export() để nhận về một mảng byte nhị phân Uint8Array. Sau đó bọc mảng này vào đối tượng Blob với kiểu MIME là application/x-sqlite3 và tạo một đường link tải xuống ảo (virtual download link) để lưu file về máy tính.

4. Thiết Kế Trực Quan Hóa Bằng SVG Động (Dynamic SVG Charts)

Đồ họa SVG là một phần của tiêu chuẩn HTML5. Chúng ta có thể tạo ra các thẻ đồ họa như <rect> (vẽ cột), <polyline> (vẽ đường nối các tọa độ) hay <text> (viết chữ nhãn) trực tiếp từ JavaScript.

Công thức tính toán tọa độ cột cho biểu đồ SVG có kích thước $W \times H$: Nếu giá trị lớn nhất trong tập dữ liệu là \(V_{\max}\), và giá trị của phần tử hiện tại là $V$, chiều cao thực tế của cột \(h\) được chuẩn hóa theo tỷ lệ: \[ h = \frac{V}{V_{\max}} \times (H - \text{padding}) \] Tọa độ gốc trên màn hình $y$ để vẽ cột từ dưới lên (vì hệ tọa độ SVG có gốc ở góc trên bên trái): \[ y = H - h - \text{offset\_bottom} \]

Việc này giúp chúng ta tạo ra các biểu đồ cực kỳ nhẹ, phản hồi tương tác mượt mà và tương thích tốt với mọi thiết kế responsive của giao diện di động.

5. Trải Nghiệm Dự Án Analytics Dashboard

Dưới đây là khu vực làm việc của dự án. Giao diện được thiết kế gồm bảng điều khiển nhập xuất file, trình gõ query SQL tùy chọn, bảng hiển thị kết quả và bộ đôi biểu đồ SVG tự động cập nhật theo kết quả query.

Trạng thái lưu trữ IndexedDB: Đang kiểm tra...
Đang tải SQLite-WASM...

📋 Sơ đồ bảng hiện có (Schema)

📊 Biểu Đồ Cột: Thống Kê Sản Lượng/Doanh Số
Bấm chạy query để vẽ biểu đồ cột...
📈 Biểu Đồ Đường: Xu Hướng Theo Thời Gian
Bấm chạy query để vẽ biểu đồ đường...
Kết quả bảng số liệu sẽ hiển thị ở đây...
🔬 Đào sâu: Persist bằng IndexedDB hoạt động thế nào dưới nắp máy?
Ứng dụng của chúng ta mở một cơ sở dữ liệu IndexedDB tên là TechMartDashboardDB, tạo một object store tên là db_store. Khi bạn sửa dữ liệu (ví dụ chạy lệnh update, insert), một hàm async sẽ xuất database ra mảng byte và thực hiện transaction ghi vào IndexedDB:
const transaction = idb.transaction(['db_store'], 'readwrite');
const store = transaction.objectStore('db_store');
store.put(db.export(), 'current_sqlite_db');
Quy trình này diễn ra hoàn toàn offline, độc lập và bảo mật cao do dữ liệu không bao giờ rời khỏi trình duyệt của người dùng.

Kiểm tra kiến thức ôn tập

Trắc nghiệm ôn tập

Câu 1: Điểm mạnh lớn nhất khi sử dụng SVG để tự thiết kế biểu đồ thay vì dùng các thư viện lớn như Chart.js là gì?

Trắc nghiệm ôn tập

Câu 2: Tại sao chúng ta cần lưu trữ file cơ sở dữ liệu nhị phân dưới dạng ArrayBuffer vào IndexedDB thay vì LocalStorage?

Trắc nghiệm ôn tập

Câu 3: Làm thế nào để ứng dụng Analytics Dashboard này có thể chạy hoàn toàn offline không cần kết nối mạng?

Trắc nghiệm ôn tập

Câu 4: Khi người dùng chọn tải lên một file database .sqlite mới, làm thế nào để engine WASM cập nhật dữ liệu mới này?

Trắc nghiệm ôn tập

Câu 5: Nhược điểm lớn nhất của việc lưu trữ nguyên khối cơ sở dữ liệu vào IndexedDB khi file database lớn hơn 100MB là gì?

📖 Tài liệu tham khảo / References

Bài viết liên quan trong series

Chúc mừng bạn đã hoàn thành trọn vẹn Lộ trình SQL! 🎓 Bài 16: WAL & Persistence Trong Browser 🐳 Quay lại Lộ trình Series SQL