Avinash Lakshman — a co-author of Amazon's Dynamo paper — and Prashant Malik wrote Cassandra at Facebook for the inbox search: the random reads and writes of p. 399. It was open-sourced in July 2008 and became a top-level Apache project in February 2010. The name is the Trojan prophetess nobody believed. (Background: Wikipedia, "Apache Cassandra".)
Column stores and the Cassandra data model
After this lesson you will explain what a column store gains and what it pays, decode a run-length-encoded column, draw Cassandra's data the way the slides do — keyspace, column family, row key, columns of name, value and timestamp, super columns — and say why Cassandra's tables start from the queries.
The lecture of this lesson is called "Columnar Databases – Cassandra" (p. 396). Its first half defines the family: "The Column Oriented Database stores data in columns", "mainly used in OLAP" (p. 401). A chart on p. 405 shows why: to read 4 bytes of each 148-byte record, a row store needs about 51 seconds — and about 51 seconds to read all 148. A column store reads the 4 bytes in about one second.
Then the deck turns to Cassandra, the database of its title, and p. 417 says in large grey letters: "think of cassandra as row-oriented". Page 412 adds "Often write-heavy workloads" — the opposite of analytics.
So is Cassandra a column store or not? Both slides are right. By the end of the lesson you will know in which sense each one is meant, and how to say it at the exam.
Wide-column stores and the workload behind them
Picture a messaging service: many users writing and reading small messages all day, in no particular order, and no two mailboxes alike. Page 399, "Issues with today's workloads", describes exactly this:
- "Data large and unstructured" — too big for one machine, with no fixed set of fields;
- "Lots of random reads and writes" — many small operations, not one long scan;
- "Foreign keys rarely needed" — each record is read on its own.
Its "Actual Needs": "Incremental Scalability", "Speed", "No Single point of failure", "Low cost (TCO) and admin" (TCO: total cost of ownership) and "Scale out, not up" — add ordinary machines instead of buying a bigger one (lesson 3).
Page 397 places the systems built for this among the NoSQL families of lesson 5: "Data stored in 4 types: Document, Graph, Key-value, Wide-column". Page 398, the title page of this part of the deck, reads "Big Column Stores"; the course notes explain the name: a single row can have "a huge number of different columns and that is why columnar db are also often called Big Column stores".
| Family on the slide | Systems on the slide | Lessons |
|---|---|---|
| Document Database | CouchDB, Couchbase, MarkLogic, mongoDB | 13–16 |
| Graph Databases | Neo4j, InfiniteGraph | 10–12 |
| Key-value Databases | redis, Amazon DynamoDB, Aerospike, riak | 17–19 |
| Wide Column Stores | Accumulo, Hypertable, Cassandra, Apache HBase, Amazon SimpleDB | 20–22 |
Page 400 recalls the CAP theorem of lesson 5 — "Consistency: all nodes have same data at any time", "Availability: the system allows operations all the time", "Partition-tolerance: the system continues to work in spite of network partitions" — and places Cassandra: "Eventual (weak) consistency, Availability, Partition-tolerance". For a mailbox that is right: a message a second late hurts nobody, a mailbox that will not open does.
"Eventual" is only the default: each read or write says how many copies must answer (the CL of section 4), so one operation can buy consistency with waiting — p. 438's "tuneably consistent" (lesson 22).
Column-oriented storage: read less, write more
Before Cassandra, pp. 401–408 describe a different design that shares the word "column". Take a chain of shops that records every sale with the five columns of p. 402: date, store, product, customer, price. The cashier adds a sale every few seconds. The analyst asks, once a night, for the revenue of each store — two of the five columns, over every sale ever made.
Page 401: "The Column Oriented Database stores data in columns", "mainly used in OLAP(online Analytical Processing), Data Mining operations". OLAP is the analyst's question: few columns, very many rows. Its opposite, OLTP (online transaction processing), is the cashier's: whole records, one at a time.
Page 402 sets the two layouts side by side. A row store keeps each record in one piece: "+ easy to add/modify a record", "- might read in unnecessary data". A column store keeps each column in a piece of its own: "+ only need to read in relevant data", "- tuple writes require multiple accesses" (a tuple is a record). Verdict: "suitable for read-mostly, read-intensive, large data repositories".
In numbers: with \(N\) records of \(m\) columns of \(w\) bytes, a query that needs \(k\) columns reads
so the column store reads the fraction \(k/m\): 2 of 8 columns, a quarter. Page 403 lists four "Pros" by name only; their mechanisms (background):
- "Data compression": a column's values share a type and repeat (below).
- "Improved Bandwidth Utilization": every byte moved belongs to a needed column.
- "Improved Code Pipelining": one tight loop over an array of one type — why a NumPy operation on a whole array beats a Python loop.
- "Improved cache locality": neighbouring values in memory are the ones read next.
The "Cons" of p. 404 are the price of cutting records apart, and the widget showed each one: "Increased Disk Seek Time" (a record lives in \(m\) files), "Increased cost of Inserts" (one record, \(m\) writes), "Increases tuple reconstruction costs" (whole records must be glued back together).
Page 405, "Tuple Reconstruction", measures the trade (Fig. 1). The row store takes the same time whatever the query selects, because it always reads whole tuples; the column store's time grows with the bytes selected and catches up only near 125 of 148. The bullet "Large prefetch hides disk seeks in columns" answers the first con: reading each column file in large chunks turns many small jumps into a few long reads.
On the right of the same slide, a box labelled Construct rebuilds the records. Position \(i\) of every column belongs to record \(i\): reconstruction is a zip by position, with no keys. And prodID is stored as the single box (4,1,4) — the value 4, from position 1, four times: the compression of the next two slides.
| prodID | storeID | custID | price | tuple |
|---|---|---|---|---|
| 4 | 2 | 2 | 7 | (4, 2, 2, 7) |
| 4 | 1 | 3 | 13 | (4, 1, 3, 13) |
| 4 | 3 | 3 | 42 | (4, 3, 3, 42) |
| 4 | 1 | 3 | 80 | (4, 1, 3, 80) |
Page 406: compression "Trades I/O for CPU" — fewer bytes to read, a little decoding — and a scan waits for the disk, so the trade pays. Column stores profit most: "Higher data value locality in column stores" (neighbours belong to one attribute), "Techniques such as run length encoding far more useful", and the space saved "Can use extra space to store multiple copies of data in different sort orders", one per kind of query.
Run-length encoding (RLE) — a run of equal neighbouring values is stored as one triple (value, start_pos, run_length): the value, its first position, the number of rows.
Page 407 encodes a sales table sorted by quarter (Fig. 2): Quarter collapses to four triples, Product ID to short runs, and Price stays raw — neighbouring prices rarely repeat, and the encoding is chosen per column.
Before: the columns as stored, position by position
After: Quarter and Product ID as runs, Price unchanged
Read one row of p. 407's table without decoding anything else:
A triple \((v, s, l)\) stands for \(l\) copies of \(v\) from position \(s\):
\[ c[i] = v \quad \text{for } s \le i \le s + l - 1 \](Q1, 1, 300) covers 1–300, (Q2, 301, 350) covers 301–650.
Each run starts where the last one ended, \(s_{j+1} = s_j + l_j\): 1 + 300 = 301, 301 + 350 = 651, 651 + 500 = 1151. The last ends at 1151 + 600 − 1 = 1750: the table has 1,750 rows.
Ask for row 304. It lies in 301–650: the quarter is Q2.
Product ID: (2, 304, 1) covers exactly 304, so the product is 2.
Price is raw: its 304th value. The slide's Q2 block starts at 301 with 3, 8, 1, 4, so the price is 4.
Zip by position: row 304 is (Q2, 2, 4) — tuple reconstruction again.
Run-length encoding needs runs. Sort the widget's sixteen sales by customer and no two neighbouring quarters are alike: sixteen runs of length 1, three times the raw column — the widget's last task.
A column store reads only the columns a query names, and pays one access per column whenever a whole record is read or written.
Page 408 closes with a "List of Databases": Cassandra, Vertica, SybaseIQ, C-Store, BigTable, MonetDB, LucidDB. Five are column stores in the sense of pp. 401–407 (C-Store, a university prototype, lives on as Vertica — background), like Snowflake (lesson 23) and Spark's columnar files (lesson 40). Cassandra and BigTable are something else.
Cassandra: Bigtable's data model on Dynamo's ring
Page 410: Cassandra was "Originally designed at Facebook" and is "Open-sourced and now within Apache foundation".
Page 411, "innovation at scale", gives the family tree. "google bigtable (2006)": "consistency model: strong", "data model: sparse map", "clones: hbase, hypertable". "amazon dynamo (2007)": "O(1) dht", "consistency model: client tune-able", "clones: riak, voldemort". In large type: "cassandra ~= bigtable + dynamo".
The data model comes from Bigtable, the distribution from Dynamo. A sparse map is a table whose rows fill only the cells they have; empty cells cost nothing. A DHT (distributed hash table) spreads keys over machines by hashing them; O(1) means any node reaches a key's owner in one hop, because every node knows the whole ring (lesson 22). The "~=" is honest: unlike Bigtable, Cassandra has no master at all.
Page 412 draws the model as the "blog keyspace" (Fig. 3): "Column Families: Like SQL tables; but may be unstructured (client-specified); Can have index tables", "No schemas", "Some columns missing from some entries", "Supports get(key) and put(key, value) operations", "Often write-heavy workloads".
Rows are found by a key — here user names. time_ordered_blogs_by_user is not an entity but an answer prepared in advance: "the entries of jbellis, in time order" is one row read. The asterisks mark secondary indexes on two columns: keep them in mind for section 4.
"Page 412 says “column-oriented databases”, so Cassandra keeps each column in a file of its own, like Vertica." No: the label comes from the column families. A Cassandra row keeps all its columns together under its row key — "think of cassandra as row-oriented" (p. 417). Kleppmann, the course textbook, calls the label "very misleading" for this family (ch. 3, background). Keep the course's word, and say which sense you mean.
Pages 413–417 give the vocabulary, from the outside in:
Keyspace — "~= database", "typically one per application", "some settings are configurable only per keyspace" (p. 415): above all the replication, how many copies of each row are kept and where (lesson 21).
Column family — "group records of similar kind", "not same kind, because CFs are sparse tables" (p. 416); settings: the comparator and the type, standard or super (p. 413). Examples: User, Address, Tweet, PointOfInterest, HotelRoom.
Row — "each row is uniquely identifiable by key"; "rows group columns and super columns" (p. 417).
Column — name, value and clock (p. 413): section 4.
Page 417 is the slide of the hook: "think of cassandra as row-oriented" — one key opens a row, and its columns come back together. Page 414 draws a "Row Key" pointing to pairs of "Column Keys/Names" and "Column Values"; p. 418 shows two rows of one family with different columns; p. 419 writes them as nested maps:
// p. 419: User as nested maps
User {
123 : { email: alison@foo.com,
icon: [the Cassandra eye] },
456 : { email: eben@bar.com,
location: The Danger Zone }
}
Think of a column family as a map of maps: row key → that row's columns, sorted by name, each one a name, a value and a timestamp.
A column has three parts, a super column has columns
Zoom into one cell. Page 420, "a column has 3 parts":
name — "byte[]", "determines sort order", "used in queries", "indexed".
value — "byte[]", "you don't query on column values".
timestamp — "long (clock)", "last write wins conflict resolution".
The name is the key of the row's inner map: sorted, so a range of names is one contiguous read, and indexed inside the row, so a name is found without scanning. The value is opaque bytes — a string, a number, the eye picture of p. 418 — stored as it comes and never sorted.
The timestamp settles disagreements between copies. One replica of row 456 holds email = eben@bar.com with timestamp 1004, another email = eben@new.com with 1010: when they are compared, 1010 wins on both. The client supplies the clock with every write (p. 428); a delete carries one too, so it can beat an older value. Simple and fast, at a price: of two concurrent writes, one is silently lost.
The sort order is set by the column family's comparator (p. 421): "byte", "utf8", "long", "timeuuid", "lexicaluuid", "<pluggable>", "ex: lat/long" — the type of the column names. For the names 2 and 10, utf8 compares characters and puts "10" first, long compares numbers and puts 2 first; timeuuid orders time-based identifiers by time, the natural choice for a timeline.
Page 422 adds a level: "super columns group columns under a common name". A super column has a name, "name: byte[]", and instead of a value a map of columns, "cols: Map<byte[], IColumn>". A column family is standard or super (the "type" of p. 413); in a super column family every row holds super columns.
Pages 423–424 fill one: PointOfInterest, keyed by ZIP code. Row 85255 holds the super columns Phoenix Zoo and Spring Training, row 10019 Central Park and Empire State Building; each holds phone and desc — or only desc, for Central Park: "flexible schema".
// p. 424: a super column family as nested maps
PointOfInterest {
key: 85255 {
Phoenix Zoo { phone: 480-555-5555,
desc: They have animals here. },
Spring Training { phone: 623-333-3333,
desc: Fun for baseball fans. },
}, //end phx
key: 10019 {
Central Park { desc: Walk around. It's pretty.} ,
Empire State Building { phone: 212-777-7777,
desc: Great view from 102nd floor. }
} //end nyc
}
Page 425: "sub-column names in a SCF are not indexed", while "top level columns (SCF Name) are always indexed" — read the parenthesis as the super column names. So one sub-column costs a read of its whole super column. Super column families are "often used for denormalizing data from standard CFs": copying data to where a query reads it.
To ask for some of a row's columns you send a slice predicate, a "data structure describing columns to return" (p. 426). Its SliceRange: "start column name", "finish column name (can be empty to stop on count)", "reverse", "count (like LIMIT)". The names are sorted, so a slice is one stretch of the row:
Pages 427–428 list the calls of the API of the time, Thrift RPC. Every call on these slides names its rows by key — one key, a list of keys or a range — never by a value, and ends with CL, the consistency level: how many replicas must answer (lesson 22). That is "tuneably consistent" in practice.
| Call | What it does |
|---|---|
get(key, path, CL) | one column or super column of one row, at its column path ("get the Col or SC at given ColPath"); it returns a ColumnOrSuperColumn |
get_slice(key, parent, predicate, CL) | the columns of one row chosen by a slice predicate ("get Cols in one row") |
multiget_slice(rowKeys, parent, predicate, CL) | the same slice for a list of keys: a map key → columns |
get_range_slices(parent, predicate, keyRange, CL) | a slice of every row in a range: "range is startkey, endkey, starttoken, endtoken" |
insert(key, parent, Column, CL) | writes one column: name, value, clock |
batch_mutate(map<byte[], map<String, List<Mutation>>>, CL) | many inserts and deletions — per row key, per column family — in one call |
remove(byte[], ColumnPath column_path, Clock, CL) | deletes a column, a super column or a row, with a clock: the deletion competes with writes by timestamp |
// p. 428: write one column (name, value, clock)
client.insert(userKeyBytes, parent,
new Column("band".getBytes(UTF8),
"Funkadelic".getBytes(), clock), CL);
The insert is p. 420 in code: name, value, clock. There is no separate update: writing an existing column stores a new value with a newer clock, and the newer clock wins.
All this is the Cassandra of 2010. Current Cassandra offers the same model through CQL tables, the language of the exercise sessions and the past papers (lesson 21); Thrift was removed in version 4.0 (2021), and super columns no longer exist. Theory questions use the slides' words, exercises the CQL ones:
| On the slides (2010) | In CQL (lesson 21) |
|---|---|
| keyspace | keyspace, CREATE KEYSPACE … WITH replication = {…} |
| column family | table — the papers write "tables (column families)" |
| row key | partition key: decides on which nodes the row lives |
| column names sorted by the comparator | clustering columns, sorted by CLUSTERING ORDER BY |
| column value, bytes | a regular column with a type (text, int, …) |
| timestamp (clock) | the write time of every cell: still last write wins |
| super column | gone: one more clustering column, or a user-defined type |
| slice predicate: start, finish, reverse, count | a range on a clustering column, ORDER BY … DESC, LIMIT |
| get, insert, remove … with a CL | SELECT, INSERT, DELETE in cqlsh, with a consistency level |
No SELECT … WHERE: start from the queries
Page 429 asks "what about… SQL?" and lists five habits: SELECT, WHERE, ORDER BY, JOIN ON, GROUP. The answer is a way of designing, not a clause.
Page 430: "rdbms: domain-based model — what answers do I have?"; "cassandra: query-based model — what questions do I have?"; "Start from queries, then design the data model". A relational designer models the domain — entities, relationships, normal forms (lessons 8–9) — and answers later questions with joins. Cassandra finds rows only by key and does not join, so a question with no structure built for it has no efficient answer: you list the questions first.
Page 431, "cassandra is an index factory": the column family USER has the key UserID and the columns username, email, birth date, city, state. It answers "give me user 123" in one read, and nothing else. "How to support this query?"
-- p. 431: the query that USER, keyed by UserID, cannot answer
SELECT * FROM User WHERE city = 'Scottsdale'
The slide's answer: "Create a new CF called UserCity", keyed by city, whose columns are the "IDs of the users in that city". The query becomes one row read, plus a multiget_slice on USER for the users. UserCity "uses the Valueless Column pattern": the information is in the column names, the values are empty. What an RDBMS does with an index, you do by hand, one column family per question, written together with USER — an index factory.
In CQL (lesson 21) UserCity is a table whose partition key is the city, with the user ids as clustering values — the valueless columns:
-- UserCity of p. 431 in CQL: one partition per city
CREATE TABLE user_city (
city text,
user_id text,
PRIMARY KEY ((city), user_id)
);
-- all the users of Scottsdale: one partition read
SELECT user_id FROM user_city WHERE city = 'Scottsdale';
Two conditions at once? Page 432: "Use an aggregate key" — state:city: { user1, user2} — then "Get rows between AZ: & AZ; for all Arizona users" and "Get rows between AZ:Scottsdale & AZ:Scottsdale1 for all Scottsdale users". Why do those bounds work?
Four rows — AZ:Phoenix, AZ:Scottsdale, AZ:Tucson, CA:Fresno — each with its users as valueless columns. Keys compare as strings, character by character, and a prefix sorts first: AZ: comes before AZ:Phoenix.
In ASCII the colon is 58 and the semicolon 59. Every Arizona key has a colon in third place, so it sorts before AZ;. CA:Fresno sorts after AZ; because C comes after A. So AZ: to AZ; holds exactly the Arizona rows.
Likewise AZ:Scottsdale1 sorts right after AZ:Scottsdale: that range holds the one Scottsdale row.
This needs rows in key order: the order-preserving partitioner. The random one keeps rows in the order of the MD5 of their keys — AZ:Phoenix (ab66…), AZ:Tucson (f163…), CA:Fresno (f3ca…), AZ:Scottsdale (f412…) — so a Californian row sits between Arizona rows, and "the rows between AZ: and AZ;" no longer means the Arizona users.
Page 433, "ORDER BY": "Columns are sorted according to CompareWith or CompareSubcolumnsWith" (the comparators of names and sub-column names); "Rows are placed according to their Partitioner: Random: MD5 of key; Order-Preserving: actual key". No ORDER BY clause: to get entries by time, make the time the column name. The last line needs a correction:
No slide answers JOIN ON and GROUP in words; the hotel example of pp. 434–435 (Fig. 4) answers them by design. Eight relational tables tied by foreign keys, where a hotel page needs several joins, become seven column families with no foreign keys, each shaped for the reads it serves.
Before (p. 434): eight tables, seven foreign keys
After (p. 435): seven column families, no foreign keys
| Relational (p. 434) | Cassandra (p. 435) | What changed |
|---|---|---|
| Hotel | <<CF>> Hotel, row key hotelID | the same entity, found by its key |
| "hotels in a city": a WHERE on City | <<CF>> HotelByCity, key city:state (printed #city:state:hotelID), columns hotel1, hotel2, … | an index column family with an aggregate key and valueless columns (pp. 431–432) |
| PointOfInterest + HotelToPOI | <<SCF>> PointOfInterest, keys HotelID and poiName, columns desc, phone | the bridge table disappears: the points of interest are copied next to each hotel |
| Room + RoomToAmenity + Amenity | <<SCF>> Room, keys hotelID and roomID, columns num, type, rate, coffee, tv, hottub, … | amenities become columns a room has or lacks: a sparse table |
| — (counted from reservations) | <<SCF>> RoomAvailability, keys hotelID and date, kk = 22, qq = 14 | counts kept up to date at write time (the slide does not expand kk and qq): our reading of how GROUP is answered |
| Guest (GuestID, Name, Email) | <<CF>> Guest, row key phone | the key is what the application looks guests up by |
| Reservation (GuestID, RoomID, StartDate, EndDate) | <<CF>> Reservation, row key resID: hotelID, roomID, phone, name, arrive, depart, rate, ccNum | the guest's phone and name and the room's rate are copied in: no join |
The price: names, phones and rates are stored twice, and the application keeps the copies in step — affordable because writes are cheap ("very fast writes", p. 438). Pages 436–437 compare Cassandra with an RDBMS:
| Property | Cassandra | RDBMS |
|---|---|---|
| Core Architecture | Masterless (no single point of failure) | Master-slave (single points of failure) |
| High Availability | Always-on continuous availability | General replication with master-slave |
| Data Model | Dynamic; structured and unstructured data | Legacy RDBMS; Structured data |
| Scalability Model | Big data/Linear scale performance | Oracle RAC or Exadata |
| Multi-Data Center Support | Multi-directional, multi-cloud availability | Nothing specific |
| Enterprise Search | Integrated search on Cassandra data. | Handled via Oracle search |
| In-Memory Database Option | Built-in in-memory option | Columnar in-memory option |
| Joining | Doesn't support joining | Supports joining |
| Referential Integrity | Cassandra has no concept of referential integrity across tables. No cascading deletes. | Supports foreign keys in a table to reference the primary key of a another table. Supports cascading delete. |
| Normalization | Tables contain duplicate denormalize data. | Tables are normalized to avoid redundancy. |
The rows of p. 437 are the data model in one breath: no joins, no foreign keys, duplicated data — joins and integrity checks would make nodes coordinate on every query. "Enterprise Search" and "In-Memory Database Option" describe DataStax's commercial edition, the table's source (background).
Page 438 closes with the checklist: "tuneably consistent", "very fast writes", "highly available", "fault tolerant", "linear, elastic scalability", "decentralized/symmetric", "~12 client languages", "Thrift RPC API", "~automatic provisioning of new nodes", "0(1) dht" (read O(1), as on p. 411), "big data" — the mechanisms of lesson 22.
When the exam's one-point table asks which DB technology fits which entities (lesson 5), motivate a columnar row with these slides: a large, continuously growing volume of records, written often and read by key along queries known in advance — "Often write-heavy workloads" (p. 412), "very fast writes" and "linear, elastic scalability" (p. 438).
The course notes say Cassandra "is almost exclusively used in OLAP systems with multiple Terabytes sized data" and list "Increased tuple reconstruction costs" among the ways it differs from relational tables: column-store facts carried over by mistake. The slides say "Often write-heavy workloads" (p. 412), and a Cassandra row is stored whole.
Start from the queries: one column family per question, the data copied to where it is read, and no joins.
Back to the puzzle. "The Column Oriented Database stores data in columns" (p. 401) and "think of cassandra as row-oriented" (p. 417) are both right, because "column" means two things. In Vertica or C-Store a column is a file: one attribute of every record, stored together, compressed and scanned — OLAP. In Cassandra a column is a name in a row's sorted map, stored with the rest of its row; a row can hold a huge number of them, hence "Big Column Stores". The deck and the papers ("you decide to store it in a columnar database", exam of 12 Sep 2025) call both "columnar": keep the label, and say which one you mean.
Exam-style questions
Theory questions in the forms of the theory part — true/false, multiple choice and short open answers, where a wrong answer may cost points — and one practical exercise in the wording of the past papers, built on the hotel of pp. 434–435.
True or false: Cassandra stores the values of each column of a column family together, separately from the other columns, as a column-oriented database such as Vertica does.
Show model answer
False. Cassandra is a wide-column (column-family) store: each row is uniquely identified by its key and keeps all its columns, sorted by name, together, so it should be thought of as row-oriented. Storing each column separately is the design of column-oriented databases such as Vertica or C-Store, built for OLAP: a query reads only the columns it needs, while inserts and whole-record reads touch every column. Cassandra is called columnar because of its column families and its very wide rows, and it is built for write-heavy workloads.
The trap is the word "column", shared by two different designs (section 2 and section 3). Wrong answers may carry a penalty, announced just before the exam: answer only what you can justify.
Explain how a column-oriented database stores a table and compare it with a row-oriented database: advantages, disadvantages, and the workloads each one suits.
Show model answer
A row-oriented database stores each record contiguously; a column-oriented database stores each column contiguously, in its own storage. A query that needs k of the m columns therefore reads only those k columns in a column store, while a row store reads whole records and may read unnecessary data. The column layout also compresses better (the values of a column share a type and repeat, so techniques such as run-length encoding work well) and improves bandwidth utilisation, code pipelining and cache locality. Its disadvantages are increased disk seek time (a record is spread over many files), an increased cost of inserts (one write per column) and increased tuple reconstruction costs (records must be rebuilt by position). Column stores therefore suit read-mostly, read-intensive, large repositories — OLAP and data mining — while row stores suit OLTP, with frequent inserts and updates of whole records.
A complete answer has four parts: the two layouts, the read argument (k of m columns), the pros and cons in the words of pp. 403–404, and the workloads. A strong extra is the chart of p. 405: the column store loses only when a query selects nearly the whole tuple.
The Quarter column of a sales table, sorted by quarter, is stored with run-length encoding as (Q1, 1, 300), (Q2, 301, 350), (Q3, 651, 500), (Q4, 1151, 600). Which statements are correct? Select all that apply.
Show model answer
The second, third and fifth statements. Each triple is (value, start_pos, run_length), so (Q2, 301, 350) covers the positions 301 to 650; the last run ends at 1151 + 600 − 1 = 1750; position 700 lies in the run of Q3, from 651 to 1150. Run-length encoding pays only when equal values are adjacent: the Quarter column is sorted and the prices are not, so the encoding is chosen column by column and Price stays raw.
The numbers are those of p. 407, so the answer can be checked against the slide. The classic slip is reading run_length as an end position. Wrong ticks may carry a penalty: tick only what you can justify.
Describe the data model of Cassandra as presented in the lecture — keyspace, column family, row, column and super column. Explain why a column family is called a sparse table and what the timestamp of a column is used for.
Show model answer
A keyspace is the outermost container, roughly a database and typically one per application; some settings, such as the replication, are configured per keyspace. It contains column families, the analogue of tables, which group records of a similar but not identical kind; their settings include the comparator and the type, standard or super. A column family holds rows, each uniquely identified by its row key; Cassandra should be thought of as row-oriented, because a row's columns are stored and returned together. A column has three parts: a name (bytes; it determines the sort order, is used in queries and is indexed), a value (bytes; values are not queried) and a timestamp. A super column groups columns under a common name: it has a name and a map of columns instead of a value, and its sub-column names are not indexed. A column family is a sparse table because each row stores only the columns it has: a missing column costs nothing and is not a NULL. The timestamp (clock) is supplied with every write and resolves conflicts between replicas: the last write wins.
Give the containment chain in order and the three parts of a column with their properties (p. 420). "Sparse" and "last write wins" are the two whys the question asks for. If you quote the slide's example of a keyspace setting, the partitioner, add that it is in fact set for the whole cluster (red pen on p. 413).
Explain the difference between the domain-based model of an RDBMS and the query-based model of Cassandra. How would you support the query SELECT * FROM User WHERE city = 'Scottsdale' on a column family User keyed by UserID, and what does the solution cost?
Show model answer
An RDBMS follows a domain-based model: you model the entities and relationships of the domain, normalise them, and answer any later question by combining tables with joins ("what answers do I have?"). Cassandra follows a query-based model: rows are found by key, column values are not queried and there are no joins, so you list the questions first and build a structure for each one ("what questions do I have?" — start from queries, then design the data model). For the query, create a second column family, UserCity, keyed by city, whose column names are the IDs of the users living there, with empty values (the Valueless Column pattern): the query becomes one row read, followed by a multiget of the users from User. This is why Cassandra is called an index factory. The cost is duplication: every user is written to both column families, and the application must keep the copies consistent, with no joins and no referential integrity to help.
Two things make the answer: the why (rows found only by key, no joins) and the price (duplication, copies kept in step by the application). In CQL the same idea is a table with PRIMARY KEY ((city), user_id). The exercise session's other tool is a secondary index, CREATE INDEX; the design question of the paper of 30 Jan 2026 rules it out with "Do not use indexes".
Our exercise, in the wording of the past papers: the hotel of pp. 434–435. Consider the following ER diagram. Hotel (Hotel_ID, Name, Phone, Address, City, State, Zip); Room (Room_ID, Room Number, Rate); Guest (Guest_ID, Name, Phone, Email). Relationships: Has, Hotel (1, N) – Room (1, 1); Reserve, Guest (0, N) – Room (0, N), with the attributes Start Date and End Date.
Suppose you expect to record a large-scale, continuously increasing number of reservations, and therefore you decide to store them in a columnar database. Write the specification of the tables (column families) of a Cassandra deployment so that one can query the hotels of a state, the hotels of a city of a given state, and the reservations of a guest by phone number, the most recent first. Do not use indexes.
Show model answer
Two tables, one per access path: the hotels by location, partitioned by state and clustered by city, and the reservations by guest phone, partitioned by phone and clustered by start date, the most recent first. Each reservation carries copies of the guest, hotel and room data it shows, because Cassandra has no joins.
-- the keyspace (one per application)
CREATE KEYSPACE hotels
WITH replication = {'class': 'SimpleStrategy',
'replication_factor': 3};
-- queries 1 and 2: the hotels of a state, or of a city in it
CREATE TABLE hotels_by_location (
state text,
city text,
hotel_id text,
name text,
phone text,
address text,
zip text,
PRIMARY KEY ((state), city, hotel_id)
);
-- query 3: a guest's reservations, with copies, newest first
CREATE TABLE reservations_by_guest_phone (
guest_phone text,
start_date date,
reservation_id uuid,
end_date date,
guest_name text,
hotel_id text,
hotel_name text,
room_number int,
rate float,
PRIMARY KEY ((guest_phone), start_date, reservation_id)
) WITH CLUSTERING ORDER BY (start_date DESC, reservation_id ASC);
-- all the hotels of Arizona: one partition
SELECT * FROM hotels_by_location WHERE state = 'AZ';
-- the hotels of Scottsdale: the first clustering column fixed
SELECT * FROM hotels_by_location
WHERE state = 'AZ' AND city = 'Scottsdale';
-- a guest's reservations, the most recent first
SELECT * FROM reservations_by_guest_phone
WHERE guest_phone = '+1 480 555 0101';
Every reservation is one INSERT into reservations_by_guest_phone, with reservation_id from uuid() and the guest's name, the hotel's name and the room's number and rate copied in.
This is pp. 431–435 in CQL. hotels_by_location is the aggregate key state:city of p. 432: the state is the partition key and the city the first clustering column, so one table answers both location questions with no key range and no order-preserving partitioner. reservations_by_guest_phone is the Reservation of p. 435: keyed by what the application searches, with copies instead of joins; reservation_id keeps two reservations of one guest on the same day apart, since an INSERT with an existing primary key overwrites. Past papers ask the same kind of question — the farmers' paper of 12 Sep 2025, exercise 1, is solved in lesson 21, together with the CQL rules behind every line.
Check yourself
A sales table has 20 columns of equal width and ten million rows. A query computes the average of one column. About what share of the row store's bytes does a column store read?
Sensor readings are stored sorted by sensor id, with 50,000 readings per sensor. Which column gains most from run-length encoding?
Two replicas of row 456 disagree: one holds email = eben@bar.com with timestamp 1004, the other email = eben@new.com with timestamp 1010. What do they hold after they are compared?
In time_ordered_blogs_by_user the column names are millisecond timestamps (comparator long). Which slice predicate returns a user's ten latest entries?
You must answer "the users in Scottsdale" and "the users in Arizona" without reading the whole User column family. Which designs from the lecture work? Select all that apply.
Both working designs are index factories: the data is copied into a family keyed by the value you search.
Why does Cassandra give up joins (p. 437)?
In the super column family PointOfInterest you need only the phone of the Empire State Building (row 10019). What does Cassandra read?
Key take-aways
- A row store keeps each record in one piece, a column store each column: a query that needs \(k\) of \(m\) columns reads the share \(k/m\) of the bytes in a column store, but reading or inserting a whole record touches \(m\) places — "suitable for read-mostly, read-intensive, large data repositories" (pp. 402–405).
- Column stores compress well because neighbouring values belong to one attribute; run-length encoding stores (value, start_pos, run_length) and pays only on sorted or clustered columns (pp. 406–407).
- "Columnar" names two families: column-oriented stores (Vertica, C-Store, MonetDB, SybaseIQ, LucidDB) and wide-column stores (Cassandra, BigTable, HBase) — "think of cassandra as row-oriented" (p. 417).
- "cassandra ~= bigtable + dynamo": Bigtable's data model (a sparse map) on Dynamo's distribution (an O(1) DHT, client-tunable consistency); by default AP and eventually consistent, "tuneably consistent" per operation.
- keyspace › column family › row (by key) › column (name, value, timestamp): names sorted by the comparator and read in slices, values not queried by design, last write wins; super columns add a level of named groups whose sub-columns are not indexed.
- Cassandra's model is query-based: start from the queries, build a column family per question (index factory, valueless columns, aggregate keys), get order by design (comparator, partitioner), and denormalise instead of joining — no joins, no referential integrity.
- Today the same model is written in CQL (lesson 21): table, partition key, clustering columns; no super columns, no Thrift.
What to watch and read
-
Logical data modeling
Apache Cassandra documentation
A hotel domain like that of pp. 434–435, modelled from its queries into CQL tables such as
hotels_by_poiandavailable_rooms_by_hotel_date. Read it next to section 5: the same "index factory", in the notation of the exercise sessions. -
F2023 #05 – Storage Models & Database Compression
Andy Pavlo, CMU Intro to Database Systems
A university lecture on row versus column layout and on compression — run-length, dictionary, bit-packing — that turns the four pros and three cons of pp. 403–404 into page layouts.
-
Bigtable: A Distributed Storage System for Structured Data
Chang et al., Google, OSDI 2006
The "sparse map" of p. 411 in its original words: rows, column families and timestamps. Section 2, on the data model, is enough.
-
Cassandra – A Decentralized Structured Storage System
Avinash Lakshman, Prashant Malik, LADIS 2009
Six pages by the two authors: the data model of this lesson and the ring of the next ones, with the inbox search that Cassandra was built for.