Collection Types in Cassandra
Master Lists, Sets, and Maps! Learn how to store multiple values in a single column with real examples and best practices.
📖 How Netflix Stores Your "My List"
When you add shows to "My List" on Netflix, they need to store multiple values for a single user. How do they do this efficiently?
-- ❌ BAD: Multiple rows per movie user_id | movie_id --------|------------ user123 | stranger_t user123 | wednesday user123 | dark -- ✅ GOOD: Single row with LIST collection user_id | my_list --------|---------------------------------- user123 | ['stranger_t', 'wednesday', 'dark']
Collections let you store multiple values in a single column!
📦 What Are Collection Types?
Simple Definition
Collection types let you store multiple values in a single column. Instead of creating multiple rows, you group related values together.
The Three Collection Types
LIST
Ordered, allows duplicates
['a', 'b', 'a']
SET
Unordered, unique only
{'a', 'b', 'c'}
MAP
Key-value pairs
{'x': 1, 'y': 2}
📋 LIST: Ordered Collections
Creating a Table with LIST
CREATE TABLE user_playlists (
user_id UUID PRIMARY KEY,
playlist_name TEXT,
song_ids LIST<TEXT>,
play_counts LIST<INT>
);
Sample Data
| user_id | song_ids (LIST) |
|---|---|
| john_123 | ['eye_tiger', 'lose_yourself', 'stronger'] |
Operations
-- Append to list UPDATE user_playlists SET song_ids = song_ids + ['new_song'] WHERE user_id = john_123; -- Prepend to list UPDATE user_playlists SET song_ids = ['first_song'] + song_ids WHERE user_id = john_123; -- Update by index UPDATE user_playlists SET song_ids[0] = 'updated_song' WHERE user_id = john_123;
🎬 LIST Operations: Live Console Example
Let's build a music playlist step-by-step with actual data!
user_id UUID PRIMARY KEY,
playlist_name TEXT,
songs LIST<TEXT>,
created_at TIMESTAMP
);
VALUES (
550e8400-e29b-41d4-a716-446655440000,
'Workout Mix',
['Eye of the Tiger', 'Lose Yourself'],
toTimestamp(now())
);
SET songs = songs + ['Stronger']
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
SET songs = ['Thunderstruck'] + songs
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
SET songs[1] = 'Born to Run'
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
📊 LIST Operation Workflow (Animated)
🔍 Deep Dive: How LIST Stores Data Internally
Behind the Scenes
In Cassandra's storage engine:
- LIST is stored as a series of columns internally
- Each element gets a unique identifier (TimeUUID)
- Elements are ordered by their identifiers
- When you read: All elements are deserialized into a list
- When you write: Entire list is serialized and stored
Storage Format:
-- Logical View (What you see): songs = ['song1', 'song2', 'song3'] -- Physical Storage (What Cassandra stores): column: songs:timeuuid1 = 'song1' column: songs:timeuuid2 = 'song2' column: songs:timeuuid3 = 'song3' This is why: - Order is preserved (sorted by TimeUUID) - Duplicates are allowed (different TimeUUIDs) - Updates are expensive (must rewrite all elements)
LIST Limitations
- Can't query individual items: No "WHERE songs CONTAINS 'x'" - must read entire list
- Duplicates allowed: Same value can appear multiple times
- Size limit: Keep under 100 items for performance (theoretical max ~65K)
- Index updates are risky: If list changes between read and update, wrong item updated
- Read-modify-write: Updates read entire list, modify, then write back
💡 Real-World LIST Examples
🚗 Uber: Trip Route
CREATE TABLE trips ( trip_id UUID PRIMARY KEY, driver_id UUID, waypoints LIST<TEXT>, timestamps LIST<TIMESTAMP> ); -- Sample Data: trip_id: abc-123 waypoints: [ 'LAT:37.7749,LON:-122.4194', -- Start 'LAT:37.7849,LON:-122.4094', -- Point 1 'LAT:37.7949,LON:-122.3994' -- End ]
Why LIST? Order matters (route sequence), duplicates possible (driver passes same location twice)
📱 WhatsApp: Message Thread
CREATE TABLE chat_threads ( thread_id UUID PRIMARY KEY, participants LIST<TEXT>, message_ids LIST<UUID>, read_by LIST<TEXT> ); -- Sample Data: thread_id: thread-456 participants: ['alice', 'bob', 'charlie'] message_ids: [ msg-001, msg-002, msg-003 ]
Why LIST? Chronological order matters, same person can send multiple messages
🛒 Amazon: Shopping Cart
CREATE TABLE shopping_carts ( user_id UUID PRIMARY KEY, product_ids LIST<TEXT>, quantities LIST<INT>, added_dates LIST<TIMESTAMP> ); -- Sample Data: user_id: user-789 product_ids: [ 'laptop-123', 'mouse-456', 'laptop-123' -- duplicate! ] quantities: [1, 2, 1]
Why LIST? Order shows add sequence, duplicates = same item added twice
📺 YouTube: Watch History
CREATE TABLE watch_history ( user_id UUID PRIMARY KEY, video_ids LIST<TEXT>, watch_times LIST<TIMESTAMP>, durations LIST<INT> ); -- Sample Data: user_id: user-999 video_ids: [ 'dQw4w9WgXcQ', -- Rick Roll 'jNQXAC9IVRw', -- Me at the zoo 'dQw4w9WgXcQ' -- Watched again! ]
Why LIST? Chronological order, can watch same video multiple times
🔷 SET: Unique Collections
Creating a Table with SET
CREATE TABLE user_profiles (
user_id UUID PRIMARY KEY,
username TEXT,
interests SET<TEXT>,
skills SET<TEXT>,
languages SET<TEXT>,
badges SET<TEXT>
);
🎬 SET Operations: Live Console Example
Let's build a user profile with tags, step-by-step with actual data!
VALUES (
660e8400-e29b-41d4-a716-446655440001,
'sarah_dev',
{'coding', 'music', 'travel'},
{'python', 'javascript'}
);
SET interests = interests + {'gaming', 'photography'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
SET interests = interests + {'coding', 'reading'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
SET interests = interests - {'gaming', 'travel'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
SET skills = {'python', 'java', 'cassandra', 'docker'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
📊 SET Operation Workflow (Animated)
🔍 Deep Dive: How SET Stores Data Internally
Behind the Scenes
In Cassandra's storage engine:
- SET uses a hash-based structure internally
- Each element's value becomes part of the column name
- No ordering guarantee - iteration order is implementation-dependent
- Duplicates impossible - same hash = same column
- Fast membership checking (O(1) lookup)
Storage Format:
-- Logical View (What you see):
interests = {'coding', 'music', 'travel'}
-- Physical Storage (What Cassandra stores):
column: interests:coding = '' (empty value)
column: interests:music = '' (empty value)
column: interests:travel = '' (empty value)
This is why:
- No duplicates (same column name = overwrites)
- No ordering (hash-based storage)
- Fast add/remove (just add/delete column)
- Efficient membership test (column exists?)
💡 Real-World SET Examples with Full Data
🏷️ E-commerce: Product Tags
CREATE TABLE products (
product_id UUID PRIMARY KEY,
name TEXT,
categories SET<TEXT>,
features SET<TEXT>,
compatible_with SET<TEXT>
);
-- Sample Product: Laptop
product_id: prod-laptop-001
name: 'MacBook Pro 16"'
categories: {
'computers',
'laptops',
'apple',
'professional'
}
features: {
'retina-display',
'touchbar',
'usb-c',
'm3-chip',
'16gb-ram'
}
compatible_with: {
'airpods',
'magic-mouse',
'thunderbolt-displays'
}
Why SET? No duplicate tags, order doesn't matter, easy to add/remove categories
👥 LinkedIn: User Skills
CREATE TABLE linkedin_profiles (
user_id UUID PRIMARY KEY,
name TEXT,
skills SET<TEXT>,
endorsements SET<UUID>,
certifications SET<TEXT>
);
-- Sample Profile
user_id: user-sarah-123
name: 'Sarah Johnson'
skills: {
'python',
'java',
'kubernetes',
'aws',
'terraform',
'docker',
'cassandra',
'kafka'
}
certifications: {
'aws-solutions-architect',
'kubernetes-admin',
'oracle-java-certified'
}
Why SET? Each skill is unique, no duplicates needed, unordered listing
🎮 Steam: Game Library
CREATE TABLE steam_users (
user_id UUID PRIMARY KEY,
username TEXT,
owned_games SET<TEXT>,
wishlist SET<TEXT>,
achievements SET<TEXT>,
friends SET<UUID>
);
-- Sample User
user_id: steam-user-456
username: 'GamerPro2024'
owned_games: {
'cyberpunk-2077',
'elden-ring',
'baldurs-gate-3',
'starfield',
'red-dead-2'
}
wishlist: {
'gta-6',
'elder-scrolls-6',
'diablo-4'
}
achievements: {
'first-blood',
'completionist',
'speed-runner'
}
Why SET? Can't own same game twice, wishlist is unique items
📧 Email: Labels/Tags
CREATE TABLE emails (
email_id UUID PRIMARY KEY,
subject TEXT,
labels SET<TEXT>,
recipients SET<TEXT>,
cc SET<TEXT>,
attachments SET<TEXT>
);
-- Sample Email
email_id: email-789
subject: 'Q4 Financial Report'
labels: {
'work',
'important',
'finance',
'quarterly-report',
'starred'
}
recipients: {
'john@company.com',
'sarah@company.com',
'mike@company.com'
}
cc: {
'ceo@company.com',
'cfo@company.com'
}
Why SET? Each label applied once, unique recipients, no duplicate tags
When to Use SET
- Tags & Categories: Product tags, email labels, article categories
- Skills & Attributes: User skills, certifications, badges
- Relationships: Friends list, followers (when order doesn't matter)
- Features: Product features, capabilities, supported formats
- Permissions: User roles, access rights, granted permissions
SET Limitations
- No ordering: Elements returned in random/internal order
- No duplicates: Same value can only appear once
- No indexing: Can't access "first" or "nth" element
- All or nothing: Must read entire set, can't query individual members
- Size limit: Same as LIST - keep under 100 items for best performance
⚡ SET Performance Comparison
| Operation | Time Complexity | Description |
|---|---|---|
| Add Element | O(1) | Fast - just adds a column |
| Remove Element | O(1) | Fast - deletes a column |
| Check Membership | O(1) | Fast - checks if column exists |
| Read Full Set | O(n) | Must read all n elements |
| Find Specific Element | O(n) | Must scan entire set (no indexing) |
🗺️ MAP: Key-Value Pairs
Creating a Table with MAP
CREATE TABLE product_catalog (
product_id UUID PRIMARY KEY,
name TEXT,
attributes MAP<TEXT, TEXT>,
prices MAP<TEXT, DECIMAL>,
stock MAP<TEXT, INT>,
ratings MAP<TEXT, FLOAT>
);
🎬 MAP Operations: Live Console Example
Let's build a product catalog with flexible attributes!
product_id, name, attributes, prices
) VALUES (
770e8400-e29b-41d4-a716-446655440002,
'MacBook Pro 16"',
{'brand': 'Apple', 'color': 'Silver', 'ram': '16GB'},
{'USD': 2499.00, 'EUR': 2299.00}
);
SET attributes = attributes + {
'storage': '512GB',
'processor': 'M3 Max',
'display': 'Liquid Retina XDR'
}
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
SET attributes['ram'] = '32GB'
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
SET prices = prices + {'GBP': 1999.00, 'JPY': 350000.00, 'INR': 205000.00}
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
FROM product_catalog
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
FROM product_catalog
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
'processor': 'M3 Max', 'display': 'Liquid Retina XDR'}
'JPY': 350000.00, 'INR': 205000.00}
📊 MAP Operation Workflow (Animated)
🔍 Deep Dive: How MAP Stores Data Internally
Behind the Scenes
In Cassandra's storage engine:
- MAP stores each key-value pair as a separate column
- Column name = map_name + ":" + key
- Column value = the mapped value
- This enables partial updates - update just one key without rewriting entire map
- Keys must be unique (same key = overwrites value)
Storage Format:
-- Logical View (What you see):
prices = {'USD': 2499.00, 'EUR': 2299.00, 'GBP': 1999.00}
-- Physical Storage (What Cassandra stores):
column: prices:USD = 2499.00
column: prices:EUR = 2299.00
column: prices:GBP = 1999.00
This is why:
- Can update prices['USD'] = 2599.00 without touching EUR/GBP
- Can delete specific key: DELETE prices['GBP']
- Can add new key: prices['INR'] = 205000.00
- Direct key access: SELECT prices['USD'] - O(1) lookup!
- Efficient for sparse data - only store keys that exist
💡 Real-World MAP Examples with Full Data
🌍 Airbnb: Multi-Currency Pricing
CREATE TABLE listings (
listing_id UUID PRIMARY KEY,
title TEXT,
base_price_usd DECIMAL,
prices_by_currency MAP<TEXT, DECIMAL>,
seasonal_multipliers MAP<TEXT, FLOAT>,
amenities_pricing MAP<TEXT, DECIMAL>
);
-- Sample Listing
listing_id: airbnb-sf-001
title: 'Downtown SF Loft'
prices_by_currency: {
'USD': 150.00,
'EUR': 138.50,
'GBP': 119.00,
'JPY': 21000.00,
'AUD': 225.00,
'CAD': 200.00,
'INR': 12500.00
}
seasonal_multipliers: {
'summer': 1.5,
'winter': 0.8,
'holidays': 2.0,
'weekends': 1.2
}
amenities_pricing: {
'extra_guest': 25.00,
'pet_fee': 50.00,
'cleaning': 75.00,
'early_checkin': 30.00
}
Why MAP? Flexible currencies, easy to add new rates, direct access to specific currency
🎮 Gaming: Player Stats & Progress
CREATE TABLE player_profiles (
player_id UUID PRIMARY KEY,
username TEXT,
stats MAP<TEXT, INT>,
achievements MAP<TEXT, TIMESTAMP>,
inventory MAP<TEXT, INT>,
skill_levels MAP<TEXT, INT>
);
-- Sample Player
player_id: player-gamer-789
username: 'ProGamer2024'
stats: {
'total_kills': 15234,
'deaths': 8910,
'wins': 456,
'losses': 234,
'headshots': 3421,
'play_time_hours': 1250,
'level': 87,
'experience': 245000
}
achievements: {
'first_win': '2024-01-15 10:30:00',
'level_50': '2024-03-22 15:45:00',
'master_sniper': '2024-05-10 20:15:00',
'team_player': '2024-06-01 12:00:00'
}
inventory: {
'gold': 15000,
'gems': 250,
'health_potions': 45,
'mana_potions': 32,
'legendary_sword': 1,
'epic_armor': 3
}
skill_levels: {
'archery': 95,
'melee': 88,
'magic': 72,
'defense': 90,
'speed': 85
}
Why MAP? Flexible stats that grow over time, easy to add new achievements
⚙️ SaaS App: User Settings
CREATE TABLE user_settings (
user_id UUID PRIMARY KEY,
email TEXT,
preferences MAP<TEXT, TEXT>,
notifications MAP<TEXT, BOOLEAN>,
theme_config MAP<TEXT, TEXT>,
privacy_settings MAP<TEXT, TEXT>
);
-- Sample User
user_id: user-sarah-456
email: 'sarah@example.com'
preferences: {
'language': 'en-US',
'timezone': 'America/Los_Angeles',
'date_format': 'MM/DD/YYYY',
'number_format': '1,234.56',
'default_view': 'dashboard',
'items_per_page': '50',
'currency': 'USD'
}
notifications: {
'email_marketing': false,
'email_updates': true,
'push_notifications': true,
'sms_alerts': false,
'weekly_digest': true,
'mention_alerts': true,
'comment_replies': true
}
theme_config: {
'mode': 'dark',
'primary_color': '#0d9488',
'font_size': 'medium',
'sidebar_position': 'left',
'compact_mode': 'false'
}
privacy_settings: {
'profile_visibility': 'public',
'show_email': 'false',
'show_activity': 'friends_only',
'searchable': 'true',
'data_collection': 'essential_only'
}
Why MAP? Settings grow over time, easy to add new preferences, flexible schema
📊 Analytics: Event Properties
CREATE TABLE events (
event_id UUID PRIMARY KEY,
event_name TEXT,
properties MAP<TEXT, TEXT>,
metrics MAP<TEXT, FLOAT>,
metadata MAP<TEXT, TEXT>,
occurred_at TIMESTAMP
);
-- Sample Event: Page View
event_id: event-abc-123
event_name: 'page_view'
properties: {
'page_url': '/products/laptop-123',
'page_title': 'MacBook Pro',
'referrer': 'google.com',
'user_agent': 'Mozilla/5.0...',
'device_type': 'desktop',
'browser': 'Chrome',
'os': 'MacOS',
'country': 'United States',
'city': 'San Francisco',
'utm_source': 'google',
'utm_campaign': 'summer_sale',
'utm_medium': 'cpc'
}
metrics: {
'page_load_time': 1.234,
'time_on_page': 45.67,
'scroll_depth': 0.85,
'bounce_rate': 0.0,
'conversion_rate': 0.03
}
metadata: {
'session_id': 'sess-xyz-789',
'user_id': 'user-456',
'ab_test_variant': 'B',
'language': 'en',
'screen_resolution': '1920x1080'
}
Why MAP? Events have different properties, flexible schema, easy to add tracking
Perfect Use Cases for MAP
- Configuration & Settings: User preferences, app config, feature flags
- Multi-language Content: Translations, localized prices, region-specific data
- Metadata & Attributes: Product specs, file metadata, custom fields
- Analytics & Metrics: Event properties, performance metrics, stats
- Flexible Schema: When columns aren't known upfront, evolving requirements
MAP Limitations
- Can't query by value: No "WHERE prices CONTAINS 100.00" - only by key
- Unique keys only: Same key overwrites previous value
- No nested maps: MAP<TEXT, MAP<TEXT, INT>> not allowed (use frozen or UDT)
- All keys read together: Reading one key still deserializes entire map
- Size limit: Keep under 100 key-value pairs for best performance
⚡ MAP Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
| Add Key-Value | O(1) | Just adds a column, very fast |
| Update Value | O(1) | Overwrites single column |
| Delete Key | O(1) | Removes single column |
| Access by Key | O(1) | Direct column lookup |
| Read Entire Map | O(n) | Must read all n key-value pairs |
| Search by Value | Not Supported | Can't query "where value = X" |
⚖️ Collection Types Comparison
| Feature | LIST | SET | MAP |
|---|---|---|---|
| Ordering | Ordered ✓ | Unordered | Unordered |
| Duplicates | Allowed ✓ | Not allowed | Unique keys |
| Structure | Values only | Values only | Key → Value |
| Index Access | Yes [0], [1] | No | Yes ['key'] |
| Best For | Playlists, history | Tags, categories | Attributes, settings |
✅ Best Practices
Do's
- Keep collections small (<100 items)
- Use for related data read together
- Use SET for uniqueness enforcement
- Use MAP for flexible attributes
Don'ts
- Don't store thousands of items
- Don't query individual elements
- Don't use for frequently updated items
- Don't use as primary data structure
💼 Top 12 Interview Questions on Collections
Answer:
- LIST: Ordered collection that allows duplicates. Example: ['a', 'b', 'a']. Use for: playlists, routes, history
- SET: Unordered collection of unique values. Example: {'a', 'b', 'c'}. Use for: tags, skills, categories
- MAP: Key-value pairs with unique keys. Example: {'name': 'John', 'age': 30}. Use for: attributes, settings, prices
Key Differences:
| Feature | LIST | SET | MAP |
|---|---|---|---|
| Order | Ordered | Unordered | Unordered |
| Duplicates | Allowed | Not allowed | Unique keys |
| Access | By index [0] | No indexing | By key ['name'] |
Use Collections when:
- Small datasets: Less than 100 items recommended
- Read together: Values are always accessed as a group
- Written together: Values updated as a unit
- No individual queries: Don't need to filter/search individual items
Use Separate Rows when:
- Large datasets: Thousands of items
- Individual queries: Need to search/filter specific items
- Independent updates: Items updated separately
- Range queries: Need to query by item attributes
Example:
-- ✅ GOOD: User's last 10 searches (small, read together) recent_searches LIST<TEXT> = ['cassandra', 'nosql', 'database'] -- ❌ BAD: All user's orders (large, query individually) -- Don't use: orders LIST<UUID> -- Instead: Create orders_by_user table with proper partitioning
Answer:
Technical Limit: Collections can hold up to approximately 65,535 items (64KB per collection by default).
Recommended Limit: Keep collections under 100 items for optimal performance.
Why it matters:
- Read/Write Performance: Entire collection is read or written as one unit - large collections slow down operations
- Memory Pressure: Large collections require more memory to deserialize
- Network Overhead: Entire collection transferred over network
- Compaction Impact: Large collections make compaction less efficient
Performance Impact:
Collection Size | Read Time | Memory Usage ----------------|-----------|------------- 10 items | 1-2ms | ~10KB 100 items | 5-10ms | ~100KB ← Recommended max 1,000 items | 50-100ms | ~1MB ⚠️ Getting slow 10,000 items | 500ms+ | ~10MB ❌ Too large!
Solution for Large Data: Use separate rows with proper partitioning instead.
Answer: Collections are stored as multiple columns internally, not as serialized blobs.
LIST Storage:
-- Logical: songs = ['song1', 'song2', 'song3'] -- Physical: column: songs:timeuuid1 → 'song1' column: songs:timeuuid2 → 'song2' column: songs:timeuuid3 → 'song3' -- TimeUUID preserves order
SET Storage:
-- Logical: tags = {'java', 'python', 'cassandra'}
-- Physical:
column: tags:java → '' (empty value)
column: tags:python → '' (empty value)
column: tags:cassandra → '' (empty value)
-- Value is part of column name = no duplicates possible
MAP Storage:
-- Logical: prices = {'USD': 100, 'EUR': 90}
-- Physical:
column: prices:USD → 100
column: prices:EUR → 90
-- Allows partial updates!
Benefits of this storage model:
- Efficient partial updates (especially MAP)
- Tombstones only for deleted elements
- Leverages Cassandra's column-oriented storage
Answer: No, you cannot query individual collection elements.
What doesn't work:
-- ❌ These queries are NOT supported: SELECT * FROM users WHERE interests CONTAINS 'coding'; SELECT * FROM products WHERE tags CONTAINS 'laptop'; SELECT * FROM orders WHERE items CONTAINS 'product-123';
Why not?
- Collections are stored within a single partition
- No secondary indexes on collection elements (by design)
- Would require full table scan = terrible performance
- Violates Cassandra's partition-first query model
Workaround - Denormalization:
-- Instead of querying collection, create reverse table: -- Original table CREATE TABLE users ( user_id UUID PRIMARY KEY, interests SET<TEXT> ); -- Add reverse lookup table CREATE TABLE users_by_interest ( interest TEXT, user_id UUID, PRIMARY KEY (interest, user_id) ); -- Now you can query: SELECT * FROM users_by_interest WHERE interest = 'coding';
Answer: Frozen collections are treated as a single immutable value, serialized as a blob.
Definition:
CREATE TABLE example ( id UUID PRIMARY KEY, -- Regular collection (mutable) tags SET<TEXT>, -- Frozen collection (immutable) coordinates FROZEN<LIST<DOUBLE>> );
Key Differences:
| Feature | Regular Collection | Frozen Collection |
|---|---|---|
| Updates | Partial updates allowed | Must replace entire collection |
| Storage | Multiple columns | Single blob |
| Nesting | Not allowed | Allowed |
| Performance | Better for large collections | Better for small, rarely changed |
Use Frozen When:
- Need nested collections:
FROZEN<MAP<TEXT, LIST<INT>>> - Collection rarely changes (immutable data)
- Want entire collection as primary key
- Small collections (<10 items)
Example:
-- GPS coordinates (never partially updated) coordinates FROZEN<LIST<DOUBLE>> = [37.7749, -122.4194] -- Update requires replacing entire list: UPDATE locations SET coordinates = [37.7849, -122.4094] WHERE location_id = 123;
Answer: Nothing happens - the operation succeeds but the SET remains unchanged.
Example:
-- Initial state
interests = {'coding', 'music', 'travel'}
-- Try adding duplicate
UPDATE users
SET interests = interests + {'coding', 'gaming'}
WHERE user_id = 123;
-- Result:
interests = {'coding', 'music', 'travel', 'gaming'}
-- 'coding' was already there - ignored
-- 'gaming' was new - added
Why it works this way:
- SET stores elements as column names with empty values
- Same column name = overwrites (but value is empty anyway)
- Result: Idempotent operation - safe to retry
- No error thrown - this is by design
Practical Impact:
This makes SETs perfect for scenarios where you want to ensure uniqueness without checking first:
-- No need to check if tag exists - just add it!
UPDATE products
SET tags = tags + {'new-arrival'}
WHERE product_id = 456;
-- Safe to call multiple times
-- No "already exists" errors
Answer: Use the subtraction operator (-) to remove elements from a LIST.
-- Remove specific value (removes ALL occurrences!) UPDATE user_playlists SET songs = songs - ['Born to Run'] WHERE user_id = 123; -- Remove multiple values UPDATE user_playlists SET songs = songs - ['song1', 'song2', 'song3'] WHERE user_id = 123;
⚠️ Important Gotcha:
The minus operator removes ALL occurrences of the value!
-- Initial list (note duplicates) songs = ['song1', 'song2', 'song1', 'song3', 'song1'] -- Remove 'song1' UPDATE ... SET songs = songs - ['song1'] ... -- Result: ALL three 'song1' entries removed! songs = ['song2', 'song3']
Alternative: Update by Index
-- Set specific index to null to "delete" -- But this doesn't actually remove the element! UPDATE user_playlists SET songs[2] = null WHERE user_id = 123; -- Better approach: Read, modify in app, write back entire list
Best Practice:
If you need fine-grained control over LIST removal (e.g., remove only first occurrence), it's better to:
- Read the current list
- Modify it in application code
- Write the entire updated list back
Answer: Only FROZEN collections can be used in primary keys.
-- ❌ NOT ALLOWED: Regular collection as primary key CREATE TABLE invalid ( user_id UUID, tags SET<TEXT>, PRIMARY KEY (user_id, tags) -- ERROR! ); -- ✅ ALLOWED: Frozen collection as primary key CREATE TABLE valid ( user_id UUID, tags FROZEN<SET<TEXT>>, PRIMARY KEY (user_id, tags) -- OK! );
Why frozen only?
- Primary keys must be immutable (frozen = immutable)
- Frozen collections serialize to a single comparable value
- Regular collections can change - would break partition/clustering
Real Use Case:
-- Composite key with coordinate pair CREATE TABLE locations ( location_id UUID, coordinates FROZEN<LIST<DOUBLE>>, name TEXT, PRIMARY KEY (location_id, coordinates) ); -- Insert INSERT INTO locations (location_id, coordinates, name) VALUES (uuid(), [37.7749, -122.4194], 'San Francisco'); -- Query by exact coordinates SELECT * FROM locations WHERE location_id = ... AND coordinates = [37.7749, -122.4194];
Limitation:
You can only query by the exact frozen collection value - no partial matching or range queries.
Answer: Both store structured data but with different use cases and constraints.
| Feature | MAP | User-Defined Type (UDT) |
|---|---|---|
| Schema | Flexible, dynamic keys | Fixed, predefined fields |
| Keys | Can add/remove anytime | Must be defined upfront |
| Type Safety | All values same type | Each field can have different type |
| Nulls | Keys not present = doesn't exist | Fields can be null |
| Access | By string key: map['key'] | By field name: udt.field |
MAP Example:
-- Flexible attributes (schema evolves)
CREATE TABLE products (
id UUID PRIMARY KEY,
attributes MAP<TEXT, TEXT>
);
-- Can add ANY key:
attributes = {
'color': 'red',
'size': 'large',
'weight': '2kg',
'custom_field_123': 'value' -- Add new keys anytime!
}
UDT Example:
-- Fixed structure (typed fields)
CREATE TYPE address (
street TEXT,
city TEXT,
zip INT,
country TEXT
);
CREATE TABLE users (
id UUID PRIMARY KEY,
home_address FROZEN<address>
);
-- Must match structure:
home_address = {
street: '123 Main St',
city: 'San Francisco',
zip: 94105,
country: 'USA'
}
When to use MAP:
- Schema changes frequently
- Keys not known upfront
- Different items have different attributes
When to use UDT:
- Fixed structure (like addresses, coordinates)
- Need type safety
- Different field types
Answer: Collections have specific performance characteristics you need to understand.
Read Performance:
- Entire collection read: Even accessing one element reads the whole collection
- Deserialization cost: Collection must be deserialized into memory
- Network transfer: Full collection sent over network
-- This still reads entire 'prices' collection: SELECT prices['USD'] FROM products WHERE id = 123; -- Network and memory impact: Collection Size | Network Bytes | Deserialize Time ----------------|---------------|------------------ 10 items | ~500 bytes | < 1ms 100 items | ~5KB | 2-5ms 1,000 items | ~50KB | 20-50ms 10,000 items | ~500KB | 200ms+ ❌ Too slow!
Write Performance:
- LIST updates: Read-modify-write cycle (expensive)
- SET/MAP updates: Can do partial updates (efficient)
- Tombstones: Deleted elements create tombstones
-- MAP: Efficient partial update (O(1)) UPDATE products SET prices['USD'] = 2599 WHERE id = 123; -- Only updates one column -- LIST: Less efficient (O(n)) UPDATE playlists SET songs[5] = 'new_song' WHERE id = 123; -- Requires reading position of element
Memory Impact:
Collections consume heap memory during queries:
-- Query returning 1000 rows, each with 100-item LIST -- Memory needed: 1000 * 100 * ~50 bytes = ~5MB -- Can cause GC pressure under load!
Best Practices for Performance:
- Keep collections under 100 items
- Use MAP for data you'll update partially
- Use separate rows for large datasets
- Monitor collection sizes in production
- Consider frozen collections for small, immutable data
Answer: Yes, but only with FROZEN collections.
-- ❌ NOT ALLOWED: Regular nested collections CREATE TABLE invalid ( id UUID PRIMARY KEY, data MAP<TEXT, LIST<INT>> -- ERROR! ); -- ✅ ALLOWED: Frozen nested collections CREATE TABLE valid ( id UUID PRIMARY KEY, data MAP<TEXT, FROZEN<LIST<INT>>> -- OK! );
Why frozen only?
- Nested collections need to be treated as atomic values
- Partial updates of nested structures are too complex
- Frozen = immutable = can be nested safely
Common Nested Patterns:
1. Map of Lists (Categories with Items):
CREATE TABLE restaurants (
id UUID PRIMARY KEY,
menu_items MAP<TEXT, FROZEN<LIST<TEXT>>>
);
-- Data:
menu_items = {
'appetizers': ['soup', 'salad', 'wings'],
'mains': ['steak', 'fish', 'pasta'],
'desserts': ['cake', 'ice cream']
}
-- Usage:
UPDATE restaurants
SET menu_items['appetizers'] = ['soup', 'salad']
WHERE id = 123;
2. List of Maps (Timeline Events):
CREATE TABLE user_activity (
user_id UUID PRIMARY KEY,
events LIST<FROZEN<MAP<TEXT, TEXT>>>
);
-- Data:
events = [
{'type': 'login', 'time': '2025-01-15T10:00:00Z'},
{'type': 'purchase', 'amount': '99.99', 'time': '2025-01-15T10:30:00Z'},
{'type': 'logout', 'time': '2025-01-15T11:00:00Z'}
]
-- Must replace entire list to update
UPDATE user_activity
SET events = events + [{'type': 'login', 'time': '...'}]
WHERE user_id = 123;
⚠️ Important Limitations:
- No partial updates: Must replace entire frozen collection
- Performance cost: Entire structure serialized/deserialized
- Size matters more: Nested structures get large quickly
Alternative: Consider UDT for complex nested structures:
-- Instead of nested collections: CREATE TYPE menu_section ( name TEXT, items LIST<TEXT> ); CREATE TABLE restaurants ( id UUID PRIMARY KEY, menu LIST<FROZEN<menu_section>> );
Responsive Ad