BlogData Analytics SeriesChapter 20
SERIES · CHAPTER 20Advanced

Data Pipelines & ETL Basics for Data Analysts

How data moves from source to dashboard — ETL vs ELT, data warehouses vs data lakes, pipeline stages, Apache Airflow DAGs, Python pipeline scripts, incremental loading, and the five most common pipeline failures with fixes. Indian tech stack context throughout.

DATA ANALYTICS SERIES:← Ch 19: Advanced SQLCh 20: Data Pipelines & ETL ←

What is a Data Pipeline?

A data pipeline is an automated sequence of steps that moves data from one or more sources to a destination where it can be analysed. Every time you open a Power BI dashboard and see yesterday's numbers, a pipeline ran overnight to collect, clean, and store that data.

TYPICAL DATA FLOW IN AN INDIAN E-COMMERCE COMPANY
SOURCES
MySQL (orders)
Shopify (catalogue)
Google Ads API
WhatsApp CRM
Shiprocket API
EXTRACT
Python scripts
REST API calls
DB connectors
Fivetran / Airbyte
TRANSFORM
Data cleaning
Joins & aggregations
Business logic
dbt / Python
LOAD / STORE
BigQuery
Snowflake
Redshift
PostgreSQL
CONSUME
Power BI / Looker
Python notebooks
SQL queries
Scheduled reports
ETL — Extract, Transform, Load
Transform data BEFORE loading. Raw data is cleaned in a staging environment; only processed data enters the warehouse.
USE WHEN:
Legacy on-premise systems, strict data governance, limited warehouse storage
COMMON TOOLS:
Informatica, Microsoft SSIS, Talend, custom Python
INDIA CONTEXT:
Banks, insurance companies, government analytics, manufacturing MIS
ELT — Extract, Load, Transform
Load raw data FIRST into the warehouse, then transform it using SQL or dbt. The warehouse does the heavy lifting.
USE WHEN:
Cloud data warehouses, fast-moving teams, complex iterative transformations
COMMON TOOLS:
Fivetran + dbt + BigQuery, Airbyte + Snowflake, Stitch + Redshift
INDIA CONTEXT:
E-commerce, fintech, SaaS, growth-stage startups in Bengaluru / Gurugram

Data Warehouse vs Data Lake vs Data Lakehouse

Data WarehouseData LakeData Lakehouse
Data formatStructured (tables)Any — CSV, JSON, Parquet, images, logsAny, with table format on top (Delta/Iceberg)
SchemaSchema-on-write (defined before load)Schema-on-read (defined at query time)Schema-on-write with flexibility
Storage costModerate to highVery low (object storage)Low (object storage + table format)
Query speedVery fast (SQL)Slow without processing engineFast (Spark, Trino, or DuckDB)
Data qualityHigh (enforced at load)Low ("data swamp" risk)High (ACID transactions + quality checks)
Indian toolsBigQuery, Snowflake, RedshiftAWS S3, GCS, Azure Blob StorageDelta Lake (Databricks), Apache Iceberg
Best forBI dashboards, SQL analytics, reportsRaw data archive, ML feature store, logsModern unified analytics + ML platforms

Part 3 — Writing a Python ETL Pipeline

A well-structured Python pipeline follows a clear pattern: extract data from the source, validate and transform it, then load it to the destination. Each stage should be a separate function so it can be tested and debugged independently.

Python · Daily Orders ETL Pipeline (MySQL → BigQuery)
import pandas as pd
import sqlalchemy
from google.cloud import bigquery
from datetime import datetime, timedelta
import logging

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(message)s')
log = logging.getLogger(__name__)

# ── CONFIG ───────────────────────────────────────────────────
MYSQL_CONN   = 'mysql+pymysql://user:pass@db.company.in/orders_prod'
BQ_PROJECT   = 'evika-analytics'
BQ_DATASET   = 'ecommerce'
BQ_TABLE     = 'daily_orders'
LOOKBACK_HRS = 26  # pull last 26 hours — catches late-arriving data


# ── EXTRACT ──────────────────────────────────────────────────
def extract_orders(cutoff_utc: datetime) -> pd.DataFrame:
    """Pull orders created or updated since cutoff from MySQL."""
    engine = sqlalchemy.create_engine(MYSQL_CONN)
    query  = f"""
        SELECT
            o.order_id,
            o.customer_id,
            o.amount_inr,
            o.discount_inr,
            o.status,
            o.payment_method,
            o.city,
            o.state,
            o.created_at   AS order_created_at,
            o.updated_at   AS order_updated_at,
            c.tier         AS city_tier
        FROM orders o
        JOIN cities c ON o.city = c.city_name
        WHERE o.updated_at >= '{cutoff_utc.strftime('%Y-%m-%d %H:%M:%S')}'
        ORDER BY o.updated_at
    """
    df = pd.read_sql(query, engine)
    log.info(f"Extracted {len(df):,} rows from MySQL")
    return df


# ── VALIDATE ─────────────────────────────────────────────────
def validate(df: pd.DataFrame) -> pd.DataFrame:
    """Fail fast on data quality issues; log warnings for softer issues."""
    assert not df.empty, "No rows extracted — check source query or schedule"
    assert df['order_id'].is_unique, "Duplicate order_ids in extract"
    assert df['amount_inr'].ge(0).all(), "Negative order amounts found"

    null_pct = df.isnull().mean()
    for col, pct in null_pct.items():
        if pct > 0.05:
            log.warning(f"Column '{col}' has {pct:.1%} nulls")

    log.info(f"Validation passed: {len(df):,} rows, {df['order_id'].nunique():,} unique orders")
    return df


# ── TRANSFORM ────────────────────────────────────────────────
def transform(df: pd.DataFrame) -> pd.DataFrame:
    """Clean and enrich the extracted data."""
    # Timestamps: source is IST, store as UTC in warehouse
    for col in ['order_created_at', 'order_updated_at']:
        df[col] = (pd.to_datetime(df[col])
                     .dt.tz_localize('Asia/Kolkata')
                     .dt.tz_convert('UTC'))

    # Derived fields
    df['net_amount_inr'] = df['amount_inr'] - df['discount_inr'].fillna(0)
    df['order_date']     = df['order_created_at'].dt.date          # IST date
    df['order_hour_ist'] = (df['order_created_at']
                              .dt.tz_convert('Asia/Kolkata').dt.hour)

    # Standardise categoricals
    df['payment_method'] = df['payment_method'].str.upper().str.strip()
    df['state']          = df['state'].str.title().str.strip()

    # Financial year (April–March, Indian FY)
    def fy(d):
        return d.year if d.month >= 4 else d.year - 1
    df['financial_year'] = df['order_date'].apply(
        lambda d: f"FY{fy(d)}-{str(fy(d)+1)[-2:]}"
    )

    # Flag Diwali orders (hardcoded for 2026; automate with holiday library)
    diwali_2026 = pd.to_datetime('2026-11-08').date()
    df['is_diwali_window'] = df['order_date'].apply(
        lambda d: abs((d - diwali_2026).days) <= 7
    )

    log.info(f"Transform complete: {len(df):,} rows, ₹{df['net_amount_inr'].sum()/1e7:.2f}Cr net GMV")
    return df


# ── LOAD ─────────────────────────────────────────────────────
def load_to_bigquery(df: pd.DataFrame, date_partition: str):
    """UPSERT: delete the partition date first, then insert. Idempotent."""
    client = bigquery.Client(project=BQ_PROJECT)
    table_ref = f"{BQ_PROJECT}.{BQ_DATASET}.{BQ_TABLE}"

    # Delete today's partition so reruns do not duplicate
    delete_sql = f"""
        DELETE FROM `{table_ref}`
        WHERE order_date = '{date_partition}'
    """
    client.query(delete_sql).result()
    log.info(f"Deleted existing rows for {date_partition}")

    # Load
    job_config = bigquery.LoadJobConfig(
        write_disposition = bigquery.WriteDisposition.WRITE_APPEND,
        time_partitioning = bigquery.TimePartitioning(field='order_date'),
        clustering_fields = ['city', 'payment_method', 'status'],
        schema = [
            bigquery.SchemaField('order_id',         'STRING'),
            bigquery.SchemaField('customer_id',      'STRING'),
            bigquery.SchemaField('amount_inr',       'FLOAT64'),
            bigquery.SchemaField('net_amount_inr',   'FLOAT64'),
            bigquery.SchemaField('order_date',       'DATE'),
            bigquery.SchemaField('financial_year',   'STRING'),
            bigquery.SchemaField('is_diwali_window', 'BOOL'),
        ],
    )
    job = client.load_table_from_dataframe(df, table_ref, job_config=job_config)
    job.result()
    log.info(f"Loaded {len(df):,} rows to {table_ref}")


# ── ORCHESTRATE ──────────────────────────────────────────────
def run_pipeline():
    cutoff   = datetime.utcnow() - timedelta(hours=LOOKBACK_HRS)
    date_str = (datetime.utcnow() - timedelta(hours=5, minutes=30)).strftime('%Y-%m-%d')

    raw       = extract_orders(cutoff)
    validated = validate(raw)
    clean     = transform(validated)
    load_to_bigquery(clean, date_str)
    log.info("Pipeline complete ✅")

if __name__ == '__main__':
    run_pipeline()
DESIGN PRINCIPLES IN THIS PIPELINE: 26-hour lookback handles late-arriving data. Delete-then-insert (not APPEND) makes reruns idempotent — safe to run twice without duplicates. IST-to-UTC conversion at transform stage ensures consistent timezone in the warehouse. Financial year and Diwali window fields are built in the pipeline, not in every downstream query.

Part 4 — Apache Airflow: Scheduling and Orchestrating Pipelines

Airflow is the most widely used pipeline orchestration tool in India's data stack. It lets you schedule pipelines, define task dependencies, retry failed tasks automatically, and monitor pipeline health from a web UI.

📋
DAG
Directed Acyclic Graph — the pipeline definition. A Python file that defines tasks and their dependencies.
⚙️
Task
One unit of work in the DAG — a Python function, a SQL query, a Bash script, or an API call.
🔧
Operator
The type of task — PythonOperator (run Python), BashOperator, SQLExecuteQueryOperator, EmailOperator.
Schedule
Cron expression defining when the DAG runs. "0 2 * * *" = every day at 2 AM. "0 8 * * 1" = every Monday at 8 AM.
🔗
XCom
Cross-communication — how tasks pass data to each other. task_a.xcom_push("row_count", 1234); task_b reads it.
👁️
Sensor
A task that waits for a condition — file to arrive in S3, a table to be populated, an API to return data.
Python · Airflow DAG — Daily Orders Pipeline
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.operators.email  import EmailOperator
from airflow.sensors.filesystem import FileSensor
from datetime import datetime, timedelta

# ── Default arguments applied to every task ──────────────────
default_args = {
    'owner':            'data-team',
    'retries':          2,                        # retry failed tasks up to 2 times
    'retry_delay':      timedelta(minutes=10),    # wait 10 min between retries
    'email_on_failure': True,
    'email':            ['data-alerts@company.in'],
}

# ── DAG Definition ───────────────────────────────────────────
with DAG(
    dag_id          = 'daily_orders_pipeline',
    description     = 'Extract orders from MySQL → BigQuery, daily at 2:30 AM IST',
    schedule_interval = '0 21 * * *',   # 21:00 UTC = 02:30 IST
    start_date      = datetime(2026, 9, 1),
    catchup         = False,            # do not backfill missed runs
    default_args    = default_args,
    tags            = ['orders', 'production', 'bigquery'],
) as dag:

    # Task 1: Extract from MySQL
    extract = PythonOperator(
        task_id         = 'extract_orders_mysql',
        python_callable = extract_orders,
        op_kwargs       = {'cutoff_utc': '{{ data_interval_start }}'},
    )

    # Task 2: Validate
    validate_task = PythonOperator(
        task_id         = 'validate_data',
        python_callable = validate,
    )

    # Task 3: Transform
    transform_task = PythonOperator(
        task_id         = 'transform_orders',
        python_callable = transform,
    )

    # Task 4: Load to BigQuery
    load = PythonOperator(
        task_id         = 'load_bigquery',
        python_callable = load_to_bigquery,
        op_kwargs       = {'date_partition': '{{ ds }}'},
    )

    # Task 5: Refresh Power BI / dbt model
    refresh_dbt = BashOperator(
        task_id  = 'run_dbt_models',
        bash_command = 'dbt run --select orders+ --profiles-dir /opt/dbt',
    )

    # Task 6: Alert on completion
    notify = EmailOperator(
        task_id   = 'notify_success',
        to        = ['analytics@company.in'],
        subject   = 'Daily orders pipeline complete — {{ ds }}',
        html_content = '<p>Orders for {{ ds }} loaded to BigQuery. ✅</p>',
    )

    # ── Task dependencies (left → right = must run before) ───
    extract >> validate_task >> transform_task >> load >> refresh_dbt >> notify
    #         ↑ validate runs only after extract succeeds
    #                          ↑ load runs only after transform succeeds
    #                                          ↑ dbt runs only after load succeeds

Part 5 — Common Pipeline Failures and Fixes

1
Source schema changes
Symptom: Pipeline crashes with "column not found" — upstream team renamed order_amount to amount_inr
Fix: Schema drift detection at extraction. Use SELECT * and validate expected columns exist before transforming. Alert on schema change rather than crashing silently.
2
Diwali / festive volume spike
Symptom: 10x normal order volume causes pipeline to run out of memory or hit API rate limits
Fix: Incremental loads in batches (process 1 day at a time, not full history). Adaptive batch sizes. Request higher API rate limits before festive season.
3
Duplicate rows on rerun
Symptom: Pipeline fails mid-run, reruns, and inserts the same rows twice — dashboard counts double
Fix: Idempotent writes: delete the partition before inserting (delete WHERE order_date = 'today'). Or use MERGE / UPSERT instead of INSERT.
4
Late-arriving data
Symptom: Shiprocket delivery status updates arrive 4–6 hours after midnight; 1 AM pipeline misses same-day deliveries
Fix: 26-hour lookback window for recent dates. Separate pipeline for delivery status updates. Partition tables so late data can be backfilled without reprocessing history.
5
Timezone confusion
Symptom: Dashboard shows Diwali spike on November 9 instead of November 8 — API timestamps are UTC, database stores IST
Fix: Store all timestamps in UTC in the warehouse. Convert to IST only in the presentation layer (dashboard calculated column, Python .tz_convert()). Document timezone of every source in a data dictionary.

Data Pipeline Tools — India Market Reference

CategoryToolWhat It DoesIndia Usage
OrchestrationApache AirflowSchedule, monitor, and retry pipeline tasksMost common; used at Flipkart, Swiggy, Ola, banks
OrchestrationPrefect / DagsterModern Python-native orchestration with better error UXGrowing at Bengaluru SaaS companies
IngestionFivetranManaged connectors for 300+ sources → warehousePopular at funded startups; no-code setup
IngestionAirbyteOpen-source alternative to Fivetran; self-hostedCost-conscious teams; AWS/GCP deployments
Transformationdbt (data build tool)Write transformations as SQL models; auto-generates docs and lineageRapidly growing adoption in India; most in-demand skill
WarehouseGoogle BigQueryServerless cloud SQL warehouse; pay-per-queryDominant at Indian startups on GCP
WarehouseSnowflakeMulti-cloud warehouse; strong data sharing featuresEnterprise and fintech; growing in India
WarehouseAmazon RedshiftAWS-native warehouse; integrates with S3 / GlueCompanies already on AWS stack
StorageAWS S3 / GCSObject storage for raw data lakeUniversal; nearly all companies use one or both
QualityGreat Expectations / dbt testsAutomated data quality checks in the pipelineGrowing adoption; prevents bad data reaching dashboards
You have completed the Data Analytics Series!

Chapters 1–20 cover the full data analyst skillset — from spreadsheets to machine learning, from basic SQL to query optimisation, from descriptive statistics to A/B testing and data pipelines.

← Ch 19: Advanced SQL

Frequently Asked Questions

What is the difference between ETL and ELT?

ETL (Extract, Transform, Load) transforms data before loading it into the destination. Raw data comes from source systems, is cleaned and restructured in a staging area or dedicated transformation layer, and only the processed data lands in the data warehouse. ELT (Extract, Load, Transform) loads raw data first into the destination, then transforms it there using SQL or dbt. ETL was the dominant approach when storage was expensive and warehouse compute was limited — you did not want to store messy raw data. ELT has become the modern standard because cloud data warehouses (BigQuery, Snowflake, Redshift) offer cheap storage and powerful parallel SQL compute, making in-warehouse transformation faster and more flexible. In Indian company tech stacks: legacy on-premise systems (banks, manufacturing, government) typically use ETL with tools like Informatica or SSIS. Modern startups and e-commerce companies use ELT with tools like dbt + BigQuery or Snowflake.

What is a data warehouse and how is it different from a data lake?

A data warehouse stores structured, processed, analysis-ready data in a predefined schema. It is optimised for fast SQL queries by analysts and BI tools. Examples: Google BigQuery, Snowflake, Amazon Redshift, AWS Redshift. Strengths: fast query performance, enforced data quality, governed and trusted. Weakness: less flexible — schema must be defined upfront; storing unstructured data (images, logs, free text) is difficult or expensive. A data lake stores raw data of any format — structured tables, JSON logs, CSV exports, images, audio, documents — at very low cost, typically on object storage (AWS S3, Google Cloud Storage, Azure Blob). Strengths: stores everything; handles unstructured data; retains raw history. Weakness: without governance, becomes a "data swamp" — everything is in there but nothing is easy to find or trust. A data lakehouse (Delta Lake, Apache Iceberg) combines both: lake-style storage with warehouse-style governance and query performance. Most Indian companies at scale use a combination: raw data in S3 / GCS, processed data in BigQuery or Redshift, with a dbt transformation layer in between.

Do data analysts in India need to know Apache Airflow?

For data analyst roles in India, a conceptual understanding of Airflow is increasingly expected — you should be able to read a DAG, understand what triggers a pipeline, and diagnose a failed task. Building Airflow DAGs from scratch is more of a data engineer responsibility. However, at many Indian startups and growth-stage companies, the data analyst and data engineer roles overlap, and analysts are expected to write and maintain simple Airflow DAGs (daily report pipelines, data refresh jobs). Being able to write a basic DAG sets you apart in interviews and is achievable with a week of practice. Other orchestration tools common in Indian companies: Apache Airflow (most common open-source), Prefect and Dagster (newer, more Python-native), dbt Cloud (for transformation scheduling), and cloud-native options (AWS Glue, Google Cloud Composer which is managed Airflow, Azure Data Factory).

What are the most common data pipeline failures and how do you fix them?

The five most common pipeline failures in Indian data stacks: (1) Source schema changes — an upstream team adds or renames a column and the pipeline breaks because the transformation code references the old name. Fix: schema validation at extraction, alerting on schema drift, flexible extraction that captures all columns. (2) Volume anomalies — daily orders spike on Diwali to 10x normal; the pipeline times out or runs out of memory. Fix: incremental loads instead of full refreshes; adaptive batch sizes; partitioned processing. (3) Duplicate records — pipeline reruns after a failure and inserts data twice. Fix: idempotent writes (INSERT IGNORE, MERGE/UPSERT, or delete-then-insert for the affected partition). (4) Late-arriving data — vendor sends previous day's transactions 2 hours after midnight; the 1 AM pipeline misses them. Fix: watermark-based loading with a 24-hour lookback window for recent dates. (5) Timezone issues — API timestamps in UTC, database in IST (UTC+5:30), dashboard shows wrong day boundaries. Fix: always store timestamps in UTC, convert to IST only at display layer.

EVIKA ACADEMY · NOIDA SECTOR 51

Learn Data Engineering Basics with Real Pipelines

Our advanced curriculum covers Python ETL pipelines, Airflow DAGs, BigQuery, and dbt — applied to real Indian business data with hands-on project work.

Book Free Demo Class →
🎓 Free Demo Class — Online & Offline · Noida Sector 51