Applies to VAST Cluster 5.5.
Functions › List, struct and map functions › Read a field. Returns the value of one field of a struct, by its name. s.field, s['field'] and array_extract(s, 'field') are the same function; on a struct with unnamed fields, struct_extract also takes a position.
Syntax
struct_extract(s, 'field')
s.field
s['field']
array_extract(s, 'field')
struct_extract(s, position)
| Argument | Type | Description |
|---|---|---|
s |
a struct | The struct to read. |
field |
a constant VARCHAR |
The name of the field. The match ignores case. |
position |
a constant BIGINT |
On a struct with unnamed fields only, such as one from row: the position of the field, starting at 1. |
Returns: the type of the field.
Aliases: s.field, s['field'], and array_extract(s, 'field') when its first argument is a struct.
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_structs_struct_extract. 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 with a struct column st of two fields, a INTEGER and b VARCHAR. Row 2 has a NULL field, row 3 has two NULL fields, row 4 has a NULL struct, and row 5 has the largest INTEGER and an empty string:
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_structs_struct_extract"'
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}.items"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, st STRUCT(a INTEGER, b VARCHAR))")
run(f"INSERT INTO {TABLE} VALUES "
"(1, {'a': 1, 'b': 'x'}), "
"(2, {'a': -7, 'b': NULL}), "
"(3, {'a': NULL, 'b': NULL}), "
"(4, NULL), "
"(5, {'a': 2147483647, 'b': ''})")
[]
[]
[(5,)]
What it does
struct_extract(s, 'field') returns the value of the field called field, with the field's type:
- Four spellings, one function.
s.field,s['field'],struct_extract(s, 'field')andarray_extract(s, 'field')return the same values. The dot form also works as(s).field,table.s.fieldand"s"."field". - Names ignore case.
struct_extract({'a': 1}, 'A')and({'A': 1}).aboth return 1. - Any name works in the string forms, including names with a space, a dot or a non-ASCII letter:
struct_extract({'a.b': 1}, 'a.b')is 1. In the dot form, quote such a name:s."a b". - Nested fields chain:
s.a.b, orstruct_extract(struct_extract(s, 'a'), 'b'). A field that is a list can be indexed:({'a': [1, 2]}).a[2]is 2. - The name must be a constant and the field must exist; both are checked when the query is planned, before any row is read.
- Unnamed fields are read by position. On a struct from row,
struct_extract(row(1, 'x'), 2)androw(1, 'x')[2]return'x'. For a named struct, use struct_extract_at.
NULLs and edge cases
| Input | Result |
|---|---|
A NULL struct: struct_extract(NULL::STRUCT(a BIGINT), 'a') |
NULL |
struct_extract(NULL, 'a') |
NULL |
A NULL name: struct_extract({'a': 1}, NULL) |
NULL |
struct_extract({'a': 1}, 'A'), struct_extract({'A': 1}, 'a') |
1 |
struct_extract({'a b': 1}, 'a b'), struct_extract({'a.b': 1}, 'a.b'), struct_extract({'\u00e9': 1}, '\u00e9') |
1 |
struct_extract(struct_extract({'a': {'b': 2}}, 'a'), 'b') |
2 |
struct_extract(row(1, 'x'), 2) |
'x' |
struct_extract({'a': 1}, 'b') (a missing field) |
Error: Binder Error: Could not find key "b" in struct, followed by the names it has |
struct_extract({'a': 1}, '') (an empty name) |
Error: Key name for struct_extract needs to be neither NULL nor empty |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run("SELECT struct_extract(NULL::STRUCT(a BIGINT), 'a'), struct_extract(NULL, 'a'), struct_extract({'a': 1}, NULL)")
run("SELECT struct_extract({'a': 1}, 'A'), struct_extract({'A': 1}, 'a'), struct_extract({'a b': 1}, 'a b'), "
"struct_extract({'a.b': 1}, 'a.b'), struct_extract({'\u00e9': 1}, '\u00e9')")
run("SELECT struct_extract(struct_extract({'a': {'b': 2}}, 'a'), 'b'), struct_extract(row(1, 'x'), 2)")
run("SELECT struct_extract({'a': 1}, 'b')")
run("SELECT struct_extract({'a': 1}, '')")
[(None, None, None)]
[(1, 1, 1, 1, 1)]
[(2, 'x')]
InternalError: Binder Error: Could not find key "b" in struct
InternalError: Binder Error: Key name for struct_extract needs to be neither NULL nor empty
A field read from a NULL struct and a NULL field both give NULL. To tell them apart, test the struct itself: st IS NULL is true for row 4 only, while st.a IS NULL is true for rows 3 and 4:
run(f"SELECT id FROM {TABLE} WHERE st IS NULL ORDER BY id LIMIT 10")
run(f"SELECT id FROM {TABLE} WHERE st.a IS NULL ORDER BY id LIMIT 10")
[(4,)]
[(3,), (4,)]
Examples
Each field, in each spelling:
run(f"SELECT id, st.a, st['b'], struct_extract(st, 'a'), array_extract(st, 'b') FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 1, 'x', 1, 'x'), (2, -7, None, -7, None), (3, None, None, None, None), (4, None, None, None, None), (5, 2147483647, '', 2147483647, '')]
Keep only the rows whose field a is positive:
run(f"SELECT id, st.b FROM {TABLE} WHERE st.a > 0 ORDER BY id LIMIT 10")
[(1, 'x'), (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):
- In the
SELECTlist, the scan reads the field the query names, and the Query Engine returns it. - In
WHERE, a comparison of a field with a constant is evaluated in VAST DataBase when it's writtens.fieldorstruct_extract(s, 'field'):=,>andBETWEENare pushed down, rows that don't match never leave storage, and the plan has noFILTERstep. - Other conditions on a field run in a
FILTERstep in the Query Engine: the bracket forms['field'] = ...,array_extract(s, 'field') = ...,IN (...),LIKE,IS NULLandIS NOT NULL, and a comparison of the whole struct.
The plan of st.a = 1 has no FILTER step; the same condition written with brackets has one:
plan(f"SELECT id FROM {TABLE} WHERE st.a = 1")
plan(f"SELECT id FROM {TABLE} WHERE st['a'] = 1")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (array_extract(st, 'a') = │
│ 1) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ st.a │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Filter with
s.fieldorstruct_extract(s, 'field'), not withs['field']orarray_extract: only the first two are pushed down to VAST DataBase. - Use
BETWEENfor a range of values.st.a BETWEEN 0 AND 2is pushed down;st.a IN (1, -7)runs in aFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE st.a BETWEEN 0 AND 2")
plan(f"SELECT id FROM {TABLE} WHERE st.a IN (1, -7)")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ ((struct_extract(st, 'a') │
│ = 1) OR (struct_extract(st│
│ , 'a') = -7)) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ st.a │
│ id │
│ │
└───────────────────────────┘
- Select the fields you need, not the whole struct.
SELECT st.breads only that field. The plan lists what the scan reads:
plan(f"SELECT st.b FROM {TABLE}")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ struct_extract(st, 'b') │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: st.b │
│ │
└───────────────────────────┘
Limitations and errors
- The field must exist. A missing name fails when the query is planned with
Could not find key "..." in struct, even if no row would reach it. - The name must be a constant.
struct_extract(st, k)with a columnk, andst[k], fail withKey name for struct_extract needs to be a constant string. - Names on named structs, positions on unnamed ones. A position on a named struct fails with
struct_extract with an integer key can only be used on unnamed structs, use a string key instead; a name on a struct fromrow()fails withstruct_extract with a string key cannot be used on an unnamed struct, use a numeric index instead. - A field keeps its type in arithmetic.
st.ais anINTEGER, sost.a + 1on row 5 fails withOverflow in addition of INT32 (2147483647 + 1)!. Cast the field toBIGINTfirst. - The first argument must be a struct. A string, number, list, map or date fails with
No function matches the given name and argument types; the error listsstruct_extract(STRUCT, VARCHAR)andstruct_extract(STRUCT, BIGINT). On a list,array_extractreads an element by position instead (list_extract). - Other SQL dialects' names aren't available.
get_fieldfails withScalar Function with name get_field does not exist!. Usestruct_extractors.field.
run(f"SELECT id, st.c FROM {TABLE} ORDER BY id LIMIT 10")
run("SELECT struct_extract({'a': 1}, 1)")
run("SELECT struct_extract(row(1, 'x'), 'a')")
run(f"SELECT id, st.a + 1 FROM {TABLE} ORDER BY id LIMIT 10")
run(f"SELECT id, CAST(st.a AS BIGINT) + 1 FROM {TABLE} ORDER BY id LIMIT 10")
run("SELECT get_field({'a': 1}, 'a')")
InternalError: Binder Error: Could not find key "c" in struct
InternalError: Binder Error: struct_extract with an integer key can only be used on unnamed structs, use a string key instead
InternalError: Binder Error: struct_extract with a string key cannot be used on an unnamed struct, use a numeric index instead
OSError: Overflow in addition of INT32 (2147483647 + 1)!
[(1, 2), (2, -6), (3, None), (4, None), (5, 2147483648)]
InternalError: Catalog Error: Scalar Function with name get_field does not exist!
Similar functions
| Function | Reads | Result for {'a': 1, 'b': 'x'} |
|---|---|---|
struct_extract(s, 'b'), s.b, s['b'], array_extract(s, 'b') |
a field by name | 'x' |
struct_extract_at(s, 2) |
a field by position | 'x' |
json_extract_string(to_json(s), '$.b') |
a field of the struct's JSON text | 'x' |
run("SELECT struct_extract({'a': 1, 'b': 'x'}, 'b'), {'a': 1, 'b': 'x'}.b, {'a': 1, 'b': 'x'}['b'], "
"array_extract({'a': 1, 'b': 'x'}, 'b'), struct_extract_at({'a': 1, 'b': 'x'}, 2), "
"json_extract_string(to_json({'a': 1, 'b': 'x'}), '$.b')")
[('x', 'x', 'x', 'x', 'x', 'x')]
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 struct functions.
- struct_pack: build a struct.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.