ETL vs ELT in Azure: Which Approach to Choose

ETL and ELT are not competing technologies — they are competing philosophies about where transformation work happens. ETL dominated for decades because transformation had to happen outside the warehouse, which had limited compute. ELT took over as cloud data platforms became capable of transforming petabytes at low cost inside the warehouse itself. Understanding which is right for a given workload is one of the most important decisions a data architect makes.

ETL: transform before loading

In the traditional ETL approach, a dedicated transformation engine extracts data from the source, applies all transformation logic (cleaning, filtering, joining, aggregating, type conversion), and loads only the final, clean, transformed data into the warehouse.

Source System          ETL Engine              Data Warehouse
─────────────    ──────────────────────    ────────────────────
SQL Server     → ADF Mapping Data Flow → Synapse dedicated pool
Salesforce     → (join, clean, validate)   (final clean data)
SAP ERP        → (aggregate, type-cast)

In Azure, the ETL engine is typically Azure Data Factory Mapping Data Flows (a visual, code-free transformation canvas that runs Spark under the hood) or a custom Python transformation in an ADF Notebook Activity.

ETL advantages:

  • The warehouse only ever contains clean, validated data — no intermediate raw state
  • Transformation logic is encapsulated in the ETL tool, separate from warehouse SQL
  • Sensitive raw data (PII, passwords, internal cost prices) can be masked or excluded before it ever lands in the warehouse

ETL disadvantages:

  • If the transformation logic is wrong, you have no raw data to reprocess from — you must re-extract from the source
  • Transformation happens outside the warehouse, so you cannot use warehouse-specific optimisations (indexes, materialised views) during transformation
  • Complex transformations in ADF Data Flows require the visual designer to be fast enough to work with — large flows become hard to manage

ELT: load raw, transform inside

In the ELT approach, raw data is landed in the data lake (ADLS bronze layer) or loaded into a staging table in the warehouse with minimal or no transformation. The actual transformation work happens inside the destination platform — using Spark in a Synapse/Databricks notebook, T-SQL in a Synapse dedicated pool, or dbt SQL models.

Source System       Fast Load              Data Lake / Warehouse
─────────────    ──────────────────    ─────────────────────────────────
SQL Server     → ADF Copy Activity → ADLS bronze (raw)
Salesforce       (no transformation)    → Spark job → silver Delta tables
SAP ERP          (just move data fast)  → dbt models → gold layer / DW

ELT advantages:

  • Raw data is preserved in the lake — if transformation logic is wrong, reprocess from bronze without re-extracting from the source
  • Transformation runs on the destination platform’s compute, which is often cheaper and faster than an intermediate ETL engine for large datasets
  • SQL engineers can write and test transformation logic directly in the warehouse using familiar tools
  • Data is available immediately after load even before transformation completes — useful for exploratory analysis of fresh raw data

ELT disadvantages:

  • Raw (possibly sensitive) data lands in the lake — requires careful access control on the bronze layer
  • The destination system (warehouse or lake) must have enough compute capacity for transformation workloads in addition to query workloads
  • Poor-quality raw data pollutes the warehouse until the transformation job runs and cleans it

ELT with Azure Data Factory: fast extract and load

In a modern Azure ELT pipeline, ADF (or Synapse Pipelines) is responsible only for extraction and raw loading. The Copy Activity moves data at maximum speed from source to bronze ADLS. All transformation happens downstream in Spark or SQL.

# ADF Copy Activity settings for ELT (Python SDK for ADF management)
# This is the ADF Copy Activity configuration for extracting from SQL Server
# Note: transformation options are minimal — we just want fast data movement

copy_activity_config = {
    "name": "CopyOrdersToLakeBronze",
    "type": "Copy",
    "source": {
        "type": "SqlServerSource",
        "sqlReaderQuery": (
            "SELECT * FROM dbo.Orders "
            "WHERE modified_date > '@{activity('GetWatermark').output.firstRow.last_load_time}'"
        ),
        "queryTimeout": "02:00:00",
        "partitionOption": "None"
    },
    "sink": {
        "type": "ParquetSink",
        "storeSettings": {
            "type": "AzureBlobFSWriteSettings",
            "copyBehavior": "PreserveHierarchy"
        },
        "formatSettings": {
            "type": "ParquetWriteSettings"
        }
    },
    "translator": {
        "type": "TabularTranslator",
        "typeConversion": True,
        "typeConversionSettings": {
            "allowDataTruncation": True,
            "treatBooleanAsNumber": False
        }
    },
    "enableStaging": False,
    "dataIntegrationUnits": 32  # Higher DIUs = faster parallel copy
}
# Result: raw Parquet files land in bronze ADLS as fast as possible
# All cleaning and transformation happens in the subsequent Spark notebook activity

ELT with dbt: SQL transformations as version-controlled code

dbt (data build tool) is a transformation framework that turns SELECT statements into managed data assets. You write a dbt model as a SQL SELECT — dbt wraps it in CREATE TABLE AS SELECT or CREATE VIEW and runs it in your warehouse. dbt handles dependency ordering, testing, documentation, and incremental materialisation.

-- dbt model: models/silver/orders.sql
-- This dbt model reads from the raw staging table and produces a clean silver orders table
-- dbt runs this as: CREATE TABLE silver.orders AS SELECT ...

{{
    config(
        materialized='incremental',
        unique_key='order_id',
        on_schema_change='fail'
    )
}}

SELECT
    CAST(order_id AS BIGINT)            AS order_id,
    CAST(customer_id AS INT)            AS customer_id,
    CAST(order_date AS DATE)            AS order_date,
    CAST(order_total AS DECIMAL(18,2))  AS order_total,
    UPPER(TRIM(status))                 AS status,
    CASE
        WHEN UPPER(TRIM(status)) IN ('COMPLETED', 'SHIPPED') THEN TRUE
        ELSE FALSE
    END                                 AS is_fulfilled,
    GETDATE()                           AS dbt_loaded_at
FROM
    {{ source('bronze', 'raw_orders') }}
WHERE
    order_id IS NOT NULL
    AND order_total >= 0
    AND CAST(order_date AS DATE) <= GETDATE()

{% if is_incremental() %}
    AND modified_date > (SELECT MAX(dbt_loaded_at) FROM {{ this }})
{% endif %}
-- dbt model: models/gold/monthly_revenue.sql
-- References the silver orders model; dbt resolves dependencies automatically

SELECT
    YEAR(order_date)    AS order_year,
    MONTH(order_date)   AS order_month,
    COUNT(order_id)     AS order_count,
    SUM(order_total)    AS total_revenue,
    AVG(order_total)    AS avg_order_value
FROM
    {{ ref('orders') }}  -- references the silver orders model
WHERE
    is_fulfilled = TRUE
GROUP BY
    YEAR(order_date),
    MONTH(order_date)

dbt connects to Synapse, Databricks SQL, or other warehouses via adapters. Run all models with dbt run; run tests with dbt test. Because dbt models are SQL files checked into Git, they get all the benefits of version control, code review, and CI/CD.

Choosing between ETL and ELT

FactorLean toward ETLLean toward ELT
Data sensitivitySource contains sensitive PII that must be masked before landing anywhere in AzureRaw data can safely land in a secured bronze lake with proper access control
Reprocessing needsSource data is easily re-extractable on demandRe-extraction from source is expensive or impossible (API rate limits, no CDC)
Team skillsTeam is primarily visual/GUI oriented; no Spark or dbt experienceTeam is SQL or Python-first; comfortable with code-based transformations
Data volumeSmall to medium (ADF Data Flows handle up to ~100 GB efficiently)Large to very large (Spark scales to petabytes; T-SQL in Synapse handles TB)
Transformation complexitySimple: type casting, renaming, basic filteringComplex: multi-source joins, ML scoring, window functions, custom logic
Latency requirementBatch OK; latency measured in minutesNear-real-time: raw data available seconds after arrival for streaming consumers

In practice, most modern Azure data architectures use a hybrid: ADF Copy Activity does fast, minimal-transformation load to the bronze lake (ELT-style), and then Spark notebooks or dbt models handle complex transformations inside the lake/warehouse (also ELT). Pure ETL (heavy transformation in ADF Data Flows before landing) is reserved for compliance use cases where raw data genuinely cannot be stored.

Common mistakes

  1. Building complex transformations in ADF Mapping Data Flows when Spark would be simpler. ADF Data Flows have a visual designer that becomes unwieldy for complex logic (many conditions, custom expressions). If your transformation logic would take 50 lines of PySpark, write it in PySpark and invoke it via a Notebook Activity — it will be easier to test, version, and maintain.
  2. Using ETL when you should use ELT for large datasets. ADF Data Flows run on Spark clusters internally but with overhead from the visual-to-code compilation. For transforming 1 TB+ of data, a direct PySpark notebook (ELT pattern) is typically faster and more cost-effective than an equivalent Data Flow.
  3. Not testing dbt models before deploying to production. dbt has a built-in testing framework. Define uniqueness tests, not-null tests, and referential integrity tests for every model. Run dbt test in CI before merging model changes, or you will deploy broken transformations that silently produce wrong data.
  4. Transforming in ADF and not preserving raw data. If your ADF pipeline transforms data in-flight and writes only the transformed result, you lose the raw data. When your transformation logic has a bug (and it will), you cannot reprocess. Always land a raw copy in the bronze layer before transformation, even if you use an ETL approach.

Frequently asked questions

What is ETL?

ETL stands for Extract, Transform, Load. In this pattern, data is extracted from a source, transformed (cleaned, joined, aggregated) before it reaches the destination, and then loaded into the final store. Transformations happen in a separate engine — traditionally a dedicated ETL server or tool — before the data touches the data warehouse.

What is ELT?

ELT stands for Extract, Load, Transform. In this pattern, raw data is extracted and loaded directly into the destination (data lake or data warehouse) without transformation. Transformations happen inside the destination system using its own compute power — Spark in a data lake, T-SQL in a Synapse dedicated pool, or dbt models in a warehouse. ELT became practical when cloud data platforms became powerful enough to transform data cheaply at scale.

Is dbt an ETL or ELT tool?

dbt (data build tool) is an ELT tool. It runs SQL transformations inside your data warehouse or data lake — Synapse, Databricks, Snowflake, BigQuery. dbt does not move data between systems; it transforms data that is already loaded into the destination using SELECT statements that create transformed tables and views. It is the most popular ELT framework for SQL-oriented teams.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.