Retail Etl Pipeline Medallion

reason-machines/data-skills/skills/retail-etl-pipeline-medallion

作者 reason-machines8e7bd5f22574無授權條款4 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫2 個月前更新

End-to-end retail ETL pipeline using PySpark, SQL Server, and Medallion Architecture (Bronze/Silver/Gold layers) for data warehousing

僅含說明Data & Analytics
AI 產生的概覽

指導使用 SQL Server 與選用 PySpark 建立包含青銅、白銀、黃金三層的零售 ETL 資料倉儲。

功能
此技能提供遵循 Medallion 架構的端到端零售 ETL 管線說明與 SQL 模式。內容涵蓋將原始 CSV 資料匯入青銅層、在白銀層進行清理與標準化,以及在黃金層產生彙總 KPI 與維度模型。它也包含選用的 PySpark 轉換範例、Airflow DAG 大綱、資料品質檢查以及疑難排解指引。
適用情境
適用於建置或重建採用青銅、白銀、黃金分層的零售資料倉儲。適合將來自 CRM 或 ERP 的銷售、庫存與供應商資料整合為可供業務分析的團隊。
執行需求
需要 SQL Server 2019 或更新版本、用於資料庫容器的 Docker 與 Docker Compose,以及 Python 3.8+ 與 PySpark 3.x(用於選用的大規模處理路徑)。Apache Airflow 為選用的協作工具。需要 SQL Server 認證與來源 CSV 資料的存取權限;此技能不附帶指令碼,僅提供說明與程式碼範例。

Retail ETL Pipeline - Medallion Architecture Skill

Skill by ara.so — Data Skills collection

Overview

The Retail ETL Pipeline project implements a complete data engineering solution for retail operations using the Medallion Architecture pattern (Bronze → Silver → Gold layers). It handles complex retail scenarios including:

  • Inventory shrinkage resolution
  • Recipe conversions for meat/poultry products
  • Supplier rebate tier tracking
  • Multi-branch sales consolidation
  • Stock level management across locations

The pipeline processes raw CSV data from CRM/ERP systems through three progressive quality layers, ultimately delivering a "Single Version of Truth" for business intelligence.

Architecture Layers

Bronze Layer (Raw Ingestion)

  • Raw data ingestion from CSV files
  • Minimal transformation, preserving source format
  • Audit columns: _loaded_at, _source_file

Silver Layer (Cleaned & Standardized)

  • Data type enforcement
  • Deduplication
  • Standardization (dates, currencies, product codes)
  • Business rule validation

Gold Layer (Business-Ready Analytics)

  • Aggregated metrics
  • Calculated KPIs (inventory turnover, shrinkage %)
  • Dimensional models for BI tools

Installation & Setup

Prerequisites

bash
# Required tools- Docker & Docker Compose- SQL Server 2019+- Python 3.8+- PySpark 3.x- Apache Airflow (optional, for orchestration)

Infrastructure Setup

bash
# Clone the repositorygit clone https://github.com/EsraaSolimanMubarak/Retail-ETL-Pipeline.gitcd Retail-ETL-Pipeline
# Start SQL Server containerdocker-compose up -d
# Wait for SQL Server to be readydocker logs -f retail-sql-server

Database Initialization

bash
# Connect to SQL Server and run schema setupsqlcmd -S localhost,1433 -U sa -P ${SQL_SERVER_PASSWORD} \  -i sql_scripts/00_create_database_and_schemas.sql

Key SQL Scripts Execution Order

The pipeline consists of 13+ SQL scripts that must be executed sequentially:

bash
# 1. Create database and schemassql_scripts/00_create_database_and_schemas.sql
# 2. Bronze layer ingestionsql_scripts/01_bronze_products.sqlsql_scripts/02_bronze_sales.sqlsql_scripts/03_bronze_stock.sql
# 3. Silver layer transformationssql_scripts/04_silver_products.sqlsql_scripts/05_silver_sales.sqlsql_scripts/06_silver_stock.sql
# 4. Gold layer aggregationssql_scripts/07_gold_sales_summary.sqlsql_scripts/08_gold_inventory_metrics.sqlsql_scripts/09_gold_product_performance.sql
# 5. Rebuild pipeline (if needed)sql_scripts/12_rebuild_inventory_pipeline_final_fix.sql

Core ETL Patterns

Pattern 1: Bronze Layer Ingestion (Raw CSV → SQL)

sql
-- Example: Bronze Products TableCREATE TABLE bronze.products (    product_id INT,    product_name NVARCHAR(255),    category NVARCHAR(100),    unit_price DECIMAL(10,2),    supplier_id INT,    _loaded_at DATETIME2 DEFAULT GETDATE(),    _source_file NVARCHAR(500));
-- Bulk insert from CSVBULK INSERT bronze.productsFROM '/data_source/000.Hypermarket Products.csv'WITH (    FIELDTERMINATOR = ',',    ROWTERMINATOR = '\n',    FIRSTROW = 2,    TABLOCK);

Pattern 2: Silver Layer Cleansing

sql
-- Example: Silver Products with Data Quality RulesCREATE PROCEDURE silver.usp_transform_productsASBEGIN    TRUNCATE TABLE silver.products;        INSERT INTO silver.products (        product_id,        product_name,        category,        unit_price,        supplier_id,        is_active,        processed_at    )    SELECT DISTINCT        product_id,        UPPER(TRIM(product_name)) AS product_name,        COALESCE(category, 'UNCATEGORIZED') AS category,        CASE             WHEN unit_price < 0 THEN 0             ELSE unit_price         END AS unit_price,        supplier_id,        1 AS is_active,        GETDATE() AS processed_at    FROM bronze.products    WHERE product_id IS NOT NULL      AND product_name IS NOT NULL;END;

Pattern 3: Gold Layer Aggregations

sql
-- Example: Sales Summary by Branch and ProductCREATE PROCEDURE gold.usp_sales_summaryASBEGIN    TRUNCATE TABLE gold.sales_summary;        INSERT INTO gold.sales_summary (        branch_name,        product_category,        total_quantity_sold,        total_revenue,        avg_unit_price,        transaction_count,        report_date    )    SELECT         s.branch_name,        p.category AS product_category,        SUM(s.quantity) AS total_quantity_sold,        SUM(s.quantity * s.unit_price) AS total_revenue,        AVG(s.unit_price) AS avg_unit_price,        COUNT(DISTINCT s.transaction_id) AS transaction_count,        CAST(GETDATE() AS DATE) AS report_date    FROM silver.sales s    INNER JOIN silver.products p ON s.product_id = p.product_id    GROUP BY s.branch_name, p.category;END;

Pattern 4: Inventory Shrinkage Calculation

sql
-- Complex business logic: Detect inventory discrepanciesCREATE PROCEDURE gold.usp_inventory_shrinkageASBEGIN    WITH StockLevels AS (        SELECT             product_id,            branch_name,            SUM(quantity_on_hand) AS current_stock        FROM silver.stock        GROUP BY product_id, branch_name    ),    ExpectedStock AS (        SELECT             s.product_id,            s.branch_name,            sl.current_stock - COALESCE(SUM(s.quantity), 0) AS expected_stock        FROM StockLevels sl        LEFT JOIN silver.sales s             ON sl.product_id = s.product_id             AND sl.branch_name = s.branch_name        GROUP BY s.product_id, s.branch_name, sl.current_stock    )    INSERT INTO gold.inventory_shrinkage (        product_id,        branch_name,        current_stock,        expected_stock,        shrinkage_qty,        shrinkage_percent,        calculated_at    )    SELECT         sl.product_id,        sl.branch_name,        sl.current_stock,        es.expected_stock,        sl.current_stock - es.expected_stock AS shrinkage_qty,        CASE             WHEN es.expected_stock > 0             THEN ((sl.current_stock - es.expected_stock) * 100.0 / es.expected_stock)            ELSE 0         END AS shrinkage_percent,        GETDATE() AS calculated_at    FROM StockLevels sl    INNER JOIN ExpectedStock es         ON sl.product_id = es.product_id         AND sl.branch_name = es.branch_name;END;

PySpark Integration (Optional)

For large-scale data processing, integrate PySpark for Silver/Gold transformations:

python
from pyspark.sql import SparkSessionfrom pyspark.sql.functions import col, sum, avg, count, when
# Initialize Spark sessionspark = SparkSession.builder \    .appName("RetailETL-Silver") \    .config("spark.sql.warehouse.dir", "/data/warehouse") \    .getOrCreate()
# Read Bronze layer (Parquet or Delta)bronze_sales_df = spark.read.parquet("/data/bronze/sales/")
# Silver transformationssilver_sales_df = bronze_sales_df \    .dropDuplicates(["transaction_id"]) \    .filter(col("quantity") > 0) \    .withColumn("unit_price",                 when(col("unit_price") < 0, 0).otherwise(col("unit_price"))) \    .withColumn("total_amount", col("quantity") * col("unit_price"))
# Write to Silver layersilver_sales_df.write \    .mode("overwrite") \    .partitionBy("branch_name", "sale_date") \    .parquet("/data/silver/sales/")
# Gold aggregationsgold_sales_summary = silver_sales_df \    .groupBy("branch_name", "product_category") \    .agg(        sum("quantity").alias("total_quantity"),        sum("total_amount").alias("total_revenue"),        avg("unit_price").alias("avg_unit_price"),        count("transaction_id").alias("transaction_count")    )
gold_sales_summary.write \    .mode("overwrite") \    .parquet("/data/gold/sales_summary/")

Airflow DAG Example

Orchestrate the entire pipeline with Apache Airflow:

python
from airflow import DAGfrom airflow.providers.microsoft.mssql.operators.mssql import MsSqlOperatorfrom airflow.operators.python import PythonOperatorfrom datetime import datetime, timedelta
default_args = {    'owner': 'data-engineering',    'retries': 2,    'retry_delay': timedelta(minutes=5),}
with DAG(    'retail_etl_medallion',    default_args=default_args,    description='Daily Retail ETL Pipeline',    schedule_interval='@daily',    start_date=datetime(2026, 1, 1),    catchup=False,) as dag:
    # Bronze ingestion    ingest_products = MsSqlOperator(        task_id='bronze_ingest_products',        mssql_conn_id='retail_sql_server',        sql='sql_scripts/01_bronze_products.sql',    )        ingest_sales = MsSqlOperator(        task_id='bronze_ingest_sales',        mssql_conn_id='retail_sql_server',        sql='sql_scripts/02_bronze_sales.sql',    )        ingest_stock = MsSqlOperator(        task_id='bronze_ingest_stock',        mssql_conn_id='retail_sql_server',        sql='sql_scripts/03_bronze_stock.sql',    )        # Silver transformations    transform_products = MsSqlOperator(        task_id='silver_transform_products',        mssql_conn_id='retail_sql_server',        sql='EXEC silver.usp_transform_products;',    )        transform_sales = MsSqlOperator(        task_id='silver_transform_sales',        mssql_conn_id='retail_sql_server',        sql='EXEC silver.usp_transform_sales;',    )        # Gold aggregations    aggregate_sales_summary = MsSqlOperator(        task_id='gold_sales_summary',        mssql_conn_id='retail_sql_server',        sql='EXEC gold.usp_sales_summary;',    )        aggregate_inventory_shrinkage = MsSqlOperator(        task_id='gold_inventory_shrinkage',        mssql_conn_id='retail_sql_server',        sql='EXEC gold.usp_inventory_shrinkage;',    )        # Define dependencies    [ingest_products, ingest_sales, ingest_stock] >> transform_products    [ingest_sales] >> transform_sales    [transform_products, transform_sales] >> aggregate_sales_summary    [transform_products, ingest_stock] >> aggregate_inventory_shrinkage

Configuration

Environment Variables

bash
# SQL Server connectionexport SQL_SERVER_HOST=localhostexport SQL_SERVER_PORT=1433export SQL_SERVER_USER=saexport SQL_SERVER_PASSWORD=${YOUR_SECURE_PASSWORD}export SQL_SERVER_DATABASE=RetailDataWarehouse
# Data pathsexport DATA_SOURCE_PATH=/data_sourceexport BRONZE_LAYER_PATH=/data/bronzeexport SILVER_LAYER_PATH=/data/silverexport GOLD_LAYER_PATH=/data/gold
# Airflow (if used)export AIRFLOW_CONN_RETAIL_SQL_SERVER="mssql://${SQL_SERVER_USER}:${SQL_SERVER_PASSWORD}@${SQL_SERVER_HOST}:${SQL_SERVER_PORT}/${SQL_SERVER_DATABASE}"

Docker Compose Configuration

yaml
# docker-compose.ymlversion: '3.8'
services:  sqlserver:    image: mcr.microsoft.com/mssql/server:2019-latest    container_name: retail-sql-server    environment:      - ACCEPT_EULA=Y      - SA_PASSWORD=${SQL_SERVER_PASSWORD}      - MSSQL_PID=Developer    ports:      - "1433:1433"    volumes:      - ./data_source:/data_source      - ./sql_scripts:/sql_scripts      - sqlserver_data:/var/opt/mssql
volumes:  sqlserver_data:

Common Patterns & Use Cases

Use Case 1: Multi-Branch Sales Consolidation

sql
-- Combine sales from Alex, Cairo, and Giza branchesCREATE VIEW gold.vw_consolidated_sales ASSELECT     'Alexandria' AS branch_name,    * FROM bronze.alex_salesUNION ALLSELECT     'Cairo' AS branch_name,    * FROM bronze.cairo_salesUNION ALLSELECT     'Giza' AS branch_name,    * FROM bronze.giza_sales;

Use Case 2: Recipe Yield Tracking (Meat/Poultry)

sql
-- Track conversion rates for processed meat productsCREATE TABLE silver.recipe_conversions (    recipe_id INT PRIMARY KEY,    raw_product_id INT,    finished_product_id INT,    conversion_ratio DECIMAL(5,2), -- e.g., 0.85 (15% waste)    effective_date DATE);
-- Calculate actual yield vs. expectedSELECT     rc.recipe_id,    SUM(s.quantity * rc.conversion_ratio) AS expected_yield,    SUM(stock.quantity_on_hand) AS actual_yield,    (SUM(stock.quantity_on_hand) - SUM(s.quantity * rc.conversion_ratio)) AS yield_varianceFROM silver.sales sINNER JOIN silver.recipe_conversions rc ON s.product_id = rc.raw_product_idINNER JOIN silver.stock stock ON rc.finished_product_id = stock.product_idGROUP BY rc.recipe_id;

Use Case 3: Supplier Rebate Tier Tracking

sql
-- Track purchase volume for supplier rebate calculationsCREATE TABLE gold.supplier_rebate_tiers (    supplier_id INT,    total_purchases DECIMAL(15,2),    rebate_tier VARCHAR(20),    rebate_percentage DECIMAL(5,2),    calculated_at DATETIME2);
INSERT INTO gold.supplier_rebate_tiersSELECT     p.supplier_id,    SUM(s.quantity * s.unit_price) AS total_purchases,    CASE         WHEN SUM(s.quantity * s.unit_price) > 100000 THEN 'Platinum'        WHEN SUM(s.quantity * s.unit_price) > 50000 THEN 'Gold'        WHEN SUM(s.quantity * s.unit_price) > 25000 THEN 'Silver'        ELSE 'Bronze'    END AS rebate_tier,    CASE         WHEN SUM(s.quantity * s.unit_price) > 100000 THEN 5.0        WHEN SUM(s.quantity * s.unit_price) > 50000 THEN 3.5        WHEN SUM(s.quantity * s.unit_price) > 25000 THEN 2.0        ELSE 1.0    END AS rebate_percentage,    GETDATE() AS calculated_atFROM silver.sales sINNER JOIN silver.products p ON s.product_id = p.product_idGROUP BY p.supplier_id;

Troubleshooting

Issue 1: CSV Bulk Insert Fails

sql
-- Check file path permissionsEXEC xp_cmdshell 'dir C:\data_source\*.csv';
-- Use ERRORFILE to capture bad rowsBULK INSERT bronze.productsFROM '/data_source/000.Hypermarket Products.csv'WITH (    FIELDTERMINATOR = ',',    ROWTERMINATOR = '\n',    FIRSTROW = 2,    ERRORFILE = '/data_source/errors/products_errors.csv',    MAXERRORS = 10);

Issue 2: Duplicate Records in Silver Layer

sql
-- Add deduplication logic with ROW_NUMBERWITH DeduplicatedSales AS (    SELECT *,        ROW_NUMBER() OVER (            PARTITION BY transaction_id             ORDER BY _loaded_at DESC        ) AS rn    FROM bronze.sales)INSERT INTO silver.salesSELECT * FROM DeduplicatedSalesWHERE rn = 1;

Issue 3: Performance Optimization

sql
-- Create indexes on frequently joined columnsCREATE NONCLUSTERED INDEX idx_sales_product_id     ON silver.sales(product_id) INCLUDE (quantity, unit_price);
CREATE NONCLUSTERED INDEX idx_stock_product_branch     ON silver.stock(product_id, branch_name) INCLUDE (quantity_on_hand);
-- Partition large tables by dateCREATE PARTITION FUNCTION pf_sales_date (DATE)AS RANGE RIGHT FOR VALUES (    '2025-01-01', '2025-02-01', '2025-03-01', '2025-04-01');

Issue 4: Rebuild Entire Pipeline

bash
# Use the rebuild script to reset and reprocess all layerssqlcmd -S ${SQL_SERVER_HOST},${SQL_SERVER_PORT} \  -U ${SQL_SERVER_USER} -P ${SQL_SERVER_PASSWORD} \  -d RetailDataWarehouse \  -i sql_scripts/12_rebuild_inventory_pipeline_final_fix.sql

Testing Data Quality

sql
-- Data quality checks for Silver layerSELECT 'Products' AS layer,    COUNT(*) AS total_records,    COUNT(DISTINCT product_id) AS unique_products,    SUM(CASE WHEN unit_price < 0 THEN 1 ELSE 0 END) AS negative_prices,    SUM(CASE WHEN product_name IS NULL THEN 1 ELSE 0 END) AS null_namesFROM silver.products
UNION ALL
SELECT 'Sales' AS layer,    COUNT(*) AS total_records,    COUNT(DISTINCT transaction_id) AS unique_transactions,    SUM(CASE WHEN quantity <= 0 THEN 1 ELSE 0 END) AS invalid_quantity,    SUM(CASE WHEN product_id NOT IN (SELECT product_id FROM silver.products) THEN 1 ELSE 0 END) AS orphaned_productsFROM silver.sales;

Best Practices

  1. Always process through layers sequentially: Bronze → Silver → Gold
  2. Use stored procedures for reusable transformations
  3. Add audit columns (_loaded_at, _source_file, processed_at)
  4. Implement idempotency: Truncate-and-load or upsert patterns
  5. Partition large tables by date or branch for performance
  6. Create comprehensive indexes on join and filter columns
  7. Use CTEs for complex business logic readability
  8. Test data quality at each layer transition
  9. Version control all SQL scripts and configurations
  10. Monitor pipeline execution with Airflow or equivalent orchestrator

來源與署名

來源:reason-machines/data-skills位於skills/retail-etl-pipeline-medallion提交8e7bd5f

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架