Chapter 08 · CQL Data Types, Collections, Tuples, UDTs, Static Columns, and Frozen Values
Frozen vs Multi-Cell Values, Nested Collections, and Mutation Granularity
Distinguish frozen single-cell values from multi-cell collections and UDTs so mutation granularity, tombstones, nesting rules, and read/write cost are predictable.
Learning outcomes
AtlasMart has two kinds of structured state: settings that
change one entry at a time and snapshots that must move as a
coherent unit. Cassandra's frozen modifier is the
line between those mutation models. Treating it as syntax trivia
produces rejected CQL, unnecessary whole-value rewrites, or
unexpected tombstone patterns.
Explain multi-cell versus frozen single-cell representation at the CQL mutation boundary.
Mutate one element of a non-frozen map and prove that the same operation is invalid for a frozen map.
Use frozen inner values where nested collection rules require them.
Compare non-frozen and frozen UDT update semantics with the same logical address data.
Choose mutation granularity from business invariants, cardinality, tombstone rate, and retry behavior.
The mandatory labs use the pinned
cassandra:5.0.9 image. Java 17,
cqlsh, and nodetool are the versions
bundled by that image. The course topology is three disposable
nodes (atlasmart-cass-1..3) in cluster
atlasmart-course, datacenter dc1,
racks rack1..rack3, 16 vnodes per node,
replication factor (RF) 3, and LOCAL_QUORUM for
consistency-sensitive examples. Authentication, client TLS,
internode TLS, and remote JMX are not enabled in this isolated
learning network; production must secure those boundaries
separately. Chapter 08 uses keyspace
atlasmart_types; new tables explicitly use
UnifiedCompactionStrategy (UCS), default table TTL is zero
unless stated, and gc_grace_seconds is not
changed. Storage-Attached Indexing (SAI) and vector search are
not required. Apache Cassandra Java Driver 4.19.3 is used only
in optional decoding snippets; the mandatory path remains
free/local with cqlsh.
The commands and CQL below are documentation- and syntax-reviewed but were not executed in this generation environment. Treat output as an expected shape, then capture exact UUIDs, time values, SSTable paths, tombstone counters, and driver-decoded values on your machine. If three nodes are too heavy, use one disposable node and RF=1 to learn type/mutation semantics, but do not treat that reduced topology as evidence about RF=3 availability or repair behavior.
1. Frozen means one value at the mutation boundary
A non-frozen collection is multi-cell: map keys/set
elements/list elements are stored as independently addressable
cells. That enables element mutations but also means element
deletion/expiry can create element tombstones. A
frozen<map<...>> is serialized as one
value. Cassandra does not let you update only one key inside it;
replace the entire map. Tuples are inherently frozen. UDTs may
be multi-cell when their definition permits, or explicitly
frozen for snapshot semantics.
2. Lab: compare the same logical data in frozen and multi-cell columns
# Verify the shared course lab if it already exists.docker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 nodetool status# Standalone local recreation path. Skip resources that already exist.docker network inspect atlasmart-cassandra >/dev/null 2>&1 || docker network create atlasmart-cassandradocker volume create atlasmart-cass-1-datadocker volume create atlasmart-cass-2-datadocker volume create atlasmart-cass-3-datadocker inspect atlasmart-cass-1 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-1 --hostname atlasmart-cass-1 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack1 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -v atlasmart-cass-1-data:/var/lib/cassandra cassandra:5.0.9# Wait for node 1 to answer before starting peers.docker exec atlasmart-cass-1 nodetool statusdocker inspect atlasmart-cass-2 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-2 --hostname atlasmart-cass-2 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack2 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-cass-1 -v atlasmart-cass-2-data:/var/lib/cassandra cassandra:5.0.9docker inspect atlasmart-cass-3 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-3 --hostname atlasmart-cass-3 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack3 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-cass-1 -v atlasmart-cass-3-data:/var/lib/cassandra cassandra:5.0.9# Continue only after all nodes show UN.docker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 cqlsh -e "CREATE KEYSPACE IF NOT EXISTS atlasmart_types WITH replication = {'class':'NetworkTopologyStrategy','dc1':3};"docker exec atlasmart-cass-1 cqlsh -e "DESCRIBE KEYSPACE atlasmart_types"
CREATE TYPE atlasmart_types.contact_v1 ( city text, country text);CREATE TABLE atlasmart_types.profile_shapes ( customer_id uuid PRIMARY KEY, live_preferences map<text,text>, preferences_snapshot frozen<map<text,text>>, live_contact atlasmart_types.contact_v1, contact_snapshot frozen<atlasmart_types.contact_v1>, channel_groups map<text,frozen<list<text>>>) WITH compaction = {'class':'UnifiedCompactionStrategy'};INSERT INTO atlasmart_types.profile_shapes(customer_id,live_preferences,preferences_snapshot,live_contact,contact_snapshot,channel_groups)VALUES(11111111-1111-1111-1111-111111111111, {'currency':'USD','locale':'en-US'}, {'currency':'USD','locale':'en-US'}, {city:'Tehran',country:'IR'}, {city:'Tehran',country:'IR'}, {'urgent':['sms','push'],'normal':['email']});SELECT * FROM atlasmart_types.profile_shapes;
UPDATE atlasmart_types.profile_shapesSET live_preferences['currency']='AZN', live_contact.city='Baku'WHERE customer_id=11111111-1111-1111-1111-111111111111;UPDATE atlasmart_types.profile_shapesSET preferences_snapshot={'currency':'AZN','locale':'en-US'}, contact_snapshot={city:'Baku',country:'AZ'}WHERE customer_id=11111111-1111-1111-1111-111111111111;SELECT * FROM atlasmart_types.profile_shapesWHERE customer_id=11111111-1111-1111-1111-111111111111;
The live columns allow targeted field/element changes. The frozen columns require whole-value replacement, which is appropriate when AtlasMart wants an immutable-ish snapshot and the value remains small.
3. Deliberately trigger the two common mistakes
-- Intentionally invalid: a frozen map has no independently mutable entry.UPDATE atlasmart_types.profile_shapesSET preferences_snapshot['currency']='EUR'WHERE customer_id=11111111-1111-1111-1111-111111111111;-- Intentionally invalid schema: nested collection value needs a frozen boundary.CREATE TABLE atlasmart_types.bad_nested ( id uuid PRIMARY KEY, groups map<text,list<text>>);
The exact error wording can vary by patch, but both requests
should be rejected. The corrected nested form in the lab uses
map<text,frozen<list<text>>>. Do
not respond to the error by freezing everything automatically:
freezing changes update cost and conflict/retry behavior because
the whole value becomes the write unit.
That erases element-level mutation and can turn a tiny setting change into a whole-value read/construct/write in the application. The opposite extreme—multi-cell values with high churn—can create many element tombstones. Choose the boundary from actual update patterns.
4. Observe storage consequences cautiously
docker exec atlasmart-cass-1 nodetool flush atlasmart_types profile_shapesdocker exec atlasmart-cass-1 nodetool tablestats atlasmart_types profile_shapes# Optional SSTable forensic view; output format/path is version dependent.docker exec atlasmart-cass-1 bash -lc 'F=$(find /var/lib/cassandra/data/atlasmart_types/profile_shapes-* -name "*Data.db" | head -1); echo "$F"; test -n "$F" && sstabledump "$F" | head -120 || true'
This experiment is qualitative. Do not infer production write amplification or compaction efficiency from one tiny fixture. Measure with your real value sizes, update frequency, TTL/delete rate, concurrency, RF/CL, disk, and compaction workload.
5. Production judgment
Frozen values are strongest when the logical object is small and should be replaced atomically as one cell: configuration snapshots, immutable coordinates, or versioned value objects. Multi-cell values are strongest when individual fields/elements change independently and the collection/UDT remains bounded. Under retries, whole-value replacements are usually easier to reason about if they are deterministic; read-modify-write in the application can still race. Multi-cell deletions and expirations increase tombstone activity. Neither choice changes partition placement: the table's primary key still determines replica ownership and hot-partition risk.
Verification checklist
- Targeted updates succeed only on the multi-cell columns.
- Frozen snapshots are changed by whole-value replacement.
- The intentionally unfrozen nested collection schema is rejected.
- The corrected nested schema has an explicit frozen inner boundary.
- Operational evidence is treated as qualitative unless measured under a representative workload.
Check your understanding
- What is the mutation unit of a frozen map?
- Why might a non-frozen map create more tombstones?
- Are tuples independently mutable by position?
- Why is freezing everything not a universal optimization?
- Does frozen change which replicas own the row?
Review the answers
1. The whole map value.
2. Individual keys are separate cells, so key removals/TTL expiry can create element-level tombstones.
3. No. Tuple values are inherently frozen and replaced as one value.
4. It removes targeted mutation and can force whole-value rewrites for small logical changes.
5. No. Partition-key/token/replication rules are unchanged.
# Destructive only to this disposable chapter keyspace.docker exec atlasmart-cass-1 cqlsh -e "DROP KEYSPACE IF EXISTS atlasmart_types;"# Keep shared course containers for Chapter 09, or remove them for a full reset:# docker rm -f atlasmart-cass-1 atlasmart-cass-2 atlasmart-cass-3# docker volume rm atlasmart-cass-1-data atlasmart-cass-2-data atlasmart-cass-3-data# docker network rm atlasmart-cassandra
Summary and next bridge
frozen defines a write/read boundary, not a
decoration. Next we apply the same physical thinking to static
columns, where one value is shared by every clustering row in a
partition.
Authoritative references
- CQL data types — scalar, collection, tuple, user-defined type, frozen, and literal semantics.
- Creating collections — bounded collection guidance and current collection guardrails.
- CQL data definition — static-column behavior, table/type definitions, and schema restrictions.
- CREATE TABLE reference — frozen/non-frozen UDT and static-column examples.
- Tombstones — deletion markers, grace, reads, and compaction implications.
- Apache Cassandra downloads — current server and Java-driver release baselines.