Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Query

Understanding Table, List, and Record Data Structures in Power Query: How M Represents and Navigates Data

M — Power Query's formula language — represents all data using three container types: Tables, Lists, and Records. Learn how each one works, how to navigate inside them with M syntax, and how they nest together to form the complex structures you see in JSON and API data.

🌱 Foundation16 min readSep 16, 2026Updated Sep 16, 2026
Understanding Table, List, and Record Data Structures in Power Query: How M Represents and Navigates Data
On this page
  • Introduction
  • Prerequisites
  • The Three Containers: A Quick Orientation
  • Tables: The Structure You Already Know
  • What a Table Looks Like in M
  • Navigating Into a Table
  • Common Table Functions
  • Lists: Ordered Sequences of Values
  • What a List Looks Like in M
  • Why Lists Matter in Practice
  • Common List Functions
  • Generating Lists Programmatically
  • Records: Named Collections of Fields
  • What a Record Looks Like in M
  • Records as Rows
  • Common Record Functions
  • How the Three Types Nest
  • A Table Is a List of Records
  • Lists and Records Inside Table Cells
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Understanding Table, List, and Record Data Structures in Power Query: How M Represents and Navigates Data

    Introduction

    You've imported a JSON file into Power Query and suddenly the column shows "List" or "Record" instead of the text values you expected. You click the expand icon, get a dozen new columns, and wonder what just happened. Or maybe you've been happily using Power Query's GUI buttons for months but you've hit a wall — the button you need doesn't exist, and you have to write some M code yourself. Either way, you're facing the same fundamental gap: you don't yet have a mental model for how M thinks about data.

    M, the formula language behind Power Query, doesn't see data the way a spreadsheet does. It has three core container types — Tables, Lists, and Records — and every transformation you perform is ultimately an operation on one or more of these structures. Once you understand what each one is, how M navigates inside it, and how they nest inside each other, the language clicks into place. That JSON expand button makes sense. The formula bar stops being intimidating. And you can start writing logic that the GUI could never produce on its own.

    By the end of this lesson, you'll be able to look at any value in Power Query's preview pane, know exactly what type it is, navigate into it using M syntax, and manipulate all three structures confidently.

    What you'll learn:

    • What Tables, Lists, and Records are and how they differ from each other
    • How to construct each type from scratch using M code
    • How to navigate into and extract values from each structure using M's indexing and field access syntax
    • How the three types nest inside each other, and why that's the key to understanding JSON, XML, and complex API responses
    • How to perform practical transformations on each type using built-in M functions

    Prerequisites

    This lesson assumes you know how to open the Power Query Editor and run a blank query. If you're brand new to Power Query, start with Power Query 101: Connect, Transform, Load to get your bearings. It also helps to have read Understanding the M Formula Language: Syntax, Data Types, and Expression Basics so you're comfortable with M expressions generally, though it isn't strictly required.


    The Three Containers: A Quick Orientation

    Before diving into syntax, let's build a clear mental model.

    Think of a Table as what you normally think of when you think of data: rows and columns, like a spreadsheet or a database result set. It has column headers and zero or more rows of data. This is the final form Power Query loads into your data model.

    A List is an ordered sequence of values, like a single column with no header. It's just values, one after another, with positions starting at zero. Lists can contain anything: numbers, text, dates, or even other Lists and Records.

    A Record is a named collection of fields — like a single row with labeled slots. Instead of positions, you navigate it by name. If a List is a column, a Record is a row.

    These three types are not interchangeable, but they compose naturally. A Table is essentially a List of Records (each record is a row), where every record shares the same field names (the column headers). This composability is what makes M both powerful and occasionally confusing.

    Key insight: Every time Power Query shows you a cell containing the word "List," "Record," or "Table," that cell holds one of these container types as a value. It's a structured value nested inside your outer table — and you can always drill into it.


    Tables: The Structure You Already Know

    What a Table Looks Like in M

    A Table is the primary output type of Power Query — it's what gets loaded into Power BI or Excel. But Tables aren't just the final result; they're also values you can create, pass around, and operate on mid-query.

    You can construct a Table from scratch using #table(). Open a blank query (in Power Query Editor, go to Home → New Source → Blank Query, then open the Advanced Editor) and paste this:

    #table(
        {"OrderID", "Customer", "Amount"},
        {
            {1001, "Contoso Ltd", 4500.00},
            {1002, "Fabrikam Inc", 2100.75},
            {1003, "Contoso Ltd", 8900.00}
        }
    )
    

    #table() takes two arguments: a list of column names, and a list of rows (each row is itself a list of values). Run this and you'll see a three-row, three-column table in the preview pane — a real table created entirely in M, without connecting to any external data source.

    Navigating Into a Table

    To get a specific cell, you use two navigation operators in sequence: first select the row by index (zero-based), then select the field by name.

    let
        SalesData = #table(
            {"OrderID", "Customer", "Amount"},
            {
                {1001, "Contoso Ltd", 4500.00},
                {1002, "Fabrikam Inc", 2100.75},
                {1003, "Contoso Ltd", 8900.00}
            }
        ),
        // Get the first row as a Record
        FirstRow = SalesData{0},
        // Get the Amount field from that record
        FirstAmount = FirstRow[Amount]
    in
        FirstAmount
    

    This returns 4500. Notice the two-step pattern: SalesData{0} extracts row zero as a Record, and then [Amount] accesses the Amount field within that Record. You'll see this curly-brace / square-bracket combination constantly in M code.

    You can also pull an entire column as a List using the Table.Column() function:

    Table.Column(SalesData, "Customer")
    // Returns: {"Contoso Ltd", "Fabrikam Inc", "Contoso Ltd"}
    

    Common Table Functions

    The M standard library has hundreds of Table.* functions. The most useful ones for everyday work:

    • Table.SelectRows(table, each [Amount] > 3000) — filter rows by a condition
    • Table.AddColumn(table, "Tax", each [Amount] * 0.1) — add a calculated column
    • Table.Sort(table, {{"Amount", Order.Descending}}) — sort rows
    • Table.Group(table, {"Customer"}, {"TotalAmount", each List.Sum([Amount]), type number}) — aggregate

    Tip: When you click a button in the Power Query GUI, it writes one of these Table.* functions into a new step behind the scenes. You can always click "View → Advanced Editor" to see the M code any button press generates. This is one of the best ways to learn M — use the GUI, then read what it wrote.


    Lists: Ordered Sequences of Values

    What a List Looks Like in M

    A List is enclosed in curly braces {}. Every value inside is separated by a comma.

    {"London", "Paris", "Tokyo", "Sydney"}
    

    That's a valid, complete M expression — it evaluates to a List of four text values. Lists are zero-indexed, meaning the first item is at position 0, not 1.

    let
        Cities = {"London", "Paris", "Tokyo", "Sydney"},
        SecondCity = Cities{1}
    in
        SecondCity
    // Returns: "Paris"
    

    Lists don't have to contain the same type of value — M won't stop you from mixing types — but in practice you'll usually work with homogeneous lists (all numbers, all text, etc.).

    Why Lists Matter in Practice

    Lists appear everywhere in Power Query once you go beyond flat CSV files:

    • When you expand a column from a JSON array, Power Query first reads each array as a List
    • When you use "Extract Values" from a list column, you're calling Text.Combine() on a List
    • Column names are stored and manipulated as Lists (Table.ColumnNames() returns a List)
    • The rows of a #table() constructor are a List of Lists

    Understanding Lists is especially critical when unpacking nested JSON and XML structures, because those nested arrays land in your table as List-typed cells.

    Common List Functions

    List.Count({"a", "b", "c"})               // 3
    List.Sum({100, 200, 300})                 // 600
    List.Distinct({"Yes", "No", "Yes"})       // {"Yes", "No"}
    List.Contains({"Yes", "No"}, "Maybe")    // false
    List.Sort({3, 1, 4, 1, 5}, Order.Ascending) // {1, 1, 3, 4, 5}
    List.Transform({1, 2, 3}, each _ * 10)   // {10, 20, 30}
    List.Select({1, 2, 3, 4, 5}, each _ > 2) // {3, 4, 5}
    

    List.Transform() and List.Select() are worth memorizing. They work like map and filter in other languages — they let you apply a function to every item or filter items by a condition, without a loop.

    Note: The underscore _ in each _ * 10 represents "the current item." You'll also see each [FieldName] in Table contexts, where the current item is a Record and you're accessing its field. Both are shorthand for a full function expression.

    Generating Lists Programmatically

    You can generate lists without typing every value:

    {1..10}              // {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    {"A".."E"}           // Not valid — M range only works with numbers
    List.Dates(
        #date(2024, 1, 1),
        12,
        #duration(31, 0, 0, 0)
    )                   // 12 dates, 31 days apart
    

    The {1..10} range syntax is a neat shortcut you'll use when building a dynamic date dimension table or generating row indices.


    Records: Named Collections of Fields

    What a Record Looks Like in M

    A Record uses square brackets [] with named field-value pairs separated by commas:

    [
        OrderID = 1001,
        Customer = "Contoso Ltd",
        Amount = 4500.00,
        OrderDate = #date(2024, 3, 15)
    ]
    

    This is a single Record with four fields. Records have no inherent order — you access values by name, not position.

    let
        Order = [
            OrderID = 1001,
            Customer = "Contoso Ltd",
            Amount = 4500.00
        ],
        CustomerName = Order[Customer]
    in
        CustomerName
    // Returns: "Contoso Ltd"
    

    Records as Rows

    Here's the connection that ties everything together: each row in a Table is a Record. When you use each in a Table function, the implicit _ is a Record representing the current row, and [ColumnName] accesses a field in that row-record.

    Table.SelectRows(
        SalesData,
        each [Amount] > 3000
    )
    

    In plain English: "For each row (Record) in SalesData, keep it if its Amount field is greater than 3000." The each [Amount] syntax is shorthand for (_) => _[Amount] — a function that takes the current row-record and returns the value of its Amount field.

    This is why the each [FieldName] pattern appears in practically every non-trivial M formula. You're not doing magic — you're accessing fields on Records that represent rows.

    Common Record Functions

    Record.FieldNames(myRecord)                    // Returns a List of field names
    Record.FieldValues(myRecord)                   // Returns a List of values
    Record.HasFields(myRecord, "Customer")         // true or false
    Record.AddField(myRecord, "Discount", 0.10)    // Returns a new Record with the added field
    Record.RemoveFields(myRecord, {"Amount"})      // Returns a Record without the Amount field
    Record.ToTable(myRecord)                       // Converts to a 2-column Table: Name, Value
    

    Record.ToTable() is particularly handy when you've received data as a Record (say, from an API response) and you need to pivot it into a proper table for reporting.

    Warning: Records in M are immutable — functions like Record.AddField() don't modify the original Record; they return a new one. This is true of all M values. If you assign a new Record to the same step name, you're creating a fresh value, not editing the old one. Forgetting this leads to confusing bugs where you "add a field" but the original step downstream still shows the old structure.


    How the Three Types Nest

    The real power of understanding Tables, Lists, and Records comes when you see how they compose.

    A Table Is a List of Records

    Conceptually:

    // This Table:
    #table({"Name", "Score"}, {{"Alice", 92}, {"Bob", 88}})
    
    // Is equivalent to a List of Records like:
    {
        [Name = "Alice", Score = 92],
        [Name = "Bob",   Score = 88]
    }
    

    You can convert between these representations:

    // Table to List of Records
    Table.ToRecords(myTable)
    
    // List of Records to Table
    Table.FromRecords(myListOfRecords)
    

    Lists and Records Inside Table Cells

    Complex data sources — especially APIs, JSON files, and SharePoint list exports — produce tables where cells contain Lists or Records. When loading data from APIs and web pages, this is almost guaranteed.

    Imagine an Orders table where each order has a nested list of line items:

    #table(
        {"OrderID", "Customer", "LineItems"},
        {
            {1001, "Contoso Ltd", {
                [Product = "Widget A", Qty = 5, Price = 100],
                [Product = "Widget B", Qty = 2, Price = 250]
            }},
            {1002, "Fabrikam Inc", {
                [Product = "Widget C", Qty = 10, Price = 75]
            }}
        }
    )
    

    The LineItems column contains a List of Records. In Power Query's preview pane, you'd see each cell show "List." Clicking the expand icon on that column tells Power Query to apply Table.ExpandListColumn() followed by Table.ExpandRecordColumn() — which is exactly what happens when you use the expand buttons in the GUI on a JSON import.

    Key insight: The expand button in the Power Query UI is literally just navigating into nested Lists and Records. Understanding the underlying structure means you can handle cases where the GUI expand doesn't give you what you want — because you can write the expansion logic yourself using Table.ExpandListColumn(), Table.ExpandRecordColumn(), or List.Transform().


    Hands-On Exercise

    Let's put it all together with a realistic scenario. You've received a product catalog from a vendor as a structured M expression (simulating what you'd get from a JSON API). Build this in a new blank query using the Advanced Editor:

    let
        // Step 1: Simulate raw API data as a List of Records
        RawData = {
            [
                SKU = "PRD-001",
                Name = "Ergonomic Chair",
                Attributes = [Color = "Black", Material = "Mesh", WeightKg = 12.5],
                Tags = {"office", "furniture", "ergonomic"}
            ],
            [
                SKU = "PRD-002",
                Name = "Standing Desk",
                Attributes = [Color = "Oak", Material = "Wood", WeightKg = 35.0],
                Tags = {"office", "furniture", "height-adjustable"}
            ],
            [
                SKU = "PRD-003",
                Name = "Monitor Stand",
                Attributes = [Color = "Silver", Material = "Aluminum", WeightKg = 2.1],
                Tags = {"office", "accessories"}
            ]
        },
    
        // Step 2: Convert List of Records to a Table
        AsTable = Table.FromRecords(RawData),
    
        // Step 3: Expand the nested Attributes record into columns
        ExpandAttributes = Table.ExpandRecordColumn(
            AsTable,
            "Attributes",
            {"Color", "Material", "WeightKg"}
        ),
    
        // Step 4: Extract the first tag from the Tags list as a new column
        AddFirstTag = Table.AddColumn(
            ExpandAttributes,
            "PrimaryTag",
            each [Tags]{0},
            type text
        ),
    
        // Step 5: Count the number of tags per product
        AddTagCount = Table.AddColumn(
            AddFirstTag,
            "TagCount",
            each List.Count([Tags]),
            type number
        ),
    
        // Step 6: Remove the Tags column (it's done its job)
        Result = Table.RemoveColumns(AddTagCount, {"Tags"})
    in
        Result
    

    Work through each step and observe the intermediate results. After Step 2, you have a proper table but the Attributes and Tags columns still show "Record" and "List." After Step 3, Attributes has been expanded. Step 4 shows you navigating into a List cell with {0}. Step 5 demonstrates calling a List function (List.Count) on a cell value.

    This mirrors exactly what you'd do after importing a product catalog from a REST API — the structure is the same, just arriving via Web.Contents() instead of a hardcoded literal.


    Common Mistakes & Troubleshooting

    Mistake 1: Using {1} when you mean the second item, forgetting zero-indexing

    M lists are zero-indexed. myList{0} is the first item, myList{1} is the second. If you expect {1} to return the first item and get the second instead, this is why. There's no setting to change it — just internalize it early.

    Mistake 2: Confusing [] for Records with [] for column references in each expressions

    [Amount] inside an each expression means "the Amount field of the current row-Record." But [Amount = 0] when used as a second argument to something like Table.SelectRows() is a Record literal being used as an options parameter. Both use square brackets, but in different syntactic positions. When in doubt, look at whether it has an = inside — a Record literal does, a field access doesn't.

    Mistake 3: Trying to modify a Record or List in place

    As noted earlier, M values are immutable. If you call Record.AddField(myRecord, "NewField", "value") but don't assign the result to a new step, nothing happens. Always capture the return value.

    Mistake 4: Expanding a List column when you needed to expand a Record column

    Table.ExpandListColumn() and Table.ExpandRecordColumn() do very different things. Applying the wrong one gives you an error or an unexpected structure. First check what type the column actually contains — hover over a cell in the preview pane and Power Query will show the type. List cells show a list icon; Record cells show a record icon.

    Warning: When you expand a List column, Power Query creates one new row per list item — your row count multiplies. When you expand a Record column, Power Query creates new columns. Mixing these up in a pipeline produces wildly incorrect row counts that can be hard to debug later. If your row count looks wrong after an expand, this is the first thing to check. For more on handling complex nested structures, see the guide on unpacking nested JSON and XML structures.

    Mistake 5: Hardcoding field names in Table.ExpandRecordColumn() when the schema might change

    If the Records in your column might have different fields across rows, hardcoding {"Color", "Material", "WeightKg"} will silently drop any other fields. To expand all fields dynamically, first collect all unique field names:

    AllFields = List.Distinct(
        List.Combine(
            List.Transform(
                Table.Column(AsTable, "Attributes"),
                each Record.FieldNames(_)
            )
        )
    )
    

    Then pass AllFields as the field names argument. This is the handling dynamic schema changes approach applied to nested Records.


    Summary & Next Steps

    You've now got the core mental model for how M represents data. Let's recap the key ideas:

    • A Table is a structured set of named columns and zero or more rows. Navigate into a row with {rowIndex} (returns a Record), then into a cell with [ColumnName].
    • A List is an ordered sequence of values enclosed in {}. Access items by zero-based position with {index}. Manipulate with List.Transform(), List.Select(), List.Count(), and friends.
    • A Record is a named set of fields enclosed in []. Access fields by name with [FieldName]. Manipulate with Record.AddField(), Record.FieldNames(), Record.ToTable(), and others.
    • These three types nest naturally. A Table is conceptually a List of Records. A cell in a Table can contain a List or a Record. Expanding these nested values is the bread-and-butter of API and JSON work.
    • The each [FieldName] pattern you see everywhere is shorthand for a function that receives the current row as a Record and accesses a named field.

    With this foundation solid, you're ready to go further. Consider exploring how to add custom columns and conditional logic using the Record-navigation skills you've built here, or see how these structures play out in a full pipeline architecture in building multi-stage staging architectures in Power Query. If you're working with aggregations, grouping and aggregating data in Power Query uses all three container types under the hood — you'll read that code with fresh eyes now.

    The best next step, though, is simply to open the Advanced Editor on a query you already have and trace through what each step returns. Now that you can name what you're looking at — Table, List, Record — the code will start to read itself.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Power Query Essentials

    Previous

    Implementing Surrogate Key Generation in Power Query: Deterministic Hashing, Sequence-Based Indexing, and Cross-Source Key Management for Data Warehouse Loads

    Related Insights

    Power QueryExpert

    Implementing Custom Binary File Format Parsers in Power Query M: Reading Fixed-Width, Delimited, and Proprietary Byte Structures into Typed Tables

    26 min
    Power QueryExpert

    Implementing Surrogate Key Generation in Power Query: Deterministic Hashing, Sequence-Based Indexing, and Cross-Source Key Management for Data Warehouse Loads

    24 min
    Power QueryPractitioner

    Implementing Custom Delta Load and Change Data Capture Patterns in Power Query M: Watermark Tracking, Hash-Based Diff Detection, and Incremental Table Synchronization Without Native CDC Support

    20 min

    On this page

    • Introduction
    • Prerequisites
    • The Three Containers: A Quick Orientation
    • Tables: The Structure You Already Know
    • What a Table Looks Like in M
    • Navigating Into a Table
    • Common Table Functions
    • Lists: Ordered Sequences of Values
    • What a List Looks Like in M
    • Why Lists Matter in Practice
    • Common List Functions
    • Generating Lists Programmatically
    • Records: Named Collections of Fields
    • What a Record Looks Like in M
    • Records as Rows
    • Common Record Functions
    • How the Three Types Nest
    • A Table Is a List of Records
    • Lists and Records Inside Table Cells
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Summary & Next Steps