Applies to VAST Cluster 5.5.
Functions › List, struct and map functions › Aggregate a list. Returns the middle value of the elements of a list, one value per row. NULL elements are skipped. It's the same as list_aggregate(l, 'median'), and it works although the median aggregate over rows isn't available.
Syntax
list_median(l)
| Argument | Type | Description |
|---|---|---|
l |
a list of numbers, text or dates | The list whose middle value you want. |
Returns: DOUBLE for a list of numbers; the element type for a list of text; TIMESTAMP for a list of dates.
Set up the examples
The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_lists_list_median. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.
This block connects, defines the run() and plan() helpers, and creates a table of five rows of lists of numbers, measurements and words. Row 3 has empty lists and row 4 has no values:
import os
import re
import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb
BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_lists_list_median"'
conn = dbapi.connect(
driver=adbc_driver_vastdb.get_driver_path(),
db_kwargs={
"vast.db.endpoint": os.environ["QE_ENDPOINT"],
"vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
"vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
},
autocommit=True,
)
def server_message(e):
"""The server's message in a driver error, without the request ID or the echoed statement."""
text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
found = (re.search(r'message: "(.*?)", request_id', text, re.S)
or re.search(r"description: (.*)\)", text, re.S))
message = found.group(1) if found else text.strip().splitlines()[-1]
message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
return message.replace(BUCKET, "<bucket>")
def run(sql):
"""Run one statement. Print the rows it returns, or the error."""
try:
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall() if cur.description else []
except (adbc_driver_manager.Error, OSError) as e:
print(f"{type(e).__name__}: {server_message(e)}")
else:
print(rows)
def plan(sql):
"""Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
with conn.cursor() as cur:
cur.execute("EXPLAIN " + sql)
steps = cur.fetchall()[0][1]
print("\n".join(line for line in steps.splitlines() if "Rows" not in line))
TABLE = f"{SCHEMA}.samples"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, nums BIGINT[], vals DOUBLE[], words VARCHAR[])")
run(f"INSERT INTO {TABLE} VALUES "
"(1, [1, 2, 3], [0.5, 2.5, -1.0], ['a', 'b', 'c']), "
"(2, [3, NULL, 3], [2.0, NULL], ['b', NULL, 'B']), "
"(3, [], [], []), "
"(4, NULL, NULL, NULL), "
"(5, [-5, 0, -5, 10], [1e308, 1e308, -1e-308], ['\u00e9', 'Zo\u00eb', '\U0001F1EB\U0001F1F7', ''])")
[]
[]
[(5,)]
What it does
list_median(l) sorts the elements of l that aren't NULL and returns the one in the middle. list_median([1, 3, 2]) returns 2.0.
- An even count of numbers gives the mean of the two middle values.
list_median([1, 2, 3, 4])returns2.5. - An even count of text values gives the lower of the two. Text can't be averaged, so
list_median(['a', 'b'])returns'a', andlist_median(['a', 'b', 'c', 'd'])returns'b'. In the example table, row 2's words'b'and'B'have the median'B'. - Dates give a
TIMESTAMP. The median ofDATE '2026-10-16'andDATE '2026-10-18'is2026-10-17 00:00:00. - The result for numbers is
DOUBLE, for integer lists too.
NULLs and edge cases
| Input | Result |
|---|---|
list_median(NULL) |
NULL |
list_median(NULL::BIGINT[]) |
NULL |
list_median([]::BIGINT[]) |
NULL |
list_median([NULL, 1, NULL]) |
1.0 |
list_median([1, 2]) |
1.5 |
list_median(['a', 'b']) |
'a' |
list_median([DATE '2026-10-16', DATE '2026-10-18']) |
2026-10-17 00:00:00 (TIMESTAMP) |
list_median([9223372036854775807, 9223372036854775806]) |
9.223372036854776e+18 |
list_median(['NaN'::DOUBLE, 1.0, 2.0]) |
1.0 |
| A number, string, date or other non-list | Error: Binder Error: No function matches the given name and argument types, for example 'list_aggr(INTEGER_LITERAL, STRING_LITERAL)' |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run("SELECT list_median(NULL), list_median(NULL::BIGINT[]), list_median([]::BIGINT[]), "
"list_median([NULL, 1, NULL]), list_median([1, 2]), list_median(['a', 'b'])")
run("SELECT list_median([DATE '2026-10-16', DATE '2026-10-18']), "
"list_median([9223372036854775807, 9223372036854775806]), list_median(['NaN'::DOUBLE, 1.0, 2.0])")
run(f"SELECT list_median(123) FROM {TABLE} LIMIT 1")
[(None, None, None, 1.0, 1.5, 'a')]
[(datetime.datetime(2026, 10, 17, 0, 0), 9.223372036854776e+18, 1.0)]
InternalError: Binder Error: No function matches the given name and argument types 'list_aggr(INTEGER_LITERAL, STRING_LITERAL)'. You might need to add explicit type casts.
Examples
The middle value of each row's numbers, measurements and words:
run(f"SELECT id, list_median(nums), list_median(vals), list_median(words) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 2.0, 0.5, 'b'), (2, 3.0, 2.0, 'B'), (3, None, None, None), (4, None, None, None), (5, -2.5, 1e+308, 'Zoë')]
Odd and even counts, for numbers and text:
run("SELECT list_median([1, 3, 2]), list_median([1, 2, 3, 4]), list_median(['a', 'b', 'c', 'd'])")
[(2.0, 2.5, 'b')]
The rows that have a median:
run(f"SELECT id FROM {TABLE} WHERE list_median(nums) IS NOT NULL ORDER BY id LIMIT 10")
[(1,), (2,), (5,)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). The scan reads a list column whole, and list_median() always runs in the Query Engine. It works within each row's list, so it's a scalar function: it returns one value per row and needs no GROUP BY.
- In the
SELECTlist, a projection step computes the median of each row's list after the scan. The plan has noFILTERstep. - In
WHERE, the scan sends the list for every row, and aFILTERstep computes the median and keeps the rows that match. No rows are skipped in storage.
The plan of the WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE list_median(nums) IS NOT NULL")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (list_aggr(nums, 'median')│
│ IS NOT NULL) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ nums │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Add a condition on a plain column. A comparison of a column with a constant, such as
id > 1, is evaluated in VAST DataBase, solist_median()runs only on the rows that pass it. The plan forid > 1alone has noFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE id > 1")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Store the median if you filter on it often. Compute it when you write the rows and keep it in its own column, where a comparison with a constant is pushed down.
Limitations and errors
- Lists only. A value that isn't a list fails with an error that names
list_aggr, the functionlist_medianis built on. - No median over rows. The
medianaggregate over a table's rows isn't available.list_medianworks on one row's list. ORDER BYneeds aLIMIT. Without one, the query fails withThe operator 'ORDER_BY' type=1 is currently not supported for this usage.
Similar functions
| Function | What it does | Result for [3, 1, NULL, 3, 10] |
|---|---|---|
list_median(l) |
middle value | 3.0 |
list_aggregate(l, 'median') |
the same, by name | 3.0 |
list_aggregate(l, 'quantile_cont', 0.5) |
the 0.5 quantile, which is the median; other fractions give other quantiles | 3.0 |
list_avg(l) |
mean | 4.25 |
list_mode(l) |
most frequent value | 3 |
run("SELECT list_median(l), list_aggregate(l, 'median'), list_aggregate(l, 'quantile_cont', 0.5), list_avg(l), list_mode(l) "
"FROM (SELECT [3, 1, NULL, 3, 10] AS l)")
[(3.0, 3.0, 3.0, 4.25, 3)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- List, struct and map functions: all the list aggregates.
- Aggregate functions: aggregates over the rows of a table.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.