dbt Setup for Azure PostgreSQL
Career relevance: Week 10 in the NL data job market
History of Analytics Engineering
Going Further: Optional Deep Dives
Build a Daily Borough Stats Mart with dbt. The chapter hands-on walked you through building fct_trips: a one-row-per-trip mart: step by step. The practice exercises drilled specific skills (macros, singular tests, debugging refs, propagating column changes) against that same project.
This assignment asks you to build a second, independent dbt project end to end, against the same raw_trips and raw_zones source data, but producing a mart at a different grain: one row per pickup_borough per date. You will redo every step (sources → staging → mart → tests → docs) on your own, without the chapter's line-by-line guidance. The goal is to prove you can take a business question and deliver a versioned, tested, documented dbt project that answers it.
By the end of this assignment, you should be able to:
dbt build, and understand without asking you questions.Your stakeholder (imagine a product manager for NYC Green Taxi) wants a dashboard showing, for each day in the January 2024 data:
EWR and Unknown are legitimate zone labels, and you will also see a dirty NaN borough. Do not hardcode a borough list, and expect NaN to appear as its own row.)fare_amount per borough per day.tip_pct per borough per day.trip_distance per borough per day.Grain of the mart: one row per (pickup_borough, pickup_date).
The load covers January 2024, but a handful of trips are timestamped in the final hours of December 31, 2023 and will land in your mart with a 2023-12-31 date. That is expected, not a bug: do not filter them out unless a task tells you to.
<aside>
💡 This is a separate project from your Week 10 chapter work. The scaffold gives you a fresh nyc_taxi_borough_daily/ project; you reuse the connection settings from Chapter 2, but the models, tests, and docs are yours to write.
</aside>
The Week 10 assignment repo lives in the HYF organization. Your cohort uses a fork in the HackYourAssignment organization: your teacher will share the link at the start of the week. Fork your cohort's repo into your own GitHub account and clone your fork. The scaffold is a ready-to-run dbt project: it ships dbt_project.yml, packages.yml, TODO stubs for every model and test, templates for reports/answers.md and AI_ASSIST.md, and an autograder you can run locally with bash .hyf/test.sh. Your job is to fill in the stubs, not to scaffold the project.
Copy profiles.yml.example to a project-local profiles.yml pointing at the same shared Azure PostgreSQL instance you used in Chapter 2, and set your personal dev_<your_name> schema.
<aside>
⚠️ Use the schema you already own. Your shared-DB login can only build into your own dev_<your_name> schema, the one you used for the Week 10 chapters: it does not have permission to create new schemas. Reusing it is fine: the assignment's stg_trips and stg_zones replace the chapter versions (identical staging models), and the new fct_daily_borough_stats mart is added alongside.
</aside>
Run dbt debug and confirm All checks passed!.
Create models/staging/_sources.yml that declares raw_trips and raw_zones in the nyc_taxi schema. This is identical to Define sources for the Week 9 raw tables: the point is that you write it again from scratch, not copy-paste.
Create two staging models:
stg_trips.sql: one row per trip. Include the columns you will need for the mart: pickup_datetime, pickup_location_id, fare_amount, tip_amount, trip_distance, plus a derived tip_pct using your own safe_divide macro. Copy macros/safe_divide.sql from your Week 10 chapter project (or from the reference repo's solution branch if you don't have it): macros are reusable infrastructure, not the modelling logic you are practising here, so copying is fine. Drop rows where pickup_location_id is null or fare_amount < 0.stg_zones.sql: one row per TLC zone. Expose at least location_id and borough.Materialize both as view.
Create models/marts/fct_daily_borough_stats.sql materialized as table, grain one row per (pickup_borough, pickup_date). Join stg_trips to stg_zones on pickup_location_id, then aggregate. Columns to expose:
<aside>
⚠️ Use an INNER JOIN, not a LEFT JOIN. A few trips carry a pickup_location_id that has no matching zone (for example 999). A LEFT JOIN keeps them with a NULL borough, which breaks your not_null test on pickup_borough. An INNER JOIN drops those few unmatched trips so pickup_borough can serve as part of the mart's primary key. The real Unknown borough is a valid zone label and stays in.
</aside>
| Column | Type | Definition |
|---|---|---|
pickup_borough |
text | Borough from stg_zones.borough |
pickup_date |
date | pickup_datetime::date |
trip_count |
bigint | count(*) |
total_fare |
numeric | sum(fare_amount) |
avg_tip_pct |
numeric | avg(tip_pct) |
avg_trip_distance |
numeric | avg(trip_distance) |
<aside>
⌨️ Sanity check: after dbt run --select +fct_daily_borough_stats, query your mart and confirm Manhattan has the highest trip_count on most days. If it does not, you likely joined on the wrong key or filtered too aggressively in staging.
</aside>
Attach at least the following to your staging and mart models via YAML schema files:
not_null on every column used as a join or group-by key.pickup_borough, pickup_date) using dbt_utils.unique_combination_of_columns. This is the mart's primary key: if it fails, your GROUP BY is wrong.tests/ that expresses a business rule your generic tests cannot. A good candidate: flag rows where avg_tip_pct > 1 (tip exceeded the fare) at severity: warn. The test is expected to return a small number of rows for the Unknown borough: you will explain this in Task 7 Q3.Install dbt_utils via packages.yml + dbt deps before running the compound uniqueness test.
Run dbt build (not dbt run + dbt test separately) and ensure the summary ends with ERROR=0. Warnings are acceptable.
description: on every column of fct_daily_borough_stats in its YAML schema file. Descriptions must explain what the column means and what its units are, not restate the column name.description: with the mart's grain, source lineage, and known caveats (rows dropped in staging, any WARN-severity tests, etc.).dbt docs generate and dbt docs serve. Take a screenshot of the lineage graph showing raw_trips + raw_zones → stg_* → fct_daily_borough_stats, and save it as docs/lineage.png in your repo.<aside>
💡 Codespace users: dbt docs serve opens localhost:8080. In a Codespace, forward the port first: in VS Code, open the Ports panel (Terminal → Ports), click Forward a Port, enter 8080, then open the forwarded URL. You can then screenshot the lineage graph in your browser.
</aside>
Query your mart to answer the stakeholder's questions. Save the queries and results in reports/answers.md:
total_fare across the whole loaded dataset?trip_count (sum across all boroughs)?avg_tip_pct seen for any (borough, day) combination, and on which borough/date?trip_count for Manhattan vs Brooklyn? Hint: Postgres has an ordered-set aggregate for median: percentile_cont. Filter by borough and apply it to trip_count.Each answer must include: the SQL you ran, the result, and one sentence interpreting it.
Use an LLM at least once during this assignment (for debugging, understanding an error, or reviewing your SQL) and document that use in AI_ASSIST.md. Describe:
<aside> ⚠️ Do not paste any real data into an LLM: the NYC TLC dataset is public so sample rows are safe here, but the discipline of scrubbing data is what you must practise.
</aside>
nyc_taxi_borough_daily/
├── .gitignore # must exclude profiles.yml
├── dbt_project.yml
├── packages.yml
├── profiles.yml.example # commit a sanitized copy (no password)
├── macros/
│ └── safe_divide.sql
├── models/
│ ├── staging/
│ │ ├── _sources.yml
│ │ ├── _stg_trips.yml
│ │ ├── _stg_zones.yml
│ │ ├── stg_trips.sql
│ │ └── stg_zones.sql
│ └── marts/
│ ├── _fct_daily_borough_stats.yml
│ └── fct_daily_borough_stats.sql
├── tests/
│ └── assert_avg_tip_pct_within_bounds.sql
├── docs/
│ └── lineage.png
├── reports/
│ └── answers.md
└── AI_ASSIST.md
dbt build must exit 0 (ERROR=0 in the summary line). Any WARN-severity failures must be called out in reports/answers.md with one sentence explaining whether they represent a real data issue or a known limitation.profiles.yml must be git-ignored; commit profiles.yml.example with password: "{{ env_var('PG_PASSWORD') }}" instead.docs/lineage.pngreports/answers.md with the four business-question answersAI_ASSIST.mdYour mentor reviews your PR along these dimensions. No point values here: the review is feedback-first, not a score.
dbt build runs green end to end, and the mart's numbers hold up against the raw data.