What a kdb+ and q interview at a trading desk actually tests

Updated · techinterview.org

A q screen at a trading firm looks nothing like a LeetCode problem. You get a trade table and a quote table, and the prompt is: for every trade, return the prevailing bid and ask at the instant it executed. Reach for a correlated subquery and you’ve already lost the round. The expected answer is a single function, aj, and the interviewer mostly wants to hear why it fits and what it does to your data.

kdb+ is the column-oriented database that a large share of banks and hedge funds run their market data on, and q is the language you query and program it in. Both came out of Kx Systems, built on Arthur Whitney’s k. The jobs that ask about it carry titles like kdb+ developer, q developer, or market-data engineer, and they sit close to the desk. Some are staffed through KX and its consulting arm, once called First Derivatives; many are in-house at firms that have run kdb for fifteen or twenty years. Pay is strong and the candidate pool is thin, which is why the process is pointed: a q coding screen or short take-home, then an onsite that pairs a live q session with a market-data systems round, often with a general data-structures round for good measure.

Why the language fights your SQL instincts

Three things trip up almost everyone coming from Postgres. There is no operator precedence: q evaluates right to left, so 2*3+4 is 14, not 10, because 3+4 resolves first and then the multiply. Interviewers will put 2 3 4 + 1 2 3 * 2 on the board and watch you work it out by hand. Get the order wrong and they know you’ve only skimmed the docs.

Then there’s the fact that everything is a list. A column is a vector, a table is a set of equal-length vectors, and most operations run over whole columns at once. You don’t loop over rows; you apply a function to the column. Asked for a running total, the answer is sums, and the follow-up is whether you know it’s the scan adverb \ doing the work behind it.

And q-sql resembles SQL without being it. select, by, where, and update all exist, but by groups and aggregates in one move, where clauses apply in sequence so their order changes performance, and there are no joins in the from clause. A day’s volume-weighted average price is one clause:

select vwap: size wavg price by sym from trade where date=.z.d

That line reads today’s partition, groups by symbol, and takes a size-weighted mean of price. Write it without hesitating, and explain that where date=.z.d is what lets kdb+ skip every other date on disk, and you’re most of the way through the coding screen.

The asof join is half the interview in one function

Back to the prevailing-quote problem. Trades and quotes arrive on separate clocks; a trade at 09:30:01.123 needs the most recent quote at or before that instant, for that symbol. That is an as-of join:

/ for each trade, the last quote at-or-before its time, matched within sym
aj[`sym`time; trade; quote]

The key list carries the whole trick. Every column except the last is an exact match (here sym), and the final column, time, is the one joined as-of. The output keeps each trade’s timestamp and grafts the matching quote columns onto it. A frequent follow-up is the gap between aj and aj0: aj returns the trade time, while aj0 returns the quote’s own timestamp, which is what you want when you’re measuring how stale the prevailing quote was. Miss that and you’ll report a latency of zero and never question it.

Join q syntax What it returns When it’s the right tool
As-of join aj[`sym`time; trade; quote] For each trade, the single most recent quote at or before its timestamp, matched within symbol; the trade’s own time is kept Prevailing bid and ask at the moment of each trade
As-of join, quote time aj0[`sym`time; trade; quote] The same match, but the output carries the quote’s timestamp instead of the trade’s Measuring how stale the prevailing quote was at fill time
Window join wj[w; `sym`time; trade; (quote;(avg;`bid))] An aggregate over every quote inside a time window around each trade, rather than a single matching row Average or max quote over the second before each fill
Left join, keyed trade lj `sym xkey ref An exact-key lookup against a keyed reference table, row for row Attaching static reference data such as sector or tick size by symbol

Window joins, bars, and what comes next

Once a point-in-time lookup is second nature, the question widens. Instead of the single prevailing quote, they want the average bid and ask over the second before each trade, or the largest quote size in that span. That’s a window join:

w: -00:00:01 00:00:00 +\: trade`time   / a [t-1s, t] window at every trade
wj[w; `sym`time; trade; (quote; (avg;`bid); (avg;`ask))]

Read it as: build two time offsets, one second back and zero, add them to each trade’s time to get a per-trade window, then for every trade aggregate the quotes whose times land inside it. The final argument is a list of table-and-aggregation specs, so you pull avg bid and ask in one pass. Candidates who have only done aj often stall here, because wj makes you reason about the window construction apart from the join itself.

Bucketing lives in the same family. Give me one-minute OHLC bars per symbol, they’ll say, and the idiom is xbar:

select open:first price, high:max price, low:min price, close:last price
  by sym, 1 xbar time.minute from trade where date=.z.d

xbar rounds each time down to a bucket boundary, so 1 xbar time.minute floors to whole minutes and by drops every trade into its bar. Change the 1 to a 5 and you have five-minute bars with nothing else touched, the kind of small, revealing edit interviewers like to request on the spot.

The tickerplant question that separates developers from users

The coding half checks whether you can write q. The systems half checks whether you understand how a kdb+ stack takes in a live market. Be able to sketch the standard architecture from memory. A feed handler parses the exchange feed and pushes rows to a tickerplant, a thin, fast process whose only jobs are logging every message to disk and publishing it to subscribers. A real-time database subscribes, holds the day’s data in memory, and answers intraday queries. At end of day it writes its tables to the historical database on disk, partitioned by date, then flushes memory for the next session.

The good questions probe failure modes. Why keep the tickerplant deliberately thin? Because it’s the one process that can never fall behind, and every bit of logic added there is latency and risk for everything downstream. What if the real-time database dies at 3pm? You replay the tickerplant log from the start of day to rebuild its in-memory state. Why partition history by date rather than symbol? Because nearly every query filters on date first, and partitioning on the most common filter is what lets kdb+ read a few hundred megabytes instead of a few hundred gigabytes. Answer those and you’ve shown you’ve run a kdb system rather than only queried one.

What the screen is actually measuring

Raw q fluency is table stakes. The deeper signal is whether you think in columns and in time. A candidate who mentally unrolls every problem into row-by-row loops writes q that works on a toy table and collapses on a billion rows. The one who reaches for aj, wj, and xbar because the data is already ordered by time is the one a desk wants beside a trader at the open.

There’s a current wrinkle worth knowing. KX has pushed kdb+ past pure market data: version 4.1 set records on the STAC-M3 time-series benchmarks, and the newer KDB-X line folds KDB.AI, a vector database, into the same engine, so similarity search and retrieval sit next to tick data. If a role touches that work, expect a question or two about vector search layered on the usual joins, because the firms adopting it want people fluent in both. The mental model underneath doesn’t change: ordered data, columns, and getting the right value as of the right time.

The phrasings recur across firms, so a few are worth having ready cold:

  • “Here’s a trades table and a quotes table. Attach the prevailing quote to each trade.”
  • “Compute VWAP per symbol for one day, then bucket it into five-minute intervals.”
  • “Walk me through a single message from the exchange feed until it lands in the historical database.”
  • “A query over a year of data is slow. Where do you look first?”

If your answer to that last one isn’t “the where clause, and whether it filters on date first,” go practice until it is.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack
Scroll to Top