This project simulates a complete real-world Business Intelligence (BI) pipeline. The goal was to transform a raw, flat dataset (Superstore) into a structured Data Warehouse capable of handling historical changes (SCD) and providing analytical insights via Power BI.
The project covers the full data lifecycle:
- Data Engineering: Data generation, cleaning, and database modeling (Normalization)
- ETL Development: Building an incremental loading strategy with SCD Type 1 and Type 2 logic
- BI Reporting: Creating an interactive Power BI dashboard with advanced DAX measures and storytelling
superstore_bi_pipeline/
├── dashboard/
│ └── superstore.pbix # Power BI Report file
├── data/
│ └── raw/ # Generated CSV files (initial & secondary)
├── database/
│ ├── config/ # DB connection setup
│ └── scripts/ # SQL scripts for the pipeline
│ ├── create_schemas.sql # DDL: Tables and constraints
│ ├── create_mart.sql # Views for Power BI (Star Schema)
│ ├── load_initial_data.sql # DML: Initial Load logic
│ └── load_secondary_data.sql # DML: Incremental Load (SCD logic)
├── notebooks/
│ └── dataset_split.ipynb # Python script for data generation
└── README.md # Project documentation
I designed a 3-Layer Architecture to ensure data integrity, scalability, and query performance.
Contains raw tables (raw_orders, delta_orders). Data is ingested here directly from CSVs without strict constraints to allow fast loading and initial data profiling.
This is the central Data Warehouse layer. I applied Pragmatic Normalization principles to organize data into logical entities.
Design Decision - Handling History (SCD Type 2): I decided to separate Customers and Addresses into different tables:
core.customers: Handles SCD Type 1 (e.g., Name corrections). We overwrite the old name because we don't need to track typo historycore.addresses: Handles SCD Type 2 (History tracking). Usesvalid_from,valid_to, andis_currentcolumns
Why? This allows tracking a customer's relocation history without duplicating their static profile data (Name, Segment) in every transaction row.
Key Features:
- Surrogate Keys: All tables use internal serial IDs (
customer_id,product_id) instead of relying solely on business keys - Audit Table:
core.load_audittracks execution time, status, and row counts for every ETL load
I created SQL Views to transform the normalized schema into a Star Schema optimized for Power BI:
fact_sales: Implements Point-in-Time logic. Joins orders to the address valid at purchase time for historical accuracydim_products&dim_customers: Denormalized dimensions for easy filtering and drilling
The ETL pipeline handles complex data scenarios during the Secondary (Incremental) Load.
The Jupyter Notebook (dataset_split.ipynb) simulates a real production environment by:
- Standardizing date formats to
YYYY-MM-DDto prevent SQL conversion errors - Generating specific test scenarios: New records, Duplicates, SCD1 changes (customer name updates), and SCD2 changes (region moves)
The load_secondary_data.sql script implements robust logic to handle changes:
SCD Type 1 (Customers):
- Updates customer names in place if they changed in the source
SCD Type 2 (Addresses):
- Identifies if a customer moved to a new region
- Closes the old record by setting
valid_toto the day before the new record starts - Logic for Date Overlaps: Includes a CASE statement to handle edge cases where new record dates conflict with existing history
- Inserts the new active record with
is_current = TRUE
Data Quality & Cleaning:
- Ship Date Repair: Uses
GREATEST(ship_date, order_date)to fix logical errors - Deduplication: Uses
ON CONFLICTandNOT EXISTSclauses to ensure idempotency
The report connects to the Mart Layer using Import Mode for better performance and DAX capabilities.
Date Table: Created a dedicated Calendar table using DAX to support Time Intelligence functions.
Key Measures Implemented:
- SUM vs SUMX: Demonstrated the difference between simple aggregation and iterative calculations
- Time Intelligence: Created Sales YoY % (Year-over-Year growth) and Sales YTD (Year-to-Date)
- Context Manipulation: Used
ALL()to calculate % of Total Sales
- Visuals: KPI Cards, Trend Line, and Map
- Key Feature: The Map uses historical data from
fact_sales. If a customer moved regions, their old sales correctly remain attributed to the original region
- Visuals: Decomposition Tree (AI visual) and Matrix with Data Bars
- Insight: Technology drives the most revenue. Furniture has high volume but critically low profit margins (needs logistics investigation)
- Visuals: Scatter Plot and Histogram
- Scatter Plot Analysis: Shows correlation between Sales and Profit. Identified "Unprofitable Customers" cluster (high sales, negative profit)
- Histogram Analysis: Shows most orders are small value (<$500), indicating a mass-market business model
# Run the Jupyter notebook to generate test data
jupyter notebook notebooks/dataset_split.ipynbThis creates initial_load.csv and secondary_load.csv in data/raw/
Execute SQL scripts in order:
-- 1. Create schemas and tables
database/scripts/create_schemas.sql
-- 2. Import initial data
-- First, import initial_load.csv into stage.raw_orders using your SQL client
-- Then run:
database/scripts/load_initial_data.sql
-- 3. Import secondary data
-- Truncate stage.delta_orders and import secondary_load.csv
-- Then run:
database/scripts/load_secondary_data.sql
-- 4. Create mart views
database/scripts/create_mart.sql