Array of rows overview

FeatureQL is entity-centric: each row in a query represents one entity (a customer, a product, an account). When an entity has many related facts — a customer's orders, a product's price history — those facts are stored as an array of rows inside the entity row rather than in a separate table.

This is the core data structure behind Entity Centric Modeling (ECM). Instead of joining across tables at query time, you keep everything nested within the entity and operate on it directly.

FeatureQL provides a rich set of operations for working with arrays of rows:

OperationSyntaxDescription
Access rowsarray[<index>] or array[<start>:<end>]Extract one row or a range of rows
Access fieldsarray[<field1>, <field2>...]Project specific fields from each row
UnwrapUNWRAP(array)Flatten a single-field array of rows into a plain array
ConcatenateARRAY_CONCAT(a, b DEDUPLICATED ON ...)Append rows from two arrays, with optional deduplication
ZipZIP(array1 AS name1, array2 AS name2)Combine separate arrays into an array of rows
Merge by positionARRAY_MERGE(a, b)Combine fields from two arrays row-by-row
Merge by joinARRAY_MERGE(a, b JOINED ON field)Combine fields by matching on a shared field
ExtendEXTEND(array WITH feature VIA ...)Enrich rows by joining on an external feature
TransformTRANSFORM(array WITH (subquery))Map, filter, or aggregate the rows

The full list of array functions is available in the Functions reference.

Creating arrays of rows

There are several ways to construct an array of rows. You can build it from ROW() literals, use ZIP() to combine separate arrays, or cast unnamed rows to give them field names.

FeatureQL
SELECT
    UNNAMED := ARRAY[
        ROW('D', 'E', 'F'),
        ROW('G', 'H', 'I')
    ],
    NAMED := ARRAY[
        ROW('D' as col1, 'E' as col2),
        ROW('G' as col1, 'H' as col2)
    ],
    RENAMED := ARRAY[
        ROW(4, 'A'),
        ROW(5, 'B'),
        ROW(6, 'C')
    ]::ARRAY(ROW(num BIGINT, str VARCHAR))
;
Result
UNNAMED VARCHARNAMED VARCHARRENAMED VARCHAR
[{field_1: D, field_2: E, field_3: F}, {field_1: G, field_2: H, field_3: I}][{col1: D, col2: E}, {col1: G, col2: H}][{num: 4, str: A}, {num: 5, str: B}, {num: 6, str: C}]

Accessing fields and rows

Use bracket notation to slice into an array of rows. A numeric index selects rows; a field name selects columns. You can chain both — for example, first pick specific rows, then extract a field — in either order.

FeatureQL
SELECT
    BASE := ARRAY[
        ROW(1, 'A'),
        ROW(2, 'B'),
        ROW(3, 'C'),
        ROW(4, 'D'),
    ]::ARRAY(ROW(col1 BIGINT, col2 VARCHAR)),
    SINGLE_ROW := BASE[2],
    MULTIPLE_ROWS := BASE[2,4],
    RANGE_ROWS := BASE[2:4],
    MULTIPLE_FIELDS := BASE[col2, col1],
    MULTIPLE_FIELDS_RENAMED := BASE[col2 as field2, col1 as field1],
    FIELD_THEN_ROW := BASE[col2][1, 3],
    ROW_THEN_FIELD := BASE[1, 3][col2],
;
Result
BASE VARCHARSINGLE_ROW ROWMULTIPLE_ROWS VARCHARRANGE_ROWS VARCHARMULTIPLE_FIELDS VARCHARMULTIPLE_FIELDS_RENAMED VARCHARFIELD_THEN_ROW ARRAYROW_THEN_FIELD ARRAY
[{col1: 1, col2: A}, {col1: 2, col2: B}, {col1: 3, col2: C}, {col1: 4, col2: D}]{col1: 2, col2: B}[{col1: 2, col2: B}, {col1: 4, col2: D}][{col1: 2, col2: B}, {col1: 3, col2: C}, {col1: 4, col2: D}][{col2: A, col1: 1}, {col2: B, col1: 2}, {col2: C, col1: 3}, {col2: D, col1: 4}][{field2: A, field1: 1}, {field2: B, field1: 2}, {field2: C, field1: 3}, {field2: D, field1: 4}][A, C][A, C]

Concatenating arrays of rows

ARRAY_CONCAT appends the rows of two arrays. When combining data from different sources, use DEDUPLICATED ON to keep only one row per key value. Rows from the first array take priority.

FeatureQL
WITH
    BASE1 := ARRAY[
        ROW(1, 'A', 'u1'),
        ROW(2, 'B', 'u2a')
    ]::ARRAY(ROW(col1 BIGINT, col2 VARCHAR, col3 VARCHAR)),
    BASE2 := ARRAY[
        ROW(2, 'BB', 'u2b'),
        ROW(2, 'BB', 'u2c'),
        ROW(3, 'CC', 'u3')
    ]::ARRAY(ROW(col1 BIGINT, col2 VARCHAR, col3 VARCHAR)),
SELECT
    NO_DEDUP := ARRAY_CONCAT(BASE1, BASE2),
    DEDUP_ON_COL1 := ARRAY_CONCAT(BASE1, BASE2 DEDUPLICATE ON col1),
    DEDUP_ON_COL1_AND_COL2 := ARRAY_CONCAT(BASE1, BASE2 DEDUPLICATE ON col1, col2),
;
Result
NO_DEDUP VARCHARDEDUP_ON_COL1 VARCHARDEDUP_ON_COL1_AND_COL2 VARCHAR
[{col1: 1, col2: A, col3: u1}, {col1: 2, col2: B, col3: u2a}, {col1: 2, col2: BB, col3: u2b}, {col1: 2, col2: BB, col3: u2c}, {col1: 3, col2: CC, col3: u3}][{col1: 1, col2: A, col3: u1}, {col1: 2, col2: BB, col3: u2c}, {col1: 3, col2: CC, col3: u3}][{col1: 1, col2: A, col3: u1}, {col1: 2, col2: B, col3: u2a}, {col1: 2, col2: BB, col3: u2c}, {col1: 3, col2: CC, col3: u3}]

Zipping arrays into rows

ZIP takes one or more plain arrays and combines them into an array of rows, pairing elements by position. If the arrays have different lengths, shorter ones are padded with NULL.

FeatureQL
SELECT
    ZIP_ONE := ZIP(ARRAY[1,2,3] AS my_field),
    ZIP_MANY := ZIP(ARRAY[1,2,3] AS my_field1, ARRAY['a','b'] AS my_field2),
;
Result
ZIP_ONE VARCHARZIP_MANY VARCHAR
[{my_field: 1}, {my_field: 2}, {my_field: 3}][{my_field1: 1, my_field2: a}, {my_field1: 2, my_field2: b}, {my_field1: 3, my_field2: NULL}]

Merging arrays of rows by position

ARRAY_MERGE without a join clause combines two arrays of rows by position: the first row of each array is merged together, the second with the second, and so on. If the arrays have different lengths, shorter ones are padded with NULL.

FeatureQL
SELECT
    ARRAY1 := ARRAY[ROW(1, 'A'), ROW(2, 'B')]::ROW(col1 BIGINT, col2 VARCHAR)[],
    ARRAY2 := ARRAY[ROW(3, 'C'), ROW(4, 'D')]::ROW(col3 BIGINT, col4 VARCHAR)[],
    MERGED := ARRAY_MERGE(ARRAY1, ARRAY2),
;
Result
ARRAY1 VARCHARARRAY2 VARCHARMERGED VARCHAR
[{col1: 1, col2: A}, {col1: 2, col2: B}][{col3: 3, col4: C}, {col3: 4, col4: D}][{col1: 1, col2: A, col3: 3, col4: C}, {col1: 2, col2: B, col3: 4, col4: D}]

Merging arrays of rows by join

When two arrays share a common field, ARRAY_MERGE ... JOINED ON matches rows by that field's value — like a SQL JOIN, but within the entity. Rows without a match get NULL for the missing fields.

FeatureQL
WITH
    KEY_ID := INPUT(BIGINT),
    ARRAY1 := ARRAY[ROW(KEY_ID as field_1, 'A'), ROW(2, 'B')]::ROW(num BIGINT, letter VARCHAR)[],
    ARRAY2 := ARRAY[ROW(1, TRUE), ROW(2, FALSE)]::ROW(num BIGINT, num_is_odd BOOLEAN)[],
SELECT
    KEY_ID,
    ARRAY1,
    ARRAY2,
    ARRAY_MERGE(ARRAY1, ARRAY2 JOIN ON num) as JOINED_MERGE
FOR
    KEY_ID := BIND_VALUES(ARRAY[1,2,3])
;
Result
KEY_ID BIGINTARRAY1 VARCHARARRAY2 VARCHARJOINED_MERGE VARCHAR
1[{num: 1, letter: A}, {num: 2, letter: B}][{num: 1, num_is_odd: true}, {num: 2, num_is_odd: false}][{num: 1, letter: A, num_is_odd: true}, {num: 2, letter: B, num_is_odd: false}]
2[{num: 2, letter: A}, {num: 2, letter: B}][{num: 1, num_is_odd: true}, {num: 2, num_is_odd: false}][{num: 2, letter: A, num_is_odd: false}, {num: 2, letter: B, num_is_odd: false}]
3[{num: 3, letter: A}, {num: 2, letter: B}][{num: 1, num_is_odd: true}, {num: 2, num_is_odd: false}][{num: 2, letter: B, num_is_odd: false}, {num: 3, letter: A, num_is_odd: NULL}]

Extending and transforming

The two most powerful operations on arrays of rows have their own pages:

  • Extend enriches each row by joining on an external feature. Use it when you need to add data from another entity (e.g., adding product names to an array of order rows).

  • Transform applies a full subquery to the rows — map, filter, aggregate, sort, or limit. Think of it as running a mini FeatureQL query inside each entity.

Last update at: 2026/03/03 16:47:38
Last updated: 2026-03-03 16:48:19