Relational Database – Vài trò không được dạy ở trường (P1)

Relational Database chắc không còn là khái niệm quá xa lạ trong giới lập trình, thậm chí từng được xem là một chuẩn mực không thể thiếu trong việc thiết kế và lưu trữ dữ liệu vẫn còn được ứng dụng rộng rãi đến nay. Tại các trường đại học về ngành công nghệ thông tin (và thậm chí cả một số trường ngoài ngành) cũng đã đưa nội dung về relational database trở thành một phần kiến thức trọng yếu.

Ở bài viết này, mình sẽ chia sẻ vào vài “trò” hay ho nằm ngoài một số khuôn khổ không được dạy ở trường, hay bám sát theo những nguyên tắc thiết kế cơ sở dữ liệu quan hệ.

Foreign key không còn quá quan trọng

Foreign key được mô tả là một chuẩn để thiết kế cơ sở dữ liệu quan hệ, thể hiện tính toàn vẹn dữ liệu, tuy nhiên đó là khi bạn thực hiện các thao tác thay đổi dữ liệu trực tiếp bằng các query trong database, và điều đó sẽ cực kì hạn chế, hoặc thậm chí là cấm kị trong các dự án thực tế (trong một số trường hợp bạn vẫn được phép thực hiện, tuy nhiên hãy đảm bảo rằng bạn biết mình đang làm gì khi run một query INSERT/DELETE/UPDATE nhé).

Thông thường việc tạo ra các record vi phạm ràng buộc khoá ngoại thường xuất phát từ ba trường hợp chính:

  • Migrate dữ liệu từ nguồn không tuân theo ràng buộc.
  • Logic code có lỗi hoặc thiếu validation khi thực hiện query.
  • Mình thích thì mình cố tình vi phạm thôi 😀

Tuy nhiên phần lớn sẽ rơi vào trường hợp thứ hai, vì vậy các ngôn ngữ backend đã xây dựng nhiều framework hỗ trợ việc validate reference trước khi bạn thực hiện câu lệnh thay đổi dữ liệu, hoặc ngay khi input được truyền đi từ client đến server hay giữa các service với nhau.

Việc đảm bảo ràng buộc khoá ngoại hiện nay đã trở thành trách nhiệm của tầng logic trong code backend thay vì tầng database, thậm chí có thể đến cả từ tầng frontend trong việc thiết kế và kiểm soát thông tin đầu vào. Việc đặt khoá ngoại khiến engine của database tiếp tục phải thực hiện check ràng buộc thêm một lần nữa, và điều này sẽ giảm performance khi thao tác với lượng lớn dữ liệu vốn đã được kiểm soát.

Mặt khác, foreign key lại có hại trong xử lí bất đồng bộ, một use case cụ thể khi bạn có các job crawl dữ liệu từ nguồn khác để thực hiện phân tích hay backup, hoặc khi chạy migrate data, vì là bất đồng bộ – các record trong table con có thể có trước table cha, nếu không cẩn thận hoặc áp dụng retry, bạn có thể bị mất data hoặc process bị dừng không mong muốn.

Đôi khi foreign key chỉ cần được thể hiện trên thiết kế là đủ

Bạn có thể tham khảo bài viết phân tích về việc dư thừa khi sử dụng khoá ngoại của bác Piotr Kononow tại đây.

Uy lực của index

Phần lớn thao tác thực hiện nhiều nhất với database trong các ứng dụng thực tế vẫn là đọc dữ liệu (tương ứng với query bắt đầu là SELECT): từ đọc tin tức, lướt săn sale, đi tìm bài nhạc yêu thích, tìm món hàng muốn mua,…

Và khi quy về bài toán tìm kiếm, sẽ luôn có một ví dụ điển hình về sự khác biệt khủng khiếp của linear search và binary search với độ phức tạp là O(N) và O(logN) trong mảng được sắp xếp khi dữ liệu ngày càng lớn.

Và dĩ nhiên với sức mạnh đó, index trở thành một thành tố vô cùng quan trọng khi thực hiện đánh dấu các record nhằm sắp xếp các dữ liệu theo điều kiện được quy chuẩn. Một ví dụ khi bạn tìm tất cả các mặt hàng trong hoá đơn với query bên dưới.

  SELECT * FROM items
  WHERE order_id = 1

Nếu không có index, database sẽ thực hiện scan qua tất cả record trong bảng items chỉ để tìm ra tất cả record thoã điều kiện order_id = 1, nếu table của bạn có đến hàng trăm triệu item thì thời gian để duyệt hết là vô cùng phí phạm.

Nhưng nếu bạn đã có index, các record đã được đánh dấu để sắp xếp theo order_id và thực hiện truy xuất đến đúng vùng đã được đánh dấu với order_id = 1, khi gặp order_id khác 1 sẽ tự động dừng lại.

Tuy nhiên cũng nên cẩn thận khi đánh index nữa

Các bạn có thể tham khảo một số nguồn phân tích rất chi tiết về lợi và hại của index tại đây, tại đâytại đây nữa nhé.

Chia để trị cùng partition

Nếu index giúp truy xuất nhanh đến nơi thoã điều kiện trong một bảng thì paritition sẽ thực hiện gom vùng để chia bảng thành các phần nhỏ hơn, về tư tưởng thì partition khá giống với index, nhưng index sẽ dùng cho việc truy xuất còn partition sẽ dùng cho việc phân chia.

Ví dụ về chia nhỏ dữ liệu theo tháng

Khi bạn sử dụng cùng giải thuật tìm kiếm trên một mảng với hàng trăm triệu phần tử so với mảng vài trăm nghìn phần tử sẽ có sự khác biệt lớn về performance đúng không?

Ví dụ như bạn cần truy xuất danh sách toàn bộ danh sách đăng kí thi đại học của các học sinh quê quán có mã tỉnh/thành là 20 trong năm 2021 như sau:

  SELECT * FROM application a
  WHERE a.application_year = 2021
  AND a.province_id = 20

Nếu table application của bạn đã được chia nhỏ thành các partition theo từng năm thì database sẽ tự động tìm kiếm tại vùng có năm đăng kí thi là 2021 (đã được phân từ trước) mà không cần lúc nào cũng kẹp thêm điều kiện đầu tiên và chỉ thực hiện check điều kiện province_id = 20.

Khi delete lại hoá update

Đôi khi một số record có thể dùng để dùng để theo dõi thông tin hay phân tích dữ liệu, thậm chí ngay cả khi bị xoá đi (hoặc bạn có cảm giác ở phần giao diện là thông tin đã bị xoá đi), như một mặt hàng được bán trong năm 2015 nhưng sau đó không bán nữa, để tránh dư thừa dữ liệu cho người dùng khi tìm kiếm nhưng vẫn bảo toàn về thông tin lịch sử thống kê (như tra cứu lại đơn hàng, thống kê doanh thu, hoặc khôi phục lại thông tin…), việc ‘xoá’ ở đây thực chất chỉ là thay đổi trạng thái tồn tại.

Vì vậy một số trường hợp khi thiết kế database sẽ có thao tác thêm một trường nhằm đánh dấu dữ liệu có tồn tại hay không, và khi người dùng thực hiện xoá, bản chất thao tác chỉ là tắt đi cờ tồn tại của record (UPDATE thay vì DELETE), nên trong một số table bạn sẽ thấy có thêm một column đánh dấu trạng thái như is_active, is_deleted, status hay state.

Đừng quên dõi theo record

Hãy luôn đảm bảo bạn biết record của mình đã được tạo và update khi nào, hiện nay nhiều database và các thư viện thao tác với database của nhiều ngôn ngữ lập trình cũng đã hỗ trợ việc tự động ghi lại thời gian tạo hay cập nhật của record, nên việc thêm 2 column created_at hay updated_at cũng không còn quá phức tạp.

Đôi khi record còn được theo dõi kĩ hơn thông qua 2 column là created_byupdated_by để truy xuất người thực hiện.

Việc có thể theo dõi trạng thái record một phần cũng giúp các developer dễ tìm bug hay thông tin tốt hơn thông qua việc khoanh vùng các khoảng thời để check log tương ứng.

Những column và table (không) thừa thãi

Đọc qua một vài trang báo hay lướt trên các trang bán hàng online, bạn có thể thấy những dòng thông tin ngắn gọn nhưng lại có thể phải join các table lại với nhau như sau:

  • Bài báo có tên tác giả, chuyên mục -> join thêm table authorscategories.
  • Món hàng có tên shop bán, khu vực -> join thêm table sellerslocations.
  • Địa chỉ hành chính đầy đủ -> join thêm table provinces, districtswards.

Nhưng trong thực tế, khi thiết kế database, ta có thể thiết kế các bảng hay column chỉ dành riêng cho việc hiển thị các thông tin trên như sau:

  • Table hiển thị tin cho trang nhất: tiêu đề, đoạn mô tả, link hình ảnh, tên tác giả và tên chuyên mục.
  • Table hiển thị món hàng: tên món hàng, giá bán, giá gốc, phần trăm giảm giá, tên shop bán, khu vực bán.
  • Table hiển thị địa chỉ: thêm một column chứa toàn bộ thông tin địa chỉ.

(Ngoài ra, trang nhất của các trang báo còn có thể tối ưu bằng cách liên tục chạy job để cập nhật tin mới và xoá bớt tin cũ để giảm số lượng dữ liệu dư thừa).

Như tờ báo này chẳng hạn

Các thiết kế trên có thể gọi là thừa thãi và không tuân thủ toàn vẹn dữ liệu, nhưng nếu xét đến việc phải join thêm bảng, thêm cột, thêm tính toán, thêm xử lý làm phức tạp câu query đồng thời giảm luôn performance trong thời đại dung lượng thì dễ dàng để mở rộng và hiệu suất lại khó khăn để tối ưu, chắc mọi người đều có đáp án cho hướng tiếp cận của mình rồi =P

Kết

Trên đây là vài trò khá hay ho khi thực hiện với relational database mà có lẽ rất ít được dạy ở trường mà phải thông qua các chia sẻ hay trải nghiệm thực tế. Bạn có góp ý hay “trò vui” nào khác không, hãy chia sẻ với mình nhé.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s