Data Modeling Projects for Data Engineers

11 to Build (2026)

The best data modeling projects for a data engineer ship a model other people build on, over public data that keeps changing. The model might be a star schema with a declared grain or a snapshot fact, a Type 2 dimension or an event model; a data vault and a semantic layer also qualify. These 11 projects come in 4 tiers that build on each other, and each one is a public repository or course you can clone today. Every dossier says what you build, what it teaches, the interview signal it sends and how to make it your own, and lists its stack along with the time and cost. 9 of the 11 run on a laptop for free.

Last updated: Proudly published by: Jeff Wahl26 min read

What makes a data modeling project worth building

A data modeling project is worth building when its deliverable is a model that other people and systems build on: a fact table an analyst can sum without double counting, a dimension that answers what was true on a past date, an event table a funnel query can trust, a metric that means the same thing on every dashboard. A normalized schema for an imaginary library is a transactional design exercise, and so is one for a school or a hotel; neither shows any of that.

Every project on this list ships 4 things. A grain statement for each table, in 1 sentence, written before any SQL: the Kimball Group's guidance on declaring the grain calls the grain declaration a binding contract on the design and requires it before any dimension or fact is chosen. Keys and tests that enforce that grain, so a duplicate fails the build. A diagram that shows each table's keys and how the tables relate. And a consumer that the model exists to answer, such as a report query or a metric, which is how you prove the grain was the right one.

Each project is also a public repository or course you can clone today, so you start from working code rather than a blank folder. What makes it yours is the extension each dossier suggests: a second grain, a weighting factor, a point-in-time join, a test on a schema version. A model that has absorbed a change in its source is the evidence reviewers look for, because it is the part of modeling that a static CSV never teaches.

The 11 data modeling projects at a glance

#ProjectDifficultyTimeCore stackCost
01A Kimball star schema with dbt on AdventureWorksBeginner1 weekenddbt · DuckDB · SQL$0
02NYC taxi trips with role-playing zonesBeginner+1 to 2 weekendsdbt · DuckDB · BigQuery$0
03AdventureWorksDW sales reasons as a many-to-many bridgeBeginner+1 weekendSQL Server · SQL · dbt$0
04AdventureWorksDW product inventory as a periodic snapshotIntermediate1 weekendSQL Server · SQL · dbt$0
05Type 2 customer history with dbt snapshotsIntermediate1 weekenddbt · DuckDB · Python$0
06GA4 events, sessions and items with dbt-ga4Intermediate2 weekendsBigQuery · dbt · SQL$0 (free tier)
07GitHub activity stream in Activity Schema 2.0Intermediate+2 weekendsPython · DuckDB · SQL$0
08A model that absorbs schema change, loaded with dltIntermediate1 to 2 weekendsPython · dlt · DuckDB$0
09TPC-DS bus matrix and drill-across martAdvanced2 weekendsdbt · DuckDB · SQL$0
10Data Vault 2.0 on TPC-H with AutomateDVAdvanced2 to 3 weekendsdbt · AutomateDV · SnowflakeCloud billing
11A semantic layer on the Jaffle Shop with MetricFlowIntermediate+1 to 2 weekendsdbt · MetricFlow · DuckDB$0

Star counts as each repository's page showed them on 27 September 2026.

How the 11 data modeling projects build on each other

The 11 projects sit in 4 tiers. Grain and the star comes first: a star schema, a role-playing dimension and a bridge table, which together teach the grain decision and the fan-outs that break it. Time and history adds a periodic snapshot and a Type 2 dimension, 2 of the ways a model records time. Events and change starts with nested event data and an activity stream, then handles a source whose schema moves. Integration and serving uses a bus matrix and a data vault to show how many models fit together, and ends with a semantic layer.

Each tier assumes the one before it. A Type 2 dimension is only useful to a fact whose grain you trust, an activity stream is easiest to judge against a star you've built, and a semantic layer compiles wrong numbers from any model with an unclear grain. Finishing 2 or 3 projects is worth more than starting 6: a reviewer reads a finished model with tests and a README faster than a folder of half-built ones, and each finished project is a set of decisions you can defend in an interview.

Which data modeling project to start with

If your situation is
Pick
Why
You have never declared a grain
Projects 1 → 2 → 3
A tutorial star schema, a role-playing dimension in a free course, then a bridge table in Microsoft's sample warehouse. All 3 run on a laptop.
You already write star schemas
Projects 5 → 4 → 3
History is where modeling interviews go next, then semi-additive snapshots, then the bridge fan-out most candidates get wrong.
You target product analytics roles
Projects 6 → 7 → 11
Nested events and sessions, the activity stream that funnels run on, then metrics defined once for every dashboard.
You target senior or platform roles
Projects 9 → 10 → 8
Conformed dimensions across processes, a vault for many sources, then a model that absorbs upstream schema change.
You want to spend nothing and stay on a laptop
Projects 1 → 5 → 7 → 11
Every step runs on DuckDB with no account, from the first star to a semantic layer.

Grain and the star: model 1 business process

Tier 1 · Projects 1-3

3 projects that teach the decision every model depends on: what 1 row of the fact table means. Each one has a fan-out waiting for a model that skips it, whether a header measure repeated on detail lines, a dimension joined in the wrong role or a bridge that multiplies a total.

Project 01 · Star schema

A Kimball star schema with dbt on AdventureWorks

Beginner1 weekendLocal & free

A step-by-step tutorial repository that builds a Kimball star schema with dbt on the AdventureWorks sample data, on DuckDB by default with a Postgres option. The fact table, fct_sales, holds 1 row per order detail. It joins a seeded dim_date plus 5 more tables (dim_product, dim_customer, dim_address, dim_credit_card, dim_order_status), and dbt_utils.generate_surrogate_key generates every key.

The tutorial works in the Kimball order: choose the business process, declare the grain, then name the dimensions and the facts. Sales orders arrive as a header and its detail lines, so the grain decision is in plain view: a column that belongs to the order, repeated on every detail row, is counted once per line by any SUM. Surrogate keys decouple the model from the source system's ids, and the seeded date dimension (the dbt_date package does not support DuckDB) shows why every star carries its own calendar.

Extend it by adding a second fact at a different grain, such as 1 row per order, and a uniqueness test on each fact's key. Then write the grain of every table as 1 sentence in the model's documentation, which is the habit every later project on this list relies on.

Interview signal

The first question in most modeling rounds is the grain of the fact table. After this project you answer it in 1 sentence, name the test that enforces it and explain the double count it prevents, which is the difference between knowing the vocabulary of a star schema and having built one.

dbtDuckDBSQL
Project 02 · Role-playing dimension

NYC taxi trips with role-playing zones

Beginner+1 to 2 weekendsLocal & free

Module 4 of the DataTalksClub Data Engineering Zoomcamp, a free course, builds a dbt project over the NYC TLC yellow and green trip records. It runs on DuckDB locally or on BigQuery. fct_trips is 1 of its 3 marts, alongside dim_zones and dim_vendors. The fact joins dim_zones twice, once as the pickup zone and once as the dropoff zone, and loads incrementally with a merge on trip_id.

1 zone dimension plays 2 roles, so the fact carries 2 foreign keys to it and every query has to say which role it means; the module tests each one with a relationships test to dim_zones.location_id. Yellow and green trips land in 1 fact with a service_type column that an accepted-values test limits to Green and Yellow, a conformance decision you should be able to defend against keeping 2 facts. Some measures mislead: the TLC data dictionary says tip_amount is filled automatically for credit card tips and leaves cash tips out, so the honest average tip filters to card payments.

The fact sets on_schema_change to append_new_columns, and the TLC keeps adding columns, such as cbd_congestion_fee from 2025 data onward. Extend the module by deciding whether earlier trips show NULL or 0 for that fee, and write the decision into the model's documentation.

Interview signal

Role-playing dimensions come up whenever an interviewer asks you to model anything with an origin and a destination. Taxi trips have both, and so do flights and shipments. Raising the tip caveat yourself shows you read the source's documentation before you trusted a measure.

dbtDuckDBBigQuery
Project 03 · Bridge table

AdventureWorksDW sales reasons as a many-to-many bridge

Beginner+1 weekendLocal & free

Microsoft's AdventureWorksDW sample warehouse, published as backups and install scripts in the SQL Server samples repository, models a bicycle maker's business as a set of star schemas. Its Internet sales star records why customers bought through FactInternetSalesReason, a bridge with 1 row per sales order line per reason. It links FactInternetSales on SalesOrderNumber and SalesOrderLineNumber to DimSalesReason on SalesReasonKey. Restore the warehouse, then rebuild that star and its bridge with dbt.

An order line with 2 reasons joins 2 bridge rows, so sales summed by reason add up to more than total sales. That is right for the question of how much revenue each reason touched and wrong for any total. The bridge carries no weight, which makes the standard fix your extension: give each row a weight of 1 divided by the line's reason count, and a weighted sum returns the original total. Build both reports and show that they disagree on purpose.

The same warehouse holds 10 fact tables, and its sales and inventory facts share DimDate and DimProduct, so it's also a reference for how several business processes meet in 1 warehouse. Read it before you design your own.

Interview signal

Interviewers ask how to count sales by category when a product sits in several categories. The bridge, the fan-out it causes and the weight that corrects it make a complete answer to one of the harder dimensional modeling questions.

SQL ServerSQLdbt
microsoft/sql-server-samples11.2k★DataAdventureWorksDW sample warehouse (backups and install scripts)
The Zoomcamp's taxi star (project 2): 1 zone dimension in 2 roles
dim_vendors
vendor_idPKINT
vendor_nameVARCHAR
fct_trips
trip_idPKVARCHAR
vendor_idFKINT
pickup_location_idFKINT
dropoff_location_idFKINT
service_typeVARCHAR
pickup_datetimeTIMESTAMP
trip_distanceDECIMAL
tip_amountDECIMAL
total_amountDECIMAL
dim_zones
location_idPKINT
boroughVARCHAR
zoneVARCHAR
service_zoneVARCHAR

Grain: 1 row per trip, unique on trip_id. The 2 zone keys are the role-playing pair, each tested against dim_zones.location_id.

1 table, 2 rolesPickup and dropoff both join dim_zones, so every query names the role it means.
Tested both waysA relationships test on each zone key catches a trip whose zone is missing from the lookup.
Prepare for the interview
01 / Open invite
02min.

Know data modeling projects the way the interviewer who asks it knows it.

a data modeling projects query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1fact_orders
2 order_id bigint PK
3 customer_sk bigint FK
4 order_date date SCD2
5
Execute your solution0.4s avg.

Time and history: snapshots and Type 2 dimensions

Tier 2 · Projects 4-5

2 projects on how a model records time: a row per product per day whether or not stock moved, and a new dimension row whenever a tracked attribute changes. Each one breaks a different assumption of an insert-only fact table. The accumulating snapshot, a row per order that fills in as milestones land, is the third fact table shape.

Project 04 · Periodic snapshot

AdventureWorksDW product inventory as a periodic snapshot

Intermediate1 weekendLocal & free

The same Microsoft sample warehouse keeps stock in FactProductInventory, a daily product balance keyed on ProductKey and DateKey with UnitsIn, UnitsOut, UnitsBalance and UnitCost. Rebuild it from the flows it summarizes: derive each product's daily movements, carry the balance forward for every product on every day, and check whether each day's balance in Microsoft's table equals the previous balance plus units in minus units out.

UnitsBalance is semi-additive. Summed across products on 1 day it gives the stock on hand; summed across days it gives nothing meaningful, so aggregate over time with the last value of the period or an average, and write that rule into the model's documentation. UnitsIn and UnitsOut are fully additive, which is why the table carries both flows and the balance.

A periodic snapshot promises a row for every product on every day whether or not stock moved, so its size grows with products times days rather than with activity. Choose the period to match the questions it answers, daily for replenishment and monthly for finance, and say what a missing day means before a dashboard reads it as 0.

Interview signal

How much stock was on hand at the end of last month cannot be answered with a SUM, and semi-additive measures are a reliable way for an interviewer to find out whether a candidate has modeled a snapshot. Rebuilding Microsoft's table from its flows shows you know how 1 is loaded, not just how it is read.

SQL ServerSQLdbt
microsoft/sql-server-samples11.2k★DataAdventureWorksDW sample warehouse (backups and install scripts)
Project 05 · SCD Type 2

Type 2 customer history with dbt snapshots

Intermediate1 weekendLocal & free

The companion repository to the Start Data Engineering dbt tutorial, on DuckDB. A Python script simulates the extract and load. dbt snapshot builds dim_customer as a Type 2 dimension, and the silver and gold layers add facts such as fct_orders and an incremental fct_clickstream. A second script, load_new_data.py, lands changed rows so you can watch new versions appear on the next snapshot.

dbt snapshot has 2 strategies: timestamp opens a version whenever an updated_at column moves, and check opens one only when a listed column changes. Choose the tracked attributes first, because they decide what a version is; a field that changes on a schedule, such as a renewal date, can be overwritten as Type 1. A snapshot records history only from its first run, so the dimension knows nothing before that day, and the README should say so.

Extend it with the join that proves the model: a fact that finds the customer version valid on its order date, and a test that every fact row finds exactly 1 version. The guide to slowly changing dimension types 1 to 6 sets Type 2 beside the alternatives.

Interview signal

Interviewers who ask about slowly changing dimensions rarely stop at the definition. They ask how a fact finds the version valid on its date and what happens to a fact that arrives late; the SCD interview questions cover that ground, and this project gives you a model to answer from.

dbtDuckDBPython
josephmachado/simple_dbt_project187★DataSimulated customers, orders and clicks (generated)Start Data Engineering dbt tutorial

Events and change: nested events, activity streams and schema drift

Tier 3 · Projects 6-8

3 projects on data that arrives as records you did not design: an analytics export whose records nest, a public timeline modeled as 1 stream, and a loader that lets its schema change as the source does. The model's job here is to decide a grain for data that did not come with one, and to keep working when the source moves.

Project 06 · Event model

GA4 events, sessions and items with dbt-ga4

Intermediate2 weekendsFree cloud tier

Velir's dbt package for the Google Analytics 4 export to BigQuery, run over Google's obfuscated sample of Google Merchandise Store events from 1 November 2020 to 31 January 2021. It builds stg_ga4__events with event and session keys and stg_ga4__event_items for the items of ecommerce events. Sessions get fct_ga4__sessions and dim_ga4__sessions while page metrics go in fct_ga4__pages, and everything loads incrementally.

GA4 exports nest everything. event_params is a repeated record of key and value pairs, and items is a repeated record whose UNNEST multiplies event rows: 1 purchase with 4 items becomes 4 rows, and event revenue summed after the unnest is 4 times too high unless it lives at the item grain. The export has no session table and no event id, so the package builds both keys; Google's guidance is that user_pseudo_id together with the ga_session_id parameter identifies a session. Read how each key is built, then write the uniqueness tests yourself.

The sample is obfuscated, with placeholder values such as <Other> and NULL, so some accepted-values tests will fail; document which placeholders map to an unknown member. The BigQuery sandbox needs no credit card and gives 1 TiB of queries a month and 10 GiB of storage, with tables that expire after 60 days, so keep the whole build reproducible from dbt.

Interview signal

Clickstream and sessionization prompts appear in modeling and SQL rounds alike. Saying what the grain becomes after each UNNEST, and why the session key needs 2 columns, shows you've modeled nested event data rather than read about it.

BigQuerydbtSQL
Project 07 · Activity stream

GitHub activity stream in Activity Schema 2.0

Intermediate+2 weekendsLocal & free

The Activity Schema repository holds the specification and an implementation guide for modeling a business as 1 time-series table of an entity doing activities. Build it over a week of hourly GH Archive files, the public GitHub timeline, loaded into DuckDB. The activity stream you publish needs the 5 columns version 2.0 requires (activity_id, ts, customer, activity, feature_json), where customer is the GitHub account and activity is a named action such as forked_repo. Add an entity table named for the entity in the plural.

The activity stream replaces a fact table per process with 1 narrow table and a vocabulary of temporal joins that the specification names, such as first ever and last before, or first after and aggregate in between, each joining activities on the entity and the timestamp alone. Answer a question such as what share of accounts open a pull request within 7 days of their first fork, then answer it again against a star with a fact per event type, and compare the SQL.

Version 2.0 removed activity_occurrence and activity_repeated_at from the standard, and the implementation guide adds them back as computed columns (row_number and lead) because first and last queries run faster with them. The guide also advises 1 stream table until the data passes roughly 50 million rows.

Interview signal

When a senior interviewer asks you to model user activity for funnel or retention analysis, they listen for whether you can draw both the star and the stream and say when each wins: the stream when activities keep being added, the star when a BI tool needs typed columns and declared joins.

PythonDuckDBSQL
Project 08 · Schema evolution

A model that absorbs schema change, loaded with dlt

Intermediate1 to 2 weekendsLocal & free

dlt is an open-source Python library that loads data into DuckDB, PostgreSQL, Snowflake, BigQuery and other destinations and evolves the destination schema as the data changes. Its schema evolution walkthrough loads nested records, then changes them in several ways at once: it adds a column, renames another, removes a third and turns an integer field into a string. Run it into DuckDB, then build dbt staging models over the tables it creates.

dlt adds a column when a field appears, and when a type changes it writes a variant column such as inventory_nr__v_text rather than overwrite the old one, so no value is lost. Nested dictionaries flatten into columns, and nested lists become child tables named for their path. The staging model absorbs the change: it declares the columns it uses, folds a variant back into 1 typed column and ignores what it does not need.

Schema contracts decide how much change is allowed: evolve accepts it, freeze rejects it, and discard_rows or discard_columns drops what does not fit. Extend the project with a contract on the tables your models read and a test that fails when a new variant column appears, which turns a silent upstream change into a reviewed one.

Interview signal

An upstream team renames a column or changes its type, and the interviewer asks what breaks and how you would have designed for it. That comes up in modeling and pipeline rounds alike, and a loader that records every change, a staging layer that declares what it uses and a contract on what may change is an answer you can run in front of them.

PythondltDuckDBdbt
dlt-hub/dlt5.9k★DataThe walkthrough's nested records, then a live API of your choicedlt schema evolution walkthrough
GA4 data at 3 grains (project 6): sessions, events and items
dim_user
user_pseudo_idPKVARCHAR
first_seen_atTIMESTAMP
dim_item
item_idPKVARCHAR
item_nameVARCHAR
item_categoryVARCHAR
fct_sessions
session_keyPKVARCHAR
user_pseudo_idFKVARCHAR
ga_session_idBIGINT
session_start_atTIMESTAMP
page_viewsINT
fct_event_items
event_keyPKVARCHAR
item_indexPKINT
item_idFKVARCHAR
quantityINT
item_revenueDECIMAL
fct_events
event_keyPKVARCHAR
session_keyFKVARCHAR
event_nameVARCHAR
event_atTIMESTAMP
page_locationVARCHAR

session_key combines user_pseudo_id and ga_session_id. Item revenue lives only at the item grain, the grain the items record unnests to.

UNNEST sets the grainEach unnest of a repeated record creates a new grain; a measure belongs to exactly 1 of them.
Keys you buildThe export has no session table and no event id, so both keys are yours to build and test.

Integration and serving: bus matrix, data vault and semantic layer

Tier 4 · Projects 9-11

Senior modeling rounds ask about many models at once, and these 3 projects cover it: how separate facts stay 1 warehouse through conformed dimensions, how a vault takes in many sources with a full history, and how a semantic layer defines each metric once for every tool that reads it.

Project 09 · Bus matrix

TPC-DS bus matrix and drill-across mart

Advanced2 weekendsLocal & free

MotherDuck's dbt-ducklake example loads the 25 TPC-DS tables into DuckLake and materializes the benchmark's 99 queries as dbt models, with a local target that needs no account. TPC-DS models a retailer that sells in stores and by catalog as well as on the web. The schema has 7 fact tables and 17 dimensions across those 3 channels. On top of it, draw the enterprise bus matrix (1 row per business process, 1 column per dimension) and build a drill-across mart of net sales after returns by item and month across all 3 channels.

The bus matrix is how a warehouse that several teams build stays 1 warehouse: each process gets its own fact table, and the facts integrate only through conformed dimensions. date_dim is one; item and customer are 2 more. The mart teaches drill-across: aggregate each fact to the shared grain of item and month first, then join the aggregates. Joining store_sales to store_returns row by row before aggregating fans out and overstates both.

The example reads scale factor 100 Parquet from public S3, which is large, so build only the models tagged raw first, or generate scale factor 1 locally with DuckDB's CALL dsdgen(sf = 1) and point the sources at it. The query models give you tested SQL against the same schema to read beside your own.

Interview signal

Senior modeling rounds ask how separate marts stay consistent and how a question that spans 2 processes gets answered. The bus matrix and drill-across are that answer, and TPC-DS lets you show them on a model of 24 tables rather than on a whiteboard.

dbtDuckDBSQL
Project 10 · Data vault

Data Vault 2.0 on TPC-H with AutomateDV

Advanced2 to 3 weekendsCloud account

The AutomateDV demo project and its worked example build a Data Vault 2.0 warehouse with dbt on Snowflake's TPC-H sample data: hubs for business keys such as customers and orders (parts have a hub too), links for the relationships between them, satellites for descriptive attributes and their history, and transactional links. The demo simulates daily feeds, so you watch each load cycle arrive.

The vault separates what an entity is (the hub), how entities relate (the link) and what was true about them when (the satellite), and it loads everything insert-only: each row gets a hash key, plus its load time and record source. Load a later cycle and watch the satellites gain rows while the hubs do not. The worked example is candid about 1 limit: the inventory view cannot be filtered by date, so it arrives whole each cycle, and its load times say little about performance at scale.

Then pay the price the vault charges at query time: build a point-in-time table and a star on top of it, because analysts don't query hubs and satellites directly. AutomateDV has macros for both, and the data vault modeling guide covers the theory in interview terms.

Interview signal

Teams that run a vault want to know whether you understand why it splits keys from relationships and both from context, and what that costs at query time. Building the vault and the star on top of it lets you argue both sides.

dbtAutomateDVSnowflake
Project 11 · Semantic layer

A semantic layer on the Jaffle Shop with MetricFlow

Intermediate+1 to 2 weekendsLocal & free

dbt Labs' template for the dbt Semantic Layer: a Jaffle Shop project with semantic models and metrics defined in YAML. Seed the data, run dbt build, check the definitions with mf validate-configs and query the large_orders metric with mf query. MetricFlow installs with the dbt adapter for your warehouse, and the DuckDB adapter keeps the whole project on a laptop.

A semantic layer moves the definition of a metric out of every dashboard and into 1 reviewed file. MetricFlow compiles each request into SQL and picks the joins from the entities you declared, so a model with an unclear grain produces wrong numbers quickly and visibly; read the SQL it generates for every query. Extend the template with a metric of each of the 4 kinds MetricFlow supports: simple and ratio metrics, then cumulative and derived ones.

The ratio metric is the lesson to keep. Average order value for a month is total revenue divided by total orders, not the average of each day's ratio, and the layer gets it right once you define it as a ratio rather than as a column you average.

Interview signal

2 dashboards disagree on revenue, and you are asked what you would do: that is a common opening for a modeling discussion. A layer you built, with each metric defined once and tested, turns the answer into a design you can show.

dbtMetricFlowDuckDB
TPC-H as a data vault (project 10): hubs, links and satellites
sat_customer_details
customer_hkPKVARCHAR
load_datetimePKTIMESTAMP
hashdiffVARCHAR
customer_nameVARCHAR
customer_addressVARCHAR
record_sourceVARCHAR
sat_order_details
order_hkPKVARCHAR
load_datetimePKTIMESTAMP
hashdiffVARCHAR
order_statusVARCHAR
total_priceDECIMAL
record_sourceVARCHAR
hub_customer
customer_hkPKVARCHAR
customer_keyINT
load_datetimeTIMESTAMP
record_sourceVARCHAR
hub_order
order_hkPKVARCHAR
order_keyINT
load_datetimeTIMESTAMP
record_sourceVARCHAR
link_customer_order
customer_order_hkPKVARCHAR
customer_hkFKVARCHAR
order_hkFKVARCHAR
load_datetimeTIMESTAMP
record_sourceVARCHAR

A changed customer address adds a sat_customer_details row with a new load_datetime and hashdiff; hub_customer does not change.

Insert-onlyEvery row carries a hash key plus its load time and record source, and no row is ever updated.
The query-time costAnalysts read a point-in-time table and a star built on the vault. They do not query hubs and satellites directly.

Telecom Network Connectivity Warehouse

> We're a telecom provider building a new data warehouse. We need to model our network infrastructure: towers, switches, fiber links, and the connections between them. We also need to track outages and capacity utilization. Can you design this?

+ Table
+ Column
PK
FK
SK
UK
Architecture
Data Modeling
Model the schema.

Click + Table in the toolbar, or right-click the canvas to add one.

Drag from a key column's edge dot to another column to draw a foreign key.

Transaction facts, periodic snapshots and accumulating snapshots

A fact table records a business process in 1 of 3 shapes, and the shape follows from how the process meets time. A transaction fact inserts 1 row per event and never changes it: an order line, a payment, a page view. A periodic snapshot inserts 1 row per entity per period whether or not anything happened, which is how station availability or an account balance is kept. An accumulating snapshot inserts 1 row when a process starts and updates it at each milestone until the process ends.

Take 1 order that's purchased on day 0, approved on day 1, handed to the carrier on day 3 and delivered on day 8. As transactions it is 4 rows, 1 per milestone. As a daily snapshot it is 9 rows, 1 per day from purchase to delivery. As an accumulating snapshot it is 1 row whose milestone dates fill in as they happen.

1 order's life from purchased on day 0 to delivered on day 8, stored 3 ways: a transaction fact writes 1 row per milestone event, a periodic snapshot writes 1 row every day whether or not anything happened, and an accumulating snapshot writes 1 row that is updated as each milestone lands1 order's life from purchased on day 0 to delivered on day 8, stored 3 ways: a transaction fact writes 1 row per milestone event, a periodic snapshot writes 1 row every day whether or not anything happened, and an accumulating snapshot writes 1 row that is updated as each milestone lands

The shape decides how the table loads and which questions are cheap. Transactions append, periodic snapshots append on a clock, and accumulating snapshots merge. The time from approval to delivery is a subtraction on 1 accumulating row but a self-join across transaction rows, while stock on hand at the end of a month is a lookup in a periodic snapshot and a running sum over transactions.

How a Type 2 dimension answers what was true on a date

A Type 2 dimension keeps a row per version of each entity. Every version has its own surrogate key and a validity range, from the date it became true to the date a tracked attribute changed, and the latest version has an open end. dbt snapshots build this table by comparing each run with the last: the timestamp strategy opens a version when an updated_at column moves, and the check strategy opens one when a listed column changes. Since dbt 1.9 the hard_deletes setting decides whether a key that vanishes from the source is ignored or closed; a third option records it as a deleted row.

The query that proves the model is a point-in-time lookup: for a date, return the version whose start is on or before it and whose end is after it or empty. Over a 90 day window, a customer that changes its name on day 30 and its status on day 62 has 3 versions, and a lookup on day 45 lands on version 2 and nowhere else.

1 entity's Type 2 history over 90 days: entity_sk 1 is valid until the name change on day 30, entity_sk 2 until the status change on day 62, entity_sk 3 is current, and a lookup on day 45 crosses only entity_sk 21 entity's Type 2 history over 90 days: entity_sk 1 is valid until the name change on day 30, entity_sk 2 until the status change on day 62, entity_sk 3 is current, and a lookup on day 45 crosses only entity_sk 2

2 rows or 0 rows for any entity on any date is a bug, and a test should say so. Facts join the dimension the same way, on the natural key plus the fact's own date, which is also where late-arriving facts show up: a fact loaded today for last month must find last month's version, not the current one.

Build Type 2 versions from daily snapshots

WITH flagged AS (
  SELECT
    snapshot_date,
    customer_id,
    customer_name,
    status,
    CASE
      WHEN LAG(customer_name) OVER (PARTITION BY customer_id ORDER BY snapshot_date) = customer_name
       AND LAG(status) OVER (PARTITION BY customer_id ORDER BY snapshot_date) = status
      THEN 0
      ELSE 1
    END AS is_new_version
  FROM customer_snapshot
),
versions AS (
  SELECT
    customer_id,
    customer_name,
    status,
    snapshot_date AS valid_from,
    LEAD(snapshot_date) OVER (PARTITION BY customer_id ORDER BY snapshot_date) AS valid_to
  FROM flagged
  WHERE is_new_version = 1
)
SELECT
  ROW_NUMBER() OVER (ORDER BY customer_id, valid_from) AS customer_sk,
  customer_id,
  customer_name,
  status,
  valid_from,
  valid_to,
  CASE WHEN valid_to IS NULL THEN 1 ELSE 0 END AS is_current
FROM versions
ORDER BY customer_id, valid_from;

2 customers over 6 daily snapshots. C_A changes its name on 2026-01-31 and its status on 2026-03-04, so it gets 3 versions; C_B never changes and keeps 1. Add WHERE valid_from <= '2026-02-15' AND (valid_to IS NULL OR valid_to > '2026-02-15') to see a point-in-time lookup.

How to choose a dataset for your own data modeling project

A dataset makes a good modeling project when it has more than 1 business process, which forces dimensions to be shared. It also needs a published data dictionary that documents what the codes and measures mean, and it needs change, so the model has to handle rows that arrive or change and fields that appear. The NYC TLC trip records have all 3. A single static CSV has none, which is why most tutorial star schemas teach so little.

Look for change first. A source that publishes monthly (the TLC trip files, bikeshare trip files), one that republishes its full population on a schedule, or a live feed (GBFS, Wikimedia EventStreams) forces the decisions that matter: how the model loads incrementally, how history is kept, and what happens when a field appears. Then read the licence. Olist's CC BY-NC-SA 4.0 is fine for a portfolio and wrong for a product.

Benchmark generators fill the remaining gap. TPC-DS and TPC-H produce a retailer's data at any scale factor inside DuckDB, with schemas large enough to need a bus matrix and queries you can check your model against. Once you've finished a project from this list, point the same model at a second dataset of your choice; the second build is where you find out which decisions were the dataset's and which were yours.

Common mistakes in data modeling projects

A staging layer that renames columns, followed by a mart that is the source schema again, isn't a model: nothing in it declares a grain or conforms a dimension, and it answers no question faster than the source did. Start every model from the business process and the question, and let the source schema be an input.

A fact table whose grain nobody wrote down gets a second grain the first time someone joins a table at a different level, as order payments do when joined onto order lines. Write the grain as a sentence in the model's documentation and enforce it with a uniqueness test on the columns the sentence names.

Balances and inventory levels are semi-additive across time, as is bikes available, and ratios are non-additive everywhere. A model that exposes them without saying how they aggregate invites the wrong dashboard. Joining 2 fact tables row by row before aggregating fans out and double counts; drill across through conformed dimensions instead, as the TPC-DS mart does.

A Type 2 dimension with valid_from and valid_to that no fact joins by date proves nothing. Ship the point-in-time join, and a test that every fact row finds exactly 1 version. And a cloned repository with no change of your own is a tutorial, not a project: reviewers recognise the popular ones, so the extension is the part they ask about.

Management schemas for libraries and schools or for hospitals and hotels are normalized designs for software that writes rows. A reviewer looks in them for a declared grain or kept history, and for someone who consumes the model. Finding none, the reviewer reads them as a signal for the wrong job.

How to turn a data modeling project into interview answers

Write the grain of every table as 1 sentence, and learn the sentences. The first question about any model you present will be what 1 row means, and a sentence you can say without looking beats a diagram you have to read aloud.

Keep a decisions log of 3 to 5 entries per project: the decision, the alternative you rejected and why. For the taxi project, 1 fact with a service type against 2 facts; for the Type 2 project, the check strategy against the timestamp strategy; for the data vault, a vault against a star loaded directly. Interviewers probe decisions more than syntax, and a log turns each entry into a prepared answer.

Break the model on purpose and keep the evidence: a duplicate row that fails the uniqueness test, a late fact that finds no dimension version, a new field that fails the schema version test. Describing what failed and how the model caught it is the most convincing thing you can say about a model you built.

Then say it out loud under time pressure, which is a separate skill from building. The data modeling interview round guide explains how the round runs and what interviewers score, so you can map each project to the questions it answers.

Practise the modeling questions these projects prepare you for

A project teaches the decisions; an interview asks you to make them in 45 minutes on a blank canvas for a business you have never seen. Close that gap by drilling the same patterns on fresh prompts: work through the data modeling interview questions that cover grain and keys along with slowly changing dimensions and fact table types, then design full models under a clock in the data modeling practice problems, which you draw on the same canvas as the diagrams in the project tiers.

Data modeling projects FAQ

What is a good data modeling project for a data engineer?+
One whose deliverable other people build on, built on public data that keeps changing and enforced with tests. The deliverable can be a star schema with a declared grain or a snapshot fact. A Type 2 dimension counts, as do event models; so do a data vault and a semantic layer. The model should serve a real consumer, such as a dashboard query or a metric, and survive at least 1 change in its source.
Which data modeling project should I start with?+
Start with the Kimball star schema tutorial on AdventureWorks if you haven't built a dimensional model before: it runs on a laptop in a weekend and teaches the grain decision every later project depends on. If you already know star schemas, start with the dbt snapshot Type 2 project or the GA4 event model, and if you target senior roles, go to the TPC-DS bus matrix or the data vault.
Where can I find datasets for data modeling practice?+
Use data with more than 1 business process and a published data dictionary, from a source that still changes. NYC TLC trip records and the Olist e-commerce dataset both qualify. So do Capital Bikeshare trip files with its GBFS feed, and the GA4 sample dataset in BigQuery. GH Archive and Wikimedia EventStreams cover event streams. AdventureWorks ships with many tutorials, and TPC-DS and TPC-H generate benchmark retail data at any scale inside DuckDB.
Should I learn Kimball or Data Vault first?+
Kimball first. Nearly every analytics model ends in facts and dimensions, and a data vault is usually queried through a star built on top of it, so dimensional modeling is the prerequisite. Learn the vault when the roles you target run one, typically where many source systems and a full audit trail matter.
Is a library or school management database a good data modeling project?+
Not for a data engineering portfolio. It is a transactional database design exercise that nobody builds on. It never changes, and it has no grain to declare. Reviewers look for an analytical model over real data, such as a fact table with tested keys, history in a dimension or events modeled for analysis.
Do I need a paid cloud warehouse for these projects?+
No. 9 of the 11 projects run on a laptop for free, most of them on DuckDB, and the GA4 project runs in the free BigQuery sandbox. The data vault is written for Snowflake, whose free trial covers a first build. The modeling decisions are the same on any engine.
How do I present a data modeling project on GitHub?+
Lead the README with a diagram of the model and 1 sentence stating the grain of every table, then list the tests that enforce each grain, the decisions you made with the alternative you rejected, and the change you made the model survive. Keep the build to 1 command so a reviewer can run it, and credit the repository you started from.
02 / Why practice

The candidate who gets the offer

  1. 01

    Reading a solution is not the same as writing one

    Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    The round is won on tradeoffs, not on the diagram

    Grain, star vs snowflake, SCD type, conformed dimensions, late-arriving data. Modeling under live pushback is what separates the bands, and it is the half almost nobody rehearses

Related guides