In this lesson, we will build a relational data model in Power BI. By connecting multiple normalized tables in a star schema, we create a flexible and efficient environment for analysis.
You can find all the resources for this lesson—including CSV files, the PostgreSQL schema, and the final Power BI template—in my GitHub repository: https://github.com/analysislessons/Article_PowerBIModel
Power BI offers multiple ways to ingest data. You can either load CSV files directly or connect to a PostgreSQL database for a more professional workflow.
Connecting to PostgreSQL requires the database to be running and accessible.
localhost or an IP address) and the Database name.
After filling in the fields and clicking Connect, another window will appear. Select all tables and click Load.
Then the tables will be imported, and relationships will be created automatically based on the database schema:
If you are using the CSV files, load all five:
traffic_violations.csv (The central fact table)drivers.csvcars.csvlocations.csvviolation_details.csv
Use the Transform Data option to open the Power Query Editor and ensure data types (like Dates and Decimals) are correctly recognized.
In Power BI, it is best practice to create a separate Date (Calendar) table rather than using the raw date column from your fact table. This enables Time Intelligence features.
Switch to the Model View (the third icon on the left sidebar). Here you can see your tables as boxes.
You can see that the tables are not organized.
You can rearrange them by clicking and dragging. Set them up in a star schema: place the fact table, traffic_violations, in the center and arrange the other tables around it.
To create a relationship, drag a field from a dimension table to the matching field in the traffic_violations fact table:
drivers.id -> traffic_violations.driver_id
cars.id -> traffic_violations.car_idlocations.id -> traffic_violations.location_idviolation_details.id -> traffic_violations.violation_idDate.Date -> traffic_violations.date.
date column in the traffic_violations table. This ensures all filters and slicers use the dedicated Date table.
Double-click each relationship line. For a standard Star Schema, ensure:
traffic_violations).
In the Model View, select the foreign key columns in the traffic_violations table (e.g., driver_id, car_id) and set Is Hidden to Yes. This keeps your Report view clean.
Let's try to calculate the total penalty points for each driver. We will use Benjamin Richardson (ID 3) as our test case. Benjamin has 5 violations in total (2x 10 points, 2x 2 points, 1x 5 points), so his correct total should be 29.
If you simply create a Table visual
and drag first_name, last_name, and then the penalty_points column from violation_details directly:
Why: Because you dragged a raw column from a dimension table, Power BI doesn't know it should sum them based on the event log. It simply shows the unique points associated with that driver through the relationship. It's essentially showing you the "types" of violations he committed.
If you create a measure: Penalty_points_sum = SUM(violation_details[penalty_points])
Why: This happens because filters flow from the "1" side to the "Many" side. When you filter by a Driver, the filter travels to the Fact Table but it cannot flow "backwards" to the Violation Details table. The SUM function calculates on the unfiltered dimension table.
The professional way is to use an iterator measure:
Create this measure in the fact table, traffic_violations.
Internals:
traffic_violations table row-by-row.
Just like Tableau's Physical Join, you can use the Merge Queries feature in the Power Query Editor (the "M" engine) to join the points directly into the fact table before the data even reaches your model. This is often better for performance in very large models, as it simplifies the DAX needed to a simple SUM.
Result: Creating the same visual as earlier, but using this time penalty_points column directly from traffic_violations correctly shows Benjamin Richardson: 29.
Why this is the "Physical" equivalent:
+48 790-430-860
analysislessons@gmail.com