Applies to VAST Cluster 5.5.
Functions › List, struct and map functions › Search a list. Returns the position, counted from 1, of the first element of a list that equals a value, or NULL if no element does. list_indexof, array_position and array_indexof are the same function.
Syntax
list_position(list, element)
list_indexof(list, element)
array_position(list, element)
array_indexof(list, element)
| Argument | Type | Description |
|---|---|---|
list |
a list of any element type | The list to search. |
element |
the list's element type, or one that converts to it | The value to look for. |
Returns: INTEGER: the position of the first match, starting at 1, or NULL if there's none.
Aliases: list_indexof, array_position, array_indexof.
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_position. 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 four delivery routes, each with its list of stops in order and a list of stop numbers. Route 2 has a NULL stop number, route 3 has no stops, and the last row 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_position"'
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}.routes"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, stops VARCHAR[], stop_ids BIGINT[])")
run(f"INSERT INTO {TABLE} VALUES "
"(1, ['depot', 'mill', 'port', 'depot'], [10, 20, 30, 10]), "
"(2, ['port', 'depot'], [30, NULL]), "
"(3, [], []), "
"(4, NULL, NULL)")
[]
[]
[(4,)]
What it does
list_position looks for the first element of list that equals element and returns its position. Positions start at 1, as in list_extract and l[i], so l[list_position(l, x)] is x when x is in the list.
- The first match wins.
list_position([5, 5], 5)is1. - No match gives NULL, not
0: test the result withIS NOT NULL. - The comparison is exact and typed, as in list_contains: case-sensitive for strings, by value for numbers (
2.0is found at position 2 of[1, 2]), whole elements for nested lists, andNaNfindsNaN.
NULLs and edge cases
| Input | Result |
|---|---|
A NULL list: list_position(NULL, 3) |
NULL |
A NULL element: list_position([1, 2], NULL) |
NULL |
list_position([1, NULL], NULL) |
NULL: a NULL element can't be found |
list_position([1, 2, 3], 42) (not in the list) |
NULL |
list_position([], 1) (empty list) |
NULL |
list_position([5, 5], 5) |
1 |
list_position([1, 2], 2.0) |
2 |
list_position(['a', 'b'], 'B') |
NULL |
list_position([[1], [2]], [2]) |
2 |
list_position([1.0, 'NaN'::DOUBLE], 'NaN'::DOUBLE) |
2 |
list_position([1, 2], 'a') (a value of another type) |
Error: list_position: Cannot match element of type 'VARCHAR' in a list of type 'INTEGER[]' - an explicit cast is required |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run("SELECT list_position(NULL, 3), list_position([1, 2], NULL), list_position([1, NULL], NULL), "
"list_position([1, 2, 3], 42), list_position([], 1), list_position([5, 5], 5)")
run("SELECT list_position([1, 2], 2.0), list_position(['a', 'b'], 'B'), list_position([[1], [2]], [2]), "
"list_position([1.0, 'NaN'::DOUBLE], 'NaN'::DOUBLE)")
run("SELECT list_position([1, 2], 'a')")
[(None, None, None, None, None, 1)]
[(2, None, 2, 2)]
InternalError: Binder Error: list_position: Cannot match element of type 'VARCHAR' in a list of type 'INTEGER[]' - an explicit cast is required
Examples
Where each route first stops at the depot and at stop 30. Route 1 visits the depot twice; the first visit counts:
run(f"SELECT id, list_position(stops, 'depot'), array_position(stop_ids, 30) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 1, 3), (2, 2, 1), (3, None, None), (4, None, None)]
The routes that stop at the port, and the stop right after it. If the port were a route's last stop, the result would be NULL, because a position past the end of a list gives NULL:
run(f"SELECT id, stops[list_position(stops, 'port') + 1] FROM {TABLE} "
f"WHERE list_position(stops, 'port') IS NOT NULL ORDER BY id LIMIT 10")
[(1, 'depot'), (2, 'depot')]
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 list columns whole, and every condition on a list, list_position() included, runs in the Query Engine:
- In the
SELECTlist, the scan sends the list column and the Query Engine searches it for each row it returns. - In
WHERE, the scan sends the list for every row, and aFILTERstep in the Query Engine searches it and keeps the rows that match. No rows are skipped in storage.
The plan of a WHERE on the position shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE list_position(stops, 'port') IS NOT NULL")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (list_position(stops, │
│ 'port') IS NOT NULL) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ stops │
│ id │
│ │
└───────────────────────────┘
Make it faster
- To test membership, use list_contains.
list_position(l, x) IS NOT NULLandlist_contains(l, x)keep the same rows;list_containssays what you mean and returns aBOOLEAN. - Pair it with a pushed-down condition. A comparison of a plain column with a constant, such as
id > 1, is evaluated in VAST DataBase, so the rows it rules out never reach theFILTERstep.
run(f"SELECT id FROM {TABLE} WHERE id > 1 AND list_contains(stops, 'port') ORDER BY id LIMIT 10")
[(2,)]
Limitations and errors
- The first argument must be a list. A number, string, date, boolean or
BLOBfails withNo function matches the given name and argument types, for example'list_position(STRING_LITERAL, STRING_LITERAL)'. To find a substring, use strpos. - The value must match the element type. Otherwise the call fails with
Cannot match element of type ... - an explicit cast is required. - Only the first match, and never a NULL.
list_positionreturns one position, and a NULL element can't be found. To test for NULL elements, see list_contains.
run("SELECT list_position('abc', 'b')")
InternalError: Binder Error: No function matches the given name and argument types 'list_position(STRING_LITERAL, STRING_LITERAL)'. You might need to add explicit type casts.
Similar functions
| Function | Returns | Result for [3, 1, NULL, 3, 10] and 3 |
|---|---|---|
list_position(l, 3) |
the first position of 3 | 1 |
list_contains(l, 3) |
whether 3 is an element | true |
list_extract(l, 2), l[2] |
the element at position 2 (the inverse) | 1 |
list_position(l, NULL) |
NULL can't be found | NULL |
run("SELECT list_position(l, 3), list_contains(l, 3), list_extract(l, 2), l[2], list_position(l, NULL) "
"FROM (SELECT [3, 1, NULL, 3, 10] AS l)")
[(1, True, 1, 1, None)]
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 functions.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.