Skip to main content
DB

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 BY and REVISION BY apply 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;

See CREATE TRANSFORMATION.

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.

On this page