End-to-end SQL analytics project on a star-schema data warehouse: from raw exploration to reusable customer and product reporting views.
Built with SQL Server (T-SQL) | 18K+ customers | 60K sales lines | 295 products | ~€29M revenue analysed
- Business Problem
- Data Model
- Project Structure
- Analytical Workflow
- Reporting Views
- Key Findings
- Presentation
- Skills Demonstrated
- How to Run
- Data Quality Notes
A bike retailer sells bikes, accessories and clothing across six countries. Leadership wants to know:
- Who are our customers, and which of them drive revenue?
- Which products and categories perform, and which do not?
- How has the business changed over time, and where is the growth coming from?
- What should we do next to grow revenue and retention?
This project answers those questions with SQL only, moving from basic exploration to production-style reporting views that BI tools (Power BI, Tableau) can connect to directly.
The warehouse follows a star schema in a gold schema (analytics-ready layer).
erDiagram
FACT_SALES }o--|| CUSTOMERS : "customer_key"
FACT_SALES }o--|| PRODUCTS : "product_key"
FACT_SALES {
nvarchar order_number
int product_key FK
int customer_key FK
date order_date
date shipping_date
date due_date
int sales_amount
tinyint quantity
int price
}
DIM_CUSTOMERS {
int customer_key PK
nvarchar customer_number
nvarchar first_name
nvarchar last_name
nvarchar country
nvarchar marital_status
nvarchar gender
date birthdate
date create_date
}
DIM_PRODUCTS {
int product_key PK
nvarchar product_number
nvarchar product_name
nvarchar category
nvarchar subcategory
int cost
nvarchar product_line
date start_date
}
| Table | Grain | Rows |
|---|---|---|
sales |
One row per order line | ~60,400 |
customers |
One row per customer | ~18,500 |
products |
One row per product | 295 |
sql-data-warehouse-analytics/
├── datasets/
│ ├── customers.csv # customers source
│ ├── products.csv # products source
│ └── sales.csv # sales source
├── scripts/
│ ├── 00_init_database.sql # Create DB, schema, tables, bulk load
│ ├── 01_database_exploration.sql # Tables and column metadata
│ ├── 02_dimensions_exploration.sql # Countries, categories, products
│ ├── 03_date_range_exploration.sql # Time boundaries, customer ages
│ ├── 04_measures_exploration.sql # Headline KPIs
│ ├── 05_magnitude_analysis.sql # Totals by dimension
│ ├── 06_ranking_analysis.sql # Top/bottom products and customers
│ ├── 07_change_over_time_analysis.sql # Monthly trends
│ ├── 08_cumulative_analysis.sql # Running totals, moving averages
│ ├── 09_performance_analysis.sql # Year-over-year, vs. average
│ ├── 10_data_segmentation.sql # Cost bands, customer segments
│ ├── 11_part_to_whole_analysis.sql # Category contribution
│ ├── 12_report_customers.sql # View: gold.report_customers
│ └── 13_report_products.sql # View: gold.report_products
├── reports/
│ ├── customers_report.csv # Export of gold.report_customers
│ ├── products_report.csv # Export of gold.report_products
│ └── Customer_Product_Insights_Report # Written findings
├── presentation/
│ └── Business_presentation.pptx # Executive deck (created with Gamma)
└── README.md
The scripts follow a structured path used in real analytics engagements: understand the data, measure it, then explain it.
| Phase | Script(s) | Question answered | Techniques |
|---|---|---|---|
| 1. Explore | 01-03 | What is in the warehouse? What period does it cover? | INFORMATION_SCHEMA, DISTINCT, MIN/MAX, DATEDIFF |
| 2. Measure | 04 | What are the headline numbers? | SUM, AVG, COUNT DISTINCT, UNION ALL KPI table |
| 3. Magnitude | 05 | How is revenue/customers distributed across countries, categories, genders? | GROUP BY, LEFT JOIN across the star schema |
| 4. Rank | 06 | Who are the top/bottom products and customers? | TOP, RANK() OVER |
| 5. Trend | 07-08 | How is the business changing? What is the running total? | DATETRUNC, FORMAT, SUM() OVER, moving average |
| 6. Performance | 09 | Is each product above or below its own average and last year? | CTEs, LAG(), AVG() OVER (PARTITION BY), CASE |
| 7. Segment | 10 | Which customers are VIP / Regular / New? How are products priced? | CASE segmentation, subqueries |
| 8. Part-to-whole | 11 | Which categories drive the total? | Window SUM() OVER (), percentage of total |
| 9. Productionise | 12-13 | Can this be reused by a BI tool? | CREATE VIEW, layered CTEs, null-safe KPIs |
The final two scripts consolidate the analysis into reusable views in the gold schema.
One row per customer, built in three layers (base query, aggregation, KPI calculation).
- Attributes: name, customer number, age, age group
- Segment: VIP (12+ months, spend > €5,000), Regular (12+ months, ≤ €5,000), New (< 12 months)
- Metrics: total orders, sales, quantity, products, last order date, lifespan (months)
- KPIs: recency, average order value, average monthly spend
One row per product, using the same layered pattern.
- Attributes: name, category, subcategory, cost
- Segment: High-Performer (> €50K), Mid-Range (€10K-€50K), Low-Performer
- Metrics: total orders, sales, quantity, unique customers, lifespan
- KPIs: recency, average selling price, average order revenue, average monthly revenue
Both views guard against division by zero (CASE WHEN ... = 0, NULLIF) and exclude rows with missing order dates.
| Area | Insight |
|---|---|
| Scale | €29.35M revenue, 27,657 orders, 18,482 buying customers, 130 active products |
| Revenue mix | Bikes generate 96.5% of revenue; Accessories (2.4%) and Clothing (1.2%) are small in value but large in units |
| Product concentration | Top 10 products = 42.5% of revenue; 66 High-Performers = 94% of revenue; 6 Low-Performers earn almost nothing |
| Customer value | VIPs are 8.9% of customers but 36.7% of revenue (avg. spend €6,510 vs. €758 for "New") |
| Retention gap | 63% of customers bought only once; the repeat 37% drive 77% of revenue |
| Geography | Australia has 19% of customers but 31% of revenue; average spend is ~2x the US, and 1 in 5 Australian customers is a VIP |
| Growth | Revenue grew 2.8x in 2013 as accessories and clothing launched and new-customer acquisition quadrupled |
| Margins | Estimated margin: Accessories ~63%, Clothing ~45%, Bikes ~39% |
The full write-up with tables, recommendations and data caveats is in docs/.
- Launch a second-purchase programme to convert one-time buyers.
- Use high-margin accessories as the retention hook and bundle them with bike orders.
- Test the Australian approach in the US and Canada, where VIP share is lowest.
- Protect availability of the Mountain-200 and Road-150 families.
- Rationalise low-performing products and review thin-margin items.
An executive-style slide deck summarises the analysis for a non-technical audience: business problem, headline KPIs, customer and product insights, and recommendations.
View the presentation on Gamma | Download the .pptx
- Data modelling: star schema, fact and dimension tables, surrogate keys
- Advanced T-SQL: CTEs, window functions (
RANK,LAG,SUM/AVG OVER),CASE, subqueries, date functions - Analytical thinking: KPI design, segmentation logic, YoY and part-to-whole analysis
- Engineering practice: idempotent scripts (
IF EXISTS ... DROP), documented headers, numbered execution order, reusable views for BI - Business communication: translating query results into insights, a written report and an executive slide deck
Prerequisites: SQL Server (2022 or later for DATETRUNC) and SSMS or Azure Data Studio.
- Clone the repository and place the CSVs where the load script expects them, or update the paths in
init_database.sql:BULK INSERT gold.dim_customers FROM 'C:\sql\sql-project\datasets\customers.csv'
- Run
init_database.sql. Warning: it drops and recreates theDataWarehouseAnalyticsdatabase. - Run scripts
01to11in order to reproduce the analysis. - Run
report_customers.sqlandreport_products.sqlto create the views, then query them:SELECT TOP 10 * FROM customers ORDER BY total_sales DESC; SELECT * FROM products WHERE product_segment = 'Low-Performer';
- One thing I realized later is that recency turned out to be not accurate because I measured against the day (GETDATE()) I ran the queries (about 137 months), So should have used the date when the data ends in January 2014.
- 17 customers have no birthdate.
- 2010 (Dec only) and 2014 (Jan only) should not be used in year-on-year comparisons; 19 order rows have no order date and are excluded from the reports.
Mohammed Adnan
Data Analyst