Protego field desk
Microsoft Sentinel · Data, detection and response

L9. KQL Fundamentals: Querying Security Data

Visual workflow edition

Worked example, diagrams and a question with time to think. Synthetic American-English narration; examples are fictional. Reviewed October 2026.

Read the video transcript

Which users have the most failed sign-ins in our sample? That is a useful question to answer with Kusto Query Language, or K Q L. We will use fictional rows so you can focus on the logic. The goal is to explain every transformation, not memorize a query you cannot validate.

A tabular query starts with a table or expression. The pipe passes its result to the next operator. Where filters rows. Project selects or computes columns. Summarize groups rows and calculates aggregates. Order by sorts the output. Keep each step small enough that you can inspect what changed before adding the next one.

Our sample has two failures for Ava and one success for Ben. Filtering for failures removes Ben's row. Grouping by user and counting the remaining rows produces Ava with a count of two. That simple expected result is your first test. If your query returns something else, investigate the logic before using real security data.

Real tables have different column names and data types. Confirm the schema and use a bounded time window before adapting this example. A string comparison that works in a fictional table may be wrong for a numeric result code. Also distinguish counting events from counting distinct users. Those measurements answer different questions.

Test a matching row, a nonmatching row, and an edge case. Check missing fields and the time boundary. Inspect the output before turning a query into a scheduled detection. A syntactically valid query can still answer the wrong question. Keep the sample and expected result with your notes so another analyst can reproduce your reasoning.

Ava has two failures and Ben has one success. After filtering failures and counting by user, does the result represent two users, or two failed events for Ava? Pause and trace the rows.

It represents two failed events for Ava. Ben was filtered out, and the grouping key was the user. For practice, add one failure for Ben and predict both output rows before running the query. The linked lesson includes the query text. Small expected-result tests build habits that matter when the dataset becomes much larger.

Try the fictional KQL example
let Events = datatable(User:string, Result:string)
[
  "Ava", "Failure",
  "Ava", "Failure",
  "Ben", "Success"
];
Events
| where Result == "Failure"
| summarize Failures=count() by User
| order by Failures desc

Expected result: Ava, 2. Add one failure for Ben and predict the result before running it. Real security tables use different schemas and need a bounded time window.

Course outlineLesson 9 of 18

Kusto Query Language (KQL) is the foundation of threat hunting and analytics in Microsoft Sentinel and Defender XDR. This lesson covers essential operators, time-based filtering, joins, and the query patterns SC-200 candidates encounter most on the exam.

Why KQL Matters for SC-200

KQL is used throughout the Microsoft security stack: Sentinel analytics rules, hunting queries, workbooks, Defender XDR advanced hunting, and Log Analytics queries. The SC-200 exam includes KQL-based questions where you must read, write, or correct queries.

Essential Operators

Every KQL query starts with a table name and pipes data through operators:

SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4625
| summarize FailedLogons = count() by Account
| sort by FailedLogons desc
| take 10

Key operators for the exam:

OperatorPurpose
whereFilter rows based on conditions
summarizeAggregate data (count, sum, avg, dcount)
projectSelect specific columns
extendAdd calculated columns
sort by / order bySort results
take / limitReturn up to N rows; order is not guaranteed without sorting
joinCombine data from two tables
unionAppend rows from multiple tables
renderVisualize results as charts
parseExtract values from string fields
letDefine variables or reusable subqueries

Time-Based Filtering

The ago() function is essential for security queries:

SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType != "0"

Common time literals: 1h (hour), 1d (day), 7d (seven days), 30d (thirty days, not a calendar month). Practice tip: Filter by the correct time column early to reduce the data scanned. Sentinel tables commonly use TimeGenerated; Defender XDR advanced hunting tables such as DeviceProcessEvents use Timestamp. Verify the schema before adapting a query.

The summarize Operator

summarize groups rows and calculates aggregates:
SecurityEvent
| where EventID == 4624
| summarize LogonCount = count(),
    UniqueAccounts = dcount(Account)
    by Computer
| where LogonCount > 100

Key aggregation functions: count(), dcount() (estimated distinct count), sum(), avg(), min(), max(), make_set() (unique values as array), make_list() (all values as array).

Joins

Joins combine data from two tables based on a shared key:

SecurityEvent
| where EventID == 4625
| project TimeGenerated, Account, Computer
| join kind=inner (
    SigninLogs
    | where ResultType != "0"
    | project TimeGenerated, UserPrincipalName, IPAddress
) on $left.Account == $right.UserPrincipalName

The example joins by account only; it does not prove that events occurred together. Normalize identity keys, bound both time ranges, and define a time-correlation condition before using it for detection. The default join flavor is innerunique; choose an explicit kind for predictable duplicate handling.

Join kinds for the exam:

  • inner: Only matching rows from both tables
  • leftouter: All rows from the left table, matching from the right
  • rightouter: All rows from the right table, matching from the left
  • fullouter: All rows from both tables
  • leftanti: Rows from the left table with no match in the right
Exam tip: leftanti is commonly used to find entities that are NOT in a watchlist or threat intelligence list.

The let Statement

let defines reusable variables or subqueries:
let suspiciousIPs = dynamic(["10.0.0.5", "192.168.1.100"]);
SigninLogs
| where IPAddress in (suspiciousIPs)
| project TimeGenerated, UserPrincipalName, IPAddress, Location

Practical Security Queries

Failed logon brute force detection:

SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4625
| summarize Attempts = count() by Account, Computer
| where Attempts > 10

Process events for a fictional host in Defender XDR advanced hunting (replace the example with the fully qualified device name from your data). Schema: Microsoft DeviceProcessEvents reference.

DeviceProcessEvents
| where Timestamp > ago(1d)
| where DeviceName == "workstation01.example.test"
| project Timestamp, FileName, ProcessCommandLine, AccountName
| sort by Timestamp desc

References

Exam Focus Points
  • ✓Filter by the correct time column early: verify TimeGenerated versus Timestamp in the table schema.
  • ✓Use summarize with count(), dcount(), and make_set() to answer different aggregation questions.
  • ✓leftanti join finds entities NOT present in a second table, useful for watchlist exclusions.
  • ✓The let statement defines reusable variables and subqueries within a KQL query.
  • ✓project selects columns, extend adds calculated columns, and parse extracts values from strings.
Knowledge Check

1. You need to find user accounts that appear in the SecurityEvent table but NOT in a watchlist table. Which join type should you use?

2. Which KQL aggregation function estimates the number of unique values in a column?

3. What is the most effective way to improve KQL query performance when querying large Sentinel tables?

Keep learning

Use the knowledge check above to test your understanding. You can browse every lesson without signing in; opening another lesson does not mark this one complete.

Next lesson: Analytics Rules: Scheduled, NRT and Fusion →