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_first

Prev Next

Applies to VAST Cluster 5.5.

Functions › List, struct and map functions › Aggregate a list. Returns the first element of a list, even when that element is NULL. To skip NULL elements, use list_any_value.

Syntax

list_first(l)
Argument Type Description
l a list of any type The list whose first element is returned.

Returns: the element type of the list (INTEGER for [1, 2], DOUBLE for a DOUBLE[]).

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_first. 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: numbers, words, measurements, flags and lists of lists. 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_first"'

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[], words VARCHAR[], vals DOUBLE[], "
    "flags BOOLEAN[], nested BIGINT[][])")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, [1, 2, 3], ['a', 'b', 'c'], [0.5, 2.5, -1.0], [true, false, true], [[1, 2], [3]]), "
    "(2, [3, NULL, 3], ['b', NULL, 'B'], [2.0, NULL], [NULL, true], [[], NULL, [4]]), "
    "(3, [], [], [], [], []), "
    "(4, NULL, NULL, NULL, NULL, NULL), "
    "(5, [-5, 0, -5, 10], ['\u00e9', 'Zo\u00eb', '\U0001F1EB\U0001F1F7', ''], [1e308, 1e308, -1e-308], "
    "[false, false], [[5, NULL], [], [6, 7, 8]])")
[]
[]
[(5,)]

What it does

list_first(l) returns the element at the first position of l, whatever its value:

  • list_first([1, 2]) is 1.
  • list_first([NULL, 2]) is NULL: the first element is NULL, and list_first doesn't skip it.
  • An empty list and a NULL list return NULL.
  • In a list of lists, the result is the first inner list: list_first([[1], [2]]) is [1].

NULLs and edge cases

Input Result
NULL NULL
[] NULL
[NULL, 2] NULL
[1, 2] 1
[[1], [2]] [1]
A number, string, BLOB, date or boolean 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 helpers from Set up the examples:

run("SELECT list_first(NULL), list_first([]::BIGINT[]), list_first([NULL, 2]), list_first([1, 2]), list_first([[1], [2]])")
run("SELECT list_first(123)")
[(None, None, None, 1, [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 first element of each list in the table. Row 2's first inner list is empty, so list_first(nested) returns [] for it:

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

The rows whose list of numbers has a first element:

run(f"SELECT id, nums FROM {TABLE} WHERE list_first(nums) IS NOT NULL ORDER BY id LIMIT 10")
[(1, [1, 2, 3]), (2, [3, None, 3]), (5, [-5, 0, -5, 10])]

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_first() always runs in the Query Engine:

  • In the SELECT list, a projection step computes list_first() for each row the scan returns.
  • In WHERE, the scan sends the list for every row, and a FILTER step computes list_first() 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_first(nums) IS NOT NULL")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│ (list_aggr(nums, 'first') │
│        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_first() 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 first element if you filter on it often. Keep it in its own column when you write the rows: first_num = 1 is pushed down, list_first(nums) = 1 isn't.

Limitations and errors

  • Lists only. Any other type fails with No function matches the given name and argument types. The error names list_aggr, the function list_first calls, and its accepted form list_aggr(ANY[], VARCHAR, [ANY...]) -> ANY.
  • No array_first. It fails with Scalar Function with name array_first does not exist!. Use list_first.
  • ORDER BY needs a LIMIT. Without one, the query fails with The operator 'ORDER_BY' type=1 is currently not supported for this usage.
run("SELECT array_first([1, 2])")
InternalError: Catalog Error: Scalar Function with name array_first does not exist!

Similar functions

Function What it returns Result for [3, 1, NULL, 3, 10]
list_first the first element, even if NULL 3
list_any_value the first element that isn't NULL 3
list_last the last element, even if NULL 10
l[-1] (list_extract) the last element, by negative index 10

On [NULL, 2], list_first returns NULL and list_any_value returns 2:

run("SELECT list_first(l), list_any_value(l), list_last(l), l[-1] FROM (SELECT [3, 1, NULL, 3, 10] AS l)")
run("SELECT list_first([NULL, 2]), list_any_value([NULL, 2])")
[(3, 3, 10, 10)]
[(None, 2)]

Clean up:

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

See also