Loop: Mô hình DWH và Các Mô Hình Dữ Liệu
Loop là dự án cá nhân của tôi. Dự án cá nhân là những không gian thử nghiệm nơi tôi có thể xây dựng bất kỳ điều gì với chu trình lặp lại nhanh chóng. Để học các tài liệu mới, tôi luôn cần những ví dụ thực tế và thực hành.
Nếu tôi đọc một cuốn sách giải thích về 3NF, cách tốt nhất để tôi hiểu nó là kiểm tra một số bảng vật lý thực tế. Nói chung, kiến thức trở nên giá trị khi được áp dụng vào công việc thực tế. Tuy nhiên, như tôi đã đề cập trước đó, dự án cá nhân là phòng thí nghiệm nơi tôi thực hiện các thí nghiệm và thử nghiệm những ý tưởng mới.
Tôi đã cần một kho dữ liệu cá nhân dành riêng cho việc học trong một thời gian dài và quyết định bắt đầu với ý tưởng sau:
- Tôi có đồng hồ Garmin và đang theo dõi thời gian dành cho công việc, dự án cá nhân, học tập và chơi game.
- Vậy hãy thu thập, mô hình và biến đổi dữ liệu này trong một kho dữ liệu được xây dựng từ đầu.
- Sau đó, hãy xây dựng một số bảng điều khiển dựa trên dữ liệu này.
Dưới đây là một số câu hỏi mà tôi thường muốn tìm câu trả lời:
- Tôi đã dành bao nhiêu thời gian mỗi ngày/tuần/tháng cho công việc, học tập hoặc chơi game?
- Tôi đã có bao nhiêu buổi tập luyện mỗi ngày/tuần/tháng?
- Khi nào tôi bắt đầu tập luyện lần đầu tiên? Tôi có nhất quán không?
- Thời gian đi ngủ và thức dậy trung bình của tôi theo tuần/tháng là gì?
Với tư cách là một Kỹ sư Dữ liệu, tôi sẽ sử dụng những công cụ sau:
- S3 và Postgres cho các lớp dữ liệu thô và chi tiết
- Apache Spark cho các biến đổi dữ liệu
- delta.io cho lớp staging
- Dagster cho việc điều phối
- Metabase để xây dựng bảng điều khiển
Trong bài viết này và các bài tiếp theo, tôi sẽ chia sẻ cách mà dự án này được xây dựng. Dưới đây là kết quả cuối cùng - một bảng điều khiển trông giống như thế này (ảnh chụp từ Metabase của tôi):
Bạn có thể thấy một số biểu đồ không ổn định như "Ngủ và Thức dậy", tôi quyết định để nguyên như vậy, nếu không tôi sẽ không bao giờ công bố bài viết này. Không có giới hạn cho sự hoàn hảo.
Tại sao lại gọi là "Loop"?
Đừng nhầm lẫn với thiết bị Loop của Polar! Tôi không có thời gian để đăng ký thương hiệu cho tên dự án, vì vậy tôi giữ nguyên "Loop" nghe giống như "Whoop". Tôi đã sử dụng một vòng Whoop trong gần 9 tháng và rất thích nó.
Thiết bị này rất ấn tượng: nó hoạt động hơn 1-2 ngày (chào Apple Watch), không có màn hình, và hoàn toàn tập trung vào những "thông tin chi tiết" kỳ diệu.
Dù sao, bây giờ tôi có Garmin và tôi hoàn toàn hài lòng. Một ngày nào đó, tôi sẽ thực hiện điều gì đó mà Whoop làm, chẳng hạn như phân tích mối tương quan giữa các chỉ số Garmin và giờ làm việc. Tôi cũng dự định thêm Google Forms để theo dõi tâm trạng và thói quen hàng ngày trong tương lai.
Nguồn dữ liệu
Hãy bắt đầu bằng cách điều tra những dữ liệu mà chúng ta có thể thu thập.
Theo dõi thời gian – EARLY
Tôi đang theo dõi thời gian của mình. Những gì tôi theo dõi: thời gian làm việc, thời gian dành cho dự án cá nhân, thời gian đọc sách kỹ thuật, học ngôn ngữ và chơi video game.
Tôi chỉ theo dõi những công việc sâu sắc và tập trung, hoàn toàn cam kết vào những gì tôi đang làm trong thời điểm đó. Nếu tôi dành 8 giờ ở văn phòng thì có thể 5-6 giờ sẽ được theo dõi. Đây chỉ là một chỉ số. Có thể thời gian nhiều hơn có thể dẫn đến kết quả tốt hơn, có thể không, không có gì đảm bảo.
Nó giống như thời gian giữ bóng trong bóng đá, số lượng commit git đã thực hiện, HRV hoặc "mức độ căng thẳng".
Dù sao, với dữ liệu này, các xu hướng có thể được phát hiện:
- Tôi đã dành bao nhiêu thời gian chơi video game tuần này
- Thời gian dành cho các cuộc họp hoặc trong chế độ tập trung
- Tôi đã đọc một cuốn sách mới về cơ sở dữ liệu, mất bao lâu
EARLY (trước đây là Timeular) là ứng dụng theo dõi thời gian mà tôi sử dụng, trả phí cho một gói đăng ký. Nó đơn giản, tối giản và thực hiện 95% những gì tôi mong đợi. Một trong những điểm bán hàng của EARLY là API. Họ cũng bán Time Tracking Cube – một khối vật lý (tôi có một cái), mà bạn có thể sử dụng nếu muốn có phản hồi vật lý về việc theo dõi.
Đây là cấu trúc thư mục và hoạt động hiện tại của tôi. Cấu trúc khá đơn giản và hoạt động hoàn hảo với tôi, bao gồm mọi thứ tôi làm hàng ngày.
Để lấy dữ liệu từ API, tôi sẽ triển khai một client HTTP đơn giản với xác thực pydantic. Những gì chúng tôi sẽ tải xuống:
- Thư mục
- Hoạt động
- Thẻ
- Các mục thời gian
Tập luyện, giấc ngủ, bước đi và nhiều hơn nữa – Đồng hồ Garmin
Tôi là một chủ sở hữu tự hào của đồng hồ Forerunner 265. Những chiếc đồng hồ này là tốt nhất mà tôi từng có. Tính năng hàng đầu đối với tôi là thời gian sử dụng pin – 5-7 ngày. Garmin thu thập nhiều chỉ số, nhưng ở điểm khởi đầu, chúng ta sẽ tập trung vào:
- Giấc ngủ
- Tập luyện
Để tải dữ liệu Garmin, tôi sẽ sử dụng thư viện Python garminconnect.
Mô hình Logic
Phần này sử dụng nhiều khái niệm từ cuốn sách Thiết kế Cơ sở Dữ liệu của Alexey Makhotkin, mà tôi đã đọc với sự thích thú trước khi tạo mô hình logic cho dự án. Cuốn sách này rất đáng đọc. Bạn cũng có thể quan tâm đến các bài viết của tác giả và Substack của tác giả Minimal Modeling. Tôi cũng đang viết một bài đánh giá về cuốn sách này.
Bạn có thể nhận thấy tôi sử dụng nhiều liên kết đến các bài viết của Alexey trong bài viết này vì chúng rất hữu ích và tác giả đề cập đến nhiều khái niệm mô hình hóa dữ liệu.
Trích dẫn từ cuốn sách: "Mô hình logic tồn tại ngay cả khi bạn từ chối công nhận nó. Khi không được viết rõ ràng, nó phân tán ở 3 nơi: 1) lược đồ db vật lý 2) mã hệ thống 3) trí nhớ của mọi người."
Mô hình Logic sẽ trở thành nguồn thông tin duy nhất cho kho dữ liệu của tôi. Nếu tôi muốn xây dựng một tính năng mới, tôi sẽ bắt đầu từ Mô hình Logic trước và sau đó chuyển sang Thực hiện Vật lý. Bạn có thể nghĩ điều này là sự quan liêu thừa thãi cho một dự án cá nhân, nhưng tôi cố gắng không trở thành "cơn lốc chiến thuật" ở đây.
Sau khi kiểm tra API EARLY và thư viện garminconnect để xem dữ liệu mà họ cung cấp, chúng ta có thể trích xuất các thực thể, thuộc tính và liên kết cho mô hình logic.
Thực thể
Chúng ta bắt đầu bằng cách trích xuất các thực thể hoặc thực thể (danh từ). Tôi cũng thêm cột "nguồn" để rõ ràng về nơi dữ liệu đến từ.
Tên | Ví dụ ID | Tên bảng (vật lý) | Nguồn |
---|---|---|---|
Thư mục Thời gian | "280326" | time_folder | API EARLY |
Hoạt động Thời gian | "2052354" | time_activity | API EARLY |
Thẻ Thời gian | 14831771 | time_tag | API EARLY |
Mục Thời gian | "106227041" | time_entry | API EARLY |
Loại Tập luyện | 21 | workout_type | Garmin Connect |
Tập luyện | 20150174780 | workout | Garmin Connect |
Giấc ngủ | 1752277343000 | sleep | Garmin Connect |
Thuộc tính
Tôi đã trích xuất bộ thuộc tính tối thiểu này bằng cách trả lời các câu hỏi.
Thực thể | Câu hỏi | Kiểu dữ liệu Logic | Ví dụ | Tên cột và kiểu (vật lý) |
---|---|---|---|---|
Thư mục Thời gian | Tên của thư mục là gì? | string | "0. Vio" | time_folder.name TEXT NOT NULL |
Thư mục Thời gian | Mô tả của thư mục là gì? | string | "Công việc" | time_folder.description TEXT NOT NULL |
Hoạt động Thời gian | Tên của hoạt động là gì? | string | "Chơi game" | time_activity.name TEXT NOT NULL |
Hoạt động Thời gian | Trạng thái của hoạt động là gì? | enum | "active", "inactive", "archived" | time_activity.status ENUM NOT NULL |
Hoạt động Thời gian | Mô tả của hoạt động là gì? | string | "Chơi video game" | time_activity.description TEXT NOT NULL |
Thẻ Thời gian | Nhãn của thẻ là gì? | string | "crafting_interpreters" | time_tag.label TEXT NOT NULL |
Mục Thời gian | Mục bắt đầu khi nào? | timestamp in UTC | "2025-08-16 06:08:43+00:00" | time_entry.start_at TIMESTAMPTZ NOT NULL |
Mục Thời gian | Mục kết thúc khi nào? | timestamp in UTC | "2025-08-16 07:22:27+00:00" | time_entry.end_at TIMESTAMPTZ NOT NULL |
Loại Tập luyện | Tên của loại là gì? | string | "yoga" | workout_type.name TEXT NOT NULL |
Tập luyện | Tên của buổi tập là gì? | string | "Yoga" | workout.name TEXT NOT NULL |
Tập luyện | Buổi tập bắt đầu khi nào? | timestamptz | "2025-08-23 10:19:05+02:00" | workout.start_at TIMESTAMPTZ NOT NULL |
Tập luyện | Buổi tập kết thúc khi nào? | timestamptz | "2025-08-23 10:31:56+02:00" | workout.end_at TIMESTAMPTZ NOT NULL |
Giấc ngủ | Giấc ngủ bắt đầu khi nào? | timestamptz | "2025-08-22 21:49:46+02:00" | sleep.start_at TIMESTAMPTZ NOT NULL |
Giấc ngủ | Giấc ngủ kết thúc khi nào? | timestamptz | "2025-08-23 05:57:46+02:00" | sleep.end_at TIMESTAMPTZ NOT NULL |
Liên kết
Hãy xác định các mối quan hệ giữa các thực thể.
Thực thể 1:Thực thể 2 | Độ Cardinality | Câu | Tên bảng hoặc cột (vật lý) |
---|---|---|---|
Thư mục Thời gian : Hoạt động Thời gian | 1:M | Thư mục Thời gian chứa nhiều Hoạt động Thời gian. Hoạt động Thời gian thuộc về chỉ một Thư mục Thời gian | time_activity.time_folder_id |
Thư mục Thời gian : Thẻ Thời gian | 1:M | Thư mục Thời gian chứa nhiều Thẻ Thời gian. Thẻ Thời gian thuộc về chỉ một Thư mục Thời gian | time_tag.time_folder_id |
Hoạt động Thời gian : Mục Thời gian | 1:M | Hoạt động Thời gian chứa nhiều Mục Thời gian. Mục Thời gian thuộc về chỉ một Hoạt động Thời gian | time_entry.time_activity_id |
Mục Thời gian : Thẻ Thời gian | M:N | Mục Thời gian được gán với nhiều Thẻ Thời gian. Thẻ Thời gian có thể gán cho nhiều Mục Thời gian | link_time_entry__time_tag |
Loại Tập luyện : Tập luyện | 1:M | Loại Tập luyện chứa nhiều Tập luyện. Tập luyện chỉ có thể thuộc về một Loại Tập luyện | workout.workout_type_id |
Sơ đồ ERD vật lý
Tôi sử dụng d2 để hình dung sơ đồ ERD. Đây là một ngôn ngữ vẽ sơ đồ hỗ trợ các bảng SQL và ký hiệu Crow's foot.
Tôi khuyên bạn nên đọc các bài viết được viết bởi Alexey:
- Sơ đồ ERD, phần I: các mối quan hệ nhiều-nhiều.
- Sơ đồ ERD, phần II: các sơ đồ vật lý.
Tôi không thêm cột ký hiệu ERD vào thiết kế logic.
Dưới đây là một đoạn mã d2 cho thấy cách sơ đồ này được xây dựng:
time_folder: {
shape: sql_table
id: text {constraint: primary_key}
name: text not null
description: text not null
}
time_activity: {
shape: sql_table
id: text {constraint: primary_key}
name: text not null
status: enum not null
description: text not null
time_folder_id: text {constraint: foreign_key}
}
time_activity.time_folder_id <-> time_folder.id: {
source-arrowhead.shape: cf-many
target-arrowhead.shape: cf-one-required
}
# ...
Lưu ý rằng chúng ta sử dụng Khóa ngoại trong thiết kế. Sau này chúng ta sẽ nới lỏng yêu cầu này. Tại sao điều này nên được nới lỏng được nêu rõ trong bài viết Khóa ngoại @ Alexey Makhotkin. Vấn đề là chúng ta cần tải hai thực thể liên kết trong một giao dịch duy nhất. Nếu chúng ta tải lại các thư mục chỉ bằng cách ghi đè bảng, chúng ta sẽ cắt ngắn nó, có nghĩa là tất cả các bài viết và thẻ liên quan phụ thuộc vào các thư mục sẽ bị xóa. Nếu có một lỗi trong việc tải các thư mục, thì các bảng hoạt động và thẻ sẽ trống rỗng.
Tóm tắt
Chúng ta đã có những gì cho đến nay:
- Các nguồn dữ liệu đã được khám phá (EARLY và Garmin)
- Mô hình logic đã được xây dựng với các thực thể, thuộc tính và liên kết
- Thiết kế vật lý đã được tạo và sơ đồ ERD đã được vẽ
Trong bài viết tiếp theo, tôi sẽ nói về các lớp DWH và việc nạp dữ liệu thô. Hãy theo dõi và đăng ký!