Skip to content

Power Query M: Functions, Tables and Records

M is functional, not a spreadsheet formula. Its three core data structures, how functions compose, and why the editor stops being scary.

Every transformation you click through in the Power Query Editor generates M code behind the scenes. The formula bar shows one line at a time, but the Advanced Editor reveals the full picture: a series of let expressions that transform data step by step. This guide covers the language fundamentals you need to read, write, and debug M.

The three data structures

Everything in M is built from three structures:

1. Table

The most familiar — rows and columns, just like Excel.

Source = #table(
    type table [Name = text, Age = number],
    {
        {"Alice", 30},
        {"Bob", 25}
    }
)

2. Record

A record is a set of named fields — like a single row or a JSON object.

Person = [Name = "Alice", Age = 30, City = "Berlin"]

Access a field with bracket notation: Person[Name] returns "Alice".

3. List

A list is a one-dimensional sequence of values.

Numbers = {1, 2, 3, 4, 5}

Access by index (zero-based): Numbers{0} returns 1.

The relationship: A table is a list of records. A record is a list of fields. Understanding this hierarchy makes many M functions intuitive — Table.SelectRows filters a list of records, Record.AddField adds a field to a record, and so on.

The let expression

M code is structured as a let expression — a sequence of variable bindings followed by an in clause that returns the final result.

let
    Source = Sql.Database("Server", "DB"),
    SalesTable = Source{[Schema="dbo", Item="Sales"]}[Data],
    FilteredRows = Table.SelectRows(SalesTable, each [Revenue] > 1000),
    SortedRows = Table.Sort(FilteredRows, {{"OrderDate", Order.Descending}}),
    Result = Table.SelectColumns(SortedRows, {"OrderDate", "Revenue"})
in
    Result

Each step references the previous one. The variable names are arbitrary — you can call them Step1, Step2, or give them descriptive names. The in clause determines what the query returns (usually the last step, but not required).

Functions

M is a functional language. Functions are first-class values — you can pass them as arguments, store them in variables, and compose them.

The syntax: parameter => expression.

// A function that squares a number
Square = (x) => x * x
Square(5)  // returns 25

Functions can take multiple parameters:

Add = (a, b) => a + b
Add(3, 4)  // returns 7

And they can be curried — partially applied:

AddFive = (a) => (b) => a + b
AddFive(3)(4)  // returns 7

Most built-in M functions accept a function as an argument. The each keyword is shorthand for (x) => x — it creates a function that takes one parameter and accesses fields via underscore.

// These two are equivalent
Table.SelectRows(Sales, each [Revenue] > 1000)
Table.SelectRows(Sales, (row) => row[Revenue] > 1000)

each is the idiomatic way to write row-level conditions in Power Query.

Common table functions

FunctionWhat it does
Table.SelectRows(table, condition)Filters rows
Table.SelectColumns(table, columns)Keeps only specified columns
Table.AddColumn(table, name, function)Adds a calculated column
Table.Sort(table, sortCriteria)Sorts by one or more columns
Table.GroupBy(table, keys, aggregations)Groups and aggregates
Table.Join(table1, table2, keys, kind)Merges two tables
Table.RenameColumns(table, renames)Renames columns
Table.TransformColumns(table, transforms)Applies a function to columns

Practical example: cleaning a sales export

Say you have a messy CSV export. Here’s a typical cleanup query:

let
    Source = Csv.Document(File.Contents("C:\Data\sales.csv"), [Delimiter=",", Encoding=65001, QuoteStyle=QuoteStyle.None]),
    PromotedHeaders = Table.PromoteHeaders(Source, [PromoteAllScalars=true]),
    TypedColumns = Table.TransformColumnTypes(PromotedHeaders, {
        {"OrderDate", type date},
        {"Revenue", Currency.Type},
        {"Quantity", Int64.Type}
    }),
    FilteredValid = Table.SelectRows(TypedColumns, each [Revenue] <> null and [Revenue] > 0),
    AddedMonth = Table.AddColumn(FilteredValid, "Month", each Date.Month([OrderDate]), Int64.Type),
    Result = Table.SelectColumns(AddedMonth, {"OrderDate", "Month", "Revenue", "Quantity"})
in
    Result

Each step is a transformation. The each keyword accesses the current row’s fields. Date.Month is a built-in date function — there are dozens for date manipulation.

Handling errors

M is strict about types. If a transformation fails (e.g., converting “abc” to a number), the cell shows Error in the result table. You can handle this with try ... otherwise:

SafeConvert = Table.AddColumn(Source, "ParsedQty", each try Number.From([Qty]) otherwise null, type nullable number)

try returns a record with HasError and Value fields; otherwise provides the fallback.

Common mistakes

Mistake 1: Confusing M with DAX. M transforms data before it loads into the model. DAX calculates after data is loaded. They serve different purposes — use M for ETL, DAX for analysis.

Mistake 2: Not setting data types. Every column should have an explicit type. Without types, Power BI treats everything as text, which breaks sorting, filtering, and relationships.

Mistake 3: Over-transforming in M. Some calculations are better done in DAX. If a value depends on user-selected filters (like “revenue for the selected period”), it belongs in DAX, not M — M runs at refresh time and can’t respond to slicer selections.

Summary

M is built on three structures (tables, records, lists) and one pattern (let ... in). Functions are the core abstraction, and each is the standard way to write row-level logic. Master these fundamentals and the Advanced Editor stops being intimidating — it becomes the most powerful part of Power Query.

Microsoft documents this feature in its Power Query M formula language reference.