Kusto Query Hints and Strategies
When you’re working with a large volume of log data, query performance becomes more important.
If you’ve ever stared at a loading screen while your Kusto query slowly runs in the background, this post is for you. Alternatively, if you grab a cup of tea while your query is running, you’re probably okay to not bother reading this. However, these are some performance hints I actually use (sometimes), not just theoretical optimisations.
Why Query Hints Matter
Kusto’s query engine makes good decisions most of the time, but hints let you share your knowledge with the engine and how it join data together.
hint.strategy = shuffle
This is the hint I reach for when I’m joining large tables or doing massive summarize operations:
SecurityEvents
| where TimeGenerated > ago(7d)
| join hint.strategy = shuffle (
SigninLogs
| where TimeGenerated > ago(7d)
) on $left.AccountName == $right.UserPrincipalName
What it does: Distributes your data processing across cluster nodes instead of doing everything on a single node. This is good when you’re joining two large tables and I use this any time I see a query timing out with large datasets
hint.shufflekey = <key>
This is the more precise version of the shuffle strategy:
SecurityEvents
| summarize hint.shufflekey = Computer count() by Computer, bin(TimeGenerated, 1h)
You can use this when you know exactly which column has high cardinality (a relationship with another table).
hint.materialized = true
This is useful for a dashboard query that run repeatedly:
let AllEvents = materialize(SecurityEvents
| where TimeGenerated > ago(1d)
| where EventID in (4624, 4625, 4634));
AllEvents | where EventID == 4624 | count;
AllEvents | where EventID == 4625 | count;
This caches intermediate results in memory so they can be reused across multiple steps. You can use this when the same filtered dataset is used multiple times.
hint.concurrency = <number>
I find this one’s hit or miss but helpful when used correctly:
SecurityEvents
| where TimeGenerated > ago(1h)
| summarize hint.concurrency = 8 count() by EventID, Computer
This can be used to speed up queries that can be parallelised. Apparently, setting this too high can actually hurt performance… I don’t use this often.
hint.num_partitions = <number>
Similar to concurrency, but focuses on data partitioning:
SecurityEvents
| where TimeGenerated > ago(7d)
| summarize hint.num_partitions = 128 count() by EventID
This is used for extremely large datasets where the default partitioning is insufficient/
Resource Management Hints
These hints help me be a good person on shared datasets, avoid query timeouts, and overloading clusters:
hint.query_timeout = <time>
SecurityEvents
| where TimeGenerated > ago(180d)
| summarize hint.query_timeout = time(10m) count() by EventID
This sets a maximum time the query will run before giving up and is good to avoid tying up resources for too long.
hint.max_memory_consumption_per_query = <size>
SecurityEvents
| where TimeGenerated > ago(90d)
| summarize hint.max_memory_consumption_per_query = 4gb count() by EventID, Computer
This is also useful for queries on shared clusters where I need to control resource usage and when I’m getting memory-related failures.
Cluster Management Hints (I don’t use this all that often)
These are more specialised but extremely useful in certain scenarios:
hint.remote = true
SecurityEvents <|
hint.remote=true
cluster('security-uksouth').database('SecLogs').SecurityEvents
This is supposed to be useful when working with data across multiple clusters and for cross-region analytics.
hint.distribution = <strategy>
SecurityEvents
| summarize hint.distribution = per_node count() by Computer
Options include:
per_node
: Each node processes its local data independentlyper_shard
: Processing happens per data shardsingle
: Forces single-node execution
When I use it:
- I don’t yet… could be helpful for you though.
An Example
Here’s an example from actual work (sanitised, of course) that uses multiple hints to optimise a complex query:
let timeframe = 14d;
let suspicious_signins =
SigninLogs
| where TimeGenerated > ago(timeframe)
| where ResultType == "50126" // Invalid username or password
| summarize hint.strategy = shuffle
hint.shufflekey = IPAddress
count() by IPAddress, bin(TimeGenerated, 1h)
| where count_ > 20; // Threshold for brute force attempts
let affected_accounts =
SigninLogs
| where TimeGenerated > ago(timeframe)
| where IPAddress in (suspicious_signins) and ResultType == "0" // Successful login
| summarize hint.materialized = true by UserPrincipalName;
affected_accounts
| join hint.strategy = broadcast (
SecurityEvents
| where TimeGenerated > ago(timeframe)
| where EventID == 4728 // Member added to security-enabled global group
) on $left.UserPrincipalName == $right.TargetUserName
This query will:
- Identify potential brute force attempts using the shuffle strategy
- Materializes the list of affected accounts for reuse
- Uses a broadcast join (small table to large table) to find security events associated with those accounts. This isn’t that important on small datasets though.
Final Thoughts
Query hints should be your last resort, not your first approach. Always try to optimise your query structure and filters first. But when you’re dealing with massive datasets and complex operations, these hints can be the difference between a query that runs in seconds and one that times out.
Remember that every hint is essentially telling the query engine “I know better than you about my data,” so use them thoughtfully and test the performance impact each time.
This post is part of my Kusto series, where I share the stuff I use daily for investigations and data analysis.