accesspatterns.dev
← All models

sales

Contacts CRM

One index answering two different questions.

Contacts searched by company and by tag. Rather than two indexes, one index whose key holds a type-prefixed value - so which question it answers depends on what was written into it.

The model

Contact

A person, plus the index rows that make them findable.

pk
CONTACT#<contactId>
sk
PROFILE

Attributes: name (S), company (S), lastSeenAt (N), GSI1PK (S), GSI1SK (S)

TagLink

A row that exists only to put a contact into the same index under a tag.

pk
CONTACT#<contactId>
sk
TAG#<tag>

Attributes: GSI1PK (S), GSI1SK (S)

Access patterns

  • Query · GSI1Contacts at a company

    Everyone at one company, alphabetically.

  • QueryA contact and its tags

    The profile row and every tag row for one person, in one read.

  • GetItemRead a contact

    One person's profile.

Design notes

The prefix is what the index is aboutwhy

GSI1PK holds COMPANY#halden-mill on one row and TAG#decision-maker on another. The index does not know the difference - it sorts strings - but a Query for COMPANY# can only match company rows, so one index behaves as several, at the cost of one index rather than three.

A tag is a row, not a listwhy

Storing tags as a list on the contact would make finding everyone with a tag a Scan. Writing one small row per tag puts each into the index under its own key, so the lookup is a Query. Adding a tag is a write, removing one is a delete, and the contact itself never changes.

Indexes cost writes, not just storagetrade-off

Every item carrying GSI1PK is copied into the index, and every update to it is a second write. Three indexes would be three copies of the projected attributes and three writes per change; one overloaded index is one. That saving is the reason to do this; every row in the index having to share one key shape is what it costs.

The tag query is real, but this page can run only onetrade-off

TAG#decision-maker rows sit in the same index as the COMPANY# rows, and a Query for GSI1PK = TAG#decision-maker returns Nadia and Priya across both companies. The console offers one index read per model - the resolver keys every index query from the same seeded row - so the tag question is in the table to be seen rather than pressed. Look at the TAG# rows: that is the second question, already indexed.

One index means one shape for everythingtrade-off

Every question this index answers has to fit its two keys, and they all share a sort key. Company rows sort by name, so tag rows must too, whether or not that is what you would have chosen. Overloading trades flexibility for cost, and the point at which a query needs a different ordering is the point at which it needs its own index.

Taught in the course

contacts-crm
Try an example

Everyone at one company, alphabetically.

PK(pk)
SK(sk)
GSI1PK
GSI1SK
company
lastSeenAt
name
CONTACT#c-4001
PROFILES
COMPANY#halden-millS
Nadia RahmanS
halden-millS
1786406400N
Nadia RahmanS
CONTACT#c-4001
TAG#decision-makerS
TAG#decision-makerS
Nadia RahmanS
CONTACT#c-4002
PROFILES
COMPANY#halden-millS
Tom ElleryS
halden-millS
1783209600N
Tom ElleryS
CONTACT#c-4002
TAG#renewal-q3S
TAG#renewal-q3S
Tom ElleryS
CONTACT#c-4003
PROFILES
COMPANY#vale-coldS
Priya AnandS
vale-coldS
1786579200N
Priya AnandS
CONTACT#c-4003
TAG#decision-makerS
TAG#decision-makerS
Priya AnandS

Run an operation to see the raw engine response.

table contacts-crmkeys PK / SKitems 6

The in-browser engine is a preview build. Transactions, vector search, streams, tags and TTL are among the operations it doesn't implement yet.