Section 4: Data Types

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!

cassandra@cqlsh> -- Step 1: Create the table
CREATE TABLE music.user_playlists (
  user_id UUID PRIMARY KEY,
  playlist_name TEXT,
  songs LIST<TEXT>,
  created_at TIMESTAMP
);
✓ Table created successfully!
cassandra@cqlsh> -- Step 2: Insert initial playlist
INSERT INTO music.user_playlists (user_id, playlist_name, songs, created_at)
VALUES (
  550e8400-e29b-41d4-a716-446655440000,
  'Workout Mix',
  ['Eye of the Tiger', 'Lose Yourself'],
  toTimestamp(now())
);
✓ Inserted 1 row | songs = ['Eye of the Tiger', 'Lose Yourself']
cassandra@cqlsh> -- Step 3: Append new song to END
UPDATE music.user_playlists
SET songs = songs + ['Stronger']
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
✓ Updated | songs = ['Eye of the Tiger', 'Lose Yourself', 'Stronger']
cassandra@cqlsh> -- Step 4: Prepend song to START
UPDATE music.user_playlists
SET songs = ['Thunderstruck'] + songs
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
✓ Updated | songs = ['Thunderstruck', 'Eye of the Tiger', 'Lose Yourself', 'Stronger']
cassandra@cqlsh> -- Step 5: Update song at index 1
UPDATE music.user_playlists
SET songs[1] = 'Born to Run'
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
✓ Updated | songs = ['Thunderstruck', 'Born to Run', 'Lose Yourself', 'Stronger']
cassandra@cqlsh> -- Step 6: Query the playlist
SELECT playlist_name, songs FROM music.user_playlists
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
playlist_name | songs
--------------+--------------------------------------------------------
Workout Mix | ['Thunderstruck', 'Born to Run', 'Lose Yourself', 'Stronger']

📊 LIST Operation Workflow (Animated)

LIST Operations: Step-by-Step 1. Initial List: Eye of the Tiger [0] Lose Yourself [1] 2. Append 'Stronger' (songs + ['Stronger']): Eye of the Tiger [0] Lose Yourself [1] Stronger [2] ← NEW! Added to END 3. Prepend 'Thunderstruck' (['Thunderstruck'] + songs): Thunderstruck [0] ← NEW! Eye of the Tiger [1] shifted Lose Yourself [2] shifted Stronger [3] shifted Added to START 4. Update Index [1] (songs[1] = 'Born to Run'): Thunderstruck [0] Born to Run [1] UPDATED! Lose Yourself [2] Stronger [3] ✓ Final Result: songs = ['Thunderstruck', 'Born to Run', 'Lose Yourself', 'Stronger']

🔍 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!

cassandra@cqlsh> -- Step 1: Create user with initial interests
INSERT INTO user_profiles (user_id, username, interests, skills)
VALUES (
  660e8400-e29b-41d4-a716-446655440001,
  'sarah_dev',
  {'coding', 'music', 'travel'},
  {'python', 'javascript'}
);
✓ Inserted | interests = {'coding', 'music', 'travel'}
cassandra@cqlsh> -- Step 2: Add new interests
UPDATE user_profiles
SET interests = interests + {'gaming', 'photography'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
✓ Updated | interests = {'coding', 'gaming', 'music', 'photography', 'travel'}
cassandra@cqlsh> -- Step 3: Try adding duplicate (has no effect!)
UPDATE user_profiles
SET interests = interests + {'coding', 'reading'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
⚠ 'coding' already exists - ignored!
✓ Updated | interests = {'coding', 'gaming', 'music', 'photography', 'reading', 'travel'}
cassandra@cqlsh> -- Step 4: Remove interests
UPDATE user_profiles
SET interests = interests - {'gaming', 'travel'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
✓ Updated | interests = {'coding', 'music', 'photography', 'reading'}
cassandra@cqlsh> -- Step 5: Replace entire set
UPDATE user_profiles
SET skills = {'python', 'java', 'cassandra', 'docker'}
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
✓ Updated | skills = {'cassandra', 'docker', 'java', 'python'} ← Alphabetically sorted!
cassandra@cqlsh> -- Step 6: Query the profile
SELECT username, interests, skills FROM user_profiles
WHERE user_id = 660e8400-e29b-41d4-a716-446655440001;
username | interests | skills
-----------+------------------------------------------+--------------------------------
sarah_dev | {'coding', 'music', 'photography', 'reading'} | {'cassandra', 'docker', 'java', 'python'}

📊 SET Operation Workflow (Animated)

SET Operations: Uniqueness Enforced 1. Initial SET: coding music travel ← Unordered, unique values 2. Add New Items (interests + {'gaming', 'photography'}): coding music travel gaming photography ✓ New items added 3. Try Adding Duplicate (interests + {'coding', 'reading'}): coding DUPLICATE! music travel gaming photography reading ✗ Ignored ✓ Added 4. Remove Items (interests - {'gaming', 'travel'}): coding music photography reading gaming travel ✗ Removed ✓ Final Result: coding music photography reading 4 unique values

🔍 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!

cassandra@cqlsh> -- Step 1: Create product with initial attributes
INSERT INTO product_catalog (
  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}
);
✓ Inserted | 3 attributes, 2 currencies
cassandra@cqlsh> -- Step 2: Add new key-value pairs
UPDATE product_catalog
SET attributes = attributes + {
  'storage': '512GB',
  'processor': 'M3 Max',
  'display': 'Liquid Retina XDR'
}
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
✓ Updated | Now has 6 attributes
cassandra@cqlsh> -- Step 3: Update existing key (overwrites value!)
UPDATE product_catalog
SET attributes['ram'] = '32GB'
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
⚠ Key 'ram' existed - value updated from '16GB' → '32GB'
✓ Updated | ram = '32GB'
cassandra@cqlsh> -- Step 4: Add new prices
UPDATE product_catalog
SET prices = prices + {'GBP': 1999.00, 'JPY': 350000.00, 'INR': 205000.00}
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
✓ Updated | Now in 5 currencies
cassandra@cqlsh> -- Step 5: Delete specific key
DELETE attributes['color']
FROM product_catalog
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
✓ Deleted | 'color' key removed
cassandra@cqlsh> -- Step 6: Access specific key
SELECT name, attributes['processor'], prices['USD']
FROM product_catalog
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
name | attributes['processor'] | prices['USD']
------------------+-------------------------+--------------
MacBook Pro 16" | M3 Max | 2499.00
cassandra@cqlsh> -- Step 7: Query full product
SELECT * FROM product_catalog
WHERE product_id = 770e8400-e29b-41d4-a716-446655440002;
attributes:
{'brand': 'Apple', 'ram': '32GB', 'storage': '512GB',
 'processor': 'M3 Max', 'display': 'Liquid Retina XDR'}
prices:
{'USD': 2499.00, 'EUR': 2299.00, 'GBP': 1999.00,
 'JPY': 350000.00, 'INR': 205000.00}

📊 MAP Operation Workflow (Animated)

MAP Operations: Key → Value Pairs 1. Initial MAP (Product Attributes): brand → Apple color → Silver ram → 16GB Key → Value structure 2. Add New Key-Value Pairs: brand → Apple storage → 512GB NEW! processor → M3 Max NEW! 3. Update Existing Key (Overwrites Value!): ram → 16GB OLD 32GB UPDATED! Key exists: Value replaced! 4. Delete Specific Key: brand → Apple color → Silver ✗ Entire key-value pair removed! 5. Access Specific Key (No need to read entire MAP!): processor → M3 Max Direct access! O(1) lookup ✓ Key Features of MAP: ✓ Unique keys ✓ Any value type ✓ Direct key access ✓ Flexible schema ✓ Update values ✓ Delete keys

🔍 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

1
What are the three collection types in Cassandra and how do they differ?
+

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']
2
When should you use collections vs separate rows in Cassandra?
+

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
3
What is the size limit for collections and why does it matter?
+

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.

4
How does Cassandra store collections internally?
+

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
5
Can you query individual elements in a collection? Why or why not?
+

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';
6
What are frozen collections and when should you use them?
+

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;
7
What happens when you try to add a duplicate to a SET?
+

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
8
How do you remove an element from a LIST?
+

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:

  1. Read the current list
  2. Modify it in application code
  3. Write the entire updated list back
9
Can you use collections as part of a primary key?
+

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.

10
What's the difference between MAP and User-Defined Type (UDT)?
+

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
11
What are the performance implications of using collections?
+

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:

  1. Keep collections under 100 items
  2. Use MAP for data you'll update partially
  3. Use separate rows for large datasets
  4. Monitor collection sizes in production
  5. Consider frozen collections for small, immutable data
12
Can you nest collections? How?
+

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>>
);
Advertisement

Responsive Ad