Azure Data Explorer and KQL Interview Questions and Answers Part 10 - Real-World Manager Scenarios

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

In this post we are going to cover real-world manager scenarios in Azure Data Explorer. 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: in the final round, the hiring manager asks you to design a real-time analytics platform with Azure Data Explorer.

These are the questions a manager asks when something has gone wrong or a decision is due: double counts on a dashboard, a cost spike, which platform to pick, an app that times out, a move to Fabric, slow old data. Interviewers ask them because the answer shows how you investigate, not just what you know.

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

The short answer

Say this first

Double counts: prove them, find the extents, drop by drop-by tag and reload with ingestIfNotExists. Cost: compute, storage, networking and markup, and .show journal for what changed. Slow old data is the caching policy. A move to Eventhouse is phased with union views, and a platform design runs from data connections to update policies, materialized views, security and monitoring.

Let's create some sample data

Every query below runs on the Kusto emulator, Microsoft's Azure Data Explorer engine in a local container, in a database called shop. kql is a small helper that sends a query or a .kql file to the emulator and prints the result as a table. setup.kql creates small invented tables: an empty Orders, three ClickEvents, an empty Telemetry, three October Invoices, eight Logs rows from September and October, and four raw AppLogs lines. The loads use .ingest inline, which is for exploration and prototyping, not production. legacy_db.kql and legacy.kql create a second database, legacy, with two September invoices; it plays the old cluster in question 5. What only a cluster in Azure can show, such as billing, autoscale, cache tiers and data connections, is shown as code.

setup.kql
.create table Orders (OrderId:string, Customer:string, Amount:real)

.create table ClickEvents (Ts:datetime, Page:string)

.ingest inline into table ClickEvents <|
2026-10-20T08:00:00Z,home
2026-10-20T08:01:00Z,cart
2026-10-20T08:02:00Z,home

.create table Telemetry (DeviceId:string, Temp:real, Ts:datetime)

.create table Invoices (InvoiceId:long, Amount:real, InvoiceDate:datetime)

.ingest inline into table Invoices <|
3001,120,2026-10-05T09:00:00Z
3002,80,2026-10-12T09:00:00Z
3003,200,2026-10-19T09:00:00Z

.create table Logs (Timestamp:datetime, Service:string, Level:string)

.ingest inline into table Logs <|
2026-09-10T08:00:00Z,api,Info
2026-09-10T09:00:00Z,api,Error
2026-09-10T10:00:00Z,api,Info
2026-09-10T11:00:00Z,web,Info
2026-09-11T08:00:00Z,api,Info
2026-09-11T09:00:00Z,web,Error
2026-10-20T08:00:00Z,api,Info
2026-10-20T09:00:00Z,web,Info

.create table AppLogs (Line:string)

.ingest inline into table AppLogs <|
2026-10-20T08:01:00Z ERROR payments timeout calling gateway
2026-10-20T08:02:00Z INFO checkout order placed
2026-10-20T08:03:00Z ERROR payments card declined
2026-10-20T08:04:00Z ERROR checkout coupon invalid
legacy_db.kql
.create database legacy persist (@"/kustodata/dbs/legacy/md", @"/kustodata/dbs/legacy/data")
legacy.kql
.create table Invoices (InvoiceId:long, Amount:real, InvoiceDate:datetime)

.ingest inline into table Invoices <|
2001,150,2026-09-08T09:00:00Z
2002,90,2026-09-22T09:00:00Z

Question 1: A dashboard shows double counts after a pipeline retry. How do you investigate and fix it?

Prove it's duplicates, find the extents they came from, drop them by tag, and reload so a retry can't load the same batch twice.

Let's reproduce it. The pipeline retried, so load.kql loads the same batch file twice with .ingest into. That command stands in for the pipeline; it's for prototyping, not production, but the retry is the same. Then check.kql looks at revenue, copies per order and extents.

batch_0101.csv
o1,ann,10
o2,bob,20
o3,cat,30
load.kql
// the pipeline retried: the same batch file, loaded twice
.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]')

.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]')
check.kql
Orders | summarize Revenue = sum(Amount), Rows = count()

Orders
| summarize Copies = count() by OrderId
| where Copies > 1
| order by OrderId asc

.show table Orders extents
| summarize Extents = count(), Rows = sum(RowCount)
Terminal
$ kql -q -f load.kql
> .ingest into table Orders (@"/kustodata/batch_0101.csv")
ok: .ingest into table Orders (@"/kustodata/batch_0101.csv")

> .ingest into table Orders (@"/kustodata/batch_0101.csv")
ok: .ingest into table Orders (@"/kustodata/batch_0101.csv")
$ kql -f check.kql
> Orders | summarize Revenue = sum(Amount), Rows = count()
Revenue  Rows
-------  ----
120      6

> Orders
OrderId  Copies
-------  ------
o1       2
o2       2
o3       2

> .show table Orders extents
Extents  Rows
-------  ----
2        6

Revenue is 120 from 6 rows, but the batch file has three orders worth 60. Counting rows per OrderId shows o1, o2 and o3 each 2 times. And there are 2 extents with 6 rows: the same batch, ingested twice.

The fix: each batch carries a drop-by and an ingest-by tag. We drop the extents with the batch's drop-by tag, then reload twice with ingestIfNotExists.

fix.kql
.drop extents <| .show table Orders extents
    where tags has 'drop-by:batch_0101'

.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]',
      ingestIfNotExists='["batch_0101"]')

.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]',
      ingestIfNotExists='["batch_0101"]')
Terminal
$ kql -q -f fix.kql
> .drop extents <| .show table Orders extents
ok: .drop extents <| .show table Orders extents

> .ingest into table Orders (@"/kustodata/batch_0101.csv")
ok: .ingest into table Orders (@"/kustodata/batch_0101.csv")

> .ingest into table Orders (@"/kustodata/batch_0101.csv")
ok: .ingest into table Orders (@"/kustodata/batch_0101.csv")
KQL - the query behind the result table
Orders | summarize Revenue = sum(Amount), Rows = count()
Result table1 row
RevenueRows
603

The second load is skipped, and revenue is back to 60 from 3 rows.

Prevention depends on the source:

Prevent or clean upHow
ingest-by tag + ingestIfNotExistsone tag per batch, not a unique tag per small call
Dedup materialized viewtake_any(*) by the business key
Soft delete.delete table T records <| ... for a one-off cleanup

One ingest-by and drop-by tag per batch, never a unique tag on every small call or file, because too many tags hurt performance.

Incident note

Say which dashboards showed wrong numbers, and for how long.

Question 2: The cluster cost doubled this month. Where do you look?

Start in Azure Cost Management, by meter. Cluster cost is compute, storage, networking and the Azure Data Explorer markup.

Cost partDriven byCheck
ComputeSKU x node countautoscale history, SKU changes
Storageretention + a recoverability bufferretention changes
Networking, featuresfollowers, private endpoints, Python sandboxeswhat was added this month
Markupper production engine coreDev/Test clusters: no markup

Compute is the SKU times the node count, so check the autoscale history: did it scale out and stay out? Then look at what makes it scale:

  • Bigger hot cache: more nodes
  • Materialized views, update policies, partitioning: more CPU
  • Streaming ingestion: uses local SSD, shrinks the cache
  • Queries on cold data: storage read transactions

The engine tells you what changed. Let's make a month of changes in changes.kql: a 365-day hot cache and five years of retention on ClickEvents, streaming on Telemetry, and a new materialized view.

changes.kql
// what changed on the cluster this month
.alter table ClickEvents policy caching hot = 365d

.alter-merge table ClickEvents policy retention softdelete = 1825d

.alter table Telemetry policy streamingingestion enable

.create materialized-view ClicksPerPage on table ClickEvents {
    ClickEvents | summarize Clicks = count() by Page, bin(Ts, 1h)
}
Terminal
$ kql -q -f changes.kql
> .alter table ClickEvents policy caching hot = 365d
ok: .alter table ClickEvents policy caching hot = 365d

> .alter-merge table ClickEvents policy retention softdelete = 1825d
ok: .alter-merge table ClickEvents policy retention softdelete = 1825d

> .alter table Telemetry policy streamingingestion enable
ok: .alter table Telemetry policy streamingingestion enable

> .create materialized-view ClicksPerPage on table ClickEvents {
ok: .create materialized-view ClicksPerPage on table ClickEvents {

The journal fills in the background on the emulator, so the full script waits at this point until the view is listed. Then we read .show journal, filtered to policies and views.

Terminal
$ kql '.show journal | where Event in ("ALTER-TABLE-CACHING-POLICY", "ALTER-TABLE-RETENTION-POLICY", "ALTER-TABLE-STREAMING-INGESTION-POLICY", "CREATE-MATERIALIZED-VIEW") | project Event, EntityName, Command = substring(ChangeCommand, 0, 66) | order by Event asc, EntityName asc'
Event                                   EntityName     Command
--------------------------------------  ------------- 
  ------------------------------------------------------------
ALTER-TABLE-CACHING-POLICY              ClickEvents    .alter table ClickEvents policy caching hot =
  365d
ALTER-TABLE-RETENTION-POLICY            ClickEvents    .alter-merge table ClickEvents policy retention
  softdelet...
ALTER-TABLE-STREAMING-INGESTION-POLICY  Telemetry      .alter table Telemetry policy streamingingestion
  enable
CREATE-MATERIALIZED-VIEW                ClicksPerPage  .create materialized-view ClicksPerPage on table
  ClickEve...
Result table4 rows
EventEntityNameCommand
ALTER-TABLE-CACHING-POLICYClickEvents.alter table ClickEvents policy caching hot = 365d
ALTER-TABLE-RETENTION-POLICYClickEvents.alter-merge table ClickEvents policy retention softdelete = 1825d
ALTER-TABLE-STREAMING-INGESTION-POLICYTelemetry.alter table Telemetry policy streamingingestion enable
CREATE-MATERIALIZED-VIEWClicksPerPage.create materialized-view ClicksPerPage on table ClickEvents {

Four entries: ALTER-TABLE-CACHING-POLICY and ALTER-TABLE-RETENTION-POLICY on ClickEvents, ALTER-TABLE-STREAMING-INGESTION-POLICY on Telemetry, and CREATE-MATERIALIZED-VIEW for ClicksPerPage, each with its command.

Two more things to check. Dev/Test clusters have no markup and no SLA, so production SKUs in dev are a common leak. And reserved capacity covers only the markup.

Real project

The fix is usually policy, not hardware: hot cache for the days people query, retention per table, and autoscale limits that match the budget.

Question 3: Someone asks whether to use Azure Data Explorer, Synapse or Databricks for a new log analytics workload. How do you decide?

By the main job. For interactive queries over logs, telemetry and time series, Azure Data Explorer, or an Eventhouse in Fabric, is the natural fit.

Azure Data Explorer / EventhouseDatabricksSQL warehouse
Built forinteractive logs, telemetry, time seriestransformation, ML, the lakehousecurated relational BI
Dataraw and semi-structured, as it arrivesfiles in the lake (Delta)modeled tables
LanguageKQLSpark SQL, PythonT-SQL
Log analyticsthe natural fitwhen logs feed ML or big ETLcurated summaries only

Why logs fit Azure Data Explorer: raw lines go in as they are, and one query filters, parses and counts them. Let's count errors per service straight from the unparsed lines in AppLogs.

logs.kql
AppLogs
| where Line has "ERROR"
| parse Line with Ts:datetime " " Level:string " " Service:string
    " " Message:string
| where Level == "ERROR"
| summarize Errors = count(), Last = max(Ts) by Service
| order by Service asc
Terminal
$ kql -f logs.kql
> AppLogs
Service   Errors  Last
--------  ------  --------------------
checkout  1       2026-10-20T08:04:00Z
payments  2       2026-10-20T08:03:00Z
Result table2 rows
ServiceErrorsLast
checkout12026-10-20T08:04:00Z
payments22026-10-20T08:03:00Z

payments has 2 errors, the last at 2026-10-20T08:03:00Z, and checkout has 1, at 08:04.

My rules for the decision:

  • Who queries it: and how fast it must answer
  • How fresh: seconds, minutes, or tomorrow morning
  • What the team runs: skills and platforms already in place
  • Often both: ADX for hot and interactive, the lake and Databricks for history and ML
Trap

If Synapse means Synapse Data Explorer, it was retired on 7 October 2025, and its successor is Eventhouse in Microsoft Fabric.

Question 4: A query works in the web UI but times out from an application. How do you troubleshoot it?

Compare the two requests, not just the query text. The app often runs as another identity, in a workload group with tighter limits.

Here is what can differ, and where to check it:

What can differWhere to check
Identity, workload group.show queries: Application, Principal, WorkloadGroup, Duration, State
Timeoutsserver: 4 minutes default, servertimeout up to 1 hour; the SDK; the HTTP client
Result size500,000 records or 64 MB: partial query failure
Throttling, consistencyHTTP 429 (request rate limits); strong vs weak consistency

First, the timeouts. The server default is four minutes for queries, and the servertimeout request property raises it up to one hour. The SDK and the app's own HTTP client have timeouts too, and the shortest one wins.

The other classic is result size: 500,000 records or 64 MB. Let's ask for 600,000 rows with our command line client.

Terminal (expected error)
$ kql --max 3 'range x from 1 to 600000 step 1'
x
-
1
2
3
... 499997 more rows (500000 rows in total)

Error (partial result): Query execution has exceeded the allowed limits (80DA0003): The results of this
  query exceed the set limit of 500000 records, so not all records were returned
    (E_QUERY_RESULT_SET_TOO_LARGE, 0x80DA0003). See https://aka.ms/kustoquerylimits for more
      information and possible solutions..

kql prints the rows it got, 500000 rows in total, and then the partial result error, E_QUERY_RESULT_SET_TOO_LARGE. Now the same query from a Python app with a 10-minute server timeout:

app.py
from datetime import timedelta
from azure.kusto.data import (ClientRequestProperties, KustoClient,
                              KustoConnectionStringBuilder)
from azure.kusto.data.exceptions import KustoServiceError

kcsb = KustoConnectionStringBuilder.with_no_authentication(
    "http://localhost:8080")                       # emulator only
client = KustoClient(kcsb)
props = ClientRequestProperties()
props.set_option("servertimeout", timedelta(minutes=10))  # max 1 hour
try:
    rows = client.execute("shop", "range x from 1 to 600000 step 1",
                          props)
    print("rows:", len(rows.primary_results[0]))
except KustoServiceError as e:
    print("failed:", type(e).__name__)
    print("too large:", "E_QUERY_RESULT_SET_TOO_LARGE" in str(e))
Terminal
$ python app.py
failed: KustoMultiApiError
too large: True

The app gets an exception instead: failed: KustoMultiApiError, and too large: True.

The fixes:

  • Return less: summarize in the engine
  • Page: with stored query results
  • notruncation: only when you need it

Also look for throttling, an HTTP 429, and the consistency setting.

My checklist

Same principal, same database, same request properties, same workload group. Usually one of them differs.

Question 5: A team wants to move from Azure Data Explorer to an Eventhouse in Microsoft Fabric. How do you plan it?

Microsoft documents a phased migration with no downtime. The steps, in order:

  • Schema: .show database schema as csl script, then .execute database script
  • A view per table: the new table, union the old cluster's table
  • Queries: repoint to the KQL database query URI
  • Ingestion: switch to its ingestion URI
  • History, then retire: Kusto Copy if needed, then retire the cluster

Step two is the clever one. A view named like each table unions the new table with the old one, so queries stay the same. Let's try it locally: the legacy database plays the old cluster. September's invoices are there, October's are in shop, and the view is called Invoices.

view.kql
// in the new database: a view named like the table
.create-or-alter function with (view=true, skipvalidation=true)
    Invoices() {
    Invoices | union database("legacy").Invoices
}

Invoices
| summarize Invoices = count(), Amount = sum(Amount)
    by Month = startofmonth(InvoiceDate)
| order by Month asc
Terminal
$ kql -q -f view.kql
> .create-or-alter function with (view=true, skipvalidation=true)
ok: .create-or-alter function with (view=true, skipvalidation=true)

> Invoices
Month                 Invoices  Amount
--------------------  --------  ------
2026-09-01T00:00:00Z  2         240
2026-10-01T00:00:00Z  3         400
Result table2 rows
MonthInvoicesAmount
2026-09-01T00:00:00Z2240
2026-10-01T00:00:00Z3400

One query returns both months: 2 invoices worth 240 in September, and 3 worth 400 in October.

In the Eventhouse, Microsoft's migration guide uses the same pattern with cluster():

view_fabric.kql (Azure, not run)
// in the Eventhouse KQL database (Microsoft's migration guide)
.create function with (view=true) Invoices() {
    Invoices
    | union cluster("YourADXCluster").database("YourDatabase").Invoices
}

Or start with a database shortcut: read only, with a slight lag.

Plan separately

Policies, row level security, workload groups, private networking and customer-managed keys each need their own plan. And size the Fabric capacity with a test.

Question 6: Users complain that last month's data is slow to query but today's is fast. What is going on?

Almost always the caching policy. Data inside the hot period sits on the nodes' local SSD and memory; older data is in cold storage, which is cheaper but slower.

  • Hot cache, today's data: local SSD and memory, fast
  • Cold storage, last month's data: cheaper, slower

The fixes:

  • Match the hot period: to what people query; or a hot window for month end
  • Warming takes up to an hour: and a bigger cache can mean more nodes, so pair it with optimized autoscale

Let's set the policy on Logs: 14 days hot, plus a hot window for September, and read it back.

cache.kql
.alter table Logs policy caching hot = 14d,
    hot_window = datetime(2026-09-01) .. datetime(2026-10-01)

.show table Logs policy caching
| extend P = parse_json(Policy)
| project Hot = tostring(P.DataHotSpan.Value),
    WindowFrom = todatetime(P.HotWindows[0].MinValue),
    WindowTo = todatetime(P.HotWindows[0].MaxValue)
Terminal
$ kql -q -f cache.kql
> .alter table Logs policy caching hot = 14d,
ok: .alter table Logs policy caching hot = 14d,

> .show table Logs policy caching
ok: .show table Logs policy caching
Result table1 row
HotWindowFromWindowTo
14.00:00:002026-09-01T00:00:00Z2026-10-01T00:00:00Z

Hot is 14.00:00:00, and the window runs from 2026-09-01T00:00:00Z to 2026-10-01T00:00:00Z. The emulator accepts it but keeps everything on local disk, so the speed difference can't be shown here.

The better fix for reports is a materialized view of daily totals, created async with backfill=true so it covers the old rows. The wait script returns once the backfill has finished, and then we read September.

mv.kql
// backfill over existing rows: the create must be async
.create async materialized-view with (backfill=true)
    DailyLogs on table Logs {
    Logs
    | summarize Requests = count(), Errors = countif(Level == "Error")
        by Day = bin(Timestamp, 1d), Service
}
wait_backfill.sh
# the view answers once its async backfill has finished
for i in $(seq 1 60); do
  n=$(kql "DailyLogs | count" 2>/dev/null | tail -1)
  [ "${n:-0}" -ge 6 ] 2>/dev/null && exit 0
  sleep 5
done
echo "backfill did not finish"; exit 1
daily.kql
DailyLogs
| where Day between (datetime(2026-09-01) .. datetime(2026-09-30))
| order by Day asc, Service asc
Terminal
$ kql -q -f mv.kql
> .create async materialized-view with (backfill=true)
ok: .create async materialized-view with (backfill=true)
$ bash wait_backfill.sh
$ kql -f daily.kql
> DailyLogs
Day                   Service  Requests  Errors
--------------------  -------  --------  ------
2026-09-10T00:00:00Z  api      3         1
2026-09-10T00:00:00Z  web      1         0
2026-09-11T00:00:00Z  api      1         0
2026-09-11T00:00:00Z  web      1         1
Result table4 rows
DayServiceRequestsErrors
2026-09-10T00:00:00Zapi31
2026-09-10T00:00:00Zweb10
2026-09-11T00:00:00Zapi10
2026-09-11T00:00:00Zweb11

September comes back as four small summary rows instead of raw log lines: on September 10, api had 3 requests and 1 error and web 1 and 0; on September 11, api 1 and 0, and web 1 and 1. Summaries are cheap to keep hot.

Quick test

Run the query with set query_datascope = "hotcache". If rows go missing, the query needed cold data.

Scenario 7: In the final round, the hiring manager asks you to design a real-time analytics platform with Azure Data Explorer. What is your design?

Left to right: sources, data connections, raw tables, transactional update policies, typed tables, materialized views, dashboards.

  • Sources: Event Hubs, IoT Hub, Event Grid
  • Data connections: managed identity, own consumer group
  • Raw tables: queued (tuned) or streaming
  • Update policies: transactional
  • Typed tables: retention, cache
  • Materialized views: dashboard aggregates
  • Dashboards: Power BI DirectQuery, Grafana

Sources send to Event Hubs or IoT Hub, and files land in storage with Event Grid. Queued ingestion is tuned for latency, and streaming is for small, latency-critical tables.

Let's build the core path on the emulator. pipeline.kql creates RawEvents with a JSON mapping, the typed Readings table, a ParseRaw() function, a transactional update policy, and an hourly materialized view, DeviceHourly. Then .ingest inline, a prototyping command, stands in for the data connection with three raw events.

pipeline.kql
.create table RawEvents (Payload:dynamic)

.create table RawEvents ingestion json mapping 'RawMap'
    '[{"column": "Payload", "Properties": {"Path": "$"}}]'

.create table Readings (Ts:datetime, DeviceId:string, Temp:real)

.create-or-alter function ParseRaw() {
    RawEvents
    | project Ts = todatetime(Payload.ts),
        DeviceId = tostring(Payload.device), Temp = toreal(Payload.temp)
}

.alter table Readings policy update ```[{"IsEnabled": true,
    "Source": "RawEvents", "Query": "ParseRaw()",
    "IsTransactional": true}]```

.create materialized-view DeviceHourly on table Readings {
    Readings | summarize AvgTemp = avg(Temp), Events = count()
        by DeviceId, Hour = bin(Ts, 1h)
}
events.kql
.ingest inline into table RawEvents
    with (format='json', ingestionMappingReference='RawMap') <|
{"ts": "2026-10-20T08:05:00Z", "device": "d1", "temp": 21.5}
{"ts": "2026-10-20T08:35:00Z", "device": "d1", "temp": 22.5}
{"ts": "2026-10-20T08:20:00Z", "device": "d2", "temp": 30.0}
Terminal
$ kql -q -f pipeline.kql
> .create table RawEvents (Payload:dynamic)
ok: .create table RawEvents (Payload:dynamic)

> .create table RawEvents ingestion json mapping 'RawMap'
ok: .create table RawEvents ingestion json mapping 'RawMap'

> .create table Readings (Ts:datetime, DeviceId:string, Temp:real)
ok: .create table Readings (Ts:datetime, DeviceId:string, Temp:real)

> .create-or-alter function ParseRaw() {
ok: .create-or-alter function ParseRaw() {

> .alter table Readings policy update ```[{"IsEnabled": true,
ok: .alter table Readings policy update ```[{"IsEnabled": true,

> .create materialized-view DeviceHourly on table Readings {
ok: .create materialized-view DeviceHourly on table Readings {
$ kql -q -f events.kql
> .ingest inline into table RawEvents
ok: .ingest inline into table RawEvents
KQL - the query behind the result table
Readings | order by Ts asc
Result table3 rows
TsDeviceIdTemp
2026-10-20T08:05:00Zd121.5
2026-10-20T08:20:00Zd230
2026-10-20T08:35:00Zd122.5

Three raw events in, three typed rows out from the update policy: d1 at 21.5 and 22.5, and d2 at 30. Now the view by name:

Terminal
$ kql 'DeviceHourly | order by DeviceId asc, Hour asc'
DeviceId  Hour                  AvgTemp  Events
--------  --------------------  -------  ------
d1        2026-10-20T08:00:00Z  22       2
d2        2026-10-20T08:00:00Z  30       1
Result table2 rows
DeviceIdHourAvgTempEvents
d12026-10-20T08:00:00Z222
d22026-10-20T08:00:00Z301

The hourly average per device: 22 for d1 from 2 events, and 30 for d2 from 1.

Around it:

  • Security: Entra groups, RLS, private endpoints, restricted outbound
  • Operations: metrics, diagnostic logs, freshness alerts; Bicep + Azure DevOps
  • Cost and scale: optimized autoscale, followers or shortcuts; Eventhouse if Fabric-first
Real project

If the company is Fabric-first, I propose the same design in an Eventhouse.

Key points to remember

  • Double counts: prove, find the extents, drop by drop-by tag, reload with ingestIfNotExists
  • Cost: compute, storage, networking, markup; .show journal shows what changed
  • Logs: Azure Data Explorer or Eventhouse; Synapse Data Explorer is retired
  • App timeouts: compare principal, workload group, request properties; mind truncation
  • Slow old data: caching policy, hot windows, materialized views

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 #10 - Real-world manager scenarios
# 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_10_Code.py   (Python 3.10 or later; kql.py itself needs no packages: it uses the REST API)
#      The python scripts in some steps use the Kusto Python SDK: pip install azure-kusto-data azure-kusto-ingest
# "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('setup.kql', """\
.create table Orders (OrderId:string, Customer:string, Amount:real)

.create table ClickEvents (Ts:datetime, Page:string)

.ingest inline into table ClickEvents <|
2026-10-20T08:00:00Z,home
2026-10-20T08:01:00Z,cart
2026-10-20T08:02:00Z,home

.create table Telemetry (DeviceId:string, Temp:real, Ts:datetime)

.create table Invoices (InvoiceId:long, Amount:real, InvoiceDate:datetime)

.ingest inline into table Invoices <|
3001,120,2026-10-05T09:00:00Z
3002,80,2026-10-12T09:00:00Z
3003,200,2026-10-19T09:00:00Z

.create table Logs (Timestamp:datetime, Service:string, Level:string)

.ingest inline into table Logs <|
2026-09-10T08:00:00Z,api,Info
2026-09-10T09:00:00Z,api,Error
2026-09-10T10:00:00Z,api,Info
2026-09-10T11:00:00Z,web,Info
2026-09-11T08:00:00Z,api,Info
2026-09-11T09:00:00Z,web,Error
2026-10-20T08:00:00Z,api,Info
2026-10-20T09:00:00Z,web,Info

.create table AppLogs (Line:string)

.ingest inline into table AppLogs <|
2026-10-20T08:01:00Z ERROR payments timeout calling gateway
2026-10-20T08:02:00Z INFO checkout order placed
2026-10-20T08:03:00Z ERROR payments card declined
2026-10-20T08:04:00Z ERROR checkout coupon invalid
""")
write('legacy_db.kql', """\
.create database legacy persist (@"/kustodata/dbs/legacy/md", @"/kustodata/dbs/legacy/data")
""")
write('legacy.kql', """\
.create table Invoices (InvoiceId:long, Amount:real, InvoiceDate:datetime)

.ingest inline into table Invoices <|
2001,150,2026-09-08T09:00:00Z
2002,90,2026-09-22T09:00:00Z
""")
run(KQL + ' -q -f setup.kql')
run(KQL + ' -d NetDefaultDB -q -f legacy_db.kql')
run(KQL + ' -d legacy -q -f legacy.kql')

# Step 1: A retry loads the same batch twice; prove the duplicates and find the two extents
write('batch_0101.csv', """\
o1,ann,10
o2,bob,20
o3,cat,30
""")
write('load.kql', """\
// the pipeline retried: the same batch file, loaded twice
.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]')

.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]')
""")
write('check.kql', """\
Orders | summarize Revenue = sum(Amount), Rows = count()

Orders
| summarize Copies = count() by OrderId
| where Copies > 1
| order by OrderId asc

.show table Orders extents
| summarize Extents = count(), Rows = sum(RowCount)
""")
run(KQL + ' -q -f load.kql')
run(KQL + ' -f check.kql')

# Step 2: Drop the batch by its drop-by tag, reload twice with ingestIfNotExists: the second load is skipped
write('fix.kql', """\
.drop extents <| .show table Orders extents
    where tags has 'drop-by:batch_0101'

.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]',
      ingestIfNotExists='["batch_0101"]')

.ingest into table Orders (@"/kustodata/batch_0101.csv")
    with (format='csv',
      tags='["ingest-by:batch_0101", "drop-by:batch_0101"]',
      ingestIfNotExists='["batch_0101"]')
""")
run(KQL + ' -q -f fix.kql')

# Step 3: A month of changes: hot cache, retention, streaming ingestion, a materialized view
write('changes.kql', """\
// what changed on the cluster this month
.alter table ClickEvents policy caching hot = 365d

.alter-merge table ClickEvents policy retention softdelete = 1825d

.alter table Telemetry policy streamingingestion enable

.create materialized-view ClicksPerPage on table ClickEvents {
    ClickEvents | summarize Clicks = count() by Page, bin(Ts, 1h)
}
""")
run(KQL + ' -q -f changes.kql')

# Step 4: (wait until the engine has flushed its journal)
run('for i in $(seq 1 60); do n=$(kql \'.show journal | where Event == "CREATE-MATERIALIZED-VIEW" and EntityName == "ClicksPerPage" | count\' | tail -1); [ "$n" -ge 1 ] && break; sleep 5; done; echo journal flushed')

# Step 5: .show journal: who changed what
run(KQL + ' \'.show journal | where Event in ("ALTER-TABLE-CACHING-POLICY", "ALTER-TABLE-RETENTION-POLICY", "ALTER-TABLE-STREAMING-INGESTION-POLICY", "CREATE-MATERIALIZED-VIEW") | project Event, EntityName, Command = substring(ChangeCommand, 0, 66) | order by Event asc, EntityName asc\'')

# Step 6: Raw log lines parsed and counted in one query
write('logs.kql', """\
AppLogs
| where Line has "ERROR"
| parse Line with Ts:datetime " " Level:string " " Service:string
    " " Message:string
| where Level == "ERROR"
| summarize Errors = count(), Last = max(Ts) by Service
| order by Service asc
""")
run(KQL + ' -f logs.kql')

# Step 7: 600,000 rows: the kql client gets 500,000 rows and the partial result error
run(KQL + " --max 3 'range x from 1 to 600000 step 1'", expect_error=True)   # EXPECTED TO FAIL: the video shows this error

# Step 8: The same query from a Python app with a 10-minute server timeout: an exception
write('app.py', """\
from datetime import timedelta
from azure.kusto.data import (ClientRequestProperties, KustoClient,
                              KustoConnectionStringBuilder)
from azure.kusto.data.exceptions import KustoServiceError

kcsb = KustoConnectionStringBuilder.with_no_authentication(
    "http://localhost:8080")                       # emulator only
client = KustoClient(kcsb)
props = ClientRequestProperties()
props.set_option("servertimeout", timedelta(minutes=10))  # max 1 hour
try:
    rows = client.execute("shop", "range x from 1 to 600000 step 1",
                          props)
    print("rows:", len(rows.primary_results[0]))
except KustoServiceError as e:
    print("failed:", type(e).__name__)
    print("too large:", "E_QUERY_RESULT_SET_TOO_LARGE" in str(e))
""")
run('python app.py')

# Step 9: Migration step 2 locally: a view named like the table unions the new and the old database
write('view.kql', """\
// in the new database: a view named like the table
.create-or-alter function with (view=true, skipvalidation=true)
    Invoices() {
    Invoices | union database("legacy").Invoices
}

Invoices
| summarize Invoices = count(), Amount = sum(Amount)
    by Month = startofmonth(InvoiceDate)
| order by Month asc
""")
run(KQL + ' -q -f view.kql')

# Step 10: A 14-day hot cache plus a hot window for September (accepted; no cache tiers on the emulator)
write('cache.kql', """\
.alter table Logs policy caching hot = 14d,
    hot_window = datetime(2026-09-01) .. datetime(2026-10-01)

.show table Logs policy caching
| extend P = parse_json(Policy)
| project Hot = tostring(P.DataHotSpan.Value),
    WindowFrom = todatetime(P.HotWindows[0].MinValue),
    WindowTo = todatetime(P.HotWindows[0].MaxValue)
""")
run(KQL + ' -q -f cache.kql')

# Step 11: A materialized view of daily totals (async backfill): September in a few summary rows
write('mv.kql', """\
// backfill over existing rows: the create must be async
.create async materialized-view with (backfill=true)
    DailyLogs on table Logs {
    Logs
    | summarize Requests = count(), Errors = countif(Level == "Error")
        by Day = bin(Timestamp, 1d), Service
}
""")
write('wait_backfill.sh', """\
# the view answers once its async backfill has finished
for i in $(seq 1 60); do
  n=$(kql "DailyLogs | count" 2>/dev/null | tail -1)
  [ "${n:-0}" -ge 6 ] 2>/dev/null && exit 0
  sleep 5
done
echo "backfill did not finish"; exit 1
""")
write('daily.kql', """\
DailyLogs
| where Day between (datetime(2026-09-01) .. datetime(2026-09-30))
| order by Day asc, Service asc
""")
run(KQL + ' -q -f mv.kql')
run('bash wait_backfill.sh')
run(KQL + ' -f daily.kql')

# Step 12: The core path: raw JSON, a transactional update policy, typed rows, an hourly materialized view
write('pipeline.kql', """\
.create table RawEvents (Payload:dynamic)

.create table RawEvents ingestion json mapping 'RawMap'
    '[{"column": "Payload", "Properties": {"Path": "$"}}]'

.create table Readings (Ts:datetime, DeviceId:string, Temp:real)

.create-or-alter function ParseRaw() {
    RawEvents
    | project Ts = todatetime(Payload.ts),
        DeviceId = tostring(Payload.device), Temp = toreal(Payload.temp)
}

.alter table Readings policy update ```[{"IsEnabled": true,
    "Source": "RawEvents", "Query": "ParseRaw()",
    "IsTransactional": true}]```

.create materialized-view DeviceHourly on table Readings {
    Readings | summarize AvgTemp = avg(Temp), Events = count()
        by DeviceId, Hour = bin(Ts, 1h)
}
""")
write('events.kql', """\
.ingest inline into table RawEvents
    with (format='json', ingestionMappingReference='RawMap') <|
{"ts": "2026-10-20T08:05:00Z", "device": "d1", "temp": 21.5}
{"ts": "2026-10-20T08:35:00Z", "device": "d1", "temp": 22.5}
{"ts": "2026-10-20T08:20:00Z", "device": "d2", "temp": 30.0}
""")
run(KQL + ' -q -f pipeline.kql')
run(KQL + ' -q -f events.kql')

# Step 13: The materialized view by name: hourly average per device
run(KQL + " 'DeviceHourly | order by DeviceId asc, Hour asc'")
That is the whole series

All 10 parts are listed on the Real Time Azure Data Explorer and KQL Interview Questions and Answers page on this blog.

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.