Azure Data Explorer and KQL Interview Questions and Answers Part 1 - Azure Data Explorer and KQL Fundamentals

Azure Data Explorer Interview Q&A · Part 1 of 10

In this post we are going to cover Azure Data Explorer and KQL fundamentals. These are 7 real Azure Data Explorer and KQL interview questions for data engineers, asked the way they come up on a real project, and the last one is a scenario: the team wants to move application logs from a SQL database to Azure Data Explorer.

Azure Data Explorer is Microsoft's managed analytics service for logs, telemetry and other event data, and KQL, the Kusto Query Language, is how we query it. Interviewers start here because everything later depends on it: how a query is built, how the data is stored, and when this service is the right tool and when it is not.

Part 1 of the Real Time Azure Data Explorer and KQL Interview Questions and Answers series on TechBrothersIT.

The short answer

Say this first

Azure Data Explorer is a fully managed, near real-time analytics platform for logs, telemetry, IoT and time series. A KQL query is a pipe of operators; management commands start with a dot. Data lives in clusters, databases, tables and immutable extents, and the same engine runs in a Fabric Eventhouse.

Let's create some sample data

Every query in this post runs on the Kusto emulator, Microsoft's Azure Data Explorer engine in a local container, in a database called shop. The sample data is a small invented request log: three CSV files of six rows each, and setup.kql creates the Requests table and loads them with .ingest into, which is for exploration and prototyping, not production. kql is a small helper that sends a query or a command to the emulator and prints the result as a text table. What only a cluster in Azure can show, such as retention and caching, is shown as code.

requests_part1.csv
2026-10-20T08:05:00Z,Info,checkout,C001,120,order placed
2026-10-20T08:12:00Z,Error,payments,C002,3050,db-timeout while charging card
2026-10-20T08:20:00Z,Info,catalog,C003,45,search ok
2026-10-20T08:41:00Z,Warning,checkout,C001,900,slow response from payments.api
2026-10-20T08:55:00Z,Error,checkout,C004,1500,Error: payment declined
2026-10-20T09:02:00Z,Info,catalog,C002,50,search ok
requests_part2.csv
2026-10-20T09:10:00Z,Error,payments,C002,3010,db-timeout while charging card
2026-10-20T09:15:00Z,Info,checkout,C005,130,order placed
2026-10-20T09:31:00Z,Error,payments,C001,2900,ERROR connecting to db.primary
2026-10-20T09:47:00Z,Info,catalog,C003,40,search ok
2026-10-20T11:03:00Z,Info,checkout,C003,110,order placed
2026-10-20T11:09:00Z,Error,checkout,C002,1700,user error: invalid coupon
requests_part3.csv
2026-10-20T11:20:00Z,Warning,catalog,C005,700,cache miss
2026-10-20T11:44:00Z,Error,payments,C004,3100,err code 42 from gateway
2026-10-20T11:58:00Z,Info,payments,C005,200,refund issued
2026-10-20T12:06:00Z,Error,payments,C002,2950,db-timeout while charging card
2026-10-20T12:30:00Z,Info,checkout,C001,125,order placed
2026-10-20T12:52:00Z,Info,catalog,C004,48,search ok
setup.kql
.create table Requests (Timestamp:datetime, Level:string, Service:string,
    CustomerId:string, DurationMs:long, Message:string)

.ingest into table Requests (@"/kustodata/requests_part1.csv") with (format="csv")

.ingest into table Requests (@"/kustodata/requests_part2.csv") with (format="csv")

.ingest into table Requests (@"/kustodata/requests_part3.csv") with (format="csv")

Question 1: Your manager asks what Azure Data Explorer is and where it fits in a data platform. What do you say?

Azure Data Explorer is a fully managed, high-performance big data analytics platform for high volumes of data in near real time: log analytics, time series, IoT and exploratory analytics.

It sits on the hot path. Here is the flow:

  • Apps and devices: logs, clicks, sensors
  • Event Hubs, Event Grid: streams and files
  • Azure Data Explorer: ingest, store, query
  • Power BI, Grafana: dashboards
  • Data lake: the long raw history

Let's see the kind of question it answers. We run q1.kql, requests and errors per hour on our sample log:

q1.kql
Requests
| where Timestamp >= datetime(2026-10-20 08:00)
| summarize Requests = count(),
    Errors = countif(Level == "Error")
    by bin(Timestamp, 1h)
| order by Timestamp asc
Terminal
$ kql -f q1.kql
> Requests
Timestamp             Requests  Errors
--------------------  --------  ------
2026-10-20T08:00:00Z  5         2
2026-10-20T09:00:00Z  5         2
2026-10-20T11:00:00Z  5         2
2026-10-20T12:00:00Z  3         1
Result table4 rows
TimestampRequestsErrors
2026-10-20T08:00:00Z52
2026-10-20T09:00:00Z52
2026-10-20T11:00:00Z52
2026-10-20T12:00:00Z31

Over the 18 sample requests we get 5 requests and 2 errors at 08:00, 09:00 and 11:00, and 3 requests with 1 error at 12:00.

Microsoft's checklist for when it fits:

  • Interactive analytics: aggregation, correlation, anomaly detection
  • Volume, velocity, variety: lots of append-only events
  • Raw data, not fully curated: query it as it lands
  • Many concurrent queries: dashboards and people at once
Real project

It doesn't replace the warehouse. The warehouse serves curated reports; Azure Data Explorer answers what is happening right now. It sits beside the warehouse, not instead of it.

Question 2: A new teammate asks how a KQL query is built. How do you explain the pipe model?

A KQL query is a data flow. It starts with a table, and rows flow through operators separated by the pipe character. Each operator takes a table from the one before and hands a new table to the next.

Let's build one question in three versions. First the table alone, then a where on the level, then a summarize by service with an order by. We run q2.kql:

q2.kql
Requests
| count

Requests
| where Level == "Error"
| count

Requests
| where Level == "Error"
| summarize Errors = count() by Service
| order by Errors desc
Terminal
$ kql -f q2.kql
> Requests
Count
-----
18

> Requests
Count
-----
7

> Requests
Service   Errors
--------  ------
payments  5
checkout  2

The Requests table alone has 18 rows. With the filter we get 7 errors. After the summarize, payments has 5 and checkout has 2.

Order matters. Filter first, so every later operator sees fewer rows. Microsoft's best practices put the where right after the table, with the time filter first, because the datetime index can skip whole shards.

let statements go in front of the query. Each one ends with a semicolon, and there are no blank lines between them and the query:

  • let errors = Requests | where Level == "Error";
  • then the query that uses errors
Tip

When a result looks wrong, comment out the lower lines and check each stage on its own.

Question 3: Someone asks how clusters, databases, tables and extents fit together. How do you explain them?

A cluster is the compute, a set of nodes running the engine. A database lives in a cluster, a table lives in a database, and a table's data lives in extents.

Let's go from the top:

  • Cluster: compute, the engine nodes
  • Databases: up to 10,000 per cluster
  • Tables: up to 10,000 per database, plus functions and policies
  • Extents: immutable and columnar, kept in Azure Storage and cached on the nodes' local SSDs

On the emulator, the cluster is one container. We run q3.kql with .show databases, .show tables and .show table Requests extents:

q3.kql
.show databases
| project DatabaseName, PersistentStorage
| order by DatabaseName asc

.show tables
| project TableName

.show table Requests extents
| project RowCount, OriginalSize
| order by OriginalSize asc
Terminal
$ kql -f q3.kql
> .show databases
DatabaseName  PersistentStorage
------------  --------------------------------------
NetDefaultDB  /kusto/tmp/Kusto.Personal/NetDefaultDB
shop          /kustodata/dbs/shop/md

> .show tables
TableName
---------
Requests

> .show table Requests extents
RowCount  OriginalSize
--------  ------------
6         461
6         477
6         481

.show databases lists NetDefaultDB, the emulator's built-in default, and shop, our database. .show tables lists Requests. We loaded Requests from three CSV files, so it has three extents of 6 rows each.

An extent is a horizontal slice of the table, stored by column and compressed, with indexes and metadata like its creation time. Extents are immutable. A change writes new extents and swaps them in, in one transaction. A cluster merges small extents in the background; the emulator never does, so here they stay three.

Why it matters

Retention and caching work on extents, by creation time. And many tiny loads make many tiny extents, so batch your loads.

Question 4: Leadership asks whether to use Azure Data Explorer or an Eventhouse in Microsoft Fabric. How do you decide?

It's the same engine and the same KQL, so the choice is about the platform around it, not the language.

An Eventhouse is Fabric's container for KQL databases. It's billed by Fabric capacity and suspends when idle, unless you set a minimum. Azure Data Explorer is a cluster you size and run, paying for its compute, storage and the service markup. Let's compare them:

Azure Data ExplorerEventhouse in Fabric
Engine, languagethe Kusto engine, KQLthe same engine and KQL
Containera cluster with databasesan Eventhouse with KQL databases
Billingthe cluster: compute (SKU, nodes), storage, markupFabric capacity; suspends when idle
You getcluster control: SKU, scale, networking, keysCopilot, Notebooks, Activator, Power BI
My pick whenyou need cluster-level controlthe company is Fabric-first

Microsoft's comparison says Azure gives pro developers maximum control for custom architectures, while Real-Time Intelligence gives an integrated workflow with fewer manually stitched services.

Then my own rule, and I say clearly in the interview that it is my rule, not Microsoft's. If the company is Fabric first, with Power BI and capacity billing, I pick an Eventhouse. If I need cluster-level control, like the SKU, private endpoints, customer-managed keys or follower databases, I pick Azure Data Explorer.

Not a one-way door

Microsoft documents a phased migration with no downtime, and a database shortcut shows an Azure Data Explorer database inside Fabric, read-only.

Question 5: A teammate asks the difference between queries and management commands. How do you explain it?

Management commands start with a dot, and no query may. Queries read data and change nothing. Commands read metadata or change the service: create tables, set policies, ingest data.

Here is the difference side by side:

QueriesManagement commands
Starts witha table name, let, printa dot: .show, .create, .alter, .ingest
Doesreads data, changes nothingreads metadata or changes the service
Can containno management commanda query, after <|
REST endpoint/v2/rest/query/v1/rest/mgmt
Who can run ita database Viewercreating a table: Database User

Let's try both. We run q5.kql: .show tables piped into project, then .create table, then .set-or-append, which runs a query and appends its result. After that we run a query that tries to pipe into .show tables:

q5.kql
.show tables
| project TableName

.create table ErrorCounts (Service:string, Errors:long)
| project TableName

.set-or-append ErrorCounts <|
    Requests
    | where Level == "Error"
    | summarize Errors = count() by Service

ErrorCounts
| order by Service asc
Terminal (expected error)
$ kql -f q5.kql
> .show tables
TableName
---------
Requests

> .create table ErrorCounts (Service:string, Errors:long)
TableName
-----------
ErrorCounts

> .set-or-append ErrorCounts <|
ExtentId                              OriginalSize  ExtentSize  CompressedSize  IndexSize  RowCount
------------------------------------  ------------  ----------  --------------  ---------  --------
<id>                                  48            1005        330             675        2

> ErrorCounts
Service   Errors
--------  ------
checkout  2
payments  5
$ kql 'Requests | where Level == "Error" | .show tables'
Error: Syntax error: SYN0002: Query operator expected. [line:position=1:36]

Yes, a .show result can flow into query operators. .create table returns ErrorCounts, and .set-or-append writes one extent of 2 rows: checkout 2 and payments 5. The other direction fails: the engine says Syntax error: SYN0002: Query operator expected. A command can contain a query, but a query can't contain a command.

Kusto keeps them apart by language, protocol and API, for security. They use different REST endpoints and permissions: a viewer can query, but creating a table takes a database user.

Real project

.set-or-append is direct ingestion, for exploration and prototyping, not production. Commands live in scripts, in source control, and run from a pipeline.

Question 6: Someone asks when you would NOT use Azure Data Explorer. What do you say?

When the workload is transactional. Extents are immutable, so there's no cheap way to update one row, and deleting records means a soft delete or a purge.

So I go through the common cases, and I name the better fit for each:

WorkloadBetter fitWhy
Order entry, frequent row updatesAzure SQL Databasetransactions, row-level updates
Small relational appAzure SQL Databasesimpler to build and run
Curated star schema for financea warehousemany dimension joins, curated model
ACID across several tablesa relational databasemulti-statement transactions
Logs, telemetry, IoT, eventsAzure Data Explorerappend-only, time-filtered, ad hoc

Order entry with frequent row updates belongs in Azure SQL Database. Small relational apps too: if the data fits in a SQL database and people edit rows all day, a SQL database is simpler to build and run.

A curated star schema for finance reporting, with many dimension joins, is a warehouse job. And if you need ACID transactions across several tables, use a relational database.

Where Azure Data Explorer shines instead: append-only logs, telemetry, IoT and events, filtered by time, with ad hoc questions over a lot of rows.

This is my view from real projects, not a Microsoft list, and I say it that way in the interview.

Interview tip

Name the alternative every time. A real platform often has all three: SQL for the app, a warehouse for curated reports, Azure Data Explorer for the event stream.

Scenario 7: The team wants to move application logs from a SQL database to Azure Data Explorer. How do you plan it?

In phases:

  • Model the table: typed columns, a datetime for the time
  • Test load and validate: row count and time range match the export
  • Rewrite the reports: SQL to KQL, compare the numbers
  • History once: LightIngest, creation time from the log date
  • New logs continuously: queued ingestion or Event Hubs
  • Run in parallel, then cut over: retention and caching set per table

The time column is a datetime, not text or a Unix number, because time filters depend on it. Let's load the SQL export with ignoreFirstRecord=true, so the header isn't a row, and validate. .ingest into here is for exploration and prototyping, not production. We count the export's data rows, run q7_load.kql, then q7_check.kql:

applogs_export.csv
LogTime,Severity,App,UserId,Message
2026-10-19T22:10:00Z,Info,web,U01,login ok
2026-10-19T22:14:00Z,Error,api,U02,timeout calling inventory
2026-10-19T23:05:00Z,Warning,jobs,,nightly export slow
2026-10-20T00:30:00Z,Error,jobs,,nightly export failed
2026-10-20T06:45:00Z,Info,web,U03,login ok
2026-10-20T07:02:00Z,Error,api,U03,timeout calling inventory
2026-10-20T07:15:00Z,Error,api,U01,invalid token
2026-10-20T07:40:00Z,Info,web,U02,checkout ok
2026-10-20T08:05:00Z,Error,web,U04,payment page 500
2026-10-20T08:20:00Z,Info,api,U04,order created
q7_load.kql
.create table AppLogs (Timestamp:datetime, Level:string,
    App:string, UserId:string, Message:string)

.ingest into table AppLogs (@"/kustodata/applogs_export.csv")
    with (format="csv", ignoreFirstRecord=true)
q7_check.kql
AppLogs
| summarize Rows = count(), First = min(Timestamp),
    Last = max(Timestamp)

AppLogs
| where isnull(Timestamp)
| count
Terminal
$ tail -n +2 applogs_export.csv | wc -l
10
$ kql -q -f q7_load.kql
> .create table AppLogs (Timestamp:datetime, Level:string,
ok: .create table AppLogs (Timestamp:datetime, Level:string,

> .ingest into table AppLogs (@"/kustodata/applogs_export.csv")
ok: .ingest into table AppLogs (@"/kustodata/applogs_export.csv")
$ kql -f q7_check.kql
> AppLogs
Rows  First                 Last
----  --------------------  --------------------
10    2026-10-19T22:10:00Z  2026-10-20T08:20:00Z

> AppLogs
Count
-----
0

The export has 10 data rows, the table has 10, the time range is 2026-10-19T22:10:00Z to 2026-10-20T08:20:00Z, and 0 rows lost their timestamp.

Then we rewrite each report. This is the SQL report today, errors per app since midnight:

report.sql (SQL Server, not run)
-- the report today, on SQL Server
SELECT App, COUNT(*) AS Errors
FROM dbo.AppLogs
WHERE Severity = 'Error'
  AND LogTime >= '2026-10-20'
GROUP BY App
ORDER BY Errors DESC, App;

In KQL it's a time filter, a level filter, summarize and order by. We run q7_report.kql:

q7_report.kql
AppLogs
| where Timestamp >= datetime(2026-10-20)
| where Level == "Error"
| summarize Errors = count() by App
| order by Errors desc, App asc
Terminal
$ kql -f q7_report.kql
> AppLogs
App   Errors
----  ------
api   2
jobs  1
web   1
Result table3 rows
AppErrors
api2
jobs1
web1

api 2, jobs 1, web 1, to compare with the SQL report. On the Azure cluster we set retention and caching per table:

policies.kql (on the cluster; the emulator does not honor them)
// on the Azure cluster: keep a year, the last 30 days hot
.alter-merge table AppLogs policy retention softdelete = 365d
.alter table AppLogs policy caching hot = 30d
Pitfall

Copying the SQL schema one to one, with text timestamps and identity keys. Model the table for the questions you will ask: time first.

Key points to remember

  • Azure Data Explorer: managed, near real-time analytics for logs, telemetry, IoT and time series
  • A KQL query is a pipe: table, then operators; filter first, time filter first
  • Cluster, databases, tables, and immutable, columnar extents
  • Eventhouse: same engine in Fabric; the choice is the platform, not the language
  • Management commands start with a dot and can contain queries; not for OLTP workloads

The full script

Here is a Python script that writes every file and runs every query in order on the Kusto emulator; start the emulator container first, as the comments at the top of the script show.

Show the full script that writes the files and runs every step
Full script - writes the files and runs every step on the Kusto emulator (start the emulator container first, see the comments at the top), run it with Python
# ========================================================================
# Azure Data Explorer Interview Q&A #1 - Azure Data Explorer and KQL fundamentals
# TechBrothersIT. Names and sample data are invented for teaching.
# Every step below was run on the Kusto emulator (Kustainer, the Azure Data Explorer engine in a Linux container,
# build 2026.11.02); the outputs in the video and the blog post are the real outputs.
# To run it yourself (Docker on Linux, macOS or Windows with WSL), in an EMPTY folder:
#   1. Start the emulator with this folder mounted at /kustodata (the steps ingest files from there):
#      docker run -d --name kustainer -e ACCEPT_EULA=Y -m 4G -p 8080:8080 -v "$PWD":/kustodata \
#          mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest
#      Wait until `docker logs kustainer` says the database "is answering queries".
#   2. python KQL_Interview_QA_01_Code.py   (Python 3.10 or later; kql.py itself needs no packages: it uses the REST API)
# "kql" in the commands is a small helper written by Step 0 (kql.py): it sends a query or a management command
# to http://localhost:8080 and prints the result as a text table (no sign-in: the emulator has no authentication).
# The emulator is for development and testing only; an Azure Data Explorer cluster needs Microsoft Entra ID sign-in
# (use the Kusto SDK or the web UI there).
# Steps that restart the emulator say so in a comment: run "docker restart kustainer" at that point.
# ========================================================================

import os, subprocess, sys, time

def write(path, text):
    if text is None:
        if os.path.exists(path): os.remove(path)
        return
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(text.strip("\n") + "\n")

def run(cmd, expect_error=False, allow_error=False, env=None):
    print("\n$ " + cmd, flush=True)
    rc = subprocess.run(cmd, shell=True, env=dict(os.environ, **(env or {}))).returncode
    if rc != 0 and not (expect_error or allow_error):
        raise SystemExit("stopped: this command was expected to succeed")

# Environment used by every step (the database the steps use)
os.environ.update({'KQL_DB': 'shop'})

# Step 0: the kql helper, the database and the starting files
write("kql.py", '"""kql: run KQL queries and management commands on the local Kusto emulator (Kustainer), print the results as text tables.\n  kql \'StormEvents | take 3\'              run one query or one management command (starts with \'.\')\n  kql -f script.kql                      run every statement in the file (statements are separated by blank lines;\n                                         // comment lines are skipped); each result is printed under its first line\n  options: -d DB (default $KQL_DB or NetDefaultDB)  --max N rows to print (default 30)  --cell N max chars per cell (default 60)\n           -q  for management commands: print one line ("ok: <command>") instead of the result table\n           --stats  after a query, print the rows / extents the query scanned (from the engine\'s own query statistics)\n           --props \'{"Options": {...}}\'  client request properties (e.g. truncationmaxrecords)\n           -e  keep going after an error (the error is printed and the exit code is 1 at the end)\n           --json  print the last statement\'s first result as JSON {"cols": [...], "rows": [...]} (used by the video generator)\nEmpty cells: a null value in a non-string column prints as "null"; an empty or null string prints as nothing.\nA query that fails after returning some rows (for example a result over the truncation limit) prints the rows it got, then\n"Error (partial result): <engine message>", and exits 1.\nThe engine answers at http://localhost:8080 (no authentication: the emulator has none)."""\nimport json, os, re, sys, urllib.request, urllib.error\n\nURL = os.environ.get(\'KQL_URL\', \'http://localhost:8080\')\n\n\ndef post(path, db, csl, props=None):\n    body = {\'db\': db, \'csl\': csl, \'properties\': json.dumps(props or {\'Options\': {}})}\n    req = urllib.request.Request(URL + path, data=json.dumps(body).encode(), method=\'POST\',\n                                 headers={\'Content-Type\': \'application/json; charset=utf-8\', \'Accept\': \'application/json\',\n                                          \'x-ms-app\': \'kql-cli\', \'x-ms-client-request-id\': \'kql-cli\'})\n    opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))\n    try:\n        with opener.open(req, timeout=600) as r: return json.loads(r.read().decode(\'utf-8\'))\n    except urllib.error.HTTPError as e:\n        txt = e.read().decode(\'utf-8\', \'replace\')\n        try:\n            er = json.loads(txt)[\'error\']\n            msg = er.get(\'@message\') or er.get(\'message\')\n        except Exception:\n            msg = txt.strip().split(\'\\n\')[0]\n        msg = re.sub(r\'^Request is invalid and cannot be processed: \', \'\', msg)\n        msg = re.sub(r\'\\s*\\(Activity stack:.*\', \'\', msg, flags=re.S)\n        raise KqlError(msg.strip())\n\n\nclass KqlError(Exception): pass\n\n\ndef cell(v, width, typ=\'\'):\n    if v is None: s = \'\' if typ in (\'string\', \'\') else \'null\'\n    elif isinstance(v, bool): s = \'true\' if v else \'false\'\n    elif isinstance(v, (dict, list)): s = json.dumps(v, separators=(\',\', \':\'), ensure_ascii=False)\n    elif isinstance(v, float):\n        s = repr(v)\n        if s.endswith(\'.0\'): s = s[:-2]\n    else: s = str(v)\n    s = s.replace(\'\\r\', \' \').replace(\'\\n\', \' \')\n    if len(s) > width: s = s[:width - 3] + \'...\'\n    return s\n\n\ndef table(cols, rows, mx, width, types=None):\n    rows = rows if isinstance(rows, list) else []\n    types = types or [\'\'] * len(cols)\n    shown = [[cell(v, width, types[i]) for i, v in enumerate(r)] for r in rows[:mx]]\n    w = [max([len(c)] + [len(r[i]) for r in shown]) for i, c in enumerate(cols)]\n    out = [\'  \'.join(c.ljust(w[i]) for i, c in enumerate(cols)).rstrip(),\n           \'  \'.join(\'-\' * w[i] for i in range(len(cols)))]\n    for r in shown:\n        out.append(\'  \'.join(v.ljust(w[i]) for i, v in enumerate(r)).rstrip())\n    n = len(rows)\n    if n > mx: out.append(\'... %d more rows (%d rows in total)\' % (n - mx, n))\n    elif n == 0: out.append(\'(no rows)\')\n    return \'\\n\'.join(out)\n\n\nLAST = {}\n\n\ndef one_api_msg(errs):\n    msgs = []\n    for e in errs or []:\n        er = e.get(\'error\', e) if isinstance(e, dict) else {}\n        m = er.get(\'@message\') or er.get(\'message\') or str(e)\n        m = re.sub(r\'\\s*\\(Activity stack:.*\', \'\', m, flags=re.S).strip()\n        if m not in msgs: msgs.append(m)\n    return \' | \'.join(msgs) or \'unknown error\'\n\n\ndef run_query(db, csl, a):\n    frames = post(\'/v2/rest/query\', db, csl, a.props)\n    outs, stats, partial = [], None, None\n    for f in frames:\n        if f.get(\'FrameType\') == \'DataSetCompletion\' and f.get(\'HasErrors\') and partial is None:\n            partial = one_api_msg(f.get(\'OneApiErrors\'))\n        if f.get(\'FrameType\') != \'DataTable\': continue\n        if f.get(\'TableKind\') == \'PrimaryResult\':\n            rows = [r for r in f[\'Rows\'] if isinstance(r, list)]\n            for r in f[\'Rows\']:\n                if isinstance(r, dict) and r.get(\'OneApiErrors\'): partial = one_api_msg(r[\'OneApiErrors\'])\n            cols = [c[\'ColumnName\'] for c in f[\'Columns\']]\n            if \'cols\' not in LAST: LAST.update(cols=cols, rows=rows)\n            outs.append(table(cols, rows, a.max, a.cell, [str(c.get(\'ColumnType\', \'\')).lower() for c in f[\'Columns\']]))\n        if f.get(\'TableKind\') == \'QueryCompletionInformation\':\n            cols = [c[\'ColumnName\'] for c in f[\'Columns\']]\n            for r in f[\'Rows\']:\n                d = dict(zip(cols, r))\n                if d.get(\'EventTypeName\') == \'QueryResourceConsumption\':\n                    try: stats = json.loads(d[\'Payload\'])\n                    except Exception: pass\n                if d.get(\'Level\') in (2, 3) and d.get(\'EventTypeName\') not in (\'QueryResourceConsumption\',):\n                    txt = d.get(\'Payload\') or \'\'\n                    try: txt = json.loads(txt).get(\'Text\', txt)\n                    except Exception: pass\n                    outs.append(\'%s: %s\' % (\'Warning\' if d.get(\'Level\') == 3 else \'Error\', txt))\n    if a.stats and stats:\n        sh = (stats.get(\'resource_usage\') or {}).get(\'cache\', {}).get(\'shards\', {})\n        ix = stats.get(\'input_dataset_statistics\') or {}\n        ex, rw = ix.get(\'extents\') or {}, ix.get(\'rows\') or {}\n        outs.append(\'scanned: %s of %s extents, %s of %s rows\' % (ex.get(\'scanned\'), ex.get(\'total\'), rw.get(\'scanned\'), rw.get(\'total\')))\n    if partial:\n        outs.append(\'Error (partial result): \' + partial)\n        raise PartialError(\'\\n\\n\'.join(outs))\n    return \'\\n\\n\'.join(outs)\n\n\nclass PartialError(Exception): pass\n\n\ndef run_mgmt(db, csl, a):\n    d = post(\'/v1/rest/mgmt\', db, csl, a.props)\n    if a.q and not a.json: return \'ok: \' + first_line(csl)\n    t = (d.get(\'Tables\') or [{}])[0]\n    LAST.update(cols=[c[\'ColumnName\'] for c in t.get(\'Columns\', [])], rows=t.get(\'Rows\', []))\n    return table([c[\'ColumnName\'] for c in t.get(\'Columns\', [])], t.get(\'Rows\', []), a.max, a.cell,\n                 [str(c.get(\'DataType\', c.get(\'ColumnType\', \'\'))).lower().replace(\'system.\', \'\') for c in t.get(\'Columns\', [])])\n\n\ndef first_line(s):\n    l = s.strip().split(\'\\n\')[0].strip()\n    return l if len(l) <= 90 else l[:87] + \'...\'\n\n\ndef statements(text):\n    blocks, cur = [], []\n    for l in text.split(\'\\n\'):\n        if not l.strip():\n            if cur: blocks.append(\'\\n\'.join(cur)); cur = []\n            continue\n        if l.strip().startswith(\'//\') and not cur: continue\n        cur.append(l)\n    if cur: blocks.append(\'\\n\'.join(cur))\n    return blocks\n\n\ndef main():\n    import argparse\n    p = argparse.ArgumentParser(add_help=False)\n    p.add_argument(\'-d\', default=os.environ.get(\'KQL_DB\', \'NetDefaultDB\'))\n    p.add_argument(\'-f\'); p.add_argument(\'-q\', action=\'store_true\'); p.add_argument(\'-e\', action=\'store_true\')\n    p.add_argument(\'--stats\', action=\'store_true\'); p.add_argument(\'--max\', type=int, default=30)\n    p.add_argument(\'--cell\', type=int, default=60); p.add_argument(\'--props\'); p.add_argument(\'-h\', \'--help\', action=\'store_true\')\n    p.add_argument(\'--json\', action=\'store_true\')\n    p.add_argument(\'text\', nargs=\'?\')\n    a = p.parse_args()\n    if a.help or (not a.f and not a.text): print(__doc__); return 0\n    a.props = json.loads(a.props) if a.props else None\n    stmts = statements(open(a.f).read()) if a.f else [a.text]\n    rc, first = 0, True\n    if a.json:\n        for s in stmts:\n            LAST.clear()\n            try:\n                run_mgmt(a.d, s, a) if s.lstrip().startswith(\'.\') else run_query(a.d, s, a)\n            except (KqlError, PartialError) as e:\n                print(\'Error: \' + str(e)); return 1\n        print(json.dumps(dict(cols=LAST.get(\'cols\', []), rows=LAST.get(\'rows\', [])), ensure_ascii=False))\n        return 0\n    for s in stmts:\n        if a.f:\n            if not first: print()\n            print(\'> \' + first_line(s))\n        first = False\n        try:\n            out = run_mgmt(a.d, s, a) if s.lstrip().startswith(\'.\') else run_query(a.d, s, a)\n            if out: print(out)\n        except PartialError as e:\n            print(str(e)); rc = 1\n            if not a.e: return 1\n        except KqlError as e:\n            print(\'Error: \' + str(e)); rc = 1\n            if not a.e: return 1\n    return rc\n\n\nif __name__ == \'__main__\':\n    sys.exit(main())\n')
KQL = '"%s" "%s"' % (sys.executable, os.path.abspath("kql.py"))   # commands below run it as: python kql.py ...
run(KQL + ' -d NetDefaultDB -q \'.create database shop persist (@"/kustodata/dbs/shop/md", @"/kustodata/dbs/shop/data")\'')
write('requests_part1.csv', """\
2026-10-20T08:05:00Z,Info,checkout,C001,120,order placed
2026-10-20T08:12:00Z,Error,payments,C002,3050,db-timeout while charging card
2026-10-20T08:20:00Z,Info,catalog,C003,45,search ok
2026-10-20T08:41:00Z,Warning,checkout,C001,900,slow response from payments.api
2026-10-20T08:55:00Z,Error,checkout,C004,1500,Error: payment declined
2026-10-20T09:02:00Z,Info,catalog,C002,50,search ok
""")
write('requests_part2.csv', """\
2026-10-20T09:10:00Z,Error,payments,C002,3010,db-timeout while charging card
2026-10-20T09:15:00Z,Info,checkout,C005,130,order placed
2026-10-20T09:31:00Z,Error,payments,C001,2900,ERROR connecting to db.primary
2026-10-20T09:47:00Z,Info,catalog,C003,40,search ok
2026-10-20T11:03:00Z,Info,checkout,C003,110,order placed
2026-10-20T11:09:00Z,Error,checkout,C002,1700,user error: invalid coupon
""")
write('requests_part3.csv', """\
2026-10-20T11:20:00Z,Warning,catalog,C005,700,cache miss
2026-10-20T11:44:00Z,Error,payments,C004,3100,err code 42 from gateway
2026-10-20T11:58:00Z,Info,payments,C005,200,refund issued
2026-10-20T12:06:00Z,Error,payments,C002,2950,db-timeout while charging card
2026-10-20T12:30:00Z,Info,checkout,C001,125,order placed
2026-10-20T12:52:00Z,Info,catalog,C004,48,search ok
""")
write('setup.kql', """\
.create table Requests (Timestamp:datetime, Level:string, Service:string,
    CustomerId:string, DurationMs:long, Message:string)

.ingest into table Requests (@"/kustodata/requests_part1.csv") with (format="csv")

.ingest into table Requests (@"/kustodata/requests_part2.csv") with (format="csv")

.ingest into table Requests (@"/kustodata/requests_part3.csv") with (format="csv")
""")
run(KQL + ' -q -f setup.kql')

# Step 1: Requests and errors per hour on the sample request log
write('q1.kql', """\
Requests
| where Timestamp >= datetime(2026-10-20 08:00)
| summarize Requests = count(),
    Errors = countif(Level == "Error")
    by bin(Timestamp, 1h)
| order by Timestamp asc
""")
run(KQL + ' -f q1.kql')

# Step 2: The pipe model: the table, then a filter, then summarize and order by
write('q2.kql', """\
Requests
| count

Requests
| where Level == "Error"
| count

Requests
| where Level == "Error"
| summarize Errors = count() by Service
| order by Errors desc
""")
run(KQL + ' -f q2.kql')

# Step 3: Databases, tables and the extents of the Requests table
write('q3.kql', """\
.show databases
| project DatabaseName, PersistentStorage
| order by DatabaseName asc

.show tables
| project TableName

.show table Requests extents
| project RowCount, OriginalSize
| order by OriginalSize asc
""")
run(KQL + ' -f q3.kql')

# Step 4: Management commands (.show, .create, .set-or-append) and a query; a query cannot contain a command
write('q5.kql', """\
.show tables
| project TableName

.create table ErrorCounts (Service:string, Errors:long)
| project TableName

.set-or-append ErrorCounts <|
    Requests
    | where Level == "Error"
    | summarize Errors = count() by Service

ErrorCounts
| order by Service asc
""")
run(KQL + ' -f q5.kql')
run(KQL + ' \'Requests | where Level == "Error" | .show tables\'', expect_error=True)   # EXPECTED TO FAIL: the video shows this error

# Step 5: SQL logs exported to CSV: create a typed table, load the file, validate the counts
write('applogs_export.csv', """\
LogTime,Severity,App,UserId,Message
2026-10-19T22:10:00Z,Info,web,U01,login ok
2026-10-19T22:14:00Z,Error,api,U02,timeout calling inventory
2026-10-19T23:05:00Z,Warning,jobs,,nightly export slow
2026-10-20T00:30:00Z,Error,jobs,,nightly export failed
2026-10-20T06:45:00Z,Info,web,U03,login ok
2026-10-20T07:02:00Z,Error,api,U03,timeout calling inventory
2026-10-20T07:15:00Z,Error,api,U01,invalid token
2026-10-20T07:40:00Z,Info,web,U02,checkout ok
2026-10-20T08:05:00Z,Error,web,U04,payment page 500
2026-10-20T08:20:00Z,Info,api,U04,order created
""")
write('q7_load.kql', """\
.create table AppLogs (Timestamp:datetime, Level:string,
    App:string, UserId:string, Message:string)

.ingest into table AppLogs (@"/kustodata/applogs_export.csv")
    with (format="csv", ignoreFirstRecord=true)
""")
write('q7_check.kql', """\
AppLogs
| summarize Rows = count(), First = min(Timestamp),
    Last = max(Timestamp)

AppLogs
| where isnull(Timestamp)
| count
""")
run('tail -n +2 applogs_export.csv | wc -l')
run(KQL + ' -q -f q7_load.kql')
run(KQL + ' -f q7_check.kql')

# Step 6: The error report rewritten from SQL to KQL
write('q7_report.kql', """\
AppLogs
| where Timestamp >= datetime(2026-10-20)
| where Level == "Error"
| summarize Errors = count() by App
| order by Errors desc, App asc
""")
run(KQL + ' -f q7_report.kql')
Next in this series

Part 2: KQL Query Essentials

The sample data on this page is invented for teaching. Every terminal output is the real output of the command above it, run on the Kusto emulator (Kustainer, the Azure Data Explorer engine in a Linux container, build 2026.11.02). Features the emulator cannot show truthfully are shown as code. Part of the Real Time Azure Data Explorer and KQL Interview Questions and Answers series on TechBrothersIT. Features, limits and their status (GA or preview) are current as of November 2026; check the official Azure Data Explorer and Kusto documentation (learn.microsoft.com/azure/data-explorer and learn.microsoft.com/kusto) for anything you quote in an interview.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.