# 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.

```sql
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:

```sql
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;
```

```sql
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.

```sql
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](/docs/sql/create-transformation).

## Adding a group later

`ALTER TABLE … ADD ATTRIBUTE GROUP` declares the group's new columns inline:

```sql
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](/docs/sql/alter).

---

Source: https://nyxdb.io/docs/concepts/attribute-groups/
