Concepts
Attribute groups
Split a keyed row into independently written and independently ordered column groups, so several writers can each own part of one row.
An attribute group is a named set of columns of a keyed table. Each group is its own write and conflict domain. A write that targets one group changes only that group's columns. It never overwrites another group, and a stale write to one group does not hold back a newer write to another. The row is still one typed row: a read returns all of its columns together.
Groups belong to the keyed modes, UPSERT and VERSIONED.
Declaring groups
Groups are declared inside CREATE TABLE, after the columns:
ATTRIBUTE GROUP <name> (<column>, …), optionally followed by the group's own
VALID BY (…) and REVISION BY (…), in either order.
CREATE TABLE balances (
chain_id UINT16 NOT NULL,
address STRING NOT NULL,
token STRING NOT NULL,
amount UINT256 NULL,
updated_at DATETIME64(3, 'UTC') NULL,
balance_index UINT128 NULL,
balance_version UINT64 NULL,
transfer_count UINT64 NULL,
last_transfer_at DATETIME64(3, 'UTC') NULL,
activity_index UINT128 NULL,
PRIMARY KEY (chain_id, address, token),
ATTRIBUTE GROUP holding (amount, updated_at, balance_index, balance_version)
VALID BY (balance_index)
REVISION BY (balance_version),
ATTRIBUTE GROUP activity (transfer_count, last_transfer_at, activity_index)
VALID BY (activity_index)
)
MODE VERSIONED
SHARD BY (address) SHARDS AUTO
ORDER BY (chain_id, address, token);holding is written by the service that tracks balances and is ordered by
balance_index; activity is maintained by a transformation over transfers and
ordered by activity_index.
Rules for members:
- Groups are disjoint: a column belongs to at most one group.
- Primary-key, shard and partition columns identify the row and sit outside the groups.
- A group's
VALID BYandREVISION BYapply only to that group's writes. A group without them is ordered by commit. - A table holds up to 256 groups.
Writing one group
INSERT, UPSERT, REPLACE and UPDATE take a trailing
ATTRIBUTE GROUP <name>. The statement then names the key columns and that
group's columns:
UPSERT INTO balances (
chain_id, address, token, amount, updated_at, balance_index, balance_version
)
VALUES (1, '0x9c2e4f1a', 'USDC', 2750000000, '2026-10-01 12:05:00.000', 1002, 1)
ATTRIBUTE GROUP holding;UPSERT INTO balances (
chain_id, address, token, amount, updated_at, balance_index, balance_version
)
VALUES (1, '0x9c2e4f1a', 'USDC', 2500000000, '2026-10-01 12:00:00.000', 1001, 1)
ATTRIBUTE GROUP holding;The second statement arrives after the first but carries an earlier
balance_index. Because holding is ordered by VALID BY (balance_index), it is
retained as an earlier version and the current amount stays 2750000000. The
activity group is unaffected by either.
DELETE removes the whole key, every group included.
Rows written one group at a time
The first group write on a new key stores a row that holds the key and that
group. Columns that no write has supplied read as NULL, so a balance row can
exist with only its activity group, before any holding write.
Groups as transformation targets
A transformation can own one group of its target with
INTO <table> ATTRIBUTE GROUP <group>. The rest of the row stays writable by
other statements and other transformations.
CREATE TRANSFORMATION balance_activity INTO balances ATTRIBUTE GROUP activity AS
SELECT
chain_id,
recipient AS address,
token,
COUNT(*) AS transfer_count,
MAX(transferred_at) AS last_transfer_at,
MAX(_index) AS activity_index
FROM transfers
GROUP BY chain_id, recipient, token;Adding a group later
ALTER TABLE … ADD ATTRIBUTE GROUP declares the group's new columns inline:
ALTER TABLE balances ADD ATTRIBUTE GROUP labels (
label_text STRING NULL,
labelled_at DATETIME64(3, 'UTC') NULL
);Transformations that write other groups of balances keep running across the
ALTER and across restarts. See ALTER TABLE.