ADF Data Flow Transformations: A Complete Reference
A reference to the transformations available in Azure Data Factory mapping data flows, grouped by task: shaping columns, filtering and marking rows, combining streams and handling nested data. Each has its SQL equivalent and the documented detail worth knowing, with row counts from a real example.
GloryTecks Team
Training & Placement Faculty
Last updated: 2 October 2026
Microsoft's documentation lists 26 transformations for mapping data flows in one long table (Microsoft Learn: transformation overview). That is a convenient layout for looking one up and a poor one for learning them. This reference groups the data flow transformations by the job you are trying to do, gives the SQL equivalent of each, and records the documented detail most likely to cost you an afternoon.
If data flows are new to you, start with mapping data flows in ADF.
The two ends: Source and Sink
Every flow needs at least one of each.
Source reads from a dataset or from an inline definition. Its Projection tab holds the columns and types, and for untyped files such as CSV that is where you set them. Options control sampling, schema drift and whether the run fails if the data does not match the projection.
Sink writes the result. For database sinks the update method is insert by default; updates, upserts and deletes need an Alter row step first. A sink can also be a cache, held in Spark memory and read back by an expression elsewhere in the same flow.
Shaping columns
| Transformation | What it does | SQL equivalent |
|---|---|---|
| Select | Renames, drops and reorders columns | The column list of a SELECT |
| Derived column | Adds a column or replaces one, from an expression | A computed column |
| Cast | Changes data types, with a check for failed conversions | CAST |
| Aggregate | Groups rows and computes aggregates | GROUP BY |
| Pivot | Turns the distinct values of one column into columns | PIVOT |
| Unpivot | Turns columns into rows | UNPIVOT |
| Window | Aggregates over a window of related rows | OVER with PARTITION BY and ORDER BY |
| Rank | Adds a rank by sort order, dense or not | RANK, DENSE_RANK |
| Surrogate key | Adds an incrementing key from a start value | An identity column |
Details worth knowing:
- Aggregate drops what you do not name. Columns that are neither grouped nor aggregated do not flow through. Microsoft's two remedies are to carry them with a function such as
first()orlast(), or to rejoin them afterwards (Microsoft Learn: Aggregate). The same technique, grouping on the key and takingfirst()of everything else, is the documented way to remove duplicates. - Pivot can hide its own output. If you do not list the pivot key values, the columns are created at run time, so they do not appear in Inspect and cannot be referenced by name downstream.
- Window is not the tool for a whole-table rank. For a rank or row number across the entire dataset, Microsoft recommends the Rank and Surrogate key transformations, which perform better (Microsoft Learn: optimizing transformations). Window is for calculations within groups, the subject of window functions in SQL.
- A surrogate key always starts where you tell it. To continue from the highest key already in the table, read that maximum, hold it in a cache sink, and add it in a derived column.
- Select with rules drops the rest. In rule-based mapping, a column that matches no rule is removed.
Filtering and marking rows
| Transformation | What it does | SQL equivalent |
|---|---|---|
| Filter | Keeps rows that meet a condition | WHERE |
| Sort | Orders the rows | ORDER BY |
| Alter row | Marks each row for insert, update, delete or upsert | The choice between INSERT, UPDATE, DELETE and MERGE |
| Assert | Marks rows that break a rule, or fails the flow | A CHECK constraint, applied in the pipeline |
Sort is fragile. A data flow runs on Spark across partitions, and any later step that repartitions the data can discard the order. The documented advice is to keep the sort as close to the sink as possible (Microsoft Learn: Sort). Sorting before a join, a habit from SSIS, is unnecessary and is not recommended.
Alter row marks; the sink acts. Conditions are checked in order and the first one that matches decides the row's policy. Three conditions must hold for anything to happen (Microsoft Learn: Alter row):
- The sink is a database, REST or Azure Cosmos DB sink.
- The sink's settings allow that policy, and name the key columns for updates, upserts and deletes.
- The flow is run from a pipeline. In a debug session the preview shows the marks, and no rows are changed.
Microsoft's own example, with a column computed earlier that holds the intended action:
SpecifyUpsertConditions alterRow(insertIf(alterRowCondition == 'insert'),
updateIf(alterRowCondition == 'update'),
deleteIf(alterRowCondition == 'delete')) ~> AlterRowAssert has three kinds of rule. Expect true, for a condition every row must meet; expect unique, for a column or expression; and expect exists, which checks a row against a second stream. A failed row can be routed away downstream using isError() and hasError(), or the whole flow can be set to fail.
Combining and splitting streams
| Transformation | What it does | SQL equivalent |
|---|---|---|
| Join | Combines two streams on a condition | JOIN |
| Lookup | Adds columns from another stream, keeping every row | LEFT JOIN |
| Exists | Keeps rows that do, or do not, have a match | WHERE EXISTS, WHERE NOT EXISTS |
| Union | Stacks streams, by column name or position | UNION ALL |
| Conditional split | Sends rows to different streams by condition | CASE |
| New branch | Uses the same stream for two sets of steps | Reusing a common table expression |
These are the transformations that change the number of rows, and where most wrong results come from. The script below runs the SQL equivalent of each on the Artist and Album tables of the Chinook sample database (Chinook on GitHub). It is SQLite, not Data Factory; the point is the counts.
import sqlite3
db = sqlite3.connect("chinook.sqlite")
count = lambda sql: db.execute(f"SELECT COUNT(*) FROM ({sql})").fetchone()[0]
print(f"{'left stream: Artist':46} {count('SELECT * FROM Artist'):6}")
print(f"{'right stream: Album':46} {count('SELECT * FROM Album'):6}")
print()
on = "ON a.ArtistId = b.ArtistId"
streams = {
"Join, inner": f"SELECT * FROM Artist a JOIN Album b {on}",
"Join, left outer": f"SELECT * FROM Artist a LEFT JOIN Album b {on}",
"Join, right outer": f"SELECT * FROM Artist a RIGHT JOIN Album b {on}",
"Join, full outer": f"SELECT * FROM Artist a FULL OUTER JOIN Album b {on}",
"Join, cross (no condition)": "SELECT * FROM Artist a CROSS JOIN Album b",
"Exists": "SELECT * FROM Artist a WHERE EXISTS (SELECT 1 FROM Album b WHERE b.ArtistId = a.ArtistId)",
"Exists, type 'doesn't exist'": "SELECT * FROM Artist a WHERE NOT EXISTS (SELECT 1 FROM Album b WHERE b.ArtistId = a.ArtistId)",
"Lookup, match multiple rows on": f"SELECT * FROM Artist a LEFT JOIN Album b {on}",
"Lookup, one match per row": "SELECT a.*, (SELECT MIN(b.Title) FROM Album b WHERE b.ArtistId = a.ArtistId) AS Title FROM Artist a",
"Union": "SELECT Name FROM Artist UNION ALL SELECT Title FROM Album",
}
for name, sql in streams.items():
print(f"{name:46} {count(sql):6}")
print()
print("Conditional split of Artist on album count, first matching condition wins:")
split = db.execute("""
SELECT CASE WHEN n = 0 THEN 'noAlbums' WHEN n = 1 THEN 'oneAlbum' ELSE 'severalAlbums' END AS stream, COUNT(*)
FROM (SELECT a.ArtistId, COUNT(b.AlbumId) AS n FROM Artist a LEFT JOIN Album b ON a.ArtistId = b.ArtistId GROUP BY a.ArtistId)
GROUP BY stream ORDER BY stream
""").fetchall()
for stream, rows in split:
print(f" {stream:44} {rows:6}")
print(f" {'total':44} {sum(r for _, r in split):6}")left stream: Artist 275
right stream: Album 347
Join, inner 347
Join, left outer 418
Join, right outer 347
Join, full outer 418
Join, cross (no condition) 95425
Exists 204
Exists, type 'doesn't exist' 71
Lookup, match multiple rows on 418
Lookup, one match per row 275
Union 622
Conditional split of Artist on album count, first matching condition wins:
noAlbums 71
oneAlbum 148
severalAlbums 56
total 275What the counts say:
- Inner join gives 347, left outer 418. The difference, 71, is artists with no album. An inner join silently drops them.
- Exists gives 204, not 347. Exists filters the left stream and adds nothing, so each artist appears once however many albums match. Its opposite gives the 71. This is the transformation for "which rows are new?" and "which rows have disappeared?".
- Lookup depends on one switch. With match multiple rows on, it behaves as a left outer join: 418. Off, it returns one row per input row: 275. Leaving it on when the other side has duplicates is how a fact table quietly grows.
- A join with no condition gives 95,425 rows, every pairing of 275 and 347.
- Conditional split accounts for every row: 71, 148 and 56 make 275. That holds because each row goes to the first condition it matches. Set to all matching conditions, a row can go to several streams.
Further details from the documentation:
- Five join types are supported: inner, left outer, right outer, full outer and custom cross join. Conditions other than equality, and conditions with OR, need the custom cross join (Microsoft Learn: Join).
- After a lookup, a function reports whether each row matched:
isMatch(), which a conditional split can use to separate the unmatched rows. A lookup on a condition other than equality needs fixed broadcasting set on the Optimize tab (Microsoft Learn: Lookup). - New branch appears only when something already follows the transformation you are branching from.
Nested data
| Transformation | What it does |
|---|---|
| Flatten | Unrolls an array inside a record into one row per item |
| Parse | Reads a string column holding JSON, XML or delimited text into typed columns |
| Stringify | Turns a structure, map or array into a single string |
Flatten is the one used daily, for JSON from APIs. One behaviour to remember: a null or empty array still produces one output row, with nulls in the unrolled columns. Unrolling several arrays in one Flatten produces every combination of their values, which is rarely what was intended.
Reuse and calling out
- Flowlet packages a group of transformations with defined inputs and outputs, so the same cleaning logic can be used in many flows.
- External call sends each row to a REST endpoint and adds the response to the stream. Row-by-row calls are slow at volume; use it for small streams.
Which transformation for which job
| You want to | Use |
|---|---|
| Add reference columns to a fact stream | Lookup, one match per row |
| Find rows that are new, or gone | Exists, and its opposite |
| Remove duplicates | Aggregate on the key with first() |
| Keep the latest row per key | Window with a row number within the key, then Filter |
| Send bad rows to a quarantine file | Assert, then Conditional split |
| Update or delete in a database | Alter row, with the sink set to allow it |
| Maintain a history of changes | Lookup, Conditional split, Alter row and Surrogate key together |
The last row is a pattern in its own right, covered in slowly changing dimensions in data flows.
A note for anyone planning a move to Microsoft Fabric: Microsoft lists Alter row, Assert, Window and Stringify as not supported in Dataflow Gen2, the Power Query based dataflow in Fabric (Microsoft Learn: Fabric guide for mapping data flow users). Flows that depend on them need a different design there, or the mapping data flow transforms that the same guide describes as available in Fabric in preview.
For how joins and partitioning affect run time, see data flow debug and performance.
What is the difference between Join and Lookup in a data flow?
Why did my columns disappear after an Aggregate?
Why does Alter row not change my table in debug?
Do I need to sort before a join?
Azure Data Factory and data transformation with Spark are covered in the Data Engineering course.
Sources
- Mapping data flow transformation overview (Microsoft Learn) β Microsoft Learn
- Aggregate transformation in mapping data flow (Microsoft Learn) β Microsoft Learn
- Join transformation in mapping data flow (Microsoft Learn) β Microsoft Learn
- Lookup transformations in mapping data flow (Microsoft Learn) β Microsoft Learn
- Alter row transformation in mapping data flow (Microsoft Learn) β Microsoft Learn
- Sort transformation in mapping data flow (Microsoft Learn) β Microsoft Learn
- Optimizing performance of transformations in mapping data flow (Microsoft Learn) β Microsoft Learn
- A guide to Fabric Dataflows for Azure Data Factory Mapping Data Flow users (Microsoft Learn) β Microsoft Learn
- Chinook sample database (GitHub) β GitHub
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Azure Data Factory?
Join GloryTecksβ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.
Microsoft Fabric vs Azure Data Factory: What's New
NextAzure Synapse Pipelines vs Standalone ADF
Related articles
Mapping Data Flows in ADF: Transformations Without Code
Mapping data flows let you design transformations on a canvas in Azure Data Factory and run them on Spark clusters the service manages. This guide explains how they work, walks through one flow stage by stage with real row counts, and sets out when a data flow is the right tool.
Azure Data Factory Roadmap 2026: A Complete Guide
A staged roadmap for learning Azure Data Factory in 2026: the six building blocks and how they relate, five stages from a first copy pipeline to deployed and monitored pipelines, a checkpoint for each stage, how Microsoft Fabric changes the picture, the state of the certifications, and the skills ADF does not replace.
ADF Pipelines Explained: Building Your First Pipeline
A beginner's guide to pipelines in Azure Data Factory: what a pipeline and an activity are, the three kinds of activity, the objects you create for a first copy from blob storage to a SQL table, the JSON behind the canvas, why Debug moves real data, what Publish does, and how to read a run's output.