Giới thiệu về Loop
Loop là dự án cá nhân của tôi, nơi tôi thực hiện các thí nghiệm và phát triển các ý tưởng mới. Dự án này giống như một bể cát nơi tôi có thể xây dựng bất kỳ lâu đài cát nào với chu kỳ lặp lại nhanh nhất. Để học các kiến thức mới, tôi luôn cần các ví dụ thực tế và thực hành.
Khi 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 thực tế. Kiến thức thực sự có giá trị khi được áp dụng vào công việc thực tế. Tuy nhiên, dự án cá nhân này là phòng thí nghiệm của tôi, nơi tôi tiến hành các thí nghiệm và kiểm tra các ý tưởng mới.
Tôi đã cần một kho dữ liệu cá nhân để phục vụ cho việc học từ lâu 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 hỏi và chơi game.
- Vậy hãy thu thập, mô hình hóa 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 tạo một số bảng điều khiển trên cơ sở dữ liệu này.
Một số câu hỏi mà tôi muốn tìm câu trả lời từ thời gian này là:
- 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 mỗi ngày/tuần/tháng?
- Khi nào tôi bắt đầu tập luyện? Tôi có duy trì được không?
- Thời gian đi ngủ và thức dậy trung bình của tôi là gì theo tuần/tháng?
Với vai trò là một Kỹ sư Dữ liệu, tôi sẽ sử dụng các công cụ sau:
- S3 và Postgres cho các lớp dữ liệu thô và chi tiết
- Apache Spark để 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ẽ nói về cách mà mọi thứ được xây dựng. Đây là kết quả cuối cùng - một bảng điều khiển trông như thế này (hình chụp từ Metabase của tôi):
Có thể bạn sẽ thấy một số đồ thị không ổn định như "Giấc ngủ", nhưng tôi quyết định để nó như vậy, nếu không tôi sẽ không bao giờ xuất bản bài viết này. Không có giới hạn cho sự hoàn hảo.
Tại sao lạ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, nên tôi sử dụng "Loop" nghe giống như "Whoop". Tôi đã sử dụng vòng tay Whoop gần 9 tháng và rất thích nó. Thiết bị này ấn tượng: nó hoạt động hơn 1-2 ngày (này Apple Watch), không có màn hình và chỉ tập trung vào một số "thông tin" kỳ diệu.
Dù sao, bây giờ tôi có Garmin và hoàn toàn hài lòng. Một ngày nào đó, tôi sẽ triển khai một cái gì đó mà Whoop làm, như phân tích mối tương quan giữa các chỉ số Garmin và thời gian 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 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, đọc sách kỹ thuật, học ngôn ngữ và chơi game.
Tôi chỉ theo dõi công việc sâu và tập trung, hoàn toàn cam kết vào những gì tôi đang làm trong khoảnh khắc đó. Nếu tôi làm việc 8 giờ trong văn phòng thì 5-6 giờ sẽ được theo dõi. Đây chỉ là một chỉ số. Có thể một lượng giờ lớn 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 cả. Nó cũng giống như tỷ lệ kiểm soát bóng trong bóng đá, số lượng cam kết git, 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 game trong 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 nhiêu thời gian
EARLY (trước đây là Timeular) là ứng dụng theo dõi thời gian mà tôi sử dụng và trả phí cho một gói đăng ký. Nó đơn giản, tối giản và đáp ứng 95% những gì tôi mong đợi. Một trong những điểm bán hàng cho EARLY là API của nó. 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.
Đó là bố cục cá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 cho tôi, bao phủ tất cả những gì tôi đang làm hàng ngày.
Để lấy dữ liệu từ API, tôi sẽ triển khai một HTTP client đơn giản với xác thực pydantic. Những gì chúng ta sẽ tải xuống:
- Thư mục
- Hoạt động
- Nhãn
- 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. Đây là những chiếc đồng hồ 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 lượng pin – 5-7 ngày. Garmin thu thập rất nhiều chỉ số, nhưng như một đ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 rất thú vị trước khi tạo mô hình logic cho dự án. Cuốn sách này được khuyến nghị đọc. Bạn cũng có thể quan tâm đến các bài viết của tác giả và Substack Minimal Modeling của tác giả. Tôi cũng đang viết một bài đánh giá về cuốn sách.
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ó được phân tán ở 3 nơi: 1) sơ đồ cơ sở dữ liệu vật lý 2) mã hệ thống 3) trí nhớ của con người."
Mô hình Logic sẽ trở thành nguồn sự thật 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 với Mô hình Logic trước và sau đó chuyển sang Triển khai Vật lý. Bạn có thể nghĩ rằng đây là một thủ tục hành chính dư thừa cho 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 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 |
Nhãn 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 | "đang hoạt động", "không hoạt động", "đã lưu trữ" | 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 |
Nhãn Thời gian | Nhãn của nhãn là gì? | string | "crafting_interpreters" | time_tag.label TEXT NOT NULL |
Mục Thời gian | Khi nào mục được bắt đầu? | timestamp in UTC | "2025-08-16 06:08:43+00:00" | time_entry.start_at TIMESTAMPTZ NOT NULL |
Mục Thời gian | Khi nào mục kết thúc? | 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 | Khi nào buổi tập bắt đầu? | timestamptz | "2025-08-23 10:19:05+02:00" | workout.start_at TIMESTAMPTZ NOT NULL |
Tập luyện | Khi nào buổi tập kết thúc? | timestamptz | "2025-08-23 10:31:56+02:00" | workout.end_at TIMESTAMPTZ NOT NULL |
Giấc ngủ | Khi nào giấc ngủ bắt đầu? | timestamptz | "2025-08-22 21:49:46+02:00" | sleep.start_at TIMESTAMPTZ NOT NULL |
Giấc ngủ | Khi nào giấc ngủ kết thúc? | timestamptz | "2025-08-23 05:57:46+02:00" | sleep.end_at TIMESTAMPTZ NOT NULL |
Liên Kết
Hãy định nghĩa các mối quan hệ giữa các thực thể.
Thực thể1:Thực thể2 | Độ lớn | 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 chỉ là một phần của một Thư mục Thời gian | time_activity.time_folder_id |
Thư mục Thời gian : Nhãn Thời gian | 1:M | Thư mục Thời gian chứa nhiều Nhãn Thời gian. Nhãn Thời gian chỉ là một phần của 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 chỉ là một phần của một Hoạt động Thời gian | time_entry.time_activity_id |
Mục Thời gian : Nhãn Thời gian | M:N | Mục Thời gian được gán nhiều Nhãn Thời gian. Nhãn 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ể là một trong các 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ợ bảng SQL và ký hiệu Crow's foot.
Tôi khuyến nghị đọc các bài viết của Alexey:
- Sơ đồ ERD, phần I: các mối quan hệ nhiều-nhiều.
- Sơ đồ ERD, phần II: sơ đồ vật lý.
Tôi không thêm cột ký hiệu ERD vào thiết kế logic.
Đây là đoạn mã d2 cho thấy cách mà 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 đề cập trong bài viết về Khóa Ngoại của 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. Nếu chúng ta tải lại thư mục bằng cách đơn giản ghi đè bảng, chúng ta sẽ xóa toàn bộ các bài viết và nhãn liên quan đến thư mục đó. Nếu có lỗi xảy ra khi tải thư mục, thì các bảng hoạt động và nhãn sẽ trống rỗng.
Tóm tắt
Những gì chúng ta đã có 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 ra 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 nhập dữ liệu thô. Hãy theo dõi và đăng ký!