KQL practice questions with answers
Fifteen free KQL practice questions with explained answers, written as short scenarios on filtering, aggregating, joining and parsing log data. Kusto Query Language is the read-only query language of Azure Monitor Logs, Microsoft Sentinel, Defender XDR advanced hunting and Fabric Eventhouses, and each question states the table columns it uses. The topic is tested in SC-200, DP-600 and DP-700, and in a lighter form in AZ-104.
How to use these questions
Read the schema in the stem first, then trace the query one operator at a time and write down which columns exist after each pipe. Commit to one letter before opening the explanation. Underline the constraint word, such as “whole term”, “never”, “latest” or “per user”, because it usually picks the operator. The guide KQL cheat sheet for certification exams summarises the operators used here.
Warm-up: the pipe model and basic operators
Question 1. The table Heartbeat has the columns TimeGenerated (datetime), Computer (string) and Category (string). Which query returns the number of heartbeat records from the last hour?
- A.
Heartbeat | count | where TimeGenerated > ago(1h) - B.
Heartbeat | where TimeGenerated < ago(1h) | count - C.
Heartbeat | where TimeGenerated > ago(1h) | count - D.
Heartbeat | where TimeGenerated > 1h | count
Show answer
Answer: C
The where operator keeps the rows from the last hour, and count then returns a single row with one column named Count. Option A counts first, so the output has only the Count column and the later filter on TimeGenerated fails. Option B counts records older than one hour. Option D compares a datetime with a timespan, which is a type error.
Question 2. The table Heartbeat has the columns TimeGenerated, Computer, Category and OSType. A query must return only the columns Computer and TimeGenerated. Which operator line does that?
- A.
project Computer, TimeGenerated - B.
project-away Computer, TimeGenerated - C.
extend Computer, TimeGenerated - D.
take 2
Show answer
Answer: A
project keeps exactly the listed columns and drops all others. project-away does the opposite and removes the listed columns, leaving Category and OSType. extend adds a column and keeps all existing ones. take limits the number of rows and has no effect on the columns.
Question 3. The table SigninLogs has the columns TimeGenerated, UserPrincipalName, ResultType (string) and IPAddress. Which query returns the five most recent sign-ins?
- A.
SigninLogs | take 5 - B.
SigninLogs | order by TimeGenerated asc | take 5 - C.
SigninLogs | limit 5 | order by TimeGenerated desc - D.
SigninLogs | top 5 by TimeGenerated desc
Show answer
Answer: D
top 5 by TimeGenerated desc sorts by time with the newest first and keeps five rows. take 5 returns five arbitrary rows with no guaranteed order. Sorting ascending and taking five returns the oldest rows. In option C the limit is applied before the sort, so five arbitrary rows are sorted and the result is not the five most recent overall.
Question 4. The table SigninLogs has the columns TimeGenerated, UserPrincipalName, ResultType (string, where 0 means success) and IPAddress. A report needs one number: how many different users had at least one failed sign-in. Which query returns it?
- A.
SigninLogs | where ResultType != "0" | summarize count() - B.
SigninLogs | where ResultType != "0" | summarize dcount(UserPrincipalName) - C.
SigninLogs | where ResultType != "0" | summarize count() by UserPrincipalName - D.
SigninLogs | where ResultType != "0" | distinct UserPrincipalName
Show answer
Answer: B
After filtering to failures, dcount counts the distinct values of UserPrincipalName and returns one number. count counts rows, so a user with ten failures is counted ten times. Summarizing count by user returns one row per user rather than a single number. distinct returns the list of users, not the count.
Question 5. The table DeviceProcessEvents has the columns Timestamp, DeviceName, FileName (string) and ProcessCommandLine. FileName holds the process image name and appears as powershell.exe on some devices and PowerShell.exe on others. Which filter matches the process on every device, whatever the case, and nothing else?
- A.
where FileName == "powershell.exe" - B.
where FileName has_cs "powershell" - C.
where FileName =~ "powershell.exe" - D.
where FileName startswith_cs "Power"
Show answer
Answer: C
The =~ operator tests equality without regard to case, so it matches both spellings and only that name. The == operator is case-sensitive and misses PowerShell.exe. has_cs is case-sensitive and also matches any value that contains the term powershell. startswith_cs with Power is case-sensitive and matches only some spellings.
Scenarios: aggregation, time and results by hand
Question 6. The table Heartbeat has the columns TimeGenerated, Computer and Category. Each healthy computer sends a heartbeat every minute. You need the list of computers whose most recent heartbeat is older than 15 minutes. Which query is correct?
- A.
Heartbeat | summarize LastBeat = max(TimeGenerated) by Computer | where LastBeat < ago(15m) - B.
Heartbeat | where TimeGenerated < ago(15m) | distinct Computer - C.
Heartbeat | where TimeGenerated > ago(15m) | distinct Computer - D.
Heartbeat | summarize LastBeat = max(TimeGenerated) | where LastBeat < ago(15m)
Show answer
Answer: A
summarize with max per computer finds each computer’s latest heartbeat, and the where afterwards keeps those whose latest beat is older than 15 minutes. Option B returns every computer that has any old heartbeat, including healthy ones, because older rows exist for all of them. Option C returns the computers that are alive. Option D takes one maximum for the whole table, so the by Computer grouping is missing and no computer names are returned.
Question 7. The table SecurityEvent has the columns TimeGenerated, Computer, Account and EventID (int). Event 4625 is a failed logon. One of the queries below fails with an error because it refers to a column that no longer exists. Which one?
- A.
SecurityEvent | where EventID == 4625 | summarize Failures = count() by Computer | where Failures > 10 - B.
SecurityEvent | where EventID == 4625 | summarize Failures = count() by Account | top 3 by Failures desc - C.
SecurityEvent | where EventID == 4625 | summarize Failures = count() by Computer | project Computer - D.
SecurityEvent | summarize Failures = count() by Computer | where EventID == 4625
Show answer
Answer: D
In option D, summarize keeps only Computer and Failures, so the later filter on EventID refers to a column that was dropped and the query fails. In option A the EventID filter comes before summarize and the where on Failures refers to a column that exists. Options B and C also filter on EventID before the aggregation and then use only columns that survive it.
Question 8. Given the datatable T below with the columns User (string) and Result (string), what does the query return? let T = datatable(User:string, Result:string) ['ann','fail','ann','ok','bob','fail','ann','fail','cy','ok']; T | where Result == 'fail' | summarize n = count() by User | order by n desc
- A. Three rows: ann 3, bob 1, cy 1
- B. Two rows: ann 2, then bob 1
- C. One row: ann 2
- D. Three rows: ann 2, bob 1, cy 0
Show answer
Answer: B
The datatable holds five rows: ann fail, ann ok, bob fail, ann fail and cy ok. The where keeps three rows: ann, bob and ann. Grouping by User gives ann with n = 2 and bob with n = 1, and the descending sort puts ann first. cy is dropped by the filter, so it appears in no row, not even with a count of 0, because summarize only returns groups that have rows.
Question 9. The table SigninLogs has the columns TimeGenerated, UserPrincipalName and ResultType (string, 0 means success). A report needs one row per user with two numbers: all sign-ins and failed sign-ins. Which query produces that?
- A.
SigninLogs | summarize Total = count(), Failed = countif(ResultType != "0") by UserPrincipalName - B.
SigninLogs | summarize Total = count() by UserPrincipalName | where ResultType != "0" - C.
SigninLogs | where ResultType != "0" | summarize Failed = count() by UserPrincipalName - D.
SigninLogs | summarize count() by UserPrincipalName, ResultType
Show answer
Answer: A
countif counts only the rows for which the predicate is true, so one summarize can return the total and the failures side by side per user. Option B refers to ResultType after summarize has dropped it. Option C filters to failures first, so the total is lost. Option D returns one row per user and result code, not two numbers in one row.
Question 10. The table SigninLogs has the columns TimeGenerated, UserPrincipalName and ResultType (string, 0 means success). You need the number of failed sign-ins per hour for the last 24 hours. Which query is correct?
- A.
SigninLogs | where TimeGenerated > ago(24h) and ResultType != "0" | summarize Failures = count() by TimeGenerated - B.
SigninLogs | where TimeGenerated > ago(24h) and ResultType != "0" | summarize Failures = count() by bin(1h) - C.
SigninLogs | where TimeGenerated > ago(24h) and ResultType != "0" | summarize Failures = count() by bin(TimeGenerated, 1h) - D.
SigninLogs | where ResultType != "0" | summarize Failures = count() by ago(1h)
Show answer
Answer: C
bin rounds TimeGenerated down to the start of each hour, so grouping by it produces one row per hour with the failure count. Grouping by TimeGenerated itself makes a group for almost every distinct timestamp. bin requires the value and the bin size as two arguments, so bin(1h) is invalid. ago() returns a single datetime and cannot create hourly groups.
Edge cases and traps: strings, joins and parsing
Question 11. The table AppEvents has the columns TimeGenerated, ClientApp (string) and Operation (string). Some rows hold the value KustoExplorerQueryRun in ClientApp. The query AppEvents | where ClientApp has 'Explorer' returns no rows for them. Which change makes those rows match?
- A.
where ClientApp == "Explorer" - B.
where ClientApp has_cs "Explorer" - C.
where ClientApp contains "Explorer" - D.
where ClientApp =~ "Explorer"
Show answer
Answer: C
The value KustoExplorerQueryRun is a single term, because terms are maximal runs of alphanumeric characters. has matches whole terms, so Explorer, which is only part of that term, does not match. contains looks for any substring and does match. has_cs is also term-based and would still fail. Equality with == or =~ would need the full value.
Question 12. Left is a datatable with the columns k (string) and v (int) and the rows (a, 1), (a, 2), (b, 3). Right is a datatable with the columns k (string) and w (int) and the rows (a, 10), (b, 20). How many rows does the query Left | join Right on k return, with no kind specified?
- A. 1
- B. 2
- C. 3
- D. 4
Show answer
Answer: B
The default join flavor is innerunique, which first removes duplicate rows on the join key from the left side. The two rows with k = a collapse to one, so the left side has the keys a and b, and each matches one row of Right, giving two rows. With kind=inner the result would be three rows, because both left rows with k = a would match. One row and four rows are not possible results.
Question 13. The table Heartbeat has the columns TimeGenerated and Computer. The table SecurityEvent has the columns TimeGenerated, Computer and EventID (int). You need the computers that sent a heartbeat but have no event 4625 at all. Which query is correct?
- A.
Heartbeat | distinct Computer | join kind=leftsemi (SecurityEvent | where EventID == 4625 | distinct Computer) on Computer - B.
Heartbeat | distinct Computer | join kind=inner (SecurityEvent | where EventID == 4625 | distinct Computer) on Computer - C.
Heartbeat | distinct Computer | join kind=rightanti (SecurityEvent | where EventID == 4625 | distinct Computer) on Computer - D.
Heartbeat | distinct Computer | join kind=leftanti (SecurityEvent | where EventID == 4625 | distinct Computer) on Computer
Show answer
Answer: D
A leftanti join returns the rows of the left side that have no match on the right, with the left columns only. Here the left side is the distinct heartbeat computers and the right side is the computers with event 4625. leftsemi returns the computers that do have such events. inner also returns only computers with a match. rightanti returns right-side rows without a match on the left, which is the opposite direction.
Question 14. The table AuthLog has the columns TimeGenerated and Message (string). One message reads: User bob logged in from 10.0.0.5. You need the columns User holding exactly bob and IP holding exactly 10.0.0.5. Which operator line works?
- A.
parse Message with "User " User " logged in from " IP - B.
parse Message with User " logged in from " IP - C.
parse Message with "User " User " from " IP - D.
extend User = split(Message, " ")[0], IP = split(Message, " ")[5]
Show answer
Answer: A
The pattern starts with the constant User followed by a space, so the column User receives the text up to the constant logged in from, which is bob, and IP receives the rest, 10.0.0.5. Option B has no leading constant, so User would hold User bob. Option C uses from as the delimiter, so User would hold bob logged in. Option D splits on spaces and takes the first word, which is User, not bob.
Question 15. The table Heartbeat has the columns TimeGenerated, Computer, Category and OSType. You need the most recent heartbeat record of every computer, with all columns of that record. Which query returns it?
- A.
Heartbeat | top 1 by TimeGenerated desc - B.
Heartbeat | summarize max(TimeGenerated) by Computer - C.
Heartbeat | order by TimeGenerated desc | distinct Computer - D.
Heartbeat | summarize arg_max(TimeGenerated, *) by Computer
Show answer
Answer: D
arg_max(TimeGenerated, *) returns, for each group, the row with the greatest TimeGenerated and all of its columns. top 1 returns a single row for the whole table. max per computer returns only the latest timestamp and drops Category and OSType. Sorting and then distinct Computer returns only the computer names.
Where to go next
Review the theory in KQL cheat sheet for certification exams, then continue with DAX practice questions and Delta Lake and Spark practice questions. The free 20-question SC-200 practice test mixes KQL with the rest of the exam, and multiple-choice exam tips cover how to handle the constraint words.