This is a bit off-topic to what I’ve been blogging about lately but I’ve been caught out by this before and the other day so was a colleague so I thought it was worth a post.
TL;DR
Be careful of the difference between DataItemLink and DataItemTableFilter properties. DataItemLinks define the join between the dataitems in the query while DataItemTableFilters are applied to the results after the join has been processed.
Intro
In theory the query object in Business Central/NAV ought to be very useful. Instead of using nested REPEAT…UNTIL loops like we used to with the associated many round-trips to the database (or at least the cache) we should be able to create a query to join multiple tables and return all the columns we need in a single round-trip.
In practice, I’ve often found queries frustrating to work with. Sometimes because they can’t support a more complex scenario, sometimes because the parameters don’t do quite what I’d expect. Maybe my expectations are wrong. Fine, but even so, trying to “debug” a query and figure out why the query you have designed gives the results that you are getting is not fun. Not quite as bad as developing reports – but still not fun.
Scenario
Let’s imagine that for some reason we need a list of items with the total base quantity from sales invoice lines – including where that total is zero. Typically you might write some code like this:
SalesLine.SetRange("Document Type",SalesLine."Document Type"::Invoice); SalesLine.SetRange(Type,SalesLine.Type::Item); if Item.FindSet() then repeat SalesLine.SetRange("No.",Item."No."); SalesLine.CalcSums("Quantity (Base)"); //use that result for something... until Item.Next() = 0;
You figure that doing a CalcSums() for each item probably isn’t going to perform too well. Surely, this is exactly the sort of thing that we have queries for?
Version One
Knowing that we need all items records, including ones that don’t have corresponding sales line records we are going to need a left join i.e. all records from table A and matching records from table B.
For starters I’m going to create a query that just shows the data we’ve got – no grouping or summing just yet.
query 50100 "Frustrating Query"
{
QueryType = Normal;
elements
{
dataitem(Item; Item)
{
column(No; "No.") {}
column(Description; Description) {}
dataitem(Sales_Line; "Sales Line")
{
SqlJoinType = LeftOuterJoin;
DataItemLink = "No." = Item."No.";
column(Document_Type;"Document Type") {}
column(Document_No;"Document No.") {}
column(Quantity;"Quantity (Base)") {}
}
}
}
}
The first few results from that query look like this.
No. | Description | Document Type | Document No. | Quantity |
---|---|---|---|---|
1896-S | ATHENS Desk | Invoice | 102201 | 1 |
1900-S | PARIS Guest Chair, black | Quote | 0 | |
1906-S | ATHENS Mobile Pedestal | Quote | 0 | |
1908-S | LONDON Swivel Chair, blue | Quote | 0 | |
1920-S | ANTWERP Conference Table | Order | 101003 | 8 |
1920-S | ANTWERP Conference Table | Invoice | 102202 | 4 |
1920-S | ANTWERP Conference Table | Invoice | 102203 | 10 |
1920-S | ANTWERP Conference Table | Invoice | 102205 | 4 |
Version Two
Cool. Now we need to Sum the Quantity column. I’ll remove the Document No. as we don’t want to group by that. Change the query design to this:
query 50100 "Frustrating Query"
{
QueryType = Normal;
elements
{
dataitem(Item; Item)
{
column(No; "No.") {}
column(Description; Description) {}
dataitem(Sales_Line; "Sales Line")
{
SqlJoinType = LeftOuterJoin;
DataItemLink = "No." = Item."No.";
column(Document_Type;"Document Type") {}
column(Quantity;"Quantity (Base)")
{
Method = Sum;
}
}
}
}
}
Now the results are:
No. | Description | Document Type | Quantity |
---|---|---|---|
1896-S | ATHENS Desk | Invoice | 1 |
1900-S | PARIS Guest Chair, black | Quote | 0 |
1906-S | ATHENS Mobile Pedestal | Quote | 0 |
1908-S | LONDON Swivel Chair, blue | Quote | 0 |
1920-S | ANTWERP Conference Table | Order | 8 |
1920-S | ANTWERP Conference Table | Invoice | 18 |
Version Three
Remember that we only wanted the sum of the base quantity for invoice lines. We’ve got a result for 1920-S order lines at the moment. That’s fine we can use the DataItemTableFilter to filter the Document Type.
At least, you’d think so. So would I…and we’d both be wrong. Adding DataItemTableFilter = “Document Type” = const(Invoice) to the query gives these results:
No. | Description | Document Type | Quantity |
---|---|---|---|
1896-S | ATHENS Desk | Invoice | 1 |
1920-S | ANTWERP Conference Table | Invoice | 18 |
Erm…where are the rest of my rows?!
Q: what has happened to items 1900-S, 1906-S and 1908-S?
A: there are no matching sales lines for those items
Q: but…that’s why we used a LeftOuterJoin. That should include items with no matching sales lines. I thought that was the point of specifying the join type?
A: yes, except DataItemTableFilter isn’t used as part of the join
Q: …eh?
Explanation
I expected, and maybe you did too, that DataItemTableFilter would be used to filter the Sales Line table before joining it to the Item table. It turns out that the join is processed first, respecting the DataItemLink properties, and the DataItemFilter property is used to filter the joined results afterwards.
In SQL terms the filters go into the HAVING clause and not the ON clause. We might have expected something like this:
SELECT Item.No_,
Item.Description,
SalesLine.[Document Type],
SUM(SalesLine.[Quantity (Base)]) AS Quantity
FROM [CRONUS International Ltd_$Item] AS Item
LEFT JOIN [CRONUS International Ltd_$Sales Line] AS SalesLine
ON SalesLine.No_ = Item.No_
AND SalesLine.[Document Type] = 2
GROUP BY Item.No_, Item.Description, SalesLine.[Document Type]
with SalesLine.[Document Type] = 2 forming part of the ON clause (the definition of the join between the tables). What you actually get is something like this:
SELECT Item.No_,
Item.Description,
SalesLine.[Document Type],
SUM(SalesLine.[Quantity (Base)]) AS Quantity
FROM [CRONUS International Ltd_$Item] AS Item
LEFT JOIN [CRONUS International Ltd_$Sales Line] AS SalesLine
ON SalesLine.No_ = Item.No_
GROUP BY Item.No_, Item.Description, SalesLine.[Document Type]
HAVING SalesLine.[Document Type] = 2
with a HAVING clause at the end which restricts the results after the tables have been joined. (The actual SQL queries you’ll see if you run SQL Server Profiler will be different – stuffed full of parameters and ISNULLs – but this is the general idea).
Conclusion
That was a long way of saying be careful how you use the DataItemTableFilter property – it might not do what you’re expecting. So how can you define an ON clause where the filter is a constant value not a field in another table? I don’t know.
As far as I can see as DataItemLink only allows you to define joins between field tables you’d need to engineer the data so that all of your joins are between fields and not constant values. I’d like to be wrong, but if I’m not this is a pretty big flaw is queries.
It’d be nice to be able add constant values into table joins for this kind of thing. While we’re wishing, it would be even better to be able to dynamically define queries at run-time, build and execute them on the fly. It seems I’m not the only one with a query wishlist: https://experience.dynamics.com/ideas/search-ideas/?q=queries&forum=e288ef32-82ed-e611-8101-5065f38b21f1
3 thoughts on “Dynamics 365 Business Central Queries: Erm…where are the rest of my rows?!”