PIVOT
Turn the values of one column into columns, like a pivot table in Excel: turnover per symbol with one column per day.
- Category: Querying
- Keywords: PIVOT, pivot table, cross tab, crosstab, rows to columns, values as columns, spread out, columns per month, columns per day, Excel pivot, wide table, long to wide
- See also: GROUP BY and HAVING, Aggregate functions (sum_if), Aggregate functions (count_if), Conditional and NULL functions (case), UNION and subqueries
A pivot table turns rows into columns. GROUP BY symbol, date gives one row for each symbol and day ("long" data). A pivot gives one row for each symbol and one column for each day ("wide" data), which is easier to read in a report:
| symbol | sep_09 | sep_10 |
|---|---|---|
| CITY | 777400 | 469830 |
| COAST | 88170 | 1139340 |
There are two ways to get it in KDB:
- The manual way (works today): one aggregate per column, each with its own condition:
sum_if(amount, date = '2026-09-09') AS sep_09(sum_if, count_if), orsum(CASE WHEN date = '2026-09-09' THEN amount END). - The PIVOT clause: write the aggregate once and list the values:
PIVOT (sum(amount) FOR date IN ('2026-09-09' AS sep_09, '2026-09-10' AS sep_10)).
Coming soon: the kdb program this page was built with reads the PIVOT clause and checks its spelling, but does not apply it yet: a query with PIVOT returns the rows as if PIVOT were not there. Until the next build, use the manual way. The PIVOT examples below show their results after that build.
Example: The manual way: turnover per symbol, one column per day (sample data: sample)
SELECT symbol,
sum_if(amount, date = '2026-09-09') AS sep_09,
sum_if(amount, date = '2026-09-10') AS sep_10,
sum(amount) AS total
FROM trade_log
GROUP BY symbol
ORDER BY symbol;
Result:
| symbol (SYMBOL) | sep_09 (DOUBLE) | sep_10 (DOUBLE) | total (DOUBLE) |
|---|---|---|---|
| CITY | 777400 | 469830 | 1247230 |
| COAST | 88170 | 1139340 | 1227510 |
| TELCO | 1760000 | 678260 | 2438260 |
| VOLT | NULL | 73950 | 73950 |
(4 rows)
Example: The manual way: app users per city, one column per plan (sample data: query-app)
SELECT coalesce(city, '(none)') AS city,
count_if(plan = 'free') AS free,
count_if(plan = 'pro') AS pro,
count_if(plan = 'admin') AS admin
FROM app_users
GROUP BY coalesce(city, '(none)')
ORDER BY city;
Result:
| city (STRING) | free (LONG) | pro (LONG) | admin (LONG) |
|---|---|---|---|
| (none) | 2 | 0 | 0 |
| Dharan | 0 | 1 | 0 |
| Kathmandu | 1 | 1 | 0 |
| Lalitpur | 0 | 0 | 1 |
| Pokhara | 1 | 1 | 0 |
(5 rows)
PIVOT (...)
Syntax:
SELECT ... FROM source PIVOT (aggregate(x) [AS name] [, ...]
FOR column IN (value [AS name] [, ...])) [[AS] alias]
SELECT ... FROM source [AS] alias PIVOT (...)
PIVOT comes right after a source in FROM: a table, a view or a subquery. For every value in the IN (...) list it makes a new column. The column holds the aggregate of the rows where column has that value.
| Part | Meaning |
|---|---|
aggregate(x) [AS name] | what goes into the new columns, like sum(amount) or count(id). Any aggregate works; you can list more than one |
FOR column | the column whose values become new columns (the day, the month, the plan) |
IN (value [AS name], ...) | the values to spread out, as constants: '2026-09-09', 1, 'pro'. AS name names the new column. With more than one aggregate there is a column for each value and aggregate, named value name, _, aggregate name: week1_debit |
- The other columns of the source (not in the aggregate and not the
FORcolumn) become the group keys: one result row for each of their value combinations. So pivot a subquery that has only the columns you need:(SELECT symbol, date, amount FROM trade_log) f. - NULL: a value from the
INlist that has no rows givesNULLin its column (likesumof no rows). Rows whoseFORcolumn is not in the list are not used. - Notes:
- A source can have only one
PIVOT. - The values must be written out. A pivot over "all days in the table" needs to know the days first: find them with
SELECT DISTINCT date ..., then write the query. - For a dynamic list of columns in an app, build the SQL text in the app, or use the manual way.
- A source can have only one
Example: Turnover per symbol, one column per day (sample data: sample)
SELECT symbol, sep_09, sep_10
FROM (SELECT symbol, date, amount FROM trade_log) f
PIVOT (sum(amount) FOR date IN ('2026-09-09' AS sep_09, '2026-09-10' AS sep_10))
ORDER BY symbol;
Result:
| symbol (SYMBOL) | sep_09 (DOUBLE) | sep_10 (DOUBLE) |
|---|---|---|
| CITY | 777400 | 469830 |
| COAST | 88170 | 1139340 |
| TELCO | 1760000 | 678260 |
| VOLT | NULL | 73950 |
(4 rows)
Example: App users per city, one column per plan (sample data: query-app)
SELECT city, free, pro, admin
FROM (SELECT city, plan, id FROM app_users) u
PIVOT (count(id) FOR plan IN ('free' AS free, 'pro' AS pro, 'admin' AS admin))
ORDER BY city;
Result:
| city (STRING) | free (LONG) | pro (LONG) | admin (LONG) |
|---|---|---|---|
| Dharan | 0 | 1 | 0 |
| Kathmandu | 1 | 1 | 0 |
| Lalitpur | 0 | 0 | 1 |
| Pokhara | 1 | 1 | 0 |
| NULL | 2 | 0 | 0 |
(5 rows)
Example: Two aggregates: debit and credit per account, before and after 6 September (sample data: query-app)
SELECT account, name, week1_debit, week1_credit, week2_debit, week2_credit
FROM (SELECT l.account, a.name,
CASE WHEN l.entry_date < '2026-09-06' THEN 1 ELSE 2 END AS week,
l.debit, l.credit
FROM ledger l JOIN accounts a ON a.code = l.account) j
PIVOT (sum(debit) AS debit, sum(credit) AS credit FOR week IN (1 AS week1, 2 AS week2))
ORDER BY account;
Result:
| account (INT) | name (STRING) | week1_debit (DECIMAL(2)) | week1_credit (DECIMAL(2)) | week2_debit (DECIMAL(2)) | week2_credit (DECIMAL(2)) |
|---|---|---|---|---|---|
| 1000 | Cash | 30000.00 | 25000.00 | NULL | NULL |
| 1100 | Bank - City | 500000.00 | 288530.00 | 18850.00 | NULL |
| 1200 | Shares at cost | 257500.00 | NULL | NULL | 10300.00 |
| 2000 | Broker payable | 257500.00 | 257500.00 | NULL | NULL |
| 3000 | Owner capital | NULL | 500000.00 | NULL | NULL |
| 4000 | Dividend income | NULL | NULL | NULL | 6250.00 |
| 4100 | Gain on sale | NULL | NULL | NULL | 2300.00 |
| 5000 | Broker commission | 1030.00 | NULL | NULL | NULL |
| 5100 | Office rent | 25000.00 | NULL | NULL | NULL |
(9 rows)
Example: The same report the manual way (works today) (sample data: query-app)
SELECT a.name,
sum_if(l.debit, l.entry_date < '2026-09-06') AS debit_week1,
sum_if(l.debit, l.entry_date >= '2026-09-06') AS debit_week2,
sum_if(l.credit, l.entry_date < '2026-09-06') AS credit_week1,
sum_if(l.credit, l.entry_date >= '2026-09-06') AS credit_week2
FROM ledger l JOIN accounts a ON a.code = l.account
GROUP BY a.name
ORDER BY a.name;
Result:
| name (STRING) | debit_week1 (DECIMAL(2)) | debit_week2 (DECIMAL(2)) | credit_week1 (DECIMAL(2)) | credit_week2 (DECIMAL(2)) |
|---|---|---|---|---|
| Bank - City | 500000.00 | 18850.00 | 288530.00 | NULL |
| Broker commission | 1030.00 | NULL | NULL | NULL |
| Broker payable | 257500.00 | NULL | 257500.00 | NULL |
| Cash | 30000.00 | NULL | 25000.00 | NULL |
| Dividend income | NULL | NULL | NULL | 6250.00 |
| Gain on sale | NULL | NULL | NULL | 2300.00 |
| Office rent | 25000.00 | NULL | NULL | NULL |
| Owner capital | NULL | NULL | 500000.00 | NULL |
| Shares at cost | 257500.00 | NULL | NULL | 10300.00 |
(9 rows)
Example: FOR and the column are needed (sample data: sample; shows an error)
SELECT * FROM trade_log PIVOT (sum(amount) IN ('CITY'));
Result:
Error: line 1, column 55: expected FOR and the column to spread out, as in PIVOT (sum(turnover) FOR month IN (1, 2)), but found ')'
Example: A source can have only one PIVOT (sample data: sample; shows an error)
SELECT *
FROM trade_log PIVOT (sum(amount) FOR symbol IN ('CITY')) f
PIVOT (sum(amount) FOR symbol IN ('COAST'));
Result:
Error: line 3, column 1: a source can have only one PIVOT