Skip to main content
Big Data Systems

NoSQL Databases

Published: 2026-08-03
Level: postgraduate
Audience: Postgraduate students in Big Data Systems

4.1 Why We Move Away from RDBMS

Hook: What happens when the database that has run your business faithfully for decades suddenly becomes the reason your website crashes on a busy sale day? The answer to that question is the whole story of why NoSQL was born.

4.1.1 The ACID promise and the system of record

Before NoSQL, the storage that was available was largely the RDBMS — the relational database management system. These systems were typically meant for OLTPonline transaction processing — and were also known as the system of record, because they carried out day-to-day business transactions. The main concern with an RDBMS is strict consistency and durability guarantees over the multiple data items involved in a transaction. To deliver those guarantees, an RDBMS commits to the ACID properties, which were discussed in detail in earlier sessions:

  • A for atomicity — a transaction either happens fully or not at all.
  • C for consistency — a transaction moves the database from one valid state to another.
  • I for isolation — concurrent transactions do not interfere with each other.
  • D for durability — once a transaction is committed, it stays committed.

Sticking to these strong guarantees is also what makes an RDBMS hard to scale.

What ACID actually looks like in a bank: Think of a money transfer: the first operation debits ₹1,000 from the sender's account, the second operation credits ₹1,000 to the receiver's account. Atomicity says both operations must complete, or both are rolled back if anything is interrupted in between — the money can never exist in neither account, and it can never be created from nothing. Consistency says the bookkeeping identity must hold: the sum of deposits minus the sum of withdrawals must always equal the account balance. Isolation says two transfers running at the same time behave as if each ran alone. Durability says that once the transfer is committed, it survives a power cut. These four guarantees together are what make an RDBMS trustworthy — and expensive to operate at scale.

4.1.2 Vertical scaling and the volume bottleneck

Because RDBMS systems strictly follow ACID, they face trouble in scaling. They can scale to some extent, but only at a very high cost, because they follow a kind of vertical scaling: if you want to increase storage capacity you have to stop your system, add newer, high-end hardware — good quality hard drives, maybe SSDs, or something better — and bring the system back up. They do not follow the horizontal scaling that distributed systems follow, which is very easy to scale: you just add another machine. Vertical scaling is expensive, disruptive, and does not grow cheaply.

Analogy — one big truck versus many small trucks: Vertical scaling is like buying a bigger truck every time you need to move more goods. It costs a lot, and while the new truck is being delivered, the whole delivery business is shut down. Horizontal scaling is like adding another ordinary truck to your fleet: the existing trucks keep running while you add one more, and you can keep adding trucks one at a time. The analogy breaks where coordination enters — many trucks need a dispatcher and routing plan, which is exactly why distributed databases have to think carefully about consistency (Section 4.5.6).

The RDBMS is also not suited for applications with very large volumes of data, because at some point the performance bottlenecks when the system has to deal with a very large volume of data. And if you want to build distributed, geoscale applications — applications hosted across the globe — the RDBMS is not the right fit either.

4.1.3 Applications that can relax consistency

There are scenarios that simply do not need strict consistency and durability for every use case. Take a social media application running on multiple data centers across the world. The application is hosted in several geographies, and some application data stays in one region while other data stays in another region, with a sync happening at some point in time — not at the same moment the write happened. Consistency is going to be compromised in certain ways, and that is acceptable.

Real-world: You have probably hit these compromise points yourself. While shopping on an e-commerce site and trying to add an item to your cart, sometimes the add does not work — a rare event, but it happens. If you hit a like button on a post, it is possible the like is not recorded. There are multiple reasons for these failures. The point is that these systems do not require much consistency; the focus is not on strict consistency.

The same idea applies to an e-commerce website: a credit card transaction absolutely needs strict consistency, but browsing products or adding items to a cart does not. Those are the scenarios where other technologies — apart from the RDBMS — can simulate the behavior with much better performance. That is why we move to NoSQL: better performance for the cases that can relax consistency.

Scope: The decision to relax consistency is per-application, not per-technology. The same e-commerce company runs a payment system that demands ACID (a lost credit card charge is catastrophic) next to a product-catalog system that tolerates eventual consistency (a slightly stale price for a few seconds is harmless). The question is never "does this business need consistency?" — it is "does this operation need it?" A money transfer must never be the operation that gets relaxed.

4.1.4 Schema rigidity: the uniformity assumption

The second reason is schema. In an RDBMS, every table has a schema, and the collection of tables forms the database. One table maintains uniformity across its records: if a table has \(n\) attributes \(A_1, A_2, \ldots, A_n\), every record inserted into that table must carry all \(n\) values. This uniformity is a guarantee, but there are scenarios where this much uniformity is not required — each row may carry a different kind of data that you want to store.

Why uniformity hurts: A table with \(n\) attributes \(A_1, A_2, \ldots, A_n\) forces every row to carry a value for all \(n\) columns — even when most rows have no meaningful value for most columns. For example, an employee table with columns for office phone, mobile phone, and emergency contact forces every employee to fill all three, although many employees have no emergency-contact entry or no office line. The RDBMS responds by making missing values NULL, but the schema is still fixed at design time: adding a brand-new attribute later means an ALTER TABLE that rewrites the structure. In NoSQL, records that need different attributes simply carry different attributes — nothing is forced, and nothing has to be migrated.

There is also the connectivity problem: certain types of information involve extreme connectivity. In a social media application, how many friends do you have between two hops? In a network topology, routers are connected to switches and to one another — a huge, complex topology. For scenarios like these, again, the RDBMS is not a good choice. For all such scenarios there is a fourth category of database — very, very important — the graph database.

4.1.5 Where RDBMS is still the right fit

The RDBMS is the right fit when you want normalized tables and you need to perform many join operations. That is its home turf. But when you want to store flat structures, do not want frequent joins, and do not want strict consistency levels, you may go for one of the other technologies — and based on the scenario given to you, you choose one of the NoSQL databases to build your application. The choice is scenario-driven: no single storage engine is right for everything.

Recap + bridge: The RDBMS wins when normalized tables and frequent joins matter, and its ACID guarantees are non-negotiable for transactions like payments. It loses when you need cheap horizontal scaling, very high data volumes, geo-distributed deployments, flexible schemas, or heavily connected data. Next we define the family of systems built for exactly those scenarios — NoSQL — and look at where its name came from.

4.2 What Is NoSQL: Origins and Shared Characteristics

Hook: A database name with two birthdays, given by different people on different continents, meaning "not SQL" — and yet today it is read as "Not Only SQL." How did a term that started as a specific project name become the label for an entire generation of storage systems?

4.2.1 The name: 1998 and 2009

The term NoSQL has two birthdays. It was first given by Carlo Strozzi in 1998 — a pretty old term — to describe a lightweight open-source database without a standard SQL interface. Then in 2009 the term was reintroduced, with the meaning we use today: non-relational databases that do not follow the relational ACID properties or the rigid schema thing — they follow something different. The name is best read as Not Only SQL: it is not the absence of SQL, it is SQL plus other things.

Two birthdays, two meanings:

  • 1998 — Carlo Strozzi used the name for his lightweight open-source relational database that deliberately avoided a standard SQL interface. In this first incarnation the name literally meant "no SQL."
  • 2009 — the movement: the term was reintroduced at a gathering of developers behind large-scale distributed storage projects (the event is commonly credited to Johan Oskarsson) to describe the growing family of non-relational databases. In this second incarnation the name took the meaning that stuck: Not Only SQL — a database can still use SQL-style querying and still be called NoSQL, because the point is the other access methods and storage models, not the rejection of SQL.

So when you see the letters N-o-S-Q-L on an exam, read them as Not Only SQL. The name is not a ban on SQL; it is an invitation to go beyond it.

4.2.2 The characteristics shared by NoSQL systems

Around those two points in time, many big tech companies were working on their own storage technologies, and each came up with its own design. Yet the technologies that gathered under the NoSQL name shared a set of common properties:

  • They were not only SQL based.
  • They were non-relational in nature — the concept of foreign keys linking two tables was not given priority.
  • They were schema-less — no rigid schemas.
  • They were trying to loosen consistency to address the scalability and availability requirements of large-scale applications.

What does "loosening consistency" look like in practice? A NoSQL system often runs in a distributed setup. If somebody hits a like on the nearest server and that server dies, the like is not communicated across the multiple machines, and the write may not succeed. If you are adding an item to a cart and that happens to be the last item, another user may have added it meanwhile, so your add fails. Any of these things may happen — and that is what we mean by relaxing consistency.

Assumption: Loosening consistency is a deliberate, bounded trade-off, not sloppiness. The system promises that the data will converge to a consistent state later (eventual consistency, Section 4.2.3) — it just does not promise when and does not block the user in the meantime. What breaks if the assumption is wrong: any operation that needs the latest value right now (a payment, an inventory decrement on the last item) can read or act on stale data. That is why the same organizations run ACID systems beside NoSQL systems rather than replacing one with the other.

4.2.3 The performance motivation behind the movement

The NoSQL databases were born out of the open-source movement of large web-scale applications. Many people wanted to build large-scale web applications that cater to huge user bases. The builders looked deeply into the databases and found where the performance bottleneck was: the RDBMS, with its strict consistency requirements, was the bottleneck. The question was — if I change the storage, will my performance improve in certain scenarios?

The answer is careful, not blanket. Not every scenario can compromise consistency, and the RDBMS is not obsolete. There are real-life scenarios that do not require consistency and durability, and for those we move to other technologies for better performance — technologies that compromise on consistency and durability. In place of strict consistency, they can go for eventual consistency: the system converges to a consistent state later, after the writes settle.

Eventual consistency in one line: the system is allowed to serve slightly out-of-date data for a while, but once the writes stop, all copies converge to the same value. The price of this flexibility is a guarantee you lose: there is no single moment where you can say "every replica now has the latest value." For a like button or a cart this price is invisible; for a bank ledger it is unacceptable.

4.2.4 Throughput, distribution, and cluster friendliness

Two more characteristics matter. First, these databases are distributed for scale: they can be hosted on multiple nodes, and you add a node to scale the system easily — horizontal scaling. Second, they are cluster friendly: they can work as one form of a cluster that coordinates to achieve a task. These characteristics were observed across all the databases we will talk about. Yahoo came out with technologies, Google came out with technologies — and a conference was held to look into all of them, which is where the name NoSQL (Not Only SQL) was settled for the group.

4.2.5 A motivating example: flash sales

Real-world: A few years back, when OnePlus phone flash sales used to happen, the phone went on sale for one or two seconds and the websites used to crash — the system could not cater to that much data arriving in a burst at a certain moment. The companies later improved their applications, and such events are rare now. The lesson is the motivation for NoSQL: we want to process a large volume of data while keeping performance in mind — high throughput without bottlenecking.

Worked picture — a flash sale burst: Suppose the sale starts at 12:00:00 and a million users hit the "buy" button within the first two seconds. Each click is a read-and-write operation on the catalog and inventory. A single RDBMS node processes these one transaction at a time under ACID locking, so requests queue up; the burst arrives faster than the single node can drain it, and the queue explodes — the site crashes. Spread the same million users across 100 NoSQL nodes, and each node only handles about 10,000 requests. Even if the data is slightly stale across nodes for a moment (one node thinks the phone is still in stock while another has just sold it), every request gets a fast answer, and the site stays up. Sense-check: the failure was a rate problem (requests per second), not a capacity problem (total storage) — which is exactly why the answer is horizontal distribution rather than a bigger single machine.

Recap + bridge: NoSQL names a family, not a product: non-relational, schema-less, consistency-relaxing, distributed, cluster-friendly systems that chase throughput and horizontal scale. The RDBMS stays for the scenarios that need its guarantees. Next we ask what data model these systems actually store — and what they give up to get their flexibility.

4.3 The NoSQL Data Model: Variety Without Schema

Hook: Every student who has filled a form with "optional" fields knows the pain of a rigid structure. What if the database itself let every record decide which fields it needs — and what would such a database be forced to give up?

4.3.1 Structured, semi-structured, and unstructured data

What is the data model for NoSQL databases? NoSQL databases support storing a variety of data. Variety here typically means three different kinds:

  • Structured data — the kind the RDBMS was typically used to store, with a rigid, pre-defined layout.
  • Semi-structured data — data that has some structure (for example, tags or fields) but not a rigid table layout.
  • Unstructured data — text, images, and other content without a fixed structure.

The RDBMS stores structured data. NoSQL databases can store semi-structured and unstructured data as well.

The three kinds of data, named precisely:

  • Structured data conforms to a pre-defined schema: rows and columns with declared data types, as in an RDBMS table. A bank's transaction table is structured data.
  • Semi-structured data is self-describing: it uses tags or labels (like JSON fields or XML tags) to carry its own structure along with the values, but it does not conform to a fixed table layout — and two records of the same kind may have different fields, or the same fields in a different order.
  • Unstructured data follows no pre-defined data model at all: free text, images, audio, video, log lines. It is by far the largest category of enterprise data — industry estimates (for example, Gartner) put roughly 80% of newly generated enterprise data in the unstructured category, with structured and semi-structured data sharing the remaining ~20%.

NoSQL's big selling point is that a single store can hold all three kinds — while a classic RDBMS can comfortably hold only the first.

4.3.2 No fixed schema: JSON documents and key-value hierarchies

There is no fixed schema. Each record could have different attributes. There could be JSON documents, some key-value pairs, or a hierarchy of key-value pairs — as is the case with MongoDB. It is not the case that every row has the same set of attributes; that is simply not the case with NoSQL databases.

Worked picture — two records, different attributes, no schema: Compare a fixed RDBMS table (every row must carry all \(n\) attributes \(A_1, A_2, \ldots, A_n\)) with a schema-less record store:

// Record 1 — a book
    { "title": "Big Data and Analytics", "author": "Seema Acharya", "year": 2011 }

    // Record 2 — a book that also carries an extra field the store never knew about
    { "title": "Big Data, Data Mining, and Machine Learning", "author": "Jared Dean", "year": 2014, "edition": 2 }

The second record simply adds a field; nothing is migrated, no ALTER TABLE runs, no column is padded with NULLs. This is exactly the flexibility NoSQL buys. Sense-check: the shape of the data is stored inside the data itself (self-describing), so the database never needs to enforce a uniform shape across records.

4.3.3 What NoSQL gives up: joins, transactions, and ACID

The NoSQL databases are non-relational in nature, so they do not support joins — typically. If you want to do joins, the RDBMS is the good choice. Some of the things can be done in NoSQL, but joins are very expensive if you do them with NoSQL databases. They are not suited for transaction semantics over multiple data items, and there is no support for ACID as in the RDBMS. The consistency semantics are relaxed. In some cases — the graph databases — the data can be modeled as graphs and queries written as graph traversals, which are nowadays getting a little more popular, specifically Neo4j.

Q: Are joins just not supported at all in NoSQL databases? A: Some level of joins is supported, but you have to pay a very heavy price: they are not very effective to implement when it comes to NoSQL data. So if you want to do frequent joins, NoSQL databases are not a good choice to go with — the RDBMS is where joins belong.

Pitfalls:

  • "NoSQL has no joins, period" is wrong. It is a cost statement, not an absence statement — MongoDB 3.2+ can join collections with the aggregate pipeline's lookup stage (Section 4.10.5), but the operation is far more expensive than a well-indexed relational join because the data was deliberately laid out without join-friendly structure.
  • "NoSQL means no transactions" is wrong too. Systems such as MongoDB and Cassandra offer limited transaction-like features (Cassandra's lightweight transactions with their IF clauses, Section 4.9.7) — but these are narrow, conditional, single-interface operations, not the multi-statement ACID transactions of an RDBMS.
  • Do not infer the data model from the name. "Not Only SQL" says nothing about storage layout; four entirely different storage families sit under the same name (Section 4.4), each with its own query style.

Recap + bridge: NoSQL trades relational structure for data variety: it stores structured, semi-structured, and unstructured data without a fixed schema, and in exchange it gives up cheap joins, multi-item transactions, and ACID guarantees. That trade-off is acceptable precisely where consistency can be relaxed. Next, the formal classification of the four NoSQL families.

4.4 The Four Families of NoSQL Databases

There are typically four kinds of NoSQL databases: key-value, document-oriented, columnar, and graph. We discussed these a little earlier; here is the formal classification.

Family Unit of data Query style Typical use case Examples
Key-value (key, value) pairs, value is opaque Fetch by key Caches, sessions, image stores Dynamo, DynamoDB, Redis, Riak
Document Self-describing documents (JSON) Query/filter on any field Catalogs, profiles, content MongoDB, CouchDB
Columnar Columns stored in blocks / column families Column-level analytics OLAP, time-series statistics HBase, Cassandra
Graph Nodes and edges with properties Traversal Social networks, network topology, fraud rings Neo4j

When to pick which: if your scenario is "look up one thing by its identifier" choose key-value; "search inside records with varying fields" choose document; "run statistics over one column of a huge table" choose columnar; "follow relationships between entities" choose graph. The professor's rule stands: the choice is scenario-driven — no single storage engine is right for everything.

4.4.1 Key-value stores

A key-value store is like a big hash table: it maintains a big hash table of keys and corresponding values. Real-world: Examples of key-value pair databases include Dynamo, DynamoDB, Redis, and a fourth name transcribed as "React" — most likely Riak. Riak is in fact a well-known open-source key-value store that auto-distributes and replicates data across nodes, which fits this list exactly.

The simplest schema-less model: data is a set of pairs (key, value). A key is a unique identifier — a string, or a hash of a value — and the value is a BLOB (Basic Large Object): text, a serialized record, an image, anything. The store exposes essentially four operations: Get(key) returns the value for a key, Put(key, value) writes or updates it, Multi-get(key1, key2, ...) fetches several values at once, and Delete(key) removes a pair. Because the value is opaque — the store does not inspect its internal structure — reads are extremely fast, and the whole store behaves like a hash table you can spread across many machines. No indexes exist on values, so you cannot search inside values the way you can with a document database.

4.4.2 Document-oriented databases

A document-oriented database stores data in the form of documents — a collection of documents. Real-world: MongoDB is one of the very good examples of a document-oriented database, and CouchDB is another.

Documents as the unit of storage: a document is a self-describing piece of data — typically a JSON object with named fields (tags). Where a key-value store treats the value as an opaque BLOB, a document database inspects the document's fields, indexes them, and lets you query on any field. This is the key upgrade over the key-value model: the database sees inside your data, so search and filter become first-class operations. We go deep on this family in Section 4.6.

4.4.3 Column-oriented databases

A column-oriented database stores data in storage blocks. This is the HBase story: the concepts of column families and blocks we discussed earlier are exactly what column-oriented databases use. Take a table with attributes \(A_1, A_2, A_3, A_4\). Normally, all the values of one column are stored together, so that whatever analytical query runs on one particular column runs much faster — the column's data is one sort of a block, and once you access one value, the whole block is brought into main memory. That is the locality of reference idea. Queries that require a bunch of values from one column — mainly statistics on that column — run faster.

Additionally, HBase-style systems let you build a column family: combine two attributes, say \(A_2\) and \(A_3\), and store those two attributes together in sequence. Any query that needs access to both \(A_2\) and \(A_3\) at the same time will run faster. Real-world: Cassandra and HBase are the two most popular column-oriented databases. HBase's storage (blocks, column families) was already covered; Cassandra is covered in detail in this session.

Why column layout beats row layout for analytics: An RDBMS stores rows physically together, so reading one column's values across a million rows means scanning a million interleaved rows and discarding the rest. A column store writes all values of one column into one contiguous block: reading the column is a single sequential read, and the whole block lands in main memory at once (locality of reference). Aggregate queries — SUM, AVG, MIN, MAX over one column — are exactly the queries that benefit most. The trade-off: operations that need many columns of the same row (a full row retrieval) become scattered reads, which is why column stores suit analytical workloads more than row-by-row OLTP.

4.4.4 Graph databases

There could be scenarios where your database is very, very connection-heavy — social media applications like Facebook and Instagram. The classic query: how many hops is this particular person away from that particular person? If you store this in an RDBMS, it becomes very, very difficult to query and retrieve the data, because one record of the database may be associated with multiple relationships — a person has written \(N\) number of posts, has commented on \(M\) number of posts, has liked \(X\) number of posts. Such cases can be modeled with graph databases, and Real-world: Neo4j is one of the popular examples.

4.4.5 The friend-of-a-friend problem

Walk through the hop-count example in detail. There is a friend list of person \(P_1\). \(P_1\) is a friend of \(P_2\), \(P_2\) is a friend of \(P_3\), and \(P_3\) is a friend of \(P_4\) — but \(P_1\) and \(P_4\) are not in direct link, and neither are \(P_1\) and \(P_3\). I want to figure out the friends of friends of \(P_1\). In a relational database this requires walking relationship joins repeatedly, and with a huge user base it becomes a bottleneck. The graph model makes this traversal natural — you follow edges from one node to the next.

Worked example — friends of friends of \(P_1\): The friendship chain is \(P_1 \to P_2 \to P_3 \to P_4\). \(P_1\) knows \(P_2\) directly; \(P_1\) does not know \(P_3\) or \(P_4\).

  • In an RDBMS, the "friends" relation holds pairs like (P1, P2), (P2, P3), (P3, P4). "Friends of friends of \(P_1\)" means: join the friends table with itself on the second column matching the first — find all \(X\) such that (P1, Y) and (Y, X) both exist. For two hops that is one self-join; for "how many hops to \(P_4\)?" you must join repeatedly until the path closes, and each join over a table of hundreds of millions of friendship rows is a full distributed scan. With a huge user base this becomes the bottleneck.
  • In a graph database, the same question is a traversal: from node \(P_1\), walk each outgoing edge to its neighbor \(P_2\), then walk each outgoing edge again to \(P_3\), and count how many steps it took to reach \(P_4\). The graph store keeps each node's neighbors physically linked (Section 4.11.1), so the walk touches only the nodes on the path — no table scans, no joins.

Final answer: friends of friends of \(P_1\) = {\(P_3\)} (two hops, via \(P_2\)); \(P_4\) is at three hops. Sense-check: starting from \(P_1\)'s single edge you reach exactly \(P_2\), and from \(P_2\)'s edges you reach \(P_1\) and \(P_3\) — so the only new person at exactly two hops is \(P_3\), matching the graph.

Recap + bridge: Four families — key-value (hash-table pairs: Dynamo, DynamoDB, Redis, Riak), document (self-describing JSON documents: MongoDB, CouchDB), columnar (column-blocks and column families: HBase, Cassandra), and graph (nodes, edges, relationships: Neo4j) — each tuned for a different data shape and query pattern. Next: the shared characteristics, the pros and cons, and the SQL-versus-NoSQL comparison that frames all four.

4.5 Characteristics, Pros and Cons, and SQL versus NoSQL

4.5.1 Scale-out architecture and the three scales

All NoSQL families follow a scale-out architecture. There are two kinds of scaling: vertical and horizontal. Horizontal scaling is your scale-out: a cluster spread across 100+ nodes, across data centers also. The NoSQL world makes three concrete scale promises:

  • Performance scale — support 100,000+ (one lakh, \(10^5\)) database reads and writes per second.
  • Storage scale — store 1 billion+ (\(10^9\)+) documents in your database.
  • Data variety — house large amounts of structured, semi-structured, and unstructured data.

The three promises in numbers: One lakh is the Indian term for one hundred thousand, written \(10^5\) — a NoSQL cluster promises 100,000+ read/write operations per second. Storage scale is one billion documents, \(10^9\). To put the two together: if the cluster serves \(10^5\) operations per second around the clock, it processes about \(10^5 \times 86{,}400 \approx 8.6 \times 10^9\) operations per day — the performance number and the storage number are both in the same "web scale" ballpark, which is the point: these are the orders of magnitude a web application actually needs.

4.5.2 Auto-sharding, replication, and failure recovery

There is no predefined schema, so data can be inserted without any predefined schema, and you can add tags — a JSON document has certain tags, and you add a tag and a corresponding value, a kind of key-value pair. The systems also follow auto-sharding. We talked about partitioning: you partition your data and give it to different nodes; the more partitions, the more jobs can be done in parallel, and the more performance. The same thing happens here: many of these databases automatically spread the data across a number of servers. Partitioning means they are sharding your data — horizontal partitioning — and even the applications are not aware of it; it happens under the cover. Partitioning helps in data balancing and failure recovery: if one partition fails, we may keep replications, and we can go to the other replicated data and get the results. This gives good support for high availability as well as fault tolerance. These are the characteristics of all NoSQL databases.

Sharding, replication, and recovery as one loop:

  1. Auto-sharding — the database splits data into shards (horizontal partitions) and distributes them across servers automatically; the application never sees the split.
  2. Replication — each shard's data is copied to multiple nodes, so no single node holds the only copy.
  3. Failure recovery — when a node dies, its shards are still served from the replicas; the cluster rebalances data to keep the workload even.

The payoff is high availability (users keep getting answers even while a node is down) and fault tolerance (a hardware failure does not lose data or stop service). A single-node RDBMS has no such story — one failed disk can mean downtime while you restore from backup.

4.5.3 The pros of NoSQL

The pros: cost-effective for large data sets; easy to implement; easy to distribute, especially across data centers; easier to scale up and down; relaxes data consistency; no predefined schema.

4.5.4 The cons of NoSQL

The cons are the other side of the same coin:

  • Does not support joins efficiently among the data sets in the tables — if you want to perform joins, go for the RDBMS.
  • Group by operations are very costly in NoSQL databases.
  • No support for ACID properties — if you want highly consistent data, NoSQL is not a good choice. It is not simply one blanket reason: configurations exist. We can configure MongoDB from different consistency levels, but as you increase the consistency, the performance will go down.
  • Lack of standardization in this space, which makes it very difficult to port from SQL across NoSQL data stores.
  • Skillset — people skilled in these technologies are fewer compared to SQL.
  • BI tool support is less — fewer business intelligence tools can be integrated with these technologies compared with the mature BI space available with the RDBMS.

Pitfall — consistency is a dial, not a switch: "No ACID" does not mean a NoSQL system is permanently inconsistent. Systems like MongoDB and Cassandra expose configurable consistency levels: you choose how many replicas must agree before a read or write is confirmed. The trap is the trade-off curve — as you turn consistency up, performance goes down, because the system must wait for more nodes to agree. Turning the dial to the maximum does not give you an RDBMS; it gives you an RDBMS's latency without its transactional semantics. Pick the level the application actually needs, and remember the professor's rule: increasing consistency reduces performance.

4.5.5 SQL versus NoSQL, side by side

Aspect SQL (RDBMS) NoSQL
Nature Largely relational Non-relational
Distribution Centralized in the classic case Distributed
Schema Predefined schema Schema-less
Data layout Table-based Multiple options: key-value, document, column-oriented, graph
Scaling Vertically scalable Horizontally scalable
Guarantees ACID properties CAP theorem
Querying Complex querying, including joins and group by Flat structures, largely denormalized data, relatively simpler querying
Vendor support Excellent vendor support Largely open source; we rely heavily on community support

Real-world: The vendors offering these different kinds of databases include Amazon, Facebook, Google, and Oracle — a mix of the companies that built the technologies and the traditional database players.

When to pick which: choose SQL when you need normalized data, complex joins, group-by analytics, mature tooling, and ACID guarantees; choose NoSQL when you need horizontal scale, flexible schemas, geo-distribution, or a specialized data shape (key-value, document, column, graph). The deciding question is the query pattern of your scenario, not brand preference.

4.5.6 The CAP theorem revisited

The CAP theorem is very, very important. It was discussed before the mid-semester: Consistency, Availability, and Partition tolerance, of which any distributed system can deliver only two at a time. Partition tolerance becomes mandatory when your application is hosted across data centers, because network partitions are bound to happen. So \(P\) is fixed; out of \(C\) and \(A\), you have to go with one — either consistency or availability — and that choice depends on your application. That is exactly how you choose your database: the SQL side bets on consistency; the NoSQL side makes the C-versus-A trade-off explicit.

The three letters, defined precisely:

  • Consistency (C) — every node observes the same data at the same time; a read sees the result of the most recent write.
  • Availability (A) — every request receives a response, on success or on failure; the system keeps answering even when part of it is down.
  • Partition tolerance (P) — the system continues to operate as a whole even when a network partition separates nodes (messages lost, nodes unreachable).

Brewer's theorem states the system cannot guarantee all three simultaneously. In a multi-data-center deployment, network partitions will happen, so \(P\) is not optional — you must choose one of the remaining two. A CP system answers only when it has the latest copy (it may refuse to answer during a partition); an AP system always answers, but may serve old or conflicting data during a partition. Cassandra is our AP example (Section 4.9); a classic RDBMS configured for strict behavior is the CP side of the spectrum.

Exam note: Expect the CAP theorem and the consistency levels to keep returning in exam material — know the three letters, the trade-off, and why partition tolerance is not optional in distributed deployments.

Recap + bridge: NoSQL's shared profile is scale-out architecture with three promises (\(10^5\) ops/second, \(10^9\) documents, full data variety), auto-sharding, replication, and failure recovery; its costs are weak joins and group-by, configurable-but-performance-taxing consistency, and a younger ecosystem. The CAP theorem frames the family's core decision: P is mandatory, so you pick C or A per application. Now we turn to the first family in depth — document-oriented databases.

4.6 Document-Oriented Databases in Detail

Hook: You have already seen the key-value store's blind spot — it cannot look inside its own values. Document databases fix exactly that: they store self-describing documents and let you search inside them. The trade-off turns out to be subtle, and the professor's Q&A on it is exam gold.

4.6.1 Documents, collections, IDs, and indexes

A document-oriented database stores data in the form of documents, typically JSON. The documents are accessible via their ID, and can be accessed through their index as well — MongoDB supports creating indexes on document-oriented data. The database maintains data in collections of documents. The mapping to the relational world is direct: a record in an RDBMS is equivalent to one document, and one table is equivalent to a collection. A collection has multiple documents in it, and you add documents to a collection the way you add records to a table — the insert operation works with document-oriented databases such as MongoDB.

The RDBMS-to-document mapping (the professor's direct translation):

RDBMS Document database (MongoDB)
Database Database (a set of collections)
Table Collection
Record / row Document
Column Field / tag
Primary key Document ID

A collection does not enforce a schema: two documents in the same collection may carry different fields, and even documents with the same fields may order them differently. Every document needs a unique identifier — in MongoDB the _id field plays the role of the primary key, and an index is automatically built on it. More indexes can be created on any other field to speed up lookups.

4.6.2 A book document and querying by tags

Here is one sort of book document: book title, publisher, year of publication. You can store this document, say in MongoDB, and a unique document ID (an object ID) will be generated automatically. If I want to query something on book title, I can do that; if I want to query on year of publication, I can do that. Because each attribute is stored as its own tag, a search can be performed on different attributes.

Worked example — a book document in MongoDB:

{
      "_id": ObjectId("507f1f77bcf86cd799439011"),
      "book_title": "Big Data and Analytics",
      "publisher": "Wiley India",
      "year_of_publication": 2011
    }

The database generates the _id automatically (an object ID), so the application never has to invent a unique key. Because each attribute lives in its own named tag, the following queries all work:

  • Search on book title: db.books.find({ "book_title": "Big Data and Analytics" })
  • Search on year of publication: db.books.find({ "year_of_publication": 2011 })

Final answer: any single attribute can serve as a search key, because the document's fields are named and visible to the database — which is precisely what a key-value store cannot do. Sense-check: the same query against a key-value store would have to scan every value because the store cannot see the "title" inside an opaque value; the document database answers directly from its index on the field.

4.6.3 Key-value store versus document-oriented: the use-case question

Q: What are the use cases for choosing a key-value store versus a document-oriented database? Also, in a document-based database, is there a document ID, and how is search performed? A: In a key-value store, the value can be a document — that is correct. In a document-oriented database, yes, there would be a document ID; that is basically the key for that document. On the use cases: in the document-oriented case there can be a hierarchy of key-value pairs — tags, like the book document I showed: book title, publisher, year of publication, each in its own tag. I can query on book title, query on year of publication — search on different attributes if they are stored in different tags. In a key-value store, the value is extracted as a whole. If "city" is my key, "New York City" is my value; "state" is a key, "New York" a value; "county" is a key and "US" a value. It is difficult to query the distinguished parts of that value. The value could be a single value, or it could even be a complex data structure — it can be a good record as well, whatever the document you choose — but querying its individual parts becomes difficult in key-value stores. In the document-oriented case you can query using different tags and refine or filter your data, whichever you want. We will see the MongoDB practicals and it will be more clear — and you can use the select-like query on your own.

The one-sentence decision rule: use a key-value store when you always fetch the whole value by its key (caches, sessions, image stores); use a document database when you must search or filter inside the data (catalogs, profiles, content). The key-value store's value is fetched as one opaque piece; the document database's fields are individually queryable tags.

4.6.4 Searching with find

For searching, MongoDB provides a find function: you specify your search criteria there, and it filters the records according to whatever documents satisfy that search criteria, and displays them as part of the result. The CRUD operations — creating data, inserting, deleting, updating — will be covered in the MongoDB practical (about 30 minutes), and a dedicated session can go deeper: what a document is, how we store it, how we query, how we search, how we update.

Pitfalls:

  • Do not assume a document database is a faster key-value store. They serve different query shapes: key-value fetches whole values by key; document databases index fields and filter documents. Pick per scenario, not per hype.
  • "A document is just a JSON string" misses the point. The value of the document model is the visibility of the fields to the query engine — a plain string stored in a key-value store is opaque to search.
  • The find filter is not a full SQL engine. Document queries are powerful on the document's own fields but are not designed for the multi-table join and group-by workloads where SQL shines (Section 4.5.4).

Recap + bridge: Document databases store self-describing JSON documents in collections, give every document a unique ID, index fields, and search inside documents — the direct upgrade over the opaque key-value model. Next: the columnar family, where the entire layout philosophy flips from rows to columns.

4.7 Column-Oriented Storage: Laying Out Columns

Hook: The same table can be sliced in two different directions — into rows or into columns — and the choice changes which queries are fast by an order of magnitude. The relational world slices by rows; the columnar world slices by columns. Why, and when does the difference matter?

4.7.1 A relational table with five columns

Look at a typical relational table with five columns: employee number, department id, hire date, last name, first name. There are unique employee numbers, the department ids in which the employees work, their hire dates, their last names, and their first names. The table may hold five records or many hundred thousand records.

Worked example — the employee table laid out two ways: Five rows of the same table, stored in an RDBMS (row-oriented) layout:

EmpNo DeptId HireDate LastName FirstName
1 1 2019-03-11 Smith Anna
2 1 2020-07-02 Jones Raj
3 1 2021-01-19 Patel Neha
4 2 2021-11-30 Lee Omar
5 2 2022-05-15 Chen Lina

In an RDBMS, each row is stored as one contiguous unit: (1, 1, 2019-03-11, Smith, Anna), then (2, 1, 2020-07-02, Jones, Raj), and so on. In column-oriented storage, the layout is regrouped: all EmpNo values together [1, 2, 3, 4, 5], then all DeptId values together [1, 1, 1, 2, 2], then all HireDate values, then all last names, then all first names — each column in its own storage block.

Final answer: a query like "average hire year" or "count of employees per department" touches only the HireDate block and the DeptId block; a row-oriented scan would have to read every row and discard the name columns. Sense-check: reading 2 blocks instead of 5 rows × 5 attributes = 25 values means roughly 5× less I/O for the columnar layout — and the advantage only grows with table size.

4.7.2 Vertical partitioning into column families

In column-oriented storage, the data is stored in a different layout: a whole bunch of values of one column are stored together — all employee numbers together, then all department ids together, then all hire dates together. This partitions the table by column into column families — a kind of vertical partitioning — where each column family is stored in its own file. Each storage block has data from one column: this will be one storage block, that will be another storage block. The systems also allow the versioning of the data values within a block.

Vertical partitioning and locality of reference: partitioning by column is vertical partitioning (columns, not rows, become the unit of distribution). The payoff is locality of reference: all values of one column sit contiguously, so a query that needs a bunch of values from one column — mainly statistics on that column — pulls one compact block into main memory instead of scanning whole rows. Versioning adds a time dimension: a block can keep multiple timestamped versions of a value, so you can ask "what was the value at time T?" — a feature HBase-style systems expose directly.

4.7.3 Building column families around query patterns

You can go beyond a one-column store and form a column family by clubbing columns, like in HBase: club employee number and department id, and store them together — (1, 1), (2, 1), (3, 1), (4, 2), and so on — because every time I refer to the employee number, I need the department id as well. Those queries become much faster. So you need to frame the column families carefully, looking at how the data will be queried — the family structure should mirror the access pattern.

Worked example — clubbing columns into a family: Suppose the most frequent query is "for each employee number, which department are they in?" Every time the query touches an employee number it also needs the department id. So the column family is defined as (EmpNo, DeptId), stored as adjacent pairs: (1, 1), (2, 1), (3, 1), (4, 2), (5, 2).

  • Query 1 — "department of employee 4": the pair (4, 2) is found in one contiguous block; both values arrive in a single read. Fast.
  • Query 2 — "department of employee 100": the pair (100, …) is in the same block layout; still one read. Fast.
  • Query 3 — "average hire date by department" in a store that separated HireDate from DeptId into different families: every hire date read must be matched back to its department id, crossing blocks. Slower.

Final answer: club the columns that queries always use together; split the columns that queries use separately. Sense-check: the column family design is a query-pattern mirror — if the access pattern changes, the family layout should change with it, because the layout exists for the query, not the other way around.

Pitfalls:

  • Do not put all columns into one family. If you do, you get a row store in disguise — every "columnar" query reads the whole family, and the locality advantage is gone.
  • Do not separate columns that are always queried together. Splitting (EmpNo, DeptId) into two families forces every joint query to read two blocks and stitch — the exact cost columnar storage was built to avoid.
  • Beware the analytical bias. Columnar layout wins for column-level statistics (aggregates, scans over one attribute) but loses for operations that need many attributes of the same row at once — those become scattered block reads.

Recap + bridge: Column-oriented storage is vertical partitioning into per-column blocks and column families, exploiting locality of reference so one-column statistics read one compact block; the family structure must mirror the query pattern. This exact storage model powers Cassandra, which we now examine end to end — starting with its origins in two other famous systems.

4.8 Graph Databases: Modeling Connections

Hook: Some data is only meaningful through its connections — who knows whom, which router talks to which switch, which user wrote which post. The professor flagged graph databases as one of the most important topics in the course. This section builds the model; Section 4.11 adds the engines and query languages.

4.8.1 Nodes, edges, and properties

Graph computing — flagged during the discussion as one of the most important topics — deals with data represented in the form of graphs. A graph consists of nodes (also called vertices) and edges: the edges connect different nodes. Data is represented as vertices and edges. The vertices have certain properties — key-value pairs — and the edges represent the relationships, and edges can carry properties too.

The graph vocabulary, formalized: a graph is written \(G = (V, E)\) where \(V = \{v_1, v_2, \ldots, v_n\}\) is the set of vertices (nodes) and \(E = \{e_1, e_2, \ldots, e_m\}\) is the set of edges connecting them.

  • Node (vertex) — an entity: a person, a product, a router, a post.
  • Edge — a relationship between two entities, often labeled with its kind (FRIEND, ACTED_IN, DIRECTED, CONNECTED_TO).
  • Property — key-value pairs attached to a node or an edge: a person node may carry {name: Alice, age: 18, id: 1}; an edge may carry {since: 2021-05-14}.

This is the property graph model: both vertices and edges can hold properties, and both can carry labels — so the model naturally distinguishes "Alice knows Bob" (edge) from "Alice" (node) and from "knows" (edge label).

4.8.2 A social graph in practice

Real-world: Picture a social media site: Alice is a friend of Bob, Bob is a friend of Charlie, and Charlie is a friend of Devin. All of these are persons — one entity — and the friend label indicates the kind of relationship between the entities. This is easy to interpret as a graph. In the storage model: a node named Alice with age 18 and id 1 is attached to another node, id 2, named Bob with age 22; the edge says Alice knows Bob since a particular date — the relationship is mentioned on the edge, with its own properties.

Worked example — the social graph as stored data:

  • Node id=1: {name: Alice, age: 18}
  • Node id=2: {name: Bob, age: 22}
  • Node id=3: {name: Charlie, age: 25}
  • Node id=4: {name: Devin, age: 30}
  • Edge id=101: (1) —KNOWS_SINCE 2021-05-14→ (2)
  • Edge id=102: (2) —KNOWS_SINCE 2020-02-09→ (3)
  • Edge id=103: (3) —KNOWS_SINCE 2019-08-30→ (4)

The vertices are the persons (all one entity type, "person"); the edges are the relationships, and each edge carries its own property (the date the friendship started). Final answer: the chain Alice → Bob → Charlie → Devin is four nodes and three edges, each edge labeled KNOWS with a property of when the two became friends. Sense-check: an RDBMS would need a friendship table with rows (1,2,2021-05-14), (2,3,2020-02-09), (3,4,2019-08-30) plus self-referential foreign keys — workable for four people, unmanageable at social-network scale (Section 4.11.2).

Visual intuition — the Alice-Bob-Charlie-Devin graph: picture a row of four dots on paper, left to right: Alice, Bob, Charlie, Devin. Three arrows connect consecutive dots, each labeled KNOWS and each carrying a small property tag ("since 2021-05-14", "since 2020-02-09", "since 2019-08-30"). The landmark to notice: Alice's dot has exactly one outgoing arrow, while the chain itself is four hops long — a traversal from Alice to Devin must cross three edges, each step moving one dot right. The takeaway: the shape of the picture is the shape of the data — no separate lookup table is needed to answer "how far is Devin from Alice?", the answer is visible in the arrow chain itself. This direct adjacency is exactly what native graph storage preserves on disk (Section 4.11.1).

4.8.3 Graph query languages: Cypher and Gremlin

To query data stored as graphs, you traverse the nodes — how that works is covered with Neo4j. A graph database is also called a network database, because it is good for representing network-style data. Two graph query languages are supported: Cypher, typically used for Neo4j, and Gremlin, for Apache TinkerPop.

Two query languages, two philosophies:

  • Cypher — Neo4j's declarative language: you describe the pattern you want, and the engine finds it. It looks like SQL with arrows: MATCH (a:Person)-[:FRIEND]->(b) reads "match all patterns where a person a has a FRIEND edge to b."
  • Gremlin — Apache TinkerPop's language, with both declarative and imperative flavors: you often walk the graph step by step, starting from the whole graph and chaining filters: g.V().hasLabel('person')... (Section 4.11.5).

Both are traversal languages at heart — they express "start here, follow these kinds of edges" — but Cypher states the target pattern while Gremlin states the traversal steps.

Pitfalls:

  • Do not think of a graph database as "tables with arrows". The storage model is nodes and edges with properties; forcing it into normalized tables destroys the traversal advantage.
  • Do not use a graph database for row-style data. If your queries never follow relationships, a graph store adds overhead without benefit — the graph model earns its keep only when connections are the query (Section 4.11.3).
  • Do not confuse the two languages on an exam. Cypher ↔ Neo4j; Gremlin ↔ Apache TinkerPop. Mixing them up is a classic easy-lost mark.

Recap + bridge: A graph is \(G = (V, E)\) — nodes (entities) with properties, edges (relationships) with labels and properties — queried by traversal, not by join, through Cypher (Neo4j) or Gremlin (TinkerPop). Also called network databases. Next, the deepest system of this session: Cassandra, the column-oriented AP database.

4.9 Cassandra: The Column-Oriented, High-Availability Database

Hook: What happens when you take a columnar storage model from one system and a peer-to-peer availability philosophy from another, and fuse them? You get Cassandra — Facebook's answer to a problem that was growing too fast for any master-slave database to handle.

4.9.1 Origins: Facebook, DynamoDB, and Bigtable

Cassandra was developed at Facebook, and it was built on the concepts of Amazon DynamoDB and Google Bigtable. You can see the two lineages in its design: the Dynamo side contributes the distribution and availability thinking; the Bigtable side contributes the column-family storage model.

Two parents, two inheritances:

  • From Amazon DynamoDB (the Dynamo paper's ideas): the peer-to-peer distribution model, automatic partitioning, replication, and the availability-first philosophy.
  • From Google Bigtable: the column-family data model — tables built from column families stored as storage blocks.

The result is a column-oriented database that is decentralized, symmetric, and built to keep running while scaling out across commodity servers. Companies that have deployed it at serious scale include Twitter, Netflix, Cisco, Adobe, eBay, and Rackspace.

4.9.2 The AP design and the peer-to-peer architecture

Cassandra follows an AP design in the CAP context. Recall: when a distributed database is hosted on multiple data centers, partition tolerance is a must because partitions are bound to happen, and the CAP theorem says we cannot guarantee all three properties at once — we can guarantee only two, and since \(P\) is mandatory, we choose one of \(C\) and \(A\). Cassandra chooses availability: it is a high availability database that can sacrifice consistency. It is high performance, high availability, with a compromise on consistency. So if you want high availability and performance, with a little compromise on consistency, you go ahead and use Cassandra.

Architecturally, each node acts the same — the nodes are peer-to-peer, symmetric nodes; there is no primary-secondary split. Column-oriented storage is built the same way we just saw: you build column families based on your requirement, and each column family is stored as a storage block. The system is also, at heart, a key-value store.

Why peer-to-peer beats master-slave for availability: in a master-slave architecture, if the master dies, writes stop until a new master is elected — a single point of failure. Cassandra has no master: every node is structurally identical, any node can serve any read or write, and each node exchanges state with its neighbors every second via the gossip protocol. A node failure degrades throughput gracefully (the cluster slows slightly) but does not take the database down. That is the AP bet: always answer, even with possibly-stale data.

4.9.3 Keyspaces, column families, and columns

Cassandra's vocabulary: you create a keyspace — a keyspace is like a database. Within a keyspace you create a column family, which is like a table. Within a column family you create attributes — called columns — with their data types. The nesting is: keyspace → column family → columns.

The professor's translation ladder: keyspace → database; column family → table; column → attribute with a data type (int, text, double, boolean, blob, timestamp, and collections like list, set, map). The nesting is exactly three levels deep: a keyspace holds column families, a column family holds columns. In CQL (Cassandra Query Language) the old term "column family" is written as CREATE TABLE — the underlying storage model stays column-family oriented.

4.9.4 The write path: commit log, memtable, and SSTable

Purpose: make writes fast and safe on distributed, commodity hardware — a write must be acknowledged quickly but must never be lost if the machine crashes mid-way.

Inputs & Outputs: input is any write (insert or update) to any node; output is an acknowledgement to the client, plus the data now durably recorded in the node's log and in flight through memory toward disk.

How do reads and writes happen in Cassandra? There is a commit log that maintains the sequence of operations happening in the database. Writes are entered into the log sequentially, and then they are deemed successful. The write goes to a single node and is committed there — there are configurations for that, as with MongoDB. Further, the data is indexed and put into an in-memory data structure called the memtable. Whatever writes are happening live in the memtable, and then the memtable is flushed to disk as an SSTable. An SSTable is immutable — you cannot update it; it is an append-only thing. Partitioning and replication happen automatically — the sharding we discussed. A default replication factor of three (\(RF = 3\)) is maintained in your distributed application automatically.

The three-step write path:

  1. Commit log — the write is appended sequentially to an on-disk log. This is the crash-safety step: a write is considered successful only once it is in the commit log.
  2. Memtable — the write is then pushed into an in-memory, indexed structure. Writes accumulate here; reads served from memory are fast. When the memtable reaches a threshold, its contents are flushed.
  3. SSTable — the flushed contents land in a disk file called an SSTable (Sorted String Table). An SSTable is immutable — once written it is never modified; new writes produce new SSTables (append-only), and old files get compacted later. The default replication factor is \(RF = 3\), meaning three copies of every piece of data are kept in the cluster automatically.

Worked trace — one write through the pipeline: A client writes INSERT INTO student (id, name) VALUES (101, 'Ravi') to node A.

  1. Node A appends the statement to its commit log on disk and acknowledges the write (with default consistency ONE).
  2. Node A indexes the row into its memtable in memory: id=101 → name=Ravi.
  3. Node A also replicates the row to two other nodes (because \(RF = 3\)): nodes B and C write it to their own commit logs and memtables.
  4. When A's memtable fills to its threshold, the data flushes to an SSTable file; if 50 more writes to student arrive, they form a second SSTable later — the old file is never edited.
  5. Node A crashes at any later moment: the memtable content is rebuilt from the commit log (step 1), so no acknowledged write is lost.

Final answer: acknowledged = commit log; fast in-memory service = memtable; durable immutable storage = SSTable; \(RF = 3\) guarantees two spare copies. Sense-check: every stage exists to decouple speed (memory writes) from safety (log + replicas) — a write is confirmed quickly, yet a crash at any point can be recovered.

4.9.5 The read path and consistency levels

For a read, the client can connect to any node and read the data. The consistency level decides when a read is returned. If I read from one node and return, my read performance increases. But if I set it so that "this many nodes in the cluster have the same value", it should read from that many nodes and then return — some performance goes down. The setting says how many replicas should contain the same copy before I say "this is the value I want to return".

Walk through a worked example with a seven-node cluster: I say that four nodes have the same copy before returning a read. If I am reading from a single node, it has to check three more nodes; if those three more nodes have the same copy, return that read value — otherwise don't. I can also configure it the other way: whatever read value I have, I return it. So the read behavior depends entirely on the consistency you have configured. The same logic applies to writes: whether you want the write to be successful as soon as it hits one particular node, or you want a certain number of nodes to successfully write this update before confirming that the write is successful.

Worked example — the seven-node cluster read: The cluster has 7 nodes; the consistency level is set to 4. The client connects to node A and asks for the value of key K.

  • The coordinator (node A) reads its own copy — that is 1 node.
  • It must confirm the same value on 3 more nodes (to reach the configured 4), checking nodes B, C, D.
  • If those 3 return the same value as A, the read returns that value.
  • If any of them holds a different (stale) value, the read does not silently return; Cassandra takes the newest version, returns it, and triggers read repair so the stale replicas catch up.

Contrast — consistency ONE: with level ONE, the same read returns whatever node A has, immediately, without contacting any other node — highest read performance, but possibly stale data. Final answer: the consistency level is a dial: 1 node = fastest but weakest guarantee; \(n\) nodes = stronger guarantee at the cost of latency. Sense-check: raising the level adds network round-trips, so performance falls exactly as the guarantee rises — the AP trade-off made visible inside a single read.

There is also read repair: if the data at a particular node is corrupted, there is a gossip protocol that can be leveraged to repair the corrupted things. That protocol is not a part of the course syllabus, but you could go through it once — Google it — to see exactly what it is.

4.9.6 Quorum consistency

You can set the consistency level to \(1, 2, 3\), up to any number \(n\) — the number of nodes that must have the same value before the write is considered successful. Write consistency \(1\): as soon as the write hits one node, the write is considered okay. But what if that single node dies? That is where consistency is compromised — and that is exactly why we call this an AP database. You can also configure the other extreme: until \(n\) number of copies all have the same value, the write is not considered successful — and then performance goes down. The same \(1..n\) choices apply to reads.

There is also the quorum option: when the majority of the nodes in the cluster have the same value, then the read or write can succeed. When the majority of nodes have been successfully written, the write is successful. So you can follow quorum consistency rather than a hard number \(n\) — the standard, balanced choice.

Quorum in one formula: for a cluster with \(n\) replicas of the data, quorum means \((n/2) + 1\) nodes (the majority). A write with level QUORUM is acknowledged once a majority of replicas have it in their commit log and memtable; a read with level QUORUM returns only when a majority agrees on the value. With \(n = 3\) replicas, quorum is 2 nodes; with \(n = 5\), quorum is 3. The balanced choice — stronger than ONE, cheaper than ALL — and the standard production default.

4.9.7 Lightweight transactions

Cassandra offers some lightweight transactions. Here is the interface with two commands — insert and update — with special clauses:

INSERT INTO cycling.cyclist (id, last_name, first_name)
    VALUES (5, 'VOS', 'Marianne') IF NOT EXISTS;

    UPDATE cycling.cyclist
    SET first_name = 'Marianne'
    WHERE id = 5
    IF first_name = 'Marianne';

The insert checks whether such a record already exists before inserting. The update changes the first name only if the current first name matches the condition. You can explore more on the linked documentation.

Q: What is named as lightweight transactions? A: Those two commands I ran — the insert and the update. It is the special significance of the IF clause: in both commands, the clause carries the condition — "if it is not exists, then only create it", "if the first name is this, then only update it". Because of that IF clause they are named lightweight transactions. We will run those queries and then the things will be more clear; there is also a link you can go through. Why it is named exactly this way, we will discuss definitely.

4.9.8 Replication strategies across data centers

The replication strategy for your user data is configurable. You can specify a replication factor of \(n\): the data is stored in \(n\) nodes of the cluster. It is not only the default of three — I can say four as well. In a network topology strategy, you can specify different replication strategies for different data centers: a data center that is heavily used may want higher replication for a certain kind of data; a data center that is not heavily used may not want a very high replication factor. This kind of query is possible: within the east data center, go with replication factor 2; within the west data center, go with replication factor 3.

Two strategies, one idea: SimpleStrategy applies one replication factor to the whole cluster — simple, single-data-center use. NetworkTopologyStrategy sets a replication factor per data center: east data center \(RF = 2\), west data center \(RF = 3\), independently. Heavily used regions keep more copies; lightly used regions keep fewer; writes in the east do not wait on west latency. This is how the same cluster can give regional data different protection without a global slowdown.

Q: Can using replication factor one result in data loss? A: That is a possibility — yes, data loss and unavailability. Because if the node dies, how will you recover? Generally we do not do that.

Pitfall — replication factor 1 is a risk you should never take in production. With \(RF = 1\) there is exactly one copy of the data; the node that holds it dies, and the data dies with it — no recovery is possible. The replication factor should be greater than one, and should never exceed the number of nodes in the cluster. This is precisely why the default is \(RF = 3\).

4.9.9 Partitioners: hashing the data across nodes

Data is partitioned — partitioned based on hashing — to distribute the data blocks from a column among the nodes. First, this is column-oriented: the data of one column is stored together. Second, the distribution is not a naive division — it is not "pick the first 20 rows for node 1, the second 20 rows for node 2". You apply a hash function to distribute the data evenly across the different blocks and nodes. There are different techniques, in two categories:

  • Crypto hash (for example, MD5) — very difficult to reverse, very, very secure, but much more expensive.
  • Non-crypto consistent hash — relatively easier to reverse compared to the crypto hash, however this is faster, and you can have about a 10% (\(10\%\)) performance improvement over the crypto hash.

These are the partitioners available; we will not go into the details of what exactly they are, but this is the way partitioning works — that is what we need to know.

What a partitioner does: a partitioner is a hash function that maps each row's partition key to a token, and the token decides which node holds the row — and which node holds the first copy. The distribution is not "first N rows to node 1" (naive division); it is a hash of the key, so rows land evenly across nodes even when keys arrive in clusters. The two flavors: crypto hashes like MD5 are secure but expensive; non-crypto consistent hashes are faster by about \(10\%\), which is why they are the popular default for partitioning. Details of the algorithms are beyond the syllabus — know the categories and the trade-off.

4.9.10 Sample Cassandra queries

Some sample queries to see the shape of the language:

CREATE KEYSPACE demo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};

    DESCRIBE KEYSPACES;

    USE demo;

    CREATE TABLE student (id int PRIMARY KEY, name text);

    DESCRIBE TABLE student;

CREATE KEYSPACE creates the keyspace with a replication strategy and factor. DESCRIBE KEYSPACES is like describe databases or describe tables in SQL. USE demo selects the keyspace — your database. CREATE TABLE student creates a table; it is very, very similar to SQL, but not exactly SQL — people who know SQL will find it simpler. DESCRIBE TABLE shows the table definition.

Worked walkthrough — the sample session line by line:

  1. CREATE KEYSPACE demo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; — creates the keyspace demo (our database) using the simple replication strategy with factor 1.
  2. DESCRIBE KEYSPACES; — lists all keyspaces; the SQL parallel is SHOW DATABASES.
  3. USE demo; — selects the demo keyspace; the SQL parallel is USE database.
  4. CREATE TABLE student (id int PRIMARY KEY, name text); — creates the column family student with columns id (int, the primary key) and name (text) — visually SQL-like, but it is CQL over the column-family model.
  5. DESCRIBE TABLE student; — prints the table definition back.

Final answer: an SQL user recognizes every statement, yet the storage underneath is column-family oriented and the cluster semantics are Cassandra's. Sense-check: familiar syntax + unfamiliar engine = the "not exactly SQL" feeling the professor described.

4.9.11 eBay: Cassandra at marketplace scale

Real-world: A major case study of Cassandra is eBay. eBay is a marketplace with 100 million active buyers, 200+ million items, around 2 billion page views, 80 billion database calls, and multi-terabyte storage capacity. That is the volume of data being dealt with. There is no transactional data, no joins, no integrity constraints of the key kind. It is a multi-data-center deployment with 400+ million hits and 200+ million leads.

What is happening: social signals on product pages, where read latency is not important, but write performance is the key. So the basis of tuning is: set the consistency level. If I want to increase write performance, I lower the number for write consistency; if I want to set read performance high, I lower the number I set for read consistency. It all depends on the configurations and the kind of scenario you are looking for. Use cases include connecting users and items via buy, sell, bid, watch events, and many time-series algorithms running to detect the frauds that may be happening across a certain period of time. This is one example of Cassandra being used at marketplace scale; the slide deck for the case study has more detail.

Worked picture — the eBay numbers: 100 million active buyers, 200+ million items, ~2 billion page views, 80 billion database calls, multi-terabyte storage, multi-data-center deployment, 400+ million hits and 200+ million leads.

The workload is write-heavy social signals on product pages: users buy, sell, bid, watch, and the page records those events continuously. Read latency does not matter much; losing writes, or slowing them down, does. So the tuning rule is: lower the write-consistency number to raise write performance; lower the read-consistency number to raise read performance. Time-series algorithms scan event streams over time windows to detect fraud.

Final answer: at this scale, the consistency dial — not the schema — is the tuning instrument; the workload is event-heavy and fraud-sensitive, exactly where an AP, write-fast database earns its place. Sense-check: a relational system forced to do 80 billion calls with joins and multi-item transactions would bottleneck at the very first spike; Cassandra trades read strictness for write throughput, which is what the marketplace actually needs.

Exam note: For Cassandra, know the write path (commit log → memtable → SSTable, immutable and append-only), consistency levels and quorum, and the \(RF = 3\) default — these recur in exam material.

Recap + bridge: Cassandra = Bigtable's column families + Dynamo's peer-to-peer availability; AP in the CAP triangle. Writes flow commit log → memtable → SSTable with \(RF = 3\); reads and writes obey a tunable consistency dial \(1..n\) or quorum (majority); lightweight transactions use the IF clause; partitioners hash keys across nodes; eBay runs it at marketplace scale. Next: MongoDB, the document-oriented system with its own scaling and consistency story.

4.10 MongoDB: The Document-Oriented Database

Hook: Cassandra spreads rows by hashing keys across identical peers. MongoDB keeps documents in a primary-secondary cluster instead, and scales by sharding. Same family goals — different machinery, different consistency vocabulary, and a different name for the same quorum idea.

4.10.1 Collections, documents, and the 16 MB limit

A MongoDB database is a set of collections. A collection corresponds to a table in the RDBMS, and it holds the documents. Documents are JSON documents with hierarchical key-value pairs — similar to rows in a table — and they can also be binary JSON (BSON) documents. The maximum document you can store is 16 MB (\(16\,\text{MB}\)), in the WiredTiger storage engine. MongoDB uses two different kinds of storage engines: WiredTiger and GridFS. With WiredTiger you can store documents up to 16 MB; for larger ones you need to use GridFS. GridFS also supports binary data — you want to store large objects, very large images, and all those things — and those are stored in chunks of 255 KB (\(255\,\text{kB}\)), with the metadata stored in a separate collection. GridFS does not support multi-document transactions, and it can be queried from WiredTiger and other engines. So the summary: data is stored in the form of documents, collections hold multiple documents, the documents can be JSON or binary JSON, and two engines cover small and large documents.

The two storage engines, side by side:

Aspect WiredTiger GridFS
Document size Up to 16 MB (\(16\,\text{MB}\)) No 16 MB cap — large objects (big images, videos)
Storage form Normal documents (JSON / BSON) Objects split into chunks of 255 KB (\(255\,\text{kB}\))
Metadata In the document itself Stored in a separate collection
Transactions Full document transactions Does not support multi-document transactions
Query Normal MongoDB queries Queryable from WiredTiger and other engines

BSON (Binary JSON) is the internal binary encoding of JSON documents — the same structure, more compact and faster for the engine to read and write. Rule of thumb: everyday documents go to WiredTiger; anything that breaks 16 MB goes to GridFS.

4.10.2 Sharding: horizontal partitioning in practice

The important concept that MongoDB uses for scaling is sharding — nothing but horizontal partitioning: data is partitioned across multiple servers. Why does horizontal partitioning give horizontal scaling? As mentioned, the more the number of partitions, the more parallel jobs you can run. Take MapReduce as an example: if I have more partitions, more map jobs can run in parallel — if I want to perform a similar kind of job, all map jobs can run in parallel. So if my data has an appropriate sharding, more jobs get done in parallel and more performance is achieved. Second, it is good for horizontal scaling: you add a node to the cluster and your capacity increases. You perform re-sharding because that node has to be allocated a certain share of data, which reduces the amount of data each node handles as the cluster grows. If your data stays the same and you add a node, the amount of data each node handles goes down, and the number of operations each node has to manage also goes down, because the data is divided. The data is replicated as well.

Why sharding gives scale — two mechanisms:

  1. Parallelism: with \(k\) shards, a job that processes the whole data set can run up to \(k\) independent map jobs in parallel — more shards, more parallelism, more throughput.
  2. Per-node load: with fixed total data \(D\) and \(k\) nodes, each node handles \(D/k\) of the data and a share of the operations. Adding a node (re-sharding) reduces \(D/k\) and spreads the operation load — each node does less, the cluster does more.

Shards are independent databases that together form the logical database, and the data is replicated across nodes as well, so sharding and replication combine: horizontal scale from shards, fault tolerance from replicas.

4.10.3 Primary-secondary architecture and read preference

MongoDB is not peer-to-peer like Cassandra: it uses a primary-secondary configuration — what you can also call a master-slave kind of thing. Performance settings can be used to tweak the write consistency, the way we discussed in Cassandra: a number of nodes is set — when you go for a write, how many nodes should be written before you consider the write successful. Data is replicated, and the performance can be tweaked. Clients usually read the data from the primary, but the read performance setting can also be tweaked from the read consistency: do you want to read from a single node and consider it successful, or read from a majority of nodes and then consider it successful? One difference from Cassandra: here the data is not time-stamped or versioned the way it was done with Cassandra — the data is used as it is.

Master-slave with an election safety net: a replica set has one primary and several secondaries. All writes go to the primary, which logs them into its oplog; secondaries replay the oplog to stay in sync. Clients usually read from the primary; a read preference can redirect reads to secondaries for load-balancing or lower latency. If the primary fails, the secondaries elect a new primary automatically — so the architecture has no permanent single point of failure, but it is still asymmetric (one node writes, others replicate) rather than Cassandra's fully symmetric peer ring. Unlike Cassandra's versioned storage, MongoDB uses the data as written — no timestamp-based conflict resolution baked in.

4.10.4 Indexes, sub-document fields, and a sample document

Some form of indexing can be performed in MongoDB: you can create an index on any field of a collection, or on a sub-document field. Here is a sample document. The document has a name, Ravi, and an address field, which is again a hierarchy: city, state, pin code. And there are tags — a collection of tags: football, cricket, badminton. This is a sort of key-value pair structure — one document — and you can store this as one record of MongoDB. Then you can query: "I want to figure out the records with the city New Delhi" — this document will appear as part of your search. Further, you can create indexes and use the find function to search by them. Creating an index on tags looks like this — and now, searching on "tags: cricket", this may speed up the performance a little bit. Inserting data, deleting data, and updating data will all be covered in the lab; creating an index is also something we can look into during the lab.

Worked example — the Ravi document and its queries:

{
      "_id": ObjectId("5f8b4d2c1a2b3c4d5e6f7081"),
      "name": "Ravi",
      "address": {
        "city": "New Delhi",
        "state": "Delhi",
        "pin_code": 110001
      },
      "tags": ["football", "cricket", "badminton"]
    }
  • Query on the sub-document field: db.users.find({ "address.city": "New Delhi" }) — matches this document, because city is a nested field inside address.
  • Index on a nested path: db.users.createIndex({ "address.city": 1 }) — indexes the sub-document field.
  • Index on tags: db.users.createIndex({ "tags": 1 }), then db.users.find({ "tags": "cricket" }) — the tag search now uses the index and speeds up.

Final answer: one document holds a hierarchy (address) and an array (tags); both are queryable, and both can be indexed — nested paths included. Sense-check: this is the schema-flexibility story in action — no second table is needed for "address", the hierarchy simply nests inside the document.

4.10.5 Joins with aggregate and the lookup stage

This slide indicates that in MongoDB 3.2 plus it is possible to join the data from two collections using the aggregate pipeline. Look at the collections: this is the collection books — one collection, one "table" — with three attributes: isbn number, title, author. And there is a book selling data collection with isbn number and copies. Using db.books.aggregate we can perform a kind of join — the lookup stage (written with a dollar sign before its name):

db.books.aggregate([
      {
        $lookup: {
          from: "books_selling_data",
          localField: "isbn",
          foreignField: "isbn",
          as: "copies"
        }
      }
    ]);

We take the books collection, look into the books_selling_data collection, match the local field isbn against the foreign field isbn, and collect the matches under the field "copies". People who know SQL know how to join two tables — join on isbn with isbn and aggregate the data; the same thing is happening here. This is the direct join in MongoDB. Although it is not preferable to use NoSQL databases where frequent joins are being used, this is one support that is there in MongoDB 3.2 plus.

The lookup join, mapped to SQL: the aggregate pipeline's lookup stage — written with a dollar sign before its name in the query — is MongoDB 3.2+'s equi-join. For each document in the input collection (books), it finds matching documents in the from collection (books_selling_data) where the localField (isbn) equals the foreignField (isbn), and attaches them as an array under as ("copies").

The SQL equivalent:

SELECT * FROM books LEFT JOIN books_selling_data
    ON books.isbn = books_selling_data.isbn;

Same join idea, same join cost caveat: this is support that exists, not a recommendation — frequent joins are still the RDBMS's home turf (Section 4.3.3).

4.10.6 Read and write concerns, and causal consistency

MongoDB offers various read and write choices for flexible consistency trade-offs with scale, performance, and durability — the read consistency and write consistency we have talked about can both be set, based on what performance and durability you are looking for. In case of a network partition, because this is a primary-secondary architecture, it may elect another primary — re-election on primary failure. The client application reads and writes from the primary; writes are communicated to the secondaries, and then the write can be considered successful. Reads can happen from the primary. If I want to increase read performance, I read from the primary. The read can also come directly from a secondary — like that arrow — if the primary is too busy; you read from the secondary for load-balancing. And if the primary dies, an election happens among the secondaries; one of them acts as the primary and starts its operations.

The read concern options: local means the client reads from the primary replica; then there are the causal-consistent-session options and available; and majority — the client wants to read what the majority of nodes have. Majority is the best option for fault tolerance and durability. The same idea as quorum in Cassandra: in Cassandra the term is quorum, in MongoDB the similar term is majority. The write concern options: write to one, write to \(n\), or write to the majority. If you want performance, go with the lower options; if you want durability, go with majority.

Read concern and write concern, side by side:

  • Read concern: local — read from the primary replica (fast, weak guarantee); available — read from the nearest node; majority — return only when the majority of nodes agree (strongest, slowest). Majority is the best option for fault tolerance and durability.
  • Write concern: write to 1 node, write to \(n\) nodes, or write to the majority — the same dial as Cassandra's consistency levels. Lower numbers = better performance; majority = better durability.

The vocabulary trap for exams: the identical majority idea is called quorum in Cassandra and majority in MongoDB. Remember the mapping, not just the concept.

The standard setup uses an odd number of nodes in a cluster. If there is a network partition or some failure where the nodes are divided into two, one partition will always have a larger number of nodes than the other. If I set read to majority and write to majority, my system gives me causal consistency. Whenever I read, I read from the majority of nodes; only when the majority of nodes have the same copy do I consider the read successful. When the majority of nodes have the new update written, I consider the write successful. So with a majority of the nodes holding the same value, the system is causally consistent — this was discussed earlier as well, along with the eventual consistency scenarios.

Why majority works with an odd node count: with an odd \(n\), a partition splits the cluster into two halves of different sizes, so exactly one side holds a strict majority — a majority of nodes can always agree, and a split-brain (two "majorities" disagreeing) is impossible. With read majority and write majority, a value is only served after a majority holds it, so reads never observe a write that a majority has not accepted — the property that yields causal consistency. The professor's example sets the majority threshold at five nodes, which implies a nine-node cluster (\((9/2) + 1 = 5\)).

Here is the causal-consistency recap, with four processes \(P_1, P_2, P_3, P_4\) on a timeline: \(P_1\) has written a value of \(x\) to 5 at this moment; \(P_2\), when it reads, reads \(x\) as 5; \(P_3\) reads \(x\) as 5 and later reads \(y\) as 10; and \(P_4\) reads \(y\) as 10 but reads \(x\) as 0 — \(x\) was updated earlier. \(P_4\) sees a write to \(y\) but not the write to \(x\). That schedule is not causally consistent — and it is also not linearizable, not strict, and not sequential. For each of these consistency levels — linearizable, strict, sequential, eventual — you can refer to the previous coverage; there is also a link in the earlier slide deck for reading more.

Worked example — the four-process schedule:

  • \(P_1\) writes \(x = 5\).
  • \(P_2\) reads \(x = 5\) (sees the write).
  • \(P_3\) reads \(x = 5\), then reads \(y = 10\).
  • \(P_4\) reads \(y = 10\), but reads \(x = 0\) — the write to \(x\) is invisible to it, even though \(P_4\)'s read of \(y\) happened after the read that saw \(x = 5\).

The write to \(x\) and the later write to \(y\) are causally related (they happened in that order in time), so any process that sees the later write (\(y = 10\)) must also see the earlier one (\(x = 5\)). \(P_4\) breaks that rule: it sees \(y = 10\) but \(x = 0\).

Final answer: the schedule is not causally consistent — and since causal consistency is the weakest of the named guarantees, it also fails linearizable, strict, and sequential consistency. Sense-check: causal consistency requires "if a read depends on an earlier write, all readers of the later event see the earlier one too"; \(P_4\)'s missing \(x\) is precisely that violation.

Exam note: Expect the consistency levels and read/write concerns to keep appearing in exam material — know the MongoDB read/write concerns (local, available, majority; 1, n, majority) and that majority is the option for fault tolerance and durability.

Recap + bridge: MongoDB = document storage (JSON/BSON, 16 MB WiredTiger cap, GridFS 255 KB chunks), sharding for horizontal scale, a primary-secondary replica set with elections and read preference, field and sub-document indexes, the lookup join, and read/write concerns where majority plays Cassandra's quorum. Next: graph computing — native versus non-native storage, when to choose a graph, and the Cypher and Gremlin query languages.

4.11 Graph Computing: Neo4j and Apache TinkerPop

Hook: The professor flagged graph databases as one of the most important topics. Now we open the engine room: why a native graph store traverses faster than a graph layer on top of an external database — and how its two query languages express the same questions in different styles.

4.11.1 Native versus non-native graph storage

There are two kinds of graph storage. Native storage stores the data in-house — the database itself manages the storage. The popular one is Neo4j, which maintains its own data storage. Non-native storage — the graph computing platforms — makes use of external databases for storage, and on top of that provides a graph layer. Apache TinkerPop is a computing platform that connects to graph databases that actually store the nodes and edges. Its built-in TinkerGraph stores data in memory only — so this is not a database storage; it actually creates a layer on top of other database storages, such as Elasticsearch or any other kind of database that can store this data — and then you access that data in a graphical way using the Gremlin language. The native approach is much faster, because adjacent nodes and edges are stored closer to each other, which gives faster traversal. In a non-native approach, extensive indexing has to be used. The native approach also scales as nodes get added.

Native versus non-native, named precisely:

  • Native storage (Neo4j): the database manages its own on-disk storage. A node's record stores a direct reference (a link) to its adjacent nodes and edges, so a traversal hops from node to node through those stored pointers — adjacent nodes and edges are physically close, and traversals do not need indexes. This is what makes native storage fast, and it scales as nodes are added.
  • Non-native storage (Apache TinkerPop / TinkerGraph): the storage lives in an external database — Elasticsearch or any other store — and the graph platform puts a graph layer on top. TinkerGraph, TinkerPop's built-in graph, holds everything in memory only: it is not a database but an in-memory layer. Because the graph is not laid out for neighbor links, extensive indexing is required to locate nodes and edges before you can traverse them.

The professor's rule of thumb: native = faster traversal because adjacency is stored directly; non-native = a flexible graph layer over whatever storage you already have, at the price of index-heavy lookups.

4.11.2 Why relational tables struggle with relationships

Why model this kind of scenario as a graph instead of a relational database? A relational database stores data in the form of tables. I can have a person table with four records — Alice, Bob, Charlie, Devin. How do I model "Alice is a friend of Bob"? I have to come up with a relationship of foreign keys, and there will be a self-referential foreign key — Alice is a friend of Bob, something like that. When you have a lot of this, it becomes very, very difficult to manage using the relational database. And this is only the friends case. What if we also need to monitor that Alice has posted 10 posts, has liked 50 posts, has joined these many groups? For each of these things I have to maintain one relational table, and Bob will be linked to 50 of the like records, and to however many groups he has joined. If I store this kind of normalized data and want to gather "how many of the posts were liked by Alice", I have to do a lot of joins. Joins are an expensive operation — and if there is a lot of data, like many users on a social platform, and I keep doing joins at the back end, all this will become a bottleneck. The solution is to model such scenarios as graphs: store the data in the form of nodes and edges, let the edges represent the relationships, and traverse the graphs based on whatever the requirement is.

The self-referential foreign key problem: to say "Alice is a friend of Bob" in SQL, the friends table needs a foreign key that points back into the person table — a self-referential foreign key: (person_id, friend_id) with both referencing person.id. One relationship type costs one table and one self-join per hop. Now multiply: Alice posts 10 posts, likes 50 posts, joins 12 groups, comments on 30 posts — each activity type is its own table, and every query that crosses activity types joins across several of them. "How many of Alice's liked posts did Bob comment on?" is a three-table join. At social-network scale, with joins running at the backend for every such query, the joins themselves become the bottleneck. The graph model removes the joins entirely: posts, likes, comments, groups are nodes; the relationships are edges; and the question is a traversal, not a join chain.

4.11.3 When to choose a graph database

When do you use a graph database? Three signals from the discussion:

  1. You have a very heavy relationship data set — a large set of data items where the connections between them matter most.
  2. Your queries are graph traversals, and you need to keep the query performance almost constant as the database grows. With a relational DB, if I have 100 records in every table, the performance may be okay. But these kinds of use cases grow exponentially, and that results in a performance bottleneck in relational databases. You want: one more user comes to the platform, tries to connect — the performance should not go down.
  3. There is a variety of queries that are asked from this data, and they almost touch upon each and every attribute. If my queries are touching each and every attribute, even creating indexes in a relational DB does not help — what if I have 100 attributes? Will I create 100 indexes? It becomes very cumbersome to manage. So: connection-heavy, relationship-heavy data, diverse queries touching every attribute, and performance that must hold as the database size grows — that is when graph computing or graph storage is a good choice.

Scope — when a graph database is NOT the right tool: graph stores shine when relationships are the query subject. They are the wrong tool when your data is naturally tabular with no meaningful connections (a simple product catalog), when your workload is aggregates over columns (that is columnar territory, Section 4.7), or when queries rarely cross relationships. Also remember the three signals are a package: relationship-heavy data, traversal queries, and diverse attribute-touching queries with near-constant performance — missing most of them means the graph model adds cost without payoff.

4.11.4 Cypher: querying the movie database

Neo4j uses Cypher, a declarative query language — like SQL, but for querying the graph. There is a popular movie search database: a person node, Tom Hanks, who has acted in a certain number of movies; the edges indicate "acted in" — Cloud Atlas, The Da Vinci Code, Charlie Wilson's War, and others. The movie nodes have incoming edges — "acted in", "directed by" — and you see that Mike Nichols directed a particular movie. Let us build the queries step by step.

First, the simple query — find the person named Tom Hanks, follow the ACTED_IN edges, collect all the movies into a set \(m\), filter with a WHERE clause like in SQL, and return just five results:

MATCH (p:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m)
    WHERE m.released > 2000
    RETURN m
    LIMIT 5;

"Match person named Tom Hanks" — the person is one entity in the database. "Further, we are looking for the acted-in edges; we are looking for all set of movies, and that is being stored in set m. Why am I storing it? Because I want to apply filters on this set." The WHERE clause filters: movies released after the year 2000, and return five results — it may return 100 results, but I am looking for the first five. The query does not just return text: it returns a graph structure, and I can click on any of these nodes to further explore it — details about Cloud Atlas, or about this person — that is also a possibility.

Second, refine: find the movies that Tom Hanks acted in and that were directed by Ron Howard:

MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m)<-[:DIRECTED]-(ron:Person {name: 'Ron Howard'})
    WHERE m.released > 2000
    RETURN m
    LIMIT 5;

This says: match a person named Tom Hanks who acted in movies set \(m\); then, with the comma, search for a person named Ron Howard who has directed certain movies; out of whatever set \(m\) was retrieved, look for any directed edge to these movies — an edge with this particular label — with the same condition \(m.released > 2000\), and return five results. So: movies Tom Hanks acted in, directed by Ron Howard, released after 2000. Directed is a relationship between a person and a movie; acted in is a relationship between a person and a movie — between the same two entity types there are multiple relationships. In SQL you would maintain two tables and put across joins; this is how it is done in the graph-traversal way.

Third, add one more condition: who were the other actors in the movies where Tom Hanks acted and Ron Howard directed?

MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m)<-[:DIRECTED]-(ron:Person {name: 'Ron Howard'}),
          (p:Person)-[:ACTED_IN]->(m)
    WHERE m.released > 2000
    RETURN p
    LIMIT 5;

Till this point we have figured out the movie data set in which Tom Hanks worked. Then, with this comma, we figure out out of these movies how many are directed by Ron Howard. Now in the persons' database again, we search how many of them have acted in the movies we have found here — the set \(m\) is getting filtered, first here, second here, third here, with the condition \(m.released > 2000\). The result set is stored in \(p\), and we list the persons. These are very simple queries to start with; a lab session on Neo4j (along with MongoDB) will show how the graph looks, how we can explore the nodes that come as part of a query result, and we will load the movie search database into Neo4j and see what kind of database it is.

Worked example — the three Cypher queries, traced:

  • Query 1 MATCH (p:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m) WHERE m.released > 2000 RETURN m LIMIT 5; — start at the Tom Hanks node, follow every ACTED_IN edge to movie nodes \(m\), keep only movies released after 2000, return the first five. Result: a graph (movies with their edges), not a plain table — each returned node remains clickable and explorable.
  • Query 2 MATCH (tom:Person {...})-[:ACTED_IN]->(m)<-[:DIRECTED]-(ron:Person {name: 'Ron Howard'}) WHERE m.released > 2000 RETURN m LIMIT 5; — the pattern requires the movie node \(m\) to have both an outgoing ACTED_IN edge from Tom Hanks and an incoming DIRECTED edge from Ron Howard; then the WHERE filters to post-2000 and the first five are returned. The two relationships ACTED_IN and DIRECTED connect the same two entity types (Person, Movie) — something SQL would need two separate tables and a join to express.
  • Query 3 adds , (p:Person)-[:ACTED_IN]->(m) — after the movie set is pinned down, the comma pattern matches every person \(p\) who also acted in those same movies, so the result is the other actors in Tom Hanks–Ron Howard movies released after 2000.

Final answer: declarative pattern matching — each query states the shape of the subgraph it wants, and the engine walks the graph to find matches. Sense-check: the same three questions in SQL would need multiple tables (person, movie, acted_in, directed) and repeated joins; in Cypher each is one pattern, because relationships are first-class.

4.11.5 Gremlin: querying with TinkerPop

TinkerPop supports the Gremlin query language, which has both declarative and imperative flavors. The sample queries: movies where Tom Hanks has acted — this is simple; we have a person, we have a movie, and we want the ACTED_IN relationship.

g.V().hasLabel('person').has('name', 'Tom Hanks').out('acted_in').values('name')

Here we are starting with the whole graph — that is what \(g\) is. I am looking for a set of vertices, \(V\), which has a label person — because some set of vertices will also have the label movie. I want to look for persons further, which have a name Tom Hanks, and which have an outgoing edge acted_in; .values('name') gives all the names of those movies Tom Hanks has acted in.

Now the second query — Tom Hanks acted in and directed by Ron Howard. Again you start with \(g\), you go to the set of vertices, you filter out the vertices with the label person, you filter out the records of the person with the name Tom Hanks, and further filter where there is the outgoing edge acted_in with that label. Now this movie should have an incoming edge directed_by — an incoming edge which has a name Ron Howard:

g.V().hasLabel('person').has('name', 'Tom Hanks').out('acted_in')
      .in('directed_by').has('name', 'Ron Howard').values('name')

In the first query we are going from the person entity to the movie entity; in the second one, from acted_in we are going from the person entity to the movie entity and then further to the director person entity — the difference is that the first time we are looking for Tom Hanks, the other time we are also looking for Ron Howard. The lab will discuss more of these things — probably not on TinkerPop, but definitely on Neo4j, with queries we will talk about in the lab session.

Worked example — the two Gremlin traversals, step by step:

  • Query 1 g.V().hasLabel('person').has('name', 'Tom Hanks').out('acted_in').values('name')
  • g.V() — start with the whole graph's vertex set.
  • .hasLabel('person') — keep only vertices labeled person (drops the movie vertices).
  • .has('name', 'Tom Hanks') — keep only the Tom Hanks vertex.
  • .out('acted_in') — walk the outgoing acted_in edges to movie vertices.
  • .values('name') — project each movie's name. Result: the names of movies Tom Hanks acted in.
  • Query 2 g.V().hasLabel('person').has('name', 'Tom Hanks').out('acted_in').in('directed_by').has('name', 'Ron Howard').values('name')
  • The first four steps are identical (Tom Hanks → movies).
  • .in('directed_by') — from each movie, walk the incoming directed_by edges to the directors.
  • .has('name', 'Ron Howard') — keep only the director named Ron Howard.
  • .values('name') — the movies stay as the step context, so the names returned are those of Tom Hanks–Ron Howard movies.

Final answer: Gremlin is an explicit step-by-step walk — start at \(g\), filter vertices, follow edges outward, follow edges inward, filter again, project values. Sense-check: query 1 stops at the movies; query 2 keeps walking from each movie to its director and filters there — the professor's "person → movie → director" path, literally written as a chain of steps.

Recap + bridge: Graph computing comes in two flavors — native storage (Neo4j, fast because adjacency is stored directly) and non-native layers (TinkerPop/TinkerGraph over external stores, index-heavy). Choose a graph when relationships dominate, traversals must stay near-constant as data grows, and queries touch every attribute. Cypher declares patterns; Gremlin walks steps. This closes the four families — next, the exam guidance summary and the industry applications.

Exam Guidance Summary

  • Graph databases are one of the most important topics. This was flagged explicitly. Know the graph model (nodes, edges, properties), when to choose a graph database (relationship-heavy data, traversal queries with near-constant performance, diverse queries touching every attribute), native versus non-native storage (Neo4j native and fast because adjacent nodes and edges are stored close; TinkerPop/TinkerGraph as an in-memory layer over external stores like Elasticsearch), and the two query languages — Cypher (Neo4j, declarative, like SQL) and Gremlin (Apache TinkerPop, declarative and imperative flavors).
  • CAP theorem — very, very important. It was flagged as very important and was discussed before the mid-semester too. Know consistency, availability, and partition tolerance; be able to say why partition tolerance is mandatory in a multi-data-center deployment and why you must choose between C and A. Cassandra is the AP example: high availability, high performance, sacrifices consistency.
  • Consistency levels and configurations are exam-grade material. The causal-consistency example (\(P_1\) writes \(x = 5\); \(P_4\) reads \(y = 10\) but \(x = 0\) — sees the write to \(y\) but not the write to \(x\) — so not causally consistent, not linearizable, not strict, not sequential) is the classic question shape. Review the full hierarchy (linearizable, strict, sequential, causal, eventual) from the earlier coverage — the revisit in this session was brief on purpose because it was already covered in detail before the mid-semester. Expect the MongoDB read/write concerns (local, available, majority; 1, n, majority) and the Cassandra consistency levels and quorum in the same family of questions. Remember: in MongoDB the option is called majority; in Cassandra the same idea is called quorum.
  • NoSQL classification and characteristics. Four families — key-value, document-oriented, column-oriented, graph — with examples (Dynamo, DynamoDB, Redis, Riak; MongoDB, CouchDB; Cassandra, HBase; Neo4j). Be ready for the SQL-versus-NoSQL comparison (schema, scaling, ACID versus CAP, joins, vendor support) and the pros/cons list.
  • Cassandra internals. Write path: commit log → memtable → SSTable (immutable, append-only); default replication factor of three, configurable per data center (east 2, west 3, for example); partitioners — crypto hash (MD5) versus non-crypto consistent hash (~10% performance advantage); consistency levels 1..n and quorum; lightweight transactions and the IF clause; the seven-node worked example (four nodes must hold the same copy before a read returns).
  • MongoDB internals. Collections and documents, JSON and BSON, the 16 MB document limit in WiredTiger and GridFS chunks of 255 KB for larger objects; sharding as horizontal partitioning and how more partitions enable more parallel jobs (the MapReduce analogy); primary-secondary architecture and re-election; indexes including sub-document fields; the lookup join (MongoDB 3.2+).
  • Rack-allocation question from the mid-semester (makeup exam). A file was given stating that a certain number of blocks exist, and you had to draw a diagram of how you would place these blocks into different racks. The rules came from a slide discussed earlier: one copy is placed on one node in one rack, the second copy in another rack, and so on, with rules on how many replicas can sit on a particular rack; if a placement satisfies the rules, you can place it immediately. There could be multiple correct answers — you were supposed to follow the rules, not a particular algorithm. One node can have multiple blocks, but you do not put the same block on the same node multiple times. The exact replica-per-rack rules live on the shared slide from the earlier session — review that slide before the exam, because the question tests the rules, not a single prescribed layout.

Q: In the mid-semester rack-allocation question, was there a single expected answer? A: There could be multiple answers for that, not a single answer. We were just supposed to follow the rules — not any particular algorithm to place the data blocks. One node can have multiple blocks; if there are different blocks, one node can have multiple blocks — but you do not put the same block on the same node multiple times.

Exam note: The guaranteed-return topics of this session are: the graph model and when to use it (flagged most important), the CAP theorem and why partition tolerance is mandatory, the consistency-level family (causal consistency example, MongoDB majority vs Cassandra quorum), the four-family NoSQL classification, and the Cassandra and MongoDB internals (write path, storage engines, consistency dials). Build your revision around these seven bullets first.

Key Industry Applications

  • Facebook — developed Cassandra, built on the concepts of Amazon DynamoDB and Google Bigtable.
  • eBay — runs Cassandra at marketplace scale: 100 million active buyers, 200+ million items, about 2 billion page views, 80 billion database calls, multi-terabyte storage, multi-data-center deployment, social signals on product pages where write performance matters and read latency does not; consistency-level tuning decides write versus read performance.
  • Amazon — the shopping-cart failure anecdote (adding an item sometimes fails) as a case where strict consistency is not required; the Dynamo and DynamoDB key-value stores.
  • Google — Bigtable (Cassandra's storage lineage) and its own NoSQL technologies at the naming conference; Yahoo likewise contributed technologies to the movement.
  • Oracle — the traditional vendor side of the SQL-versus-NoSQL comparison (excellent vendor support on the SQL side; community support on the NoSQL side).
  • OnePlus — flash-sale website crashes from a few years back as the motivating example of burst traffic and the need for throughput without bottlenecking.
  • Redis, Riak, Dynamo, DynamoDB — key-value stores; MongoDB, CouchDB — document-oriented; Cassandra, HBase — column-oriented; Neo4j — graph.
  • Neo4j — the movie search database (Tom Hanks, Cloud Atlas, The Da Vinci Code, Charlie Wilson's War, Ron Howard, Mike Nichols) queried with Cypher.
  • Apache TinkerPop / TinkerGraph / Gremlin — the in-memory graph layer over external storages such as Elasticsearch, queried with Gremlin.
  • WiredTiger and GridFS — MongoDB storage engines for documents up to 16 MB and larger objects in 255 KB chunks.
  • MD5 — the crypto hash used in Cassandra partitioning; non-crypto consistent hashing as the faster alternative with about a 10% performance edge.

Why these companies matter for the exam: each entry in this list is a concrete answer to "who uses which NoSQL technology and why" — the kind of named application the professor expects you to connect to the underlying concept (Facebook → Cassandra's origins; eBay → Cassandra at write-heavy marketplace scale; Amazon → the relaxed-consistency cart story and the key-value family; Google → Bigtable's column-family lineage; Neo4j → the movie database behind the Cypher examples).

BDS Lecture 4 notes

Big Data Systems· postgraduate· 2026-08-03

Sections Breakdown

1Why We Move Away from RDBMS

RDBMS guarantees ACID (atomicity, consistency, isolation, durability) for OLTP as the system of record, but vertical scaling is expensive and disruptive; schema rigidity and extreme connectivity push some applications toward NoSQL, while normalized tables and frequent joins remain RDBMS territory.

2What Is NoSQL: Origins and Shared Characteristics

The name NoSQL has two birthdays (Carlo Strozzi 1998; reintroduced 2009) and is best read as Not Only SQL; the systems gathered under it are non-relational, schema-less, consistency-relaxing, distributed, cluster-friendly, and built for throughput.

3The NoSQL Data Model: Variety Without Schema

NoSQL stores structured, semi-structured, and unstructured data without a fixed schema (JSON documents, key-value hierarchies); it gives up cheap joins, multi-item transaction semantics, and ACID, keeping relaxed consistency.

4The Four Families of NoSQL Databases

Four families: key-value (hash table of pairs: Dynamo, DynamoDB, Redis, Riak), document (self-describing documents: MongoDB, CouchDB), columnar (column blocks and column families with locality of reference: HBase, Cassandra), and graph (nodes and edges for connection-heavy data: Neo4j); the friend-of-a-friend example shows traversal beating repeated joins.

5Characteristics, Pros and Cons, and SQL versus NoSQL

NoSQL follows scale-out architecture with three promises (100,000+ ops/sec = 10^5, 1 billion+ documents = 10^9, full data variety), auto-sharding with replication for high availability and fault tolerance; pros and cons mirror each other, and the CAP theorem makes P mandatory so the choice is C or A.

6Document-Oriented Databases in Detail

Document databases store self-describing JSON documents in collections; a record maps to a document and a table maps to a collection; documents get unique IDs, indexable fields, and tag-based search (find), unlike key-value stores where the value is opaque.

7Column-Oriented Storage: Laying Out Columns

Column-oriented storage is vertical partitioning: all values of one column live in one storage block (locality of reference), and column families club columns that are queried together, so the family structure must mirror the access pattern.

8Graph Databases: Modeling Connections

Graph computing, flagged as one of the most important topics, models data as G=(V,E): nodes (vertices) with properties and edges (relationships) with labels and properties; also called network databases; queried by traversal with Cypher (Neo4j) or Gremlin (TinkerPop).

9Cassandra: The Column-Oriented, High-Availability Database

Cassandra (Facebook) fuses Dynamo's distribution/availability with Bigtable's column-family storage; it is an AP, peer-to-peer database with the write path commit log → memtable → SSTable (immutable, append-only), tunable consistency 1..n plus quorum, lightweight transactions via the IF clause, configurable replication factors, and hash-based partitioners.

10MongoDB: The Document-Oriented Database

MongoDB stores JSON/BSON documents in collections with a 16 MB WiredTiger cap and GridFS 255 KB chunks for larger objects; scales by sharding (horizontal partitioning) over a primary-secondary replica set with elections; supports field and sub-document indexes, the $lookup join (3.2+), and read/write concerns where majority plays Cassandra's quorum and gives causal consistency.

11Graph Computing: Neo4j and Apache TinkerPop

Native graph storage (Neo4j) stores adjacent nodes/edges close for fast traversal; non-native platforms (Apache TinkerPop, TinkerGraph) layer a graph over external stores like Elasticsearch with heavy indexing; choose graphs for relationship-heavy data with traversal queries; Cypher (declarative) and Gremlin (declarative + imperative) query the movie database.

12Exam Guidance Summary

Exam-relevant recap: graph databases and CAP theorem are the most important topics; consistency levels and the causal-consistency example are classic question shapes; the four-family classification, Cassandra internals, MongoDB internals, and the rack-allocation rules round out the revision list.

13Key Industry Applications

Named industry use: Facebook built Cassandra on DynamoDB and Bigtable concepts; eBay runs Cassandra at marketplace scale (100M buyers, 80B DB calls) with write-first tuning; Amazon, Google, Oracle, Yahoo, OnePlus, Neo4j, TinkerPop, WiredTiger/GridFS, and MD5 each illustrate a lecture concept.

Postgraduate students in Big Data Systems

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

Why We Move Away from RDBMS

Must-know: ACID stands for atomicity, consistency, isolation, durability; RDBMS scales vertically (stop, upgrade hardware, restart) which is expensive, while distributed systems scale horizontally by adding machines.

⚠️ Top pitfall: Treating 'RDBMS is obsolete' as the lesson — the RDBMS stays right for normalized tables, frequent joins, and ACID-required transactions like payments.

Self-check: Why is a credit card transaction treated differently from adding an item to a cart?

Connects to: What Is NoSQL: Origins and Shared Characteristics (4.2), Characteristics, Pros and Cons, and SQL versus NoSQL (4.5)

What Is NoSQL: Origins and Shared Characteristics

Must-know: NoSQL = Not Only SQL; shared characteristics are non-relational, schema-less, loosened consistency, distributed for scale, cluster friendly; motivation is performance/throughput for scenarios that can relax consistency.

⚠️ Top pitfall: Reading 'NoSQL' as 'no SQL at all' — it means SQL plus other things.

Self-check: What does eventual consistency mean in one line?

Connects to: Why We Move Away from RDBMS (4.1), The NoSQL Data Model: Variety Without Schema (4.3)

The NoSQL Data Model: Variety Without Schema

Must-know: Three data kinds: structured (RDBMS), semi-structured (self-describing tags, JSON/XML), unstructured (text, images, ~80% of enterprise data). NoSQL has no fixed schema; joins are possible but expensive; no multi-item ACID.

⚠️ Top pitfall: Believing NoSQL has no joins and no transactions at all — joins exist at heavy cost (e.g., MongoDB $lookup) and limited lightweight transactions exist.

Self-check: Why is a JSON document called self-describing?

Connects to: Why We Move Away from RDBMS (4.1), The Four Families of NoSQL Databases (4.4), Document-Oriented Databases in Detail (4.6)

The Four Families of NoSQL Databases

Must-know: Four families with examples: key-value (Dynamo, DynamoDB, Redis, Riak), document (MongoDB, CouchDB), columnar (HBase, Cassandra), graph (Neo4j); column stores exploit locality of reference by storing one column's values together.

⚠️ Top pitfall: Mixing up family examples on the exam — e.g., placing MongoDB in the key-value family.

Self-check: Why does friends-of-friends become a bottleneck in an RDBMS at social-network scale?

Connects to: The NoSQL Data Model: Variety Without Schema (4.3), Column-Oriented Storage: Laying Out Columns (4.7), Graph Databases: Modeling Connections (4.8)

Characteristics, Pros and Cons, and SQL versus NoSQL

Must-know: Scale promises: 10^5 reads/writes per second and 10^9+ documents. CAP: Consistency, Availability, Partition tolerance; a distributed system delivers only two, P is mandatory across data centers, so choose C or A per application.

\[10^5 \text{ ops/sec},\quad 10^9 \text{ documents}\]

⚠️ Top pitfall: Thinking consistency is binary — it is a configurable dial and increasing consistency reduces performance.

Self-check: Why is partition tolerance mandatory in a multi-data-center deployment?

Connects to: Why We Move Away from RDBMS (4.1), Cassandra: The Column-Oriented, High-Availability Database (4.9), MongoDB: The Document-Oriented Database (4.10)

Document-Oriented Databases in Detail

Must-know: Document ID is the key for the document; a collection does not enforce a schema; fields are tags that can be queried and indexed (book title, year of publication); key-value stores extract the whole value and cannot query its parts.

⚠️ Top pitfall: Assuming a document database is just a faster key-value store — they answer different query shapes (search inside fields vs fetch whole value by key).

Self-check: In MongoDB, what is the equivalent of a table, a record, and a primary key?

Connects to: The Four Families of NoSQL Databases (4.4), MongoDB: The Document-Oriented Database (4.10)

Column-Oriented Storage: Laying Out Columns

Must-know: Column values stored together in blocks; column families are vertical partitions; club columns always queried together (e.g., EmpNo + DeptId as pairs (1,1),(2,1),(3,1),(4,2)); versioning of values within a block is supported.

⚠️ Top pitfall: Putting every column in one family (turns a column store into a row store in disguise) or splitting columns that are always queried together.

Self-check: Why does an average-hire-date query run faster on columnar storage?

Connects to: The Four Families of NoSQL Databases (4.4), Cassandra: The Column-Oriented, High-Availability Database (4.9)

Graph Databases: Modeling Connections

Must-know: Graph = G(V,E); nodes are entities with key-value properties, edges are labeled relationships and can carry properties (e.g., knows since a date); a graph database is also called a network database; two query languages: Cypher (Neo4j) and Gremlin (Apache TinkerPop).

\[G = (V, E)\]

⚠️ Top pitfall: Mixing up the query languages on the exam — Cypher belongs to Neo4j, Gremlin to Apache TinkerPop.

Self-check: Where does the edge label and edge property appear in 'Alice knows Bob since 2021-05-14'?

Connects to: The Four Families of NoSQL Databases (4.4), Graph Computing: Neo4j and Apache TinkerPop (4.11)

Cassandra: The Column-Oriented, High-Availability Database

Must-know: Write path: commit log → memtable → SSTable (immutable, append-only); default replication factor RF = 3; consistency levels 1..n and quorum (majority, (n/2)+1); 7-node example needs 4 nodes to agree; read repair via gossip; partitioners: crypto hash (MD5) vs non-crypto consistent hash (~10% faster).

\[RF = 3,\quad \text{quorum} = \lfloor n/2 \rfloor + 1\]

⚠️ Top pitfall: Replication factor 1 risks data loss and unavailability — if the single node dies there is no recovery; also increasing consistency reduces performance.

Self-check: Why is a write considered successful only after it reaches the commit log?

Connects to: Characteristics, Pros and Cons, and SQL versus NoSQL (4.5), Column-Oriented Storage: Laying Out Columns (4.7), MongoDB: The Document-Oriented Database (4.10)

MongoDB: The Document-Oriented Database

Must-know: 16 MB max document in WiredTiger; GridFS chunks of 255 KB with metadata in a separate collection; read concern: local, available, majority; write concern: 1, n, majority; majority = quorum (Cassandra); read+write majority with odd node count gives causal consistency; P4 example is not causally consistent.

\[16\,\text{MB},\quad 255\,\text{kB},\quad \text{majority} = \lfloor n/2 \rfloor + 1\]

⚠️ Top pitfall: Confusing MongoDB majority with Cassandra quorum as different ideas — they are the same majority idea under different names.

Self-check: Why does the P4 schedule (reads y=10 but x=0) violate causal consistency?

Connects to: Characteristics, Pros and Cons, and SQL versus NoSQL (4.5), Document-Oriented Databases in Detail (4.6), Cassandra: The Column-Oriented, High-Availability Database (4.9)

Graph Computing: Neo4j and Apache TinkerPop

Must-know: Native (Neo4j): adjacency stored directly, fast traversal, scales; non-native (TinkerPop/TinkerGraph over Elasticsearch): in-memory graph layer, extensive indexing needed; Cypher = declarative patterns (MATCH ... -[:ACTED_IN]->(m)); Gremlin = step chains (g.V().hasLabel('person').has('name','Tom Hanks').out('acted_in').values('name')).

⚠️ Top pitfall: Believing a graph layer on top of a database is the same as native storage — traversal performance differs by an order of magnitude.

Self-check: In Gremlin query 2, what does .in('directed_by') walk to?

Connects to: Graph Databases: Modeling Connections (4.8), The Four Families of NoSQL Databases (4.4)

Exam Guidance Summary

Must-know: Graph databases (flagged most important), CAP theorem (P mandatory, choose C or A), causal-consistency example (P4 reads y=10 but x=0 is not causally consistent), MongoDB majority vs Cassandra quorum, four NoSQL families, Cassandra and MongoDB internals, rack-allocation rules.

⚠️ Top pitfall: Assuming the rack-allocation question has one correct answer — multiple placements can satisfy the rules; follow the rules, not an algorithm.

Self-check: Why is a schedule where a process sees a later write but not the earlier causal write not causally consistent?

Connects to: Characteristics, Pros and Cons, and SQL versus NoSQL (4.5), Cassandra: The Column-Oriented, High-Availability Database (4.9), MongoDB: The Document-Oriented Database (4.10), Graph Computing: Neo4j and Apache TinkerPop (4.11)

Key Industry Applications

Must-know: eBay = 100M active buyers, 200M+ items, 2B page views, 80B database calls, multi-terabyte, write performance key; Facebook developed Cassandra; Amazon provides the relaxed-consistency cart anecdote and Dynamo/DynamoDB; OnePlus flash sales motivate NoSQL.

⚠️ Top pitfall: Quoting eBay numbers without the reason — the tuning logic is: lower write-consistency number to raise write performance.

Self-check: Why is write performance, not read latency, the key at eBay?

Connects to: Cassandra: The Column-Oriented, High-Availability Database (4.9), What Is NoSQL: Origins and Shared Characteristics (4.2)

Was this lecture useful?

Loading comments…