Chapter 05 · Core Querying: Joins, Subqueries, APPLY, CTEs, and Set Operations

CROSS/OUTER APPLY, Table-Valued Functions, and Lateral-Style Query Patterns

Use CROSS/OUTER APPLY and table-valued functions as relational abstractions while accounting for inline visibility, MSTVF behavior, and modern interleaved execution.

Intermediate110–140 minutesAPPLY + iTVF/MSTVF plan labSQL Server 2025 · compatibility 170Developer/Express · IQP-aware evidenceLast reviewed: August 2026

Learning outcomes

The ServiceHub API needs each work order plus its latest visit, or NULL visit columns when no visit exists. A scalar subquery can return one column, but the API needs visit ID, timestamp, and outcome together. A teammate proposes a cursor; another creates a multi-statement table-valued function and assumes SQL Server will estimate it like a normal query. APPLY gives a relational way to evaluate a table expression in the context of each left row, while function shape determines how much optimization information the engine can use.

01

Explain CROSS APPLY and OUTER APPLY as lateral/per-left-row table-source composition.

02

Use APPLY for deterministic TOP-per-group patterns that return multiple columns.

03

Create and invoke an inline table-valued function (iTVF) and contrast its optimization shape with a multi-statement TVF (MSTVF).

04

Account for SQL Server 2017+ interleaved execution instead of repeating obsolete fixed-cardinality rules as universal truth.

05

Inspect plans and measured rows before deciding whether a TVF abstraction is appropriate.

Lab bootstrap: preserve Chapter 05 continuity without hidden dependencies

The APPLY examples need deterministic visit rows. This setup is safe to rerun and creates them only when Lesson 2 has not already done so.

sql · ensure APPLY source rows exist
USE ServiceHubLab;GOIF SCHEMA_ID(N'lab05') IS NULL EXEC(N'CREATE SCHEMA lab05 AUTHORIZATION dbo;');GOIF OBJECT_ID(N'lab05.WorkOrderVisit',N'U') IS NULLBEGIN    CREATE TABLE lab05.WorkOrderVisit    (visit_id int IDENTITY(1,1) NOT NULL CONSTRAINT PK_lab05_Visit PRIMARY KEY,     work_order_id bigint NOT NULL, visit_started_at datetime2(0) NOT NULL, outcome varchar(20) NOT NULL);    INSERT lab05.WorkOrderVisit(work_order_id,visit_started_at,outcome) VALUES    (1001,'2026-08-10T10:00:00','inspection'),    (1001,'2026-08-10T11:30:00','follow-up'),    (1002,'2026-08-10T11:00:00','replacement'),    (1004,'2026-08-11T09:30:00','calibration');END;GO

Check your understanding

  1. What happens to a left row when CROSS APPLY right side returns zero rows?
  2. How does OUTER APPLY differ?
  3. Why is an inline TVF generally easier for the optimizer to reason about than an MSTVF?
  4. Why is “MSTVFs always estimate 100 rows” incomplete for SQL Server 2025?
  5. Does APPLY syntax prove procedural row-by-row execution?
Review the answers

CROSS APPLY omits that left row.

OUTER APPLY preserves it and null-extends right-side columns.

An iTVF is a single relational SELECT whose definition is visible to optimization rather than a populated table return variable.

Eligible queries at compatibility level 140+ can use interleaved execution to obtain actual MSTVF cardinality during optimization.

No. APPLY defines lateral dependency; the optimizer chooses an equivalent physical implementation.

1. APPLY lets the right table source reference the left row

Microsoft defines APPLY so the right table source is evaluated in the context of each row from the left table source. CROSS APPLY returns a left row only when the right side produces rows. OUTER APPLY preserves the left row and null-extends the right columns when the right side is empty. This resembles a lateral join in other SQL dialects.

sql · latest visit per work order with OUTER APPLY
USE ServiceHubLab;GOSELECT w.work_order_id,       w.status,       latest.visit_id,       latest.visit_started_at,       latest.outcomeFROM ops.WorkOrder AS wOUTER APPLY(    SELECT TOP (1)           v.visit_id, v.visit_started_at, v.outcome    FROM lab05.WorkOrderVisit AS v    WHERE v.work_order_id = w.work_order_id    ORDER BY v.visit_started_at DESC, v.visit_id DESC) AS latestORDER BY w.work_order_id;GO

Work orders without visits remain visible because OUTER APPLY null-extends the right side. Replace it with CROSS APPLY and those work orders disappear. The TOP is deterministic because the right-side ORDER BY includes a unique visit ID tie-breaker.

2. APPLY is not synonymous with row-by-row procedural execution

The syntax describes a dependency: the right expression may reference the current left row. The optimizer can still transform, reorder, or implement the query using relational operators when legal. Do not infer “one function call per row” merely from the text. Use the actual execution plan and runtime evidence to see what happened for the compiled statement.

sql · compare CROSS and OUTER APPLY row preservation
USE ServiceHubLab;GOSELECT COUNT_BIG(*) AS cross_apply_rowsFROM ops.WorkOrder AS wCROSS APPLY(    SELECT TOP (1) v.visit_id    FROM lab05.WorkOrderVisit AS v    WHERE v.work_order_id = w.work_order_id    ORDER BY v.visit_started_at DESC, v.visit_id DESC) AS x;SELECT COUNT_BIG(*) AS outer_apply_rowsFROM ops.WorkOrder AS wOUTER APPLY(    SELECT TOP (1) v.visit_id    FROM lab05.WorkOrderVisit AS v    WHERE v.work_order_id = w.work_order_id    ORDER BY v.visit_started_at DESC, v.visit_id DESC) AS x;GO

The second count should equal the number of work orders in the left input; the first equals only work orders for which the right expression found a visit. That row-preservation rule is semantic and independent of the physical join operator chosen later.

3. Inline TVFs are parameterized table expressions

An inline table-valued function (iTVF) returns the result of one SELECT statement. It has no table return variable. This shape gives the optimizer visibility into the relational expression, so predicates from the outer query can often be optimized together with the function definition. An iTVF is useful when a parameterized relational expression is reused and its abstraction remains understandable.

sql · create and apply an inline TVF
USE ServiceHubLab;GOCREATE OR ALTER FUNCTION lab05.VisitForWorkOrder(@work_order_id bigint)RETURNS TABLEASRETURN(    SELECT v.visit_id, v.visit_started_at, v.outcome    FROM lab05.WorkOrderVisit AS v    WHERE v.work_order_id = @work_order_id);GOSELECT w.work_order_id, v.visit_id, v.outcomeFROM ops.WorkOrder AS wCROSS APPLY lab05.VisitForWorkOrder(w.work_order_id) AS vORDER BY w.work_order_id, v.visit_id;GO

This function returns all matching visits, so the query grain is work-order × visit. If the requirement is one latest visit, keep the TOP/order logic in the table expression or define an appropriately named iTVF whose contract is explicit.

4. Multi-statement TVFs create an optimization boundary—but modern SQL Server can interleave

A multi-statement table-valued function (MSTVF) populates a table return variable across multiple statements. Historically, SQL Server used fixed cardinality guesses for MSTVFs, which could produce poor plans. Microsoft documents a fixed guess of 100 from SQL Server 2014, but starting with SQL Server 2017 and compatibility level 140, eligible queries can use interleaved execution: execution pauses optimization, materializes the MSTVF result to learn its cardinality, and resumes optimization with that information. Therefore “MSTVFs always estimate 100 rows” is obsolete as a universal statement.

sql · create an MSTVF for comparison
USE ServiceHubLab;GOCREATE OR ALTER FUNCTION lab05.VisitSummary_MSTVF(@work_order_id bigint)RETURNS @r TABLE(    visit_count int NOT NULL,    latest_visit_at datetime2(0) NULL)ASBEGIN    INSERT @r(visit_count, latest_visit_at)    SELECT COUNT(*), MAX(v.visit_started_at)    FROM lab05.WorkOrderVisit AS v    WHERE v.work_order_id = @work_order_id;    RETURN;END;GOSELECT w.work_order_id, s.visit_count, s.latest_visit_atFROM ops.WorkOrder AS wCROSS APPLY lab05.VisitSummary_MSTVF(w.work_order_id) AS sORDER BY w.work_order_id;GO

At compatibility 170, interleaved execution is available in principle, but eligibility and plan shape must be observed. Do not claim it fired just because the database level is high enough. Inspect the actual plan and relevant database-scoped configuration. More importantly, ask whether the MSTVF is needed at all; base-table joins or an iTVF often expose more relational structure to optimization.

5. Observe database prerequisites and plan evidence

sql · record IQP and function evidence
USE ServiceHubLab;GOSELECT compatibility_levelFROM sys.databasesWHERE name = DB_NAME();SELECT name, value, value_for_secondaryFROM sys.database_scoped_configurationsWHERE name = 'DISABLE_INTERLEAVED_EXECUTION_TVF';SELECT o.name, o.type_descFROM sys.objects AS oWHERE o.object_id IN(    OBJECT_ID(N'lab05.VisitForWorkOrder'),    OBJECT_ID(N'lab05.VisitSummary_MSTVF'));GOSET STATISTICS IO ON;SELECT w.work_order_id, s.visit_countFROM ops.WorkOrder AS wCROSS APPLY lab05.VisitSummary_MSTVF(w.work_order_id) AS sORDER BY w.work_order_id;SET STATISTICS IO OFF;GO

If you capture the actual plan, record estimated and actual rows around the TVF and whether interleaved execution affected compilation. Query Store and IQP are covered later; Chapter 05 only requires that you stop using version-obsolete cardinality folklore as a design argument.

6. Failure analysis, cleanup, and production judgment

Wrong approach

Wrap a complex multi-step query in an MSTVF “for reuse,” then blame indexes when the outer plan cannot optimize well. The abstraction can hide relational information and introduce table-variable behavior; modern interleaving helps eligible cases but does not make every MSTVF equivalent to an inline expression.

Repair

Prefer the simplest relational form that expresses the contract. Use APPLY when left-row correlation is natural, iTVFs for reusable single-query table expressions, and MSTVFs only when their multi-statement behavior is justified and measured under the target compatibility level.

APPLY and T-SQL TVFs are available in the free local Developer/Express path used here. Creating functions requires appropriate database DDL permission. No restart or paid topology is needed. Monitor actual row counts, compilation behavior, CPU/reads, function use, and plan regressions before standardizing an abstraction.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.