KDB knowledge

Aggregate functions

Make one value from many rows: count, sum, avg, min, max, first, last, median, percentile, vwap, twap, conditional counts and sums, the row of the maximum, and lists of text.

An aggregate function reads many rows and gives back one value. For example, sum(quantity) adds up the quantity of every row.

Example: A daily candle for each symbol, made from one-minute candles (sample data: sample)

SELECT symbol,
       first(open)  AS open,
       max(high)    AS high,
       min(low)     AS low,
       last(close)  AS close,
       sum(volume)  AS volume,
       count(*)     AS minutes
FROM ochl_minutes
WHERE time IN '2026-09-10'
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)open (DOUBLE)high (DOUBLE)low (DOUBLE)close (DOUBLE)volume (DOUBLE)minutes (LONG)
CITY504.6507500.7504.9183040240
COAST380.1382373.2376.1186200236

(2 rows)

Example: Aggregates cannot be used in WHERE (sample data: sample; shows an error)

SELECT symbol FROM trade_log WHERE sum(quantity) > 1000;

Result:

Error: line 1, column 36: 'sum' works on many rows and can only be used in a SELECT list or HAVING

Example: GROUP BY can use the AS name of a column (sample data: sample)

SELECT floor(rate / 100) * 100 AS band, count(*) AS trades
FROM trade_log
GROUP BY band
ORDER BY band;

Result:

band (DOUBLE)trades (LONG)
1004
3007
4004
50011
8006

(5 rows)

count(*)

Syntax:

count(*)
count()

The number of rows. NULL values do not matter here: every row counts. count() without * is the same (QuestDB style).

Example: Trades per symbol on one day (sample data: sample)

SELECT symbol, count(*) AS trades
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY symbol
ORDER BY trades DESC;

Result:

symbol (SYMBOL)trades (LONG)
COAST9
CITY7
TELCO4
VOLT4

(4 rows)

Example: New app users per Nepal day (sample data: fn-dates)

SELECT to_date(created_at + 20700000) AS nepal_day, count(*) AS signups
FROM signups
GROUP BY to_date(created_at + 20700000)
ORDER BY nepal_day;

Result:

nepal_day (DATE)signups (LONG)
2026-09-013
2026-09-022
2026-09-033
2026-09-041
2026-09-061

(5 rows)

Example: No rows: count is 0, sum is NULL (sample data: sample)

SELECT count(*) AS rows, count() AS rows_again, sum(close) AS total
FROM ochl_day
WHERE symbol = 'TELCO';

Result:

rows (LONG)rows_again (LONG)total (STRING)
00NULL

(1 row)

count(x)

Syntax:

count(x)

The number of rows where x is not NULL.

ArgumentTypeMeaning
xany typethe value to look at

Example: Users who gave their city, and users who never logged in (sample data: query-app)

SELECT count(*) AS users,
       count(city) AS with_city,
       count(*) - count(last_login) AS never_logged_in
FROM app_users;

Result:

users (LONG)with_city (LONG)never_logged_in (LONG)
862

(1 row)

count(DISTINCT x)

Syntax:

count(DISTINCT x)

The number of different values of x. The same value many times counts once.

Example: How many different brokers bought each symbol (sample data: sample)

SELECT symbol, count(*) AS trades, count(DISTINCT buyer) AS buying_brokers
FROM trade_log
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)trades (LONG)buying_brokers (LONG)
CITY114
COAST115
TELCO65
VOLT43

(4 rows)

Example: NULL is not a value; 1 and 1.0 are the same value

SELECT count(*) AS rows, count(x) AS not_null, count(DISTINCT x) AS different
FROM (SELECT NULL AS x UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1.0 UNION ALL SELECT 2) t;

Result:

rows (LONG)not_null (LONG)different (LONG)
542

(1 row)

count_distinct

Syntax:

count_distinct(x)

The number of different values of x, without NULL. The same as count(DISTINCT x).

ArgumentTypeMeaning
xany typethe value to look at

Example: Posts and different writers per forum topic (sample data: fn-dates)

SELECT topic, count(*) AS posts, count_distinct(author) AS writers
FROM forum_posts
GROUP BY topic
ORDER BY posts DESC;

Result:

topic (STRING)posts (LONG)writers (LONG)
Market today55
CITY dividend33
Hydro IPOs32
COAST merger33

(4 rows)

sum

Syntax:

sum(x)

The total of all values of x.

ArgumentTypeMeaning
xany numberthe value to add up

Example: Broker buy totals on one day (trade log) (sample data: sample)

SELECT buyer, b.name, count(*) AS trades, sum(quantity) AS shares, sum(amount) AS turnover
FROM trade_log f JOIN brokers b ON b.id = f.buyer
WHERE date IN '2026-09-10'
GROUP BY buyer, b.name
ORDER BY turnover DESC;

Result:

buyer (INT)name (STRING)trades (LONG)shares (LONG)turnover (DOUBLE)
49Online Securities51740960370
45Imperial Securities51560695950
58Northgate Securities4760260190
1Keystone Securities7570239420
34Vision Securities3300205450

(5 rows)

Example: Accounting month report: debit and credit per account type (DECIMAL stays exact) (sample data: query-app)

SELECT a.type, sum(l.debit) AS debit, sum(l.credit) AS credit
FROM ledger l JOIN accounts a ON a.code = l.account
WHERE l.entry_date IN '2026-09'
GROUP BY a.type
ORDER BY a.type;

Result:

type (STRING)debit (DECIMAL(2))credit (DECIMAL(2))
asset806350.00323830.00
equityNULL500000.00
expense26030.00NULL
incomeNULL8550.00
liability257500.00257500.00

(5 rows)

Example: No values give NULL; coalesce makes it 0.00 (sample data: query-app)

SELECT sum(debit) AS debit,
       coalesce(sum(debit), CAST(0 AS DECIMAL(2))) AS debit_or_zero
FROM ledger
WHERE account = 3000;

Result:

debit (STRING)debit_or_zero (DECIMAL(2))
NULL0.00

(1 row)

Example: Check the books: every journal entry must have debits = credits (sample data: query-app)

SELECT entry_id, count(*) AS lines, sum(debit) AS debit, sum(credit) AS credit,
       sum(debit) = sum(credit) AS balanced
FROM ledger
GROUP BY entry_id
ORDER BY entry_id;

Result:

entry_id (INT)lines (LONG)debit (DECIMAL(2))credit (DECIMAL(2))balanced (BOOL)
12500000.00500000.00true
22257500.00257500.00true
32257500.00257500.00true
421030.001030.00true
5230000.0030000.00true
6225000.0025000.00true
726250.006250.00true
8312600.0012600.00true

(8 rows)

Example: A DOUBLE total is compensated: exactly 55128735, not 55128734.99999999

CREATE TABLE vol (v DOUBLE);
INSERT INTO vol VALUES (15380798.2), (10594146.6), (12320992.8), (6619323.5), (10213473.9);
SELECT sum(v) = 55128735 AS exact, ksum(v) = sum(v) AS same_as_ksum FROM vol;

Result:

CREATE TABLE vol (

table 'vol' created

INSERT INTO vol VALUES (15380798.2), (10594146.6), (12320992.8), (6619323.5), (10213473.9)

5 row(s) inserted

SELECT sum(v) = 55128735 AS exact, sum(v) = sum(v) AS same_as_ksum FROM vol

exact (BOOL)same_as_ksum (BOOL)
truetrue

(1 row)

Example: Text cannot be added (sample data: sample; shows an error)

SELECT sum(symbol) FROM trade_log;

Result:

Error: line 1, column 8: sum needs numbers, got 'CITY'

avg

Syntax:

avg(x)

The average (mean): the sum of the values divided by how many values there are.

ArgumentTypeMeaning
xany numberthe value

Example: Average close and volume per symbol in September (sample data: sample)

SELECT symbol, round(avg(close), 2) AS avg_close, round(avg(volume)) AS avg_volume
FROM ochl_day
WHERE time IN '2026-09'
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)avg_close (DOUBLE)avg_volume (DOUBLE)
CITY500.18109526
COAST374.73113784
VOLT179.389164

(3 rows)

Example: NULL is skipped, not counted as 0

SELECT avg(x) AS avg, sum(x) / count(*) AS sum_by_all_rows
FROM (SELECT 10 AS x UNION ALL SELECT NULL UNION ALL SELECT 20) t;

Result:

avg (DOUBLE)sum_by_all_rows (DOUBLE)
1510

(1 row)

min

Syntax:

min(x)

The smallest value. Works for numbers, text (A before B), times and dates, and BOOL (false before true).

Example: Lowest price and first trade day of each symbol (sample data: sample)

SELECT symbol, min(low) AS lowest, min(time) AS first_day
FROM ochl_day
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)lowest (DOUBLE)first_day (TIMESTAMP)
CITY494.12025-12-28T00:00:00.000Z
COAST364.12025-12-28T00:00:00.000Z
VOLT167.32025-12-28T00:00:00.000Z

(3 rows)

Example: Text and dates (sample data: query-app)

SELECT min(username) AS first_name_a_to_z, min(created_at) AS first_signup, min(last_login) AS oldest_login
FROM app_users;

Result:

first_name_a_to_z (STRING)first_signup (TIMESTAMP)oldest_login (TIMESTAMP)
anita2026-01-01T03:15:00.000Z2026-08-30T06:15:00.000Z

(1 row)

max

Syntax:

max(x)

The biggest value. Works for numbers, text, times and dates, and BOOL.

Example: High, low and range of each symbol since 23 August (sample data: sample)

SELECT symbol, max(high) AS highest, min(low) AS lowest, round(max(high) - min(low), 2) AS range
FROM ochl_day
WHERE time >= '2026-08-23'
GROUP BY symbol
ORDER BY range DESC;

Result:

symbol (SYMBOL)highest (DOUBLE)lowest (DOUBLE)range (DOUBLE)
COAST398.8364.134.7
VOLT192.1167.324.8
CITY517494.122.9

(3 rows)

Example: The last login of every plan (sample data: query-app)

SELECT plan, count(*) AS users, max(last_login) AS latest_login
FROM app_users
GROUP BY plan
ORDER BY plan;

Result:

plan (STRING)users (LONG)latest_login (TIMESTAMP)
admin12026-09-11T04:20:00.000Z
free42026-09-09T02:15:00.000Z
pro32026-09-11T00:45:00.000Z

(3 rows)

first

Syntax:

first(x)

The value of x in the earliest row. On a time-series table, earliest means the smallest time. first(open) is the open price of a candle.

ArgumentTypeMeaning
xany typethe value to take

Example: Open, close and change of each symbol in September (sample data: sample)

SELECT symbol, first(open) AS open, last(close) AS close,
       round((last(close) - first(open)) / first(open) * 100, 2) AS change_pct
FROM ochl_day
WHERE time IN '2026-09'
GROUP BY symbol
ORDER BY change_pct DESC;

Result:

symbol (SYMBOL)open (DOUBLE)close (DOUBLE)change_pct (DOUBLE)
VOLT171.3191.211.62
COAST367376.12.48
CITY504.9504.90

(3 rows)

Example: The first post of each thread (sample data: content-forum)

SELECT thread_id, first(author) AS started_by, first(created_at) AS started, count(*) AS posts
FROM posts
GROUP BY thread_id
ORDER BY thread_id;

Result:

thread_id (LONG)started_by (STRING)started (TIMESTAMP)posts (LONG)
1ram2026-09-01T04:15:00.000Z6
2hari2026-09-02T03:15:00.000Z2

(2 rows)

last

Syntax:

last(x)

The value of x in the latest row (the biggest time). last(close) is the close price of a candle.

ArgumentTypeMeaning
xany typethe value to take

Example: The last price of every symbol in the live data (sample data: sample)

SELECT symbol, last(ltp) AS ltp, last(timestamp) AS at, max(high) AS day_high
FROM live_data
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)ltp (DOUBLE)at (TIMESTAMP)day_high (DOUBLE)
CITY527.12026-09-10T09:14:00.000Z528.1
COAST401.92026-09-10T09:14:00.000Z402.9
TELCO897.72026-09-10T09:14:00.000Z898.7
VOLT1842026-09-10T09:14:00.000Z186.6

(4 rows)

Example: 15-minute candles with SAMPLE BY (sample data: sample)

SELECT time, first(open) AS open, max(high) AS high, min(low) AS low, last(close) AS close, sum(volume) AS volume
FROM ochl_minutes
WHERE symbol = 'CITY' AND time IN '2026-09-10'
SAMPLE BY 15m
LIMIT 4;

Result:

time (TIMESTAMP)open (DOUBLE)high (DOUBLE)low (DOUBLE)close (DOUBLE)volume (DOUBLE)
2026-09-10T05:15:00.000Z504.6505.3503.9504.413210
2026-09-10T05:30:00.000Z504.4506.8503.8506.511690
2026-09-10T05:45:00.000Z506.5507505.3506.411270
2026-09-10T06:00:00.000Z506.4506.6504.9505.310850

(4 rows)

first_not_null, last_not_null

Syntax:

first_not_null(x)
last_not_null(x)

The value of x in the earliest / latest row where x is not NULL (QuestDB). Use them when a missing value must not become the answer, for example a candle whose first minute has no open price yet.

ArgumentTypeMeaning
xany typethe value to take

Example: first keeps a NULL first row, first_not_null skips it

Setup:

CREATE TABLE px (time TIMESTAMP, symbol SYMBOL, open DOUBLE) TIMESERIES(time) PARTITION BY DAY;
INSERT INTO px VALUES ('2026-09-10T05:00:00Z', 'CITY', NULL), ('2026-09-10T05:01:00Z', 'CITY', 512.5),
                      ('2026-09-10T05:02:00Z', 'CITY', 513), ('2026-09-10T05:03:00Z', 'CITY', NULL);
SELECT symbol, first(open), first_not_null(open), last(open), last_not_null(open) FROM px GROUP BY symbol;

Result:

symbol (SYMBOL)first(open) (DOUBLE)first_not_null(open) (DOUBLE)last(open) (DOUBLE)last_not_null(open) (DOUBLE)
CITYNULL512.5NULL513

(1 row)

median

Syntax:

median(x)

The middle value: half of the values are smaller, half are bigger. With an even number of values, it is the average of the two in the middle. Unlike avg, one very big trade does not move the median much.

Example: Typical trade size: median against average (sample data: sample)

SELECT symbol, count(*) AS trades, median(quantity) AS median_qty, round(avg(quantity), 1) AS avg_qty
FROM trade_log
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)trades (LONG)median_qty (DOUBLE)avg_qty (DOUBLE)
CITY11100220
COAST1150279.1
TELCO6350461.7
VOLT475100

(4 rows)

Example: Even number of values: the average of the two in the middle

SELECT median(x) AS median
FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 100) t;

Result:

median (DOUBLE)
2.5

(1 row)

percentile

Syntax:

percentile(x, p)

The value below which a part p of the values lie. percentile(x, 0.9) is the value that 90% of the values are at or below. Between two values, KDB goes the right part of the way from one to the other (linear interpolation, like PostgreSQL percentile_cont).

ArgumentTypeMeaning
xany numberthe values
pnumberthe part, from 0 to 1 (0.9). A number above 1 is read as a percent (90 means 0.9)

Example: Big trades: 90th percentile of the trade amount per symbol (sample data: sample)

SELECT symbol, percentile(amount, 0.9) AS p90_amount, max(amount) AS biggest
FROM trade_log
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)p90_amount (DOUBLE)biggest (DOUBLE)
CITY258000258500
COAST398000402000
TELCO880000881000
VOLT3137036800

(4 rows)

Example: 0.9 and 90 are the same; values in between are interpolated

SELECT percentile(x, 0.9) AS p90, percentile(x, 90) AS p90_percent, percentile(x, 0.25) AS p25, percentile(x, 0) AS p0, percentile(x, 1) AS p100
FROM (SELECT 10 AS x UNION ALL SELECT 20 UNION ALL SELECT 30 UNION ALL SELECT 40 UNION ALL SELECT 50) t;

Result:

p90 (DOUBLE)p90_percent (DOUBLE)p25 (DOUBLE)p0 (DOUBLE)p100 (DOUBLE)
4646201050

(1 row)

Example: p must be a number (sample data: sample; shows an error)

SELECT percentile(rate, 'high') FROM trade_log;

Result:

Error: line 1, column 8: percentile needs a number like 0.95

vwap

Syntax:

vwap(price, volume)

The volume-weighted average price: sum(price × volume) / sum(volume). A trade of 1,000 shares counts 100 times more than a trade of 10 shares. Traders use VWAP as the "fair" average price of a day.

ArgumentTypeMeaning
pricenumberprice of each trade or candle
volumenumberhow many shares

Example: VWAP per symbol from the trade log (sample data: sample)

SELECT symbol, sum(quantity) AS shares, round(vwap(rate, quantity), 2) AS vwap, round(avg(rate), 2) AS plain_avg
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)shares (LONG)vwap (DOUBLE)plain_avg (DOUBLE)
CITY910516.3516
COAST2850399.77399.28
TELCO770880.86880.75
VOLT400184.88185

(4 rows)

Example: Hourly VWAP from one-minute candles (Nepal hours) (sample data: sample)

SELECT time, round(vwap(close, volume), 2) AS vwap, sum(volume) AS volume
FROM ochl_minutes
WHERE symbol = 'COAST' AND time IN '2026-09-10'
SAMPLE BY 1h TIME ZONE 'Asia/Kathmandu';

Result:

time (TIMESTAMP)vwap (DOUBLE)volume (DOUBLE)
2026-09-10T05:15:00.000Z380.4447660
2026-09-10T06:15:00.000Z379.7145870
2026-09-10T07:15:00.000Z377.643950
2026-09-10T08:15:00.000Z375.6748720

(4 rows)

Example: Both arguments are needed (sample data: sample; shows an error)

SELECT vwap(rate) FROM trade_log;

Result:

Error: line 1, column 8: function 'vwap' takes 2 argument(s), got 1

twap

Syntax:

twap(price, time)

The time-weighted average price. Each price counts for the time until the next price. A price that stayed for 2 hours counts twice as much as a price that stayed for 1 hour. Traders use TWAP to judge a price when trades came at uneven times, for example live price snapshots.

ArgumentTypeMeaning
pricenumberthe price
timeTIMESTAMP, DATE or whole number (milliseconds)when this price started

Example: Live prices came at 11:05, 13:00 and 14:59: TWAP against the plain average (sample data: sample)

SELECT symbol, round(twap(ltp, timestamp), 2) AS twap, round(avg(ltp), 2) AS plain_avg
FROM live_data
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)twap (DOUBLE)plain_avg (DOUBLE)
CITY514.32518.57
COAST391.01394.63
TELCO864.82875.73
VOLT184.51184.33

(4 rows)

Example: How it works: 513 for 115 minutes and 515.6 for 119 minutes; 527.1 is the last price (sample data: sample)

SELECT timestamp, ltp FROM live_data WHERE symbol = 'CITY' ORDER BY timestamp;
SELECT round((513 * 115 + 515.6 * 119) / 234.0, 2) AS by_hand, round(twap(ltp, timestamp), 2) AS twap
FROM live_data
WHERE symbol = 'CITY';

Result:

SELECT timestamp, ltp FROM live_data WHERE symbol = 'CITY' ORDER BY timestamp

timestamp (TIMESTAMP)ltp (DOUBLE)
2026-09-10T05:20:00.000Z513
2026-09-10T07:15:00.000Z515.6
2026-09-10T09:14:00.000Z527.1

(3 rows)

SELECT round((513 * 115 + 515.6 * 119) / 234.0, 2) AS by_hand, round(twap(ltp, timestamp), 2) AS twa

by_hand (DOUBLE)twap (DOUBLE)
514.32514.32

(1 row)

Example: Hourly TWAP of one-minute closes (sample data: sample)

SELECT time, round(twap(close, time), 2) AS twap, round(avg(close), 2) AS plain_avg
FROM ochl_minutes
WHERE symbol = 'COAST' AND time IN '2026-09-10'
SAMPLE BY 1h
LIMIT 3;

Result:

time (TIMESTAMP)twap (DOUBLE)plain_avg (DOUBLE)
2026-09-10T05:00:00.000Z380.41380.39
2026-09-10T06:00:00.000Z380.32380.31
2026-09-10T07:00:00.000Z378.05378.06

(3 rows)

weighted_avg, wavg

Syntax:

weighted_avg(x, weight)
wavg(x, weight)

The weighted average: sum(x × weight) / sum(weight). A row with weight 2 counts twice as much as a row with weight 1. wavg is the same function.

ArgumentTypeMeaning
xnumberthe values
weightnumberhow much each value counts

Example: Average buy rate of each broker, weighted by the shares bought (sample data: sample)

SELECT buyer, count(*) AS trades, sum(quantity) AS shares,
       round(weighted_avg(rate, quantity), 2) AS avg_rate_weighted,
       round(avg(rate), 2) AS avg_rate_plain
FROM trade_log
WHERE symbol = 'CITY'
GROUP BY buyer
ORDER BY buyer;

Result:

buyer (INT)trades (LONG)shares (LONG)avg_rate_weighted (DOUBLE)avg_rate_plain (DOUBLE)
151170515.06515.5
341500514.5514.5
453530515.94515.33
492220517.77516.75

(4 rows)

Example: Average interest rate of three loans

SELECT round(wavg(rate_pct, amount), 3) AS weighted_rate, round(avg(rate_pct), 3) AS plain_avg
FROM (SELECT 500000 AS amount, 9.5 AS rate_pct
      UNION ALL SELECT 100000, 12.0
      UNION ALL SELECT 50000, 14.0) loans;

Result:

weighted_rate (DOUBLE)plain_avg (DOUBLE)
10.23111.833

(1 row)

count_if

Syntax:

count_if(condition)

Counts the rows where the condition is true. count_if(plan = 'pro') is the number of pro users. It is shorter than sum(CASE WHEN plan = 'pro' THEN 1 ELSE 0 END).

ArgumentTypeMeaning
conditiontrue/falsea comparison or any true/false expression

Example: App users per city: all, pro, and who logged in since 10 September (sample data: query-app)

SELECT city, count(*) AS users,
       count_if(plan = 'pro') AS pro_users,
       count_if(last_login >= '2026-09-10') AS active_users
FROM app_users
GROUP BY city
ORDER BY users DESC, city;

Result:

city (STRING)users (LONG)pro_users (LONG)active_users (LONG)
Kathmandu211
Pokhara211
NULL200
Dharan111
Lalitpur101

(5 rows)

Example: Up days of each symbol in the last 5 days (with OVER) (sample data: sample)

SELECT time, symbol, open, close,
       count_if(close > open) OVER (PARTITION BY symbol ORDER BY time ROWS 4 PRECEDING) AS up_days_of_5
FROM ochl_day
WHERE time >= '2026-08-30'
ORDER BY symbol, time
LIMIT 8;

Result:

time (TIMESTAMP)symbol (SYMBOL)open (DOUBLE)close (DOUBLE)up_days_of_5 (LONG)
2026-08-30T00:00:00.000ZCITY507.3503.10
2026-08-31T00:00:00.000ZCITY502.5505.71
2026-09-01T00:00:00.000ZCITY504.9502.41
2026-09-02T00:00:00.000ZCITY502.1496.41
2026-09-03T00:00:00.000ZCITY497.7497.92
2026-09-06T00:00:00.000ZCITY497.44993
2026-09-07T00:00:00.000ZCITY497.3498.73
2026-09-08T00:00:00.000ZCITY498.2497.53

(8 rows)

Example: The argument must be a condition (sample data: query-app; shows an error)

SELECT count_if(plan) FROM app_users;

Result:

Error: line 1, column 8: count_if: needs a condition (true/false), got 'pro'

sum_if

Syntax:

sum_if(x, condition)

Adds up x only in the rows where the condition is true. Shorter than sum(CASE WHEN condition THEN x END).

ArgumentTypeMeaning
xany numberthe value to add
conditiontrue/falsewhich rows to add

Example: Money into and out of the bank account, and only the CITY entries (sample data: query-app)

SELECT sum(debit) AS money_in, sum(credit) AS money_out,
       sum_if(debit, memo LIKE '%CITY%') AS city_in,
       sum_if(credit, memo LIKE '%CITY%') AS city_out
FROM ledger
WHERE account = 1100;

Result:

money_in (DECIMAL(2))money_out (DECIMAL(2))city_in (DECIMAL(2))city_out (DECIMAL(2))
518850.00288530.0018850.001030.00

(1 row)

Example: Buy and sell turnover of broker 1 on each day (sample data: sample)

SELECT date,
       sum_if(amount, buyer = 1) AS bought,
       sum_if(amount, seller = 1) AS sold,
       coalesce(sum_if(amount, buyer = 1), 0) - coalesce(sum_if(amount, seller = 1), 0) AS net
FROM trade_log
GROUP BY date
ORDER BY date;

Result:

date (TIMESTAMP)bought (DOUBLE)sold (DOUBLE)net (DOUBLE)
2026-09-09T00:00:00.000Z5150005150509850
2026-09-10T00:00:00.000Z239420486650-247230

(2 rows)

avg_if

Syntax:

avg_if(x, condition)

The average of x over only the rows where the condition is true.

ArgumentTypeMeaning
xany numberthe values
conditiontrue/falsewhich rows to use

Example: Average volume on up days and on down days (sample data: sample)

SELECT symbol,
       round(avg_if(volume, close > open)) AS avg_volume_up_days,
       round(avg_if(volume, close < open)) AS avg_volume_down_days
FROM ochl_day
WHERE time >= '2026-08-23'
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)avg_volume_up_days (DOUBLE)avg_volume_down_days (DOUBLE)
CITY123828102885
COAST9595399090
VOLT8742197304

(3 rows)

product

Syntax:

product(x)

All values multiplied together. Traders use it to compound daily returns: a rise of 1% and then 2% is 1.01 × 1.02 = 1.0302, a growth of 3.02%.

ArgumentTypeMeaning
xany numberthe values

Example: Growth in September: multiply the daily price ratios (sample data: sample)

SELECT symbol, count(ratio) AS days, round(product(ratio), 4) AS growth
FROM (SELECT symbol, close / lag(close) OVER (PARTITION BY symbol ORDER BY time) AS ratio
      FROM ochl_day
      WHERE time >= '2026-09-01') t
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)days (LONG)growth (DOUBLE)
CITY71.005
COAST71.0237
VOLT71.1314

(3 rows)

Example

SELECT product(x) AS product FROM (SELECT 2 AS x UNION ALL SELECT 3 UNION ALL SELECT NULL UNION ALL SELECT 4) t;

Result:

product (DOUBLE)
24

(1 row)

bool_and, every

Syntax:

bool_and(condition)
every(condition)

true when the condition is true in every row, else false. every is the same function.

ArgumentTypeMeaning
conditiontrue/falsethe check for each row

Example: Did every user log in? Is every email good? Is there an admin? (sample data: query-app)

SELECT bool_and(last_login IS NOT NULL) AS all_logged_in,
       every(email LIKE '%@%') AS all_emails_have_at,
       bool_or(plan = 'admin') AS has_admin
FROM app_users;

Result:

all_logged_in (BOOL)all_emails_have_at (BOOL)has_admin (BOOL)
falsetruetrue

(1 row)

Example: Did the share close above its open on every day of the week? (sample data: sample)

SELECT symbol, bool_and(close > open) AS up_every_day, bool_or(close > open) AS up_some_day
FROM ochl_day
WHERE time >= '2026-09-06'
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)up_every_day (BOOL)up_some_day (BOOL)
CITYfalsetrue
COASTfalsetrue
VOLTfalsetrue

(3 rows)

bool_or

Syntax:

bool_or(condition)

true when the condition is true in at least one row, else false.

ArgumentTypeMeaning
conditiontrue/falsethe check for each row

Example: Which brokers made a trade of 1,000 shares or more? (sample data: sample)

SELECT buyer, count(*) AS trades, bool_or(quantity >= 1000) AS has_big_trade
FROM trade_log
GROUP BY buyer
ORDER BY buyer;

Result:

buyer (INT)trades (LONG)has_big_trade (BOOL)
19false
345false
456true
496true
586true

(5 rows)

arg_max, argmax, max_by

Syntax:

arg_max(value, by)
argmax(value, by)
max_by(value, by)

The value from the row where by is biggest. arg_max(time, close) answers "on which day was the close highest?". max(close) only gives the highest close, not its day. argmax and max_by are the same function.

ArgumentTypeMeaning
valueany typewhat to give back
bynumber, text or timewhat to find the biggest of

Example: The day of the highest close and of the lowest close (sample data: sample)

SELECT symbol,
       max(close) AS highest_close, arg_max(time, close) AS highest_day,
       min(close) AS lowest_close, arg_min(time, close) AS lowest_day
FROM ochl_day
WHERE time >= '2026-08-23'
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)highest_close (DOUBLE)highest_day (TIMESTAMP)lowest_close (DOUBLE)lowest_day (TIMESTAMP)
CITY513.32026-08-25T00:00:00.000Z496.42026-09-02T00:00:00.000Z
COAST3912026-08-23T00:00:00.000Z367.12026-08-27T00:00:00.000Z
VOLT191.22026-09-10T00:00:00.000Z1692026-09-01T00:00:00.000Z

(3 rows)

Example: The user who logged in last, and the newest user's plan (sample data: query-app)

SELECT max_by(username, last_login) AS last_seen_user,
       argmax(plan, created_at) AS newest_users_plan
FROM app_users;

Result:

last_seen_user (STRING)newest_users_plan (STRING)
bikashfree

(1 row)

arg_min, argmin, min_by

Syntax:

arg_min(value, by)
argmin(value, by)
min_by(value, by)

The value from the row where by is smallest. arg_min(username, created_at) is the first user who signed up. argmin and min_by are the same function.

ArgumentTypeMeaning
valueany typewhat to give back
bynumber, text or timewhat to find the smallest of

Example: First user of each plan, and the cheapest trade of each symbol (sample data: sample, query-app)

SELECT plan, arg_min(username, created_at) AS first_user FROM app_users GROUP BY plan ORDER BY plan;
SELECT symbol, min(rate) AS lowest_rate, min_by(transaction_id, rate) AS trade, argmin(buyer, rate) AS buyer
FROM trade_log
GROUP BY symbol
ORDER BY symbol;

Result:

SELECT plan, arg_min(username, created_at) AS first_user FROM app_users GROUP BY plan ORDER BY plan

plan (STRING)first_user (STRING)
adminbikash
freesita
proram

(3 rows)

SELECT symbol, min(rate) AS lowest_rate, min_by(transaction_id, rate) AS trade, argmin(buyer, rate)

symbol (SYMBOL)lowest_rate (DOUBLE)trade (LONG)buyer (LONG)
CITY5131000031
COAST39810001358
TELCO8781000211
VOLT1841000111

(4 rows)

string_agg

Syntax:

string_agg(text, separator)

Joins the values of many rows into one text, with separator between them: 'ram, hari_99'. Useful for a short list per group in a report.

ArgumentTypeMeaning
textany typethe value of each row (numbers and times are written as text)
separatortextwhat goes between two values, like ', '

Example: Usernames per city (sample data: query-app)

SELECT city, count(*) AS users, string_agg(username, ', ') AS usernames
FROM app_users
GROUP BY city
ORDER BY city;

Result:

city (STRING)users (LONG)usernames (STRING)
Dharan1gita
Kathmandu2ram, hari_99
Lalitpur1bikash
Pokhara2sita, suman
NULL2anita, test_user

(5 rows)

Example: The trades of VOLT on each day, in one line (sample data: sample)

SELECT date, string_agg(quantity || ' @ ' || rate, ' | ') AS trades
FROM trade_log
WHERE symbol = 'VOLT'
GROUP BY date
ORDER BY date;

Result:

date (TIMESTAMP)trades (STRING)
2026-09-10T00:00:00.000Z50 @ 184 | 200 @ 184 | 50 @ 185 | 100 @ 187

(1 row)

Example: Sort the rows in a subquery to choose the order (sample data: sample)

SELECT string_agg(symbol, ' > ') AS biggest_first
FROM (SELECT symbol, sum(amount) AS turnover FROM trade_log GROUP BY symbol ORDER BY turnover DESC) t;

Result:

biggest_first (STRING)
TELCO > CITY > COAST > VOLT

(1 row)

listagg

Syntax:

listagg(text)
listagg(text, separator)

The same as string_agg, but the separator is optional: without it, the values are joined with ','. The name comes from Oracle and Snowflake.

ArgumentTypeMeaning
textany typethe value of each row
separatortextwhat goes between two values; default ','

Example: Symbols per sector (sample data: sample)

SELECT sector, listagg(symbol) AS symbols, listagg(symbol, ' / ') AS with_slash
FROM securities
GROUP BY sector
ORDER BY sector;

Result:

sector (STRING)symbols (STRING)with_slash (STRING)
BankingCITY,COASTCITY / COAST
EnergyVOLTVOLT
InvestmentACMEACME
TelecomTELCOTELCO

(4 rows)

array_agg, json_agg, jsonb_agg

Syntax:

array_agg(x)
json_agg(x)
jsonb_agg(x)

Collects the values of many rows into one JSON array: [513,515.6,527.1]. An app can read the array in one piece, for example the points of a small chart. json_agg and jsonb_agg are the same function; Postgres clients get jsonb_agg as PostgreSQL's jsonb type ([513, 515.6, 527.1]).

ArgumentTypeMeaning
xany typethe value of each row

Example: The live prices of each symbol as a JSON array, for a sparkline chart (sample data: sample)

SELECT symbol, array_agg(ltp) AS prices
FROM live_data
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)prices (DOUBLE[])
CITY[513,515.6,527.1]
COAST[390.6,391.4,401.9]
TELCO[860.5,869,897.7]
VOLT[184,185,184]

(4 rows)

Example: NULL values stay in the array (sample data: query-app)

SELECT plan, json_agg(full_name) AS names, array_agg(id) AS ids
FROM app_users
GROUP BY plan
ORDER BY plan;

Result:

plan (STRING)names (JSON)ids (INT[])
admin["Bikash Shrestha"][5]
free["Sita Karki","Hari Thapa","Anita Magar",null][2,3,6,8]
pro["Ram Sharma","Gita Rai","Suman Gurung"][1,4,7]

(3 rows)

json_object_agg, jsonb_object_agg

Syntax:

json_object_agg(key, value)
jsonb_object_agg(key, value)

Collects the rows into one JSON object: the key of each row, and its value. {"CITY":513,"COAST":287.5}.

ArgumentTypeMeaning
keyany type but JSONthe key; a number becomes text (1 gives "1")
valueany typeits value; JSON stays JSON

Example: Prices by symbol in one object (a symbol seen twice keeps its last row) (sample data: sample)

SELECT jsonb_object_agg(symbol, ltp ORDER BY symbol) AS prices
FROM live_data;

Result:

prices (JSON)
{"CITY":527.1,"COAST":401.9,"TELCO":897.7,"VOLT":184}

(1 row)