Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

list_mode

Prev Next

Applies to VAST Cluster 5.5.

Functions › List, struct and map functions › Aggregate a list. Returns the most frequent element of a list, one value per row. NULL elements are skipped. It's the same as list_aggregate(l, 'mode'), and it works although the mode aggregate over rows isn't available.

Syntax

list_mode(l)
Argument Type Description
l a list of any type The list whose most frequent element you want.

Returns: the element type of l.

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_mode. 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_mode"'

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_mode(l) counts how often each element of l that isn't NULL appears, and returns the one that appears most. list_mode([1, 2, 2, 3]) returns 2.

  • NULLs don't count. list_mode([NULL, NULL, 1]) returns 1.
  • Any type that can be compared for equality works: numbers, text (list_mode(['b', 'a', 'a']) returns 'a') and lists (list_mode([[1], [1], [2]]) returns [1]).
  • Ties. When several elements are equally frequent, one of them is returned. In the cases on this page, it's the one that appears first: list_mode([2, 1, 1, 2]) returns 2 and list_mode([3, 1, 2]) returns 3. Don't rely on which tied element comes back; if you need a rule, write it with list_aggregate(l, 'histogram'), which returns every count.
  • The result keeps the element type: INTEGER for [1, 2], DOUBLE for a DOUBLE list.

NULLs and edge cases

Input Result
list_mode(NULL) NULL
list_mode(NULL::BIGINT[]) NULL
list_mode([]::BIGINT[]) NULL
list_mode([NULL, NULL, 1]) 1
list_mode([2, 1, 1, 2]) 2
list_mode([3, 1, 2]) 3
list_mode(['NaN'::DOUBLE, 'NaN'::DOUBLE, 1.0]) nan
list_mode([[1], [1], [2]]) [1]
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_mode(NULL), list_mode(NULL::BIGINT[]), list_mode([]::BIGINT[]), list_mode([NULL, NULL, 1])")
run("SELECT list_mode([2, 1, 1, 2]), list_mode([3, 1, 2]), list_mode(['NaN'::DOUBLE, 'NaN'::DOUBLE, 1.0]), "
    "list_mode([[1], [1], [2]])")
run(f"SELECT list_mode(123) FROM {TABLE} LIMIT 1")
[(None, None, None, 1)]
[(2, 3, nan, [1])]
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 most frequent element of each row's numbers, measurements and words. Row 2 has 3 twice and row 5 has -5 twice; in the other rows every element appears once:

run(f"SELECT id, list_mode(nums), list_mode(vals), list_mode(words) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 1, 0.5, 'a'), (2, 3, 2.0, 'b'), (3, None, None, None), (4, None, None, None), (5, -5, 1e+308, 'é')]

The most frequent value, and the count of every value:

run("SELECT list_mode([1, 2, 2, 3]), list_aggregate([1, 2, 2], 'histogram'), list_mode(['b', 'a', 'a'])")
[(2, [(1, 1), (2, 2)], 'a')]

The rows that have a most frequent number:

run(f"SELECT id FROM {TABLE} WHERE list_mode(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_mode() 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 SELECT list, a projection step computes the mode of each row's list after the scan. The plan has no FILTER step.
  • In WHERE, the scan sends the list for every row, and a FILTER step computes the mode 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_mode(nums) IS NOT NULL")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│  (list_aggr(nums, 'mode') │
│        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, so list_mode() runs only on the rows that pass it. The plan for id > 1 alone has no FILTER step:
plan(f"SELECT id FROM {TABLE} WHERE id > 1")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
  • Store the mode 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 function list_mode is built on.
  • No mode over rows. The mode aggregate over a table's rows isn't available. list_mode works on one row's list.
  • ORDER BY needs a LIMIT. Without one, the query fails with The 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_mode(l) most frequent value 3
list_aggregate(l, 'mode') the same, by name 3
list_histogram(l) how often each value appears [(1, 1), (3, 2), (10, 1)]: each value with its count
list_median(l) middle value 3.0
list_avg(l) mean 4.25
run("SELECT list_mode(l), list_aggregate(l, 'mode'), list_histogram(l), list_median(l), list_avg(l) "
    "FROM (SELECT [3, 1, NULL, 3, 10] AS l)")
[(3, 3, [(1, 1), (3, 2), (10, 1)], 3.0, 4.25)]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also