9.3 Query Objects: Joins, Aggregation & Query as Report Data Source
Key Takeaways
- Query objects (query) in AL execute single, highly-optimized Transact-SQL statements against the database engine, drastically outperforming nested AL record loops for analytical datasets.
- SqlJoinType supports InnerJoin, LeftOuterJoin, RightOuterJoin, FullOuterJoin, and CrossJoin to define relational dataset joining between parent and child dataitems.
- Aggregate functions (Sum, Avg, Min, Max, Count) compute server-side summaries; any column without an aggregation method automatically forms part of the SQL GROUP BY clause.
- MethodType = Date with DateMethod properties (Year, Month, Day) enables native date-based grouping and time-series aggregation directly inside SQL Server.
- Queries can be consumed programmatically via Open(), Read(), and Close(), exposed as REST/OData endpoints via QueryType = API, or used as report dataset dataitems with DataAccessIntent = ReadOnly.
9.3 Query Objects: Joins, Aggregation & Query as Report Data Source
In Microsoft Dynamics 365 Business Central, Query objects (query) provide a declarative mechanism for retrieving, joining, filtering, and aggregating data across multiple relational database tables. Unlike iterative AL record loops (FindSet / Next) and FlowField evaluations (CalcFields), which execute multiple sequential round-trips to SQL Server, query objects compile into a single, highly-optimized SQL SELECT statement executed entirely within the database engine.
For the MB-820 certification exam, developers must master query structure, join mechanics (SqlJoinType), aggregate functions, date grouping methods, programmatic consumption, API queries, and using queries as report data sources.
1. Query Object Anatomy in AL
A query object definition consists of top-level properties and an elements hierarchy containing dataitem, column, and filter elements.
query 50100 "Customer Sales Analysis"
{
QueryType = Normal;
Caption = 'Customer Sales Analysis';
OrderBy = descending(TotalSalesLCY);
TopNumberOfRows = 50;
DataAccessIntent = ReadOnly;
elements
{
dataitem(Customer; Customer)
{
column(CustomerNo; "No.") { }
column(CustomerName; Name) { }
column(CustomerPostingGroup; "Customer Posting Group") { }
filter(BalanceFilter; "Balance (LCY)")
{
ColumnFilter = BalanceFilter = filter(> 0);
}
dataitem(CustLedgerEntry; "Cust. Ledger Entry")
{
DataItemLink = "Customer No." = Customer."No.";
SqlJoinType = InnerJoin;
DataItemTableFilter = "Document Type" = filter(Invoice | "Credit Memo");
column(TotalSalesLCY; "Sales (LCY)")
{
Method = Sum;
}
column(TransactionCount; "Entry No.")
{
Method = Count;
}
column(PostingYear; "Posting Date")
{
MethodType = Date;
DateMethod = Year;
}
column(PostingMonth; "Posting Date")
{
MethodType = Date;
DateMethod = Month;
}
}
}
}
}
Top-Level Query Properties
| Property | Type / Options | Description & Runtime Behavior |
|---|---|---|
QueryType | Normal, API | Normal (default) is used for AL procedural consumption, reports, and charts. API exposes the query as an OData v4 / REST API endpoint using API metadata (APIPublisher, APIGroup, APIVersion, EntityName, EntitySetName). |
OrderBy | Sorting expression | Defines the default sorting sequence across query columns (ascending or descending), compiling into a SQL ORDER BY clause. |
TopNumberOfRows | Integer | Restricts the maximum number of rows returned by the query, compiling into a SQL TOP (N) clause for high-speed dashboard widgets and top-N reporting. |
DataAccessIntent | ReadOnly, ReadWrite | When set to ReadOnly, directs the SQL query to a read-only database replica in Azure SQL Database (Read Scale-Out), preventing lock contention on primary transactional nodes. |
2. SQL Join Types (SqlJoinType)
When nesting child dataitems beneath a parent dataitem, the SqlJoinType property determines how the database engine merges rows between tables:
SqlJoinType | SQL Equivalent | Relational Behavior & Output |
|---|---|---|
InnerJoin | INNER JOIN | Returns only rows where the DataItemLink join condition finds matching records in both the parent and child tables. If a parent record has zero child records, the parent is omitted entirely. |
LeftOuterJoin | LEFT OUTER JOIN | Returns all rows from the parent dataitem (left table), regardless of whether matching child records exist. If no matching child record is found, child columns return NULL / blank. This is the default join type in AL. |
RightOuterJoin | RIGHT OUTER JOIN | Returns all rows from the child dataitem (right table), matching parent rows where possible. If a child record has no matching parent, parent columns return NULL. |
FullOuterJoin | FULL OUTER JOIN | Returns all rows when there is a match in either the parent or child table. Unmatched columns from either side contain NULL. |
CrossJoin | CROSS JOIN | Produces a Cartesian product combining every row from the parent table with every row from the child table. DataItemLink must not be specified. |
DataItemLink vs. DataItemTableFilter
DataItemLink: Defines the relational foreign key equality joining the child table to the parent table (e.g.,DataItemLink = "Customer No." = Customer."No.";). Compiles into the SQLONclause.DataItemTableFilter: Defines static pre-filters applied to the dataitem table before join evaluation (e.g.,DataItemTableFilter = "Document Type" = const(Invoice);). Compiles into the SQLWHEREclause.
3. Aggregations, Grouping & Date Methods
AL queries provide native server-side aggregation, replacing manual accumulator variables and nested loops in AL code.
Aggregate Methods (Method)
When a column specifies a Method property, SQL Server calculates the aggregate over the grouped dataset:
Sum: Calculates the mathematical sum of numeric values (Decimal, Integer, BigInteger).Avg: Computes the arithmetic average across rows in the group.Min/Max: Retrieves the minimum or maximum value within the group.Count: Counts the number of non-null records matching the group.
The Automatic
GROUP BYRule: Any column in an AL query that does not declare an aggregateMethodis automatically included in the SQLGROUP BYclause. If you includeCustomerNoandCustomerNameas standard columns andTotalSalesLCYwithMethod = Sum, SQL Server groups results uniquely by(CustomerNo, CustomerName).
Date Grouping (MethodType = Date)
Queries support grouping date/time fields into calendar periods directly in the database engine:
DateMethod = Year: Groups transactions by calendar year (e.g.,2025,2026).DateMethod = Month: Groups transactions by month number (1through12).DateMethod = Day: Groups transactions by day of the month (1through31).
column(PostingYear; "Posting Date")
{
MethodType = Date;
DateMethod = Year;
}
column(PostingMonth; "Posting Date")
{
MethodType = Date;
DateMethod = Month;
}
4. Query Consumption in AL Code, APIs & Reports
Programmatic Query Execution Lifecycle
To execute a query in AL, declare a query variable and follow the standard execution sequence: SetRange() / SetFilter(), Open(), Read(), and Close().
codeunit 50110 "Sales Query Processor"
{
procedure ExportTopCustomers()
var
CustSalesQuery: Query "Customer Sales Analysis";
begin
// 1. Apply runtime filters prior to opening cursor
CustSalesQuery.SetRange(CustomerPostingGroup, 'DOMESTIC');
CustSalesQuery.SetFilter(TotalSalesLCY, '> %1', 50000);
CustSalesQuery.TopNumberOfRows(10);
// 2. Open the SQL query cursor
if CustSalesQuery.Open() then begin
// 3. Iterate through aggregated result rows
while CustSalesQuery.Read() do begin
Message('Customer: %1, Sales: %2, Year: %3',
CustSalesQuery.CustomerName,
CustSalesQuery.TotalSalesLCY,
CustSalesQuery.PostingYear);
end;
// 4. Close the cursor to release SQL resources
CustSalesQuery.Close();
end;
end;
}
File Streaming Methods
AL queries provide built-in file export methods that stream output directly to external files:
CustSalesQuery.SaveAsCsv('C:\Exports\Sales.csv');CustSalesQuery.SaveAsXml('C:\Exports\Sales.xml');
Using a Query as a Report DataItem Source
In modern Business Central reporting, developers can use a query as the direct data source for a report dataitem. This technique leverages the server-side aggregation and joining power of queries to accelerate report dataset generation, drastically reducing report execution time:
report 50120 "Top Customer Sales Report"
{
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;
DefaultRenderingLayout = StandardRDLC;
dataset
{
dataitem(SalesAnalysis; "Customer Sales Analysis")
{
column(CustNo; CustomerNo) { }
column(CustName; CustomerName) { }
column(SalesTotal; TotalSalesLCY) { }
column(TxCount; TransactionCount) { }
column(Year; PostingYear) { }
column(Month; PostingMonth) { }
}
}
}
API Queries (QueryType = API)
When building analytical REST endpoints for Power BI or third-party platforms, setting QueryType = API exposes the query as an OData v4 / REST endpoint with server-side aggregation:
query 50125 "Sales Analytics API"
{
QueryType = API;
APIPublisher = 'partner';
APIGroup = 'analytics';
APIVersion = 'v1.0';
EntityName = 'salesAnalysis';
EntitySetName = 'salesAnalyses';
DataAccessIntent = ReadOnly;
elements
{
dataitem(Customer; Customer)
{
column(id; SystemId) { }
column(number; "No.") { }
column(displayName; Name) { }
dataitem(CustLedger; "Cust. Ledger Entry")
{
DataItemLink = "Customer No." = Customer."No.";
SqlJoinType = InnerJoin;
column(totalSales; "Sales (LCY)")
{
Method = Sum;
}
}
}
}
}
A developer needs to build an AL query combining 'Customer' and 'Sales Header'. The query must include all customers in the output dataset, even if a customer currently has zero open sales headers. Which SqlJoinType should be configured on the 'Sales Header' child dataitem?
An AL query contains three columns: CustomerNo (from Customer), PostingDate (from Cust. Ledger Entry with MethodType = Date and DateMethod = Month), and Amount (from Cust. Ledger Entry with Method = Sum). How will the database engine group and return the results?
What is the correct sequence of AL methods required to execute a query object variable, iterate through its returned result rows, and release its database cursor?
A developer creates a heavy analytical query used to populate a Power BI dashboard. To prevent the query from creating read locks or competing for SQL resources against active sales order posting transactions in Business Central SaaS, which query property should be configured?