Skip to content

Query Optimization

Microsoft Sentinel uses Kusto Query Language (KQL) to analyze log data, but inefficient queries can lead to high resource consumption and slow execution. Optimizing KQL queries reduces costs, improves responsiveness, and ensures scalability. This section outlines best practices for optimizing query performance in Microsoft Sentinel.


Filter Early and Use Time Constraints

Filtering data early in the query pipeline minimizes the volume of data processed in subsequent operations. Always apply time constraints first to limit the dataset before joins, aggregations, or other computationally intensive operations.

Example:

SecurityEvent
| where Time > ago(7d)  // Filter by time first
| where EventID == 4624 // Then apply event-specific filters

Tip: Use the ago() function for relative time ranges (e.g., ago(7d)) or explicit time ranges (e.g., Time > "2023-10-01"). Avoid relying on the UI's time range unless the query is executed as-is.


Leverage Indexed Fields

KQL automatically indexes fields like Time, Computer, and SourceIP, but explicitly using these fields in where clauses ensures efficient data retrieval. Avoid unindexed fields in filters unless necessary.

Example:

SecurityEvent
| where SourceComputer == "host123"  // Indexed field
| where EventID == 4624

Tip: Use the performance tab in the query editor to identify slow operations and verify if indexed fields are being utilized effectively.


Avoid Expensive Operations

Operations like mv-expand or nested subqueries can significantly slow performance. Replace them with more efficient alternatives like datatable, join, or project when possible.

Example:
Instead of:

SecurityEvent
| mv-expand EventData
| where EventData contains "suspicious"
Use:
datatable(Computer: string, EventID: int)
| project Computer, EventID
| where EventID == 4624

Tip: Use join for cross-referencing datasets instead of subqueries, and specify kind=inner or kind=left to control join behavior.


Use Projection to Minimize Data Transfer

Select only the fields required for analysis to reduce memory usage and network overhead. Use project to explicitly define the output schema.

Example:

SecurityEvent
| where Time > ago(7d)
| project Time, SourceComputer, EventID, Summary

Tip: Avoid using * to select all fields. Instead, list only the columns needed for the analysis.


Break Queries into Batches

Large queries can overwhelm the system. Split time-based analysis into smaller batches (e.g., daily or hourly intervals) to improve scalability.

Example:

SecurityEvent
| where Time > ago(7d) and Time < ago(6d)  // Process one day at a time

Tip: Use take or limit to restrict the number of rows returned for exploratory queries, but avoid using it for full dataset analysis.


Monitor and Test Performance

Use the "Performance" tab in the query editor to identify bottlenecks like slow joins or expensive aggregations. Test queries with different time ranges to ensure they scale efficiently.

Example:

// Test with a smaller time range first
SecurityEvent
| where Time > ago(1d)
| summarize count() by SourceComputer


Key takeaways

  • Filter early using time constraints and indexed fields to reduce data volume.
  • Avoid expensive operations like mv-expand and nested subqueries.
  • Use projection to minimize data transfer and focus on relevant fields.
  • Break queries into batches for scalability and test performance across time ranges.
  • Leverage the query editor's performance tools to identify and resolve bottlenecks.