Advanced KQL: Optimising Query Performance.
The query worked perfectly.
Then it was run across 90 days of telemetry from thousands of endpoints, identities, emails and cloud events.
Suddenly it was slow. Then it timed out.
Microsoft Defender XDR was not broken. The query simply had not been designed to scale. Agent Foskett realised that writing correct KQL is only half the job. Great security analysts also know how to write fast KQL.
Lesson overview
Learn how experienced Microsoft Defender XDR analysts make KQL queries faster, cleaner and more scalable across large security environments.
Why query performance matters
The performance mindset
Rule 1 — Filter early
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
// Poor pattern: expensive work before filtering
DeviceProcessEvents
| summarize EventCount = count() by DeviceName, FileName, Timestamp
| where Timestamp > ago(7d)
// Better pattern: reduce rows first
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName has_any ("powershell.exe", "cmd.exe", "wscript.exe")
| summarize EventCount = count() by DeviceName, FileNameRule 2 — Target the right table
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
// Avoid this during normal hunting unless you truly need a global search search "invoice.zip" // Better: target the relevant table EmailAttachmentInfo | where Timestamp > ago(14d) | where FileName has "invoice" | project Timestamp, RecipientEmailAddress, SenderFromAddress, FileName, SHA256
Rule 3 — Project only useful columns
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
let TimeFrame = 7d;
DeviceNetworkEvents
| where Timestamp > ago(TimeFrame)
| where RemoteUrl has_any ("pastebin", "ngrok", "duckdns")
| project Timestamp,
DeviceName,
InitiatingProcessAccountName,
InitiatingProcessFileName,
RemoteUrl,
RemoteIP,
ActionType
| order by Timestamp descRule 4 — Use precise time windows
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
// Start with the suspected compromise window let StartTime = datetime(2026-07-01 08:00:00); let EndTime = datetime(2026-07-01 18:00:00); DeviceProcessEvents | where Timestamp between (StartTime .. EndTime) | where DeviceName =~ "WORKSTATION-17" | project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine | order by Timestamp asc
Rule 5 — Be careful with mv-expand
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
let TimeFrame = 7d;
AlertEvidence
| where Timestamp > ago(TimeFrame)
| where Title has_any ("Suspicious", "Malware", "Phishing")
| project Timestamp, AlertId, Title, EntityType, EvidenceRole, AdditionalFields
| extend ParsedFields = parse_json(AdditionalFields)
| mv-expand ParsedFields
| project Timestamp, AlertId, Title, EntityType, EvidenceRole, ParsedFieldsRule 6 — Parse JSON once
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
let TimeFrame = 7d;
CloudAppEvents
| where Timestamp > ago(TimeFrame)
| where ActionType has_any ("FileDownloaded", "FileAccessed", "MailItemsAccessed")
| extend Parsed = parse_json(RawEventData)
| extend UserAgent = tostring(Parsed.UserAgent),
Operation = tostring(Parsed.Operation),
Workload = tostring(Parsed.Workload)
| project Timestamp, AccountDisplayName, ActionType, Application, Workload, Operation, UserAgent, IPAddressRule 7 — Optimise join order
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
let TimeFrame = 7d;
let SuspiciousHashes =
DeviceFileEvents
| where Timestamp > ago(TimeFrame)
| where FileName endswith ".ps1" or FileName endswith ".js"
| summarize by SHA256;
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where SHA256 in (SuspiciousHashes)
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, SHA256
| order by Timestamp descRule 8 — Summarize with purpose
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 13
let TimeFrame = 24h;
DeviceLogonEvents
| where Timestamp > ago(TimeFrame)
| where ActionType == "LogonSuccess"
| where LogonType has_any ("RemoteInteractive", "Network")
| summarize LogonCount = count(),
Devices = dcount(DeviceName),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by AccountName, RemoteIP
| where LogonCount > 10 or Devices > 3
| order by LogonCount descRule 9 — Use top and take while testing
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
let TimeFrame = 3d;
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where ProcessCommandLine has_any ("EncodedCommand", "FromBase64String", "DownloadString")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine
| top 100 by Timestamp descRule 10 — Build reusable performance patterns
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let TimeFrame = 14d;
let SuspectUser = "user@contoso.com";
let SuspectDevices =
DeviceLogonEvents
| where Timestamp > ago(TimeFrame)
| where AccountUpn =~ SuspectUser
| summarize by DeviceName;
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where DeviceName in (SuspectDevices)
| where FileName has_any ("powershell.exe", "cmd.exe", "rundll32.exe")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine
| order by Timestamp descPerformance comparison
Real-world optimisation walkthrough
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
// Optimised version
let TimeFrame = 7d;
let SuspiciousTerms = dynamic(["EncodedCommand", "FromBase64String", "DownloadString", "IEX", "WebClient"]);
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where FileName =~ "powershell.exe"
| where ProcessCommandLine has_any (SuspiciousTerms)
| project Timestamp,
DeviceName,
AccountName,
InitiatingProcessFileName,
FileName,
ProcessCommandLine,
ReportId
| order by Timestamp descWhat changed in the optimised version?
Query performance checklist
Related Agent Foskett Academy lessons
Coming next
Final thought
Advanced KQL Optimising Query Performance in Microsoft Defender XDR
Agent Foskett Academy Lesson 124 teaches Microsoft security analysts how to optimise KQL query performance in Microsoft Defender XDR by filtering early, projecting useful columns, reducing dynamic parsing, optimising joins and avoiding unnecessary mv-expand operations.
Learn KQL query optimisation for Microsoft Defender XDR hunting
This advanced KQL lesson explains how to write faster and more scalable Defender XDR hunting queries across endpoint, identity, email and cloud telemetry tables.
Microsoft Defender XDR KQL performance tuning tutorial
Security analysts can use this lesson to improve KQL query speed, reduce scanned data, build reusable performance patterns and prepare for enterprise hunting workflows.
