<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arseny Egorov</title>
    <description>The latest articles on DEV Community by Arseny Egorov (@arseny-egorov).</description>
    <link>https://dev.to/arseny-egorov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3517350%2Fe6736dba-4dca-434d-b21c-621436feb23f.jpg</url>
      <title>DEV Community: Arseny Egorov</title>
      <link>https://dev.to/arseny-egorov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arseny-egorov"/>
    <language>en</language>
    <item>
      <title>Loop: life DWH, logical and physical models. Part 1</title>
      <dc:creator>Arseny Egorov</dc:creator>
      <pubDate>Sat, 20 Sep 2025 06:24:52 +0000</pubDate>
      <link>https://dev.to/arseny-egorov/loop-life-dwh-logical-and-physical-models-part-1-435d</link>
      <guid>https://dev.to/arseny-egorov/loop-life-dwh-logical-and-physical-models-part-1-435d</guid>
      <description>&lt;p&gt;&lt;strong&gt;Loop&lt;/strong&gt; is my pet project. Pet projects are sandboxes where any sandcastles can be built with the fastest iteration cycle.&lt;br&gt;
To learn new material, I always need real-life examples and practice.&lt;br&gt;
If I read a book explaining what 3NF is, then the best way for me to understand it is to check some real, physical tables.&lt;br&gt;
In general, knowledge is really valuable when applied to real work.&lt;br&gt;
However, the pet project, as I said above, is my laboratory where I conduct experiments and put new ideas to initial tests.&lt;/p&gt;

&lt;p&gt;I've needed my personal data warehouse dedicated to learning for a long time, and decided to start with the following idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have Garmin watches and I'm tracking time spent on work, pet projects, learning new things, and gaming&lt;/li&gt;
&lt;li&gt;So let's collect, model, and transform this data in a data warehouse built from scratch&lt;/li&gt;
&lt;li&gt;Then let's build some dashboards on top of this data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are examples of questions on which I want to get the answers from time to time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How much time did I spend by day/week/month working, learning, or gaming?&lt;/li&gt;
&lt;li&gt;How many workouts did I have by day/week/month?&lt;/li&gt;
&lt;li&gt;When did I first start to workout? How consistent am I?&lt;/li&gt;
&lt;li&gt;What are my median/average bedtime and wake-up times by week/month?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm a Data Engineer, so my hammer will be used for the nails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 and Postgres for the raw and detailed layers&lt;/li&gt;
&lt;li&gt;Apache Spark for data transformations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/delta-io/delta" rel="noopener noreferrer"&gt;delta.io&lt;/a&gt; for the staging layer&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dagster-io/dagster" rel="noopener noreferrer"&gt;Dagster&lt;/a&gt; for the orchestration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/metabase/metabase" rel="noopener noreferrer"&gt;Metabase&lt;/a&gt; to build dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this and the following posts I'm going to tell how the thing was built.&lt;br&gt;
Here's the final result – a dashboard which looks like this (screenshots from my Metabase):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjzl70or2a50fyknv55vr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjzl70or2a50fyknv55vr.png" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedhsflsysfjbnerx8d2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedhsflsysfjbnerx8d2w.png" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may see some flaky graphs like "Sleep Fall Asleep", I decided to leave it as is, otherwise I will never publish this post.&lt;br&gt;
There is no limit to perfection.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why "Loop"?
&lt;/h2&gt;

&lt;p&gt;Don't confuse with &lt;a href="https://www.polar.com/nl/loop" rel="noopener noreferrer"&gt;Polar's Loop device&lt;/a&gt;!&lt;br&gt;
I didn't have time to trademark the project name, so I'm sticking with "Loop" which sounds like "Whoop".&lt;br&gt;
I used a Whoop band for nearly 9 months and loved it. &lt;br&gt;
The device is impressive: it works more than 1-2 days(hey Apple Watch), doesn't have screen, and is solely focused on some magical "insights".&lt;/p&gt;

&lt;p&gt;Anyway, now I have Garmin and I'm absolutely happy.&lt;br&gt;
One day I'll implement something what Whoop does, like analyzing correlations between Garmin metrics and working hours.&lt;br&gt;
I also plan to add Google Forms for daily mood and habits tracking in the future.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd8rbx8ahhipen6rwsuaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd8rbx8ahhipen6rwsuaz.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Let's start by investigating what data we can collect.&lt;/p&gt;
&lt;h3&gt;
  
  
  Time tracking – EARLY
&lt;/h3&gt;

&lt;p&gt;I'm tracking my time. What I'm tracking: working time, time spent on pet projects, on reading technical books, on learning languages, playing video games.&lt;/p&gt;

&lt;p&gt;I track only deep and focused work, fully commiting to what I'm doing in the moment.&lt;br&gt;
If spend 8 hours in the office then 5-6 hours will be tracked.&lt;br&gt;
It's just a metric. Maybe a large amount of hours can lead to better results, maybe not, there are no guarantees.&lt;br&gt;
It's the same as possession in football, number of git commits made, HRV, or the "stress level".&lt;/p&gt;

&lt;p&gt;Anyway, with this data, trends can be spotted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much time I spent playing video games this week&lt;/li&gt;
&lt;li&gt;Time spent on a meetings or in the focusing mode&lt;/li&gt;
&lt;li&gt;I read a new book about databases, how much time it took&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://early.app" rel="noopener noreferrer"&gt;EARLY&lt;/a&gt;(formerly Timeular) is the time tracking app I use, paying for a subscription.&lt;br&gt;
It's simple, minimal, and does 95% of what I expect.&lt;br&gt;
One of the selling points for EARLY is an &lt;a href="https://developers.early.app" rel="noopener noreferrer"&gt;API&lt;/a&gt;.&lt;br&gt;
They also sell &lt;a href="https://early.app/tracker/" rel="noopener noreferrer"&gt;Time Tracking Cube&lt;/a&gt; – physical cube (I have one),&lt;br&gt;
which you can use if you want physical feedback on tracking.&lt;/p&gt;

&lt;p&gt;This is my current folders and activities layout.&lt;br&gt;
The hierarchy is rather simple and works perfectly for me, covering everything I'm doing on a daily basis. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3z1t6ajvvob5888ncr7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3z1t6ajvvob5888ncr7.png" alt="Early categories" width="554" height="1307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get data from the API I will implement simple HTTP client with &lt;a href="https://docs.pydantic.dev/latest/" rel="noopener noreferrer"&gt;pydantic&lt;/a&gt; validation.&lt;br&gt;
What we will download:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Folders&lt;/li&gt;
&lt;li&gt;Activities&lt;/li&gt;
&lt;li&gt;Tags&lt;/li&gt;
&lt;li&gt;Time entries&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Workouts, sleep, steps and many more – Garmin watches
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg16hvls9amj1qpnzp3yf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg16hvls9amj1qpnzp3yf.png" alt="Garmin watches" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm a proud owner of a &lt;a href="https://www.garmin.com/nl-NL/p/886785/" rel="noopener noreferrer"&gt;Forerunner 265&lt;/a&gt;.&lt;br&gt;
These watches are the best I've ever had.&lt;br&gt;
The top feature for me is the battery life – 5-7 days.&lt;br&gt;
Garmin collects a lot of metrics, but as a starting point we will focus on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sleep&lt;/li&gt;
&lt;li&gt;Workouts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To download Garmin data I'll use the &lt;a href="https://github.com/cyberjunky/python-garminconnect" rel="noopener noreferrer"&gt;garminconnect&lt;/a&gt; Python library.&lt;/p&gt;
&lt;h2&gt;
  
  
  Logical model
&lt;/h2&gt;

&lt;p&gt;This section heavily uses concepts from the &lt;a href="https://databasedesignbook.com" rel="noopener noreferrer"&gt;Database Design Book&lt;/a&gt; written by Alexey Makhotkin, which I read with pleasure before creating the logical model for the project. The book is highly recommended reading. You might also be interested in the author's &lt;a href="https://kb.databasedesignbook.com" rel="noopener noreferrer"&gt;articles&lt;/a&gt; and author's Substack &lt;a href="https://minimalmodeling.substack.com/" rel="noopener noreferrer"&gt;Minimal Modeling&lt;/a&gt;.&lt;br&gt;
Also, I'm writing a review of the book.&lt;br&gt;
You can notice I use lots of links to Alexey's articles throughout this post because they're quite useful and the author covers many data modeling concepts.&lt;/p&gt;

&lt;p&gt;Quote from the book: "Logical model exists even if you refuse to acknowledge it. When not written explicitly it's scattered in 3 places: 1) physical db schema 2) system code 3) people's memory."&lt;/p&gt;

&lt;p&gt;The Logical Model should become the single source of truth for my data warehouse.&lt;br&gt;
If I want to build a new feature, I'll start with the Logical Model first and then move to the Physical Implementation.&lt;br&gt;
You may think this is redundant bureaucracy for the pet project, but I try not to be &lt;a href="https://www.amazon.nl/-/en/John-Ousterhout/dp/173210221X" rel="noopener noreferrer"&gt;"tactical tornado"&lt;/a&gt; here.&lt;/p&gt;

&lt;p&gt;After checking the EARLY API and garminconnect library for what data they provide, we can extract anchors, attributes, and links for the logical model.&lt;/p&gt;
&lt;h3&gt;
  
  
  Anchors
&lt;/h3&gt;

&lt;p&gt;We start from extracting entities or anchors (nouns).&lt;br&gt;
I also added the "source" column so it's clear where the data comes from.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;ID examples&lt;/th&gt;
&lt;th&gt;Table name (physical)&lt;/th&gt;
&lt;th&gt;Sources&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time Folder&lt;/td&gt;
&lt;td&gt;"280326"&lt;/td&gt;
&lt;td&gt;time_folder&lt;/td&gt;
&lt;td&gt;EARLY api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Activity&lt;/td&gt;
&lt;td&gt;"2052354"&lt;/td&gt;
&lt;td&gt;time_activity&lt;/td&gt;
&lt;td&gt;EARLY api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Tag&lt;/td&gt;
&lt;td&gt;14831771&lt;/td&gt;
&lt;td&gt;time_tag&lt;/td&gt;
&lt;td&gt;EARLY api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Entry&lt;/td&gt;
&lt;td&gt;"106227041"&lt;/td&gt;
&lt;td&gt;time_entry&lt;/td&gt;
&lt;td&gt;EARLY api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout Type&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;workout_type&lt;/td&gt;
&lt;td&gt;Garmin Connect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout&lt;/td&gt;
&lt;td&gt;20150174780&lt;/td&gt;
&lt;td&gt;workout&lt;/td&gt;
&lt;td&gt;Garmin Connect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep&lt;/td&gt;
&lt;td&gt;1752277343000&lt;/td&gt;
&lt;td&gt;sleep&lt;/td&gt;
&lt;td&gt;Garmin Connect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Attributes
&lt;/h3&gt;

&lt;p&gt;I extracted this minimal set of attributes by answering questions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anchor&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Logical Data Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Column name and type (physical)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time Folder&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;name&lt;/em&gt; of the folder?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"0. Vio"&lt;/td&gt;
&lt;td&gt;time_folder.name TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Folder&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;description&lt;/em&gt; of the folder?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"Work"&lt;/td&gt;
&lt;td&gt;time_folder.description TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Activity&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;name&lt;/em&gt; of the activity?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"Gaming"&lt;/td&gt;
&lt;td&gt;time_activity.name TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Activity&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;status&lt;/em&gt; of the activity?&lt;/td&gt;
&lt;td&gt;enum&lt;/td&gt;
&lt;td&gt;"active", "inactive", "archived"&lt;/td&gt;
&lt;td&gt;time_activity.status ENUM NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Activity&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;description&lt;/em&gt; of the activity?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"Playing video games"&lt;/td&gt;
&lt;td&gt;time_activity.description TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Tag&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;label&lt;/em&gt; of the tag?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"crafting_interpreters"&lt;/td&gt;
&lt;td&gt;time_tag.label TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Entry&lt;/td&gt;
&lt;td&gt;When the entry was &lt;em&gt;started&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;timestamp in UTC&lt;/td&gt;
&lt;td&gt;"2025-08-16 06:08:43+00:00"&lt;/td&gt;
&lt;td&gt;time_entry.start_at TIMESTAMPTZ NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Entry&lt;/td&gt;
&lt;td&gt;When the entry was &lt;em&gt;ended&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;timestamp in UTC&lt;/td&gt;
&lt;td&gt;"2025-08-16 07:22:27+00:00"&lt;/td&gt;
&lt;td&gt;time_entry.end_at TIMESTAMPTZ NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout Type&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;name&lt;/em&gt; of the type?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"yoga"&lt;/td&gt;
&lt;td&gt;workout_type.name TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout&lt;/td&gt;
&lt;td&gt;What is the &lt;em&gt;name&lt;/em&gt; of the workout?&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;"Yoga"&lt;/td&gt;
&lt;td&gt;workout.name TEXT NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout&lt;/td&gt;
&lt;td&gt;When the workout &lt;em&gt;started&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;timestamptz&lt;/td&gt;
&lt;td&gt;"2025-08-23 10:19:05+02:00"&lt;/td&gt;
&lt;td&gt;workout.start_at TIMESTAMPTZ NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout&lt;/td&gt;
&lt;td&gt;When the workout &lt;em&gt;ended&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;timestamptz&lt;/td&gt;
&lt;td&gt;"2025-08-23 10:31:56+02:00"&lt;/td&gt;
&lt;td&gt;workout.end_at TIMESTAMPTZ NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep&lt;/td&gt;
&lt;td&gt;When the sleep &lt;em&gt;started&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;timestamptz&lt;/td&gt;
&lt;td&gt;"2025-08-22 21:49:46+02:00"&lt;/td&gt;
&lt;td&gt;sleep.start_at TIMESTAMPTZ NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep&lt;/td&gt;
&lt;td&gt;When the sleep &lt;em&gt;ended&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;timestamptz&lt;/td&gt;
&lt;td&gt;"2025-08-23 05:57:46+02:00"&lt;/td&gt;
&lt;td&gt;sleep.end_at TIMESTAMPTZ NOT NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;p&gt;Let's define relations between anchors.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anchor1:Anchor2&lt;/th&gt;
&lt;th&gt;Cardinality&lt;/th&gt;
&lt;th&gt;Sentences&lt;/th&gt;
&lt;th&gt;Table or column name (physical)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time Folder : Time Activity&lt;/td&gt;
&lt;td&gt;1:M&lt;/td&gt;
&lt;td&gt;Time Folder contains multiple Time Activities. Time Activity is part of only one Time Folder&lt;/td&gt;
&lt;td&gt;time_activity.time_folder_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Folder : Time Tag&lt;/td&gt;
&lt;td&gt;1:M&lt;/td&gt;
&lt;td&gt;Time Folder contains multiple Time Tags. Time Tag is part of only one Time Folder&lt;/td&gt;
&lt;td&gt;time_tag.time_folder_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Activity : Time Entry&lt;/td&gt;
&lt;td&gt;1:M&lt;/td&gt;
&lt;td&gt;Time Activity contains multiple Time Entries. Time Entry is part of only one Time Activity&lt;/td&gt;
&lt;td&gt;time_entry.time_activity_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time Entry : Time Tag&lt;/td&gt;
&lt;td&gt;M:N&lt;/td&gt;
&lt;td&gt;Time Entry is labeled with multiple Time Tags. Time Tag can label multiple Time Entries&lt;/td&gt;
&lt;td&gt;link_time_entry__time_tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workout Type : Workout&lt;/td&gt;
&lt;td&gt;1:M&lt;/td&gt;
&lt;td&gt;Workout Type contains multiple Workouts. Workout can be only one of Workout Type&lt;/td&gt;
&lt;td&gt;workout.workout_type_id&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Physical ERD diagram
&lt;/h3&gt;

&lt;p&gt;I use &lt;a href="https://d2lang.com/" rel="noopener noreferrer"&gt;d2&lt;/a&gt; to visualize the ERD diagram. It's a diagramming language that supports &lt;br&gt;
&lt;a href="https://d2lang.com/tour/sql-tables/" rel="noopener noreferrer"&gt;SQL tables&lt;/a&gt; and &lt;a href="https://d2lang.com/tour/connections/#arrowheads" rel="noopener noreferrer"&gt;Crow's foot notation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I recommend reading these articles written by Alexey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://kb.databasedesignbook.com/posts/erd-diagrams/" rel="noopener noreferrer"&gt;ERD diagrams, pt. I: many-to-many relationships&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kb.databasedesignbook.com/posts/erd-diagrams-2/" rel="noopener noreferrer"&gt;ERD diagrams, pt. II: physical diagrams&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't add ERD notation column to the logical design though.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhws1quwxb06fjk6t5h0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhws1quwxb06fjk6t5h0q.png" alt="ERD" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a d2 code snippet showing how this diagram is built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;time_folder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;shape&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;sql_table&lt;/span&gt;
  &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;constraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;primary_key&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text not null&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text not null&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;time_activity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;shape&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;sql_table&lt;/span&gt;
  &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;constraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;primary_key&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text not null&lt;/span&gt;
  &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;enum not null&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text not null&lt;/span&gt;
  &lt;span class="nv"&gt;time_folder_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;constraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;foreign_key&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;time_activity.time_folder_id &amp;lt;-&amp;gt; time_folder.id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;source-arrowhead.shape&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;cf-many&lt;/span&gt;
  &lt;span class="nv"&gt;target-arrowhead.shape&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;cf-one-required&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we use Foreign Keys in the design. Later we will relax this requirement.&lt;br&gt;
Why this should be relaxed is highlighted in the &lt;a href="https://kb.databasedesignbook.com/posts/foreign-keys/" rel="noopener noreferrer"&gt;Foreign Keys @ Alexey Makhotkin&lt;/a&gt; article.&lt;br&gt;
The problem is that we need to load two connected entities in a single transaction.&lt;br&gt;
If we reload folders by simply overwriting the table, we truncate it, meaning all related articles and tags that depend on folders will be deleted.&lt;br&gt;
If there is a failure loading the folders, then the activities and tags tables will be empty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;What do we have so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data sources were explored (EARLY and Garmin)&lt;/li&gt;
&lt;li&gt;Logical model was built with anchors, attributes, and links&lt;/li&gt;
&lt;li&gt;Physical design was created and ERD diagram was drawn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next post I will tell about DWH layers and raw data ingestion. Stay tuned and subscribe!&lt;/p&gt;

</description>
      <category>datamodeling</category>
      <category>learning</category>
      <category>database</category>
      <category>petproject</category>
    </item>
  </channel>
</rss>
