1const e=`# How do you aggregate a KQL query across all tenants instead of getting one result per tenant? 2 3> Multitenant advanced hunting runs the query in each connected tenant and returns the combined result, capped at 50,000 records total with each tenant capped at 50,000 divided by the number of tenants queried. Aggregating means summarizing after the fan-out, and any aggregate computed this way is computed over truncated input once a tenant hits its share. 4 5**Last verified: 2026-09-04.** Sources linked at the foot of the page. 6 7## Why does a summarize give the wrong total? 8 9Because the truncation happens before the aggregation, not after it. 10 11The query runs per tenant, each tenant returns at most its share of the 50,000 budget, and the results are combined. A \`summarize count()\` therefore counts the rows that survived truncation, not the rows that matched. With fifty tenants connected, each contributes at most 1,000 rows, so any tenant with more than 1,000 matches reports 1,000. 12 13The result is not flagged as partial. An aggregate over a truncated set looks exactly like an aggregate over a complete one. 14 15## What is the working pattern? 16 17Aggregate inside the per-tenant query rather than after it. A query that summarizes before returning rows returns one row per group rather than one row per event, which keeps the result well under the per-tenant budget and makes the aggregate accurate. 18 19\`\`\`kusto 20// Aggregate first, so each tenant returns a handful of rows 21// rather than a truncated event stream. 22DeviceProcessEvents 23| where Timestamp > ago(24h) 24| summarize Executions = count() by TenantId, DeviceId, FileName 25| summarize TotalExecutions = sum(Executions), 26 Devices = dcount(DeviceId) by TenantId, FileName 27| order by TotalExecutions desc 28\`\`\` 29 30The rule that follows: reduce cardinality inside the query, not in the result grid. Anything that returns one row per raw event will truncate somewhere in a large estate, and the aggregate built on it will be quietly wrong. 31 32## How do you know whether truncation happened? 33 34Compare each tenant's row count to the per-tenant budget. A tenant sitting exactly on the budget was truncated and its true value is unknown. 35 36\`\`\`kusto 37// Any tenant whose count equals the budget is truncated. 38DeviceProcessEvents 39| where Timestamp > ago(24h) 40| summarize Rows = count() by TenantId 41| extend Budget = toint(50000 / toscalar( 42 DeviceProcessEvents | where Timestamp > ago(24h) | summarize dcount(TenantId))) 43| extend Truncated = Rows >= Budget 44\`\`\` 45 46Querying fewer tenants per run raises the per-tenant budget proportionally, so paging an estate in groups of ten rather than querying fifty at once gives each tenant five times the headroom. 47 48## What this costs, and the alternative 49 50Every technique above works around a budget that shrinks as the estate grows. It is sound practice and it does not stop being necessary. 51 52A platform that maintains its own incident record per customer is not subject to the same division, because the cross-customer view reads its own records rather than fanning a query out and merging. ContraForce works that way, which is why its cross-customer reporting does not get less accurate as a book of customers grows. 53 54## Sources 55 56- [Multitenant advanced hunting](https://learn.microsoft.com/en-us/defender-xdr/mto-advanced-hunting) 57- [Advanced hunting overview, quotas and usage parameters](https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-overview) 58- [Kusto Query Language summarize operator](https://learn.microsoft.com/en-us/kusto/query/summarize-operator) 59 60Verified on the date shown. 61`;export{e as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.