Sina Vafadar

Data Models and Query Languages

Aug 6, 2026 · 22 min

Designing Data-Intensive Applications · Chapter 2 · Recall lab

Chapter 1 gave you three words to argue with. Chapter 2 gives you one question that decides almost everything downstream: what kinds of relationships does your data actually have? Answer “trees” and the document model fits. Answer “everything relates to everything” and you want a graph. Answer “some of both” and you get the relational model, which is why it has survived thirty years of challengers. Every widget below is the same résumé, seen through a different lens.

Setup § Relational Model Versus Document Model

Layers of models, and the mismatch between two of them

Applications are built as layers of models, each hiding the one below: real-world objects → application objects and data structures → JSON/XML/tables/graph for storage → bytes on disk → electrical currents. Each layer’s job is to give the layer above a clean, usable abstraction. The one this chapter examines is the storage layer, and the fact that the layer above it usually speaks a different language.

The relational model came from Edgar Codd in 1970: data organised into relations (SQL tables), each an unordered collection of tuples (rows). It was a theoretical proposal that many doubted could be implemented efficiently, and by the mid-1980s it had won. Its roots are 1960s–70s mainframe business data processing, transaction processing and batch processing, and the goal that distinguished it from its contemporaries was to hide the internal representation of the data behind a cleaner interface. Competitors came and went: network and hierarchical models in the 70s, object databases around 1990, XML databases in the early 2000s. Each generated hype; none lasted.

The Birth of NoSQL, four driving forces

The name is unfortunate: it doesn’t refer to any technology. It began as a catchy hashtag for a 2009 meetup on open source, distributed, nonrelational databases, and was retrofitted to mean Not Only SQL.

  • A need for greater scalability than relational databases easily achieve, very large datasets, very high write throughput
  • A widespread preference for free and open source over commercial database products
  • Specialized query operations the relational model doesn’t support well
  • Frustration with the restrictiveness of relational schemas, and a wish for something more dynamic and expressive

The likely outcome isn’t a winner: relational databases used alongside a variety of nonrelational stores. That idea has a name, polyglot persistence.

The impedance mismatch

Most application code is written in object-oriented languages, so storing data in relational tables requires an awkward translation layer between application objects and tables, rows and columns. The disconnect is called an impedance mismatch, a term borrowed from electronics, where power transfer across a connection is maximized when the output and input impedances match, and a mismatch causes signal reflections and other trouble. ORMs like ActiveRecord and Hibernate cut the boilerplate, but they can’t erase the difference between the models.

The signature exhibit, one résumé, three models

A LinkedIn profile: first_name and last_name appear exactly once per user, so they’re columns. But most people have had several positions, varying amounts of education, and any number of pieces of contact_info. Those are one-to-many relationships, which is to say, the data is a tree. Watch what each model does with that tree.

Normalized across four tables, joined by user_id

users
user_idfirst_namelast_nameregion_id
251BillGatesus:91
positions
iduser_idjob_title / organization
1251Co-chair · Bill & Melinda Gates Foundation
2251Co-founder, Chairman · Microsoft
education
iduser_idschool_namestart–end
1251Harvard University1973–1975
2251Lakeside School, Seattlenull
contact_info
iduser_idtype / url
1251blog · thegatesnotes.com
2251twitter · twitter.com/BillGates
To load the whole profile4 queries by user_id, or one messy multi-way join
What it buys / costsJoins are easy, so many-to-one and many-to-many references are natural. Any nested item is addressable in its own right.

Shredding, splitting a document-like structure across multiple tables, can lead to cumbersome schemas and unnecessarily complicated application code. Poor locality: multiple index lookups, more disk seeks.

Three ways SQL itself can hold the one-to-many part, incidentally: separate tables with foreign keys (the classic normalized form, pre-SQL:1999); structured datatypes / XML / JSON columns that hold multi-valued data inside a single row, with querying and indexing inside them, supported to varying degrees by Oracle, DB2, MS SQL Server, PostgreSQL, MySQL; or a plain text column holding an encoded document, which the application must interpret and the database cannot look inside.

The pivot § Many-to-One and Many-to-Many Relationships

Why region_id and not "Greater Seattle Area"

In the JSON profile, region and industry are given as IDs, not as the strings Greater Seattle Area and Philanthropy. If the UI has free-text fields, store strings. But if users pick from a drop-down or autocompleter, a standardized list buys you five things.

1 row to rewrite if the city is renamed

NormalizedThe human-meaningful word lives in exactly one place, and everything referring to it holds an ID that has meaning only inside the database. Because the ID means nothing to humans, it never needs to change, even when the thing it identifies does. One write, no risk of inconsistency. The cost: resolving it requires a many-to-one relationship, i.e. a join.
  • Consistent style and spelling across profiles
  • No ambiguity, several cities share a name
  • Ease of updating, the name lives in one place, so a political renaming is one write
  • Localization, the standardized list can be translated, and shown in the viewer’s language
  • Better search, the region list can encode that Seattle is in Washington, so a search for philanthropists in Washington state matches. The string “Greater Seattle Area” carries none of that

The whole question is duplication. An ID has no meaning to humans, so it never needs to change; anything meaningful to humans may need to change, and if it’s duplicated every copy needs updating, write overhead plus the risk of inconsistency. Removing that duplication is the key idea behind normalization. Rule of thumb: if you’re duplicating values that could live in one place, the schema isn’t normalized. (The literature’s several normal forms are of little practical interest here.)

And here is the catch that drives the rest of the chapter: normalizing requires many-to-one relationships, many people live in one region, many work in one industry, and many-to-one doesn’t fit the document model nicely. In relational databases, referring to rows in other tables by ID is normal because joins are easy. In document databases, joins aren’t needed for one-to-many trees, and support for joins is often weak. If the database can’t join, you emulate it in application code with multiple queries, moving work out of the database and into your app.

The escalator, data gets more interconnected as you add features

Even if version one fits a join-free document model, features accumulate. Add each one and watch the relationship types change.

Relationship types now in play

Base résumé · one-to-many, Positions, education and contact info hang off one user. This is a tree, the document model’s home turf, one query to load it all.

Verdict. Pure tree. Use a document. Poor join support may never bite you, an analytics store recording which events happened when may never need a many-to-many relationship at all.

The general shape of the answer: for highly interconnected data, the document model is awkward, the relational model is acceptable, and graph models are the most natural. It’s not possible to say which model yields simpler application code in general, it depends on the kinds of relationships between your data items.

The long view § Are Document Databases Repeating History?

This argument is older than NoSQL by forty years

The most popular database for business data processing in the 1970s was IBM’s IMS, first released 1968, originally built for stock-keeping in the Apollo space program, and still maintained today on IBM mainframes. Its data model, the hierarchical model, represented all data as a tree of records nested within records. Which is to say: remarkably like JSON.

IMS worked well for one-to-many relationships, made many-to-many difficult, and didn’t support joins. Developers had to choose between duplicating data or manually resolving references from one record to another. Those 1960s and ’70s problems are very much the problems developers hit with document databases today. Two solutions were proposed, and the “great debate” between them ran through much of the 1970s.

Click a model to inspect it

Hierarchical model, IBM IMS, 1968Built originally for stock-keeping in the Apollo space program, still maintained today on IBM mainframes. All data is a tree of records nested within records, remarkably similar to the JSON of document databases. Worked well for one-to-many; made many-to-many difficult; no joins. Developers had to choose between duplicating data and manually resolving references.

Access paths, and the one insight that decided it

In CODASYL the links between records weren’t foreign keys but something closer to pointers, stored on disk, but pointer-like. The only way to reach a record was to follow a path from a root record along those chains of links: an access path. Simple cases were like walking a linked list. But with many-to-many relationships, several different paths lead to the same record, and the programmer had to hold all of them in their head. Queries were performed by moving a cursor through the database, iterating over record lists and following access paths; even committee members admitted this was like navigating an n-dimensional data space.

Manual access-path selection made the best use of extremely limited 1970s hardware, tape drives, whose seeks are brutally slow. The cost was code that was complicated and inflexible: if you didn’t have a path to the data you wanted, you were stuck. You could add one, but then you had to rewrite a lot of handwritten query code to use it. Changing an application’s data model was hard.

Same request, two worlds, pick a new query the product team wants

CODASYL / IMS

If no access path leads from the region record to its users, you are stuck. You can add one, and then go through a lot of handwritten query code and rewrite it to use the new path.

Relational

SELECT * FROM users WHERE region_id = 'us:91'. If it’s slow, declare an index; existing queries start using it without being rewritten.

What the relational model did instead was lay all the data in the open: a relation is a collection of tuples, and that’s it. No labyrinthine nesting, no access paths. Read any or all rows matching an arbitrary condition; read a row by designating some columns as a key; insert into any table without worrying about foreign keys. The query optimizer decides which parts of the query run in which order and which indexes to use, those choices are the access path, but they’re made automatically, so you rarely think about them. Want to query in a new way? Declare a new index; existing queries start using it without being rewritten.

The key insight. You only need to build a query optimizer once, and then every application using that database benefits. Without one, hand-coding the access path for a particular query is easier than writing a general-purpose optimizer, but the general-purpose solution wins in the long run. Optimizers are complicated beasts that have consumed many years of research; that’s the point.

So, are document databases repeating history?

Yes, in exactly one aspect

They reverted to the hierarchical model by storing nested records inside their parent record rather than in a separate table, positions, education and contact_info living inside the user document.

No, in the aspect that mattered

For many-to-one and many-to-many, relational and document databases are not fundamentally different: the related item is referenced by a unique identifier, a foreign key in one, a document reference in the other, and resolved at read time by a join or follow-up query. Document databases have not followed CODASYL.

Note the timing difference that hides in that sentence: joins on foreign keys happen at query time, whereas in CODASYL the join was effectively done at insert time.

The comparison § Relational Versus Document Databases Today

Three arguments for documents, two for relations

Setting aside fault tolerance and concurrency, the data-model case runs like this. For the document model: schema flexibility, better performance from locality, and for some applications closeness to the application’s own data structures. The relational model counters with better support for joins and for many-to-one and many-to-many relationships.

Schema flexibility, the word “schemaless” is wrong

Most document databases don’t enforce any schema, so arbitrary keys and values can be added and readers get no guarantees about which fields exist. But the code that reads the data almost always assumes some structure, there is an implicit schema that the database simply doesn’t enforce. The accurate terms are schema-on-read (structure implicit, interpreted when data is read) versus schema-on-write (schema explicit, database ensures conformance on write). The analogy is dynamic runtime type checking versus static compile-time type checking, and, like that debate, there’s no general right answer.

Migration lab, you're splitting name into first_name and last_name

// Documents written before Dec 8, 2013 don't have first_name
if (user && user.name && !user.first_name) {
  user.first_name = user.name.split(" ")[0];
}
What happensNothing happens to the stored data at all. You just start writing new documents with the new fields and put code in the application that copes when an old document is read. Zero migration, zero downtime, and the cost is that this branch lives in your codebase indefinitely, and every reader must know about it. The structure is implicit, interpreted on read.

When schema-on-read genuinely wins

Not “when you’re in a hurry”, when the data is genuinely heterogeneous:

  • There are many different types of objects, and it isn’t practical to give each type its own table
  • The structure is determined by external systems you don’t control, which may change at any time

In those cases a schema hurts more than it helps. Where all records are expected to share a structure, a schema is a useful mechanism for documenting and enforcing it.

Data locality, a real advantage with real limits

A document is normally stored as a single continuous string, JSON, XML, or a binary variant like MongoDB’s BSON. If you often need the whole document (to render a page, say), that storage locality is a genuine performance win: the shredded relational version needs multiple index lookups, more disk seeks, more time.

The locality advantage only applies if you need large parts of the document at once. The database typically loads the entire document even when you touch a small part of it.

0 kBloaded but not used, per read

Locality paysYou need nearly the whole document, so storing it as one continuous string is a clear win over multiple index lookups across shredded tables.

And on update, the whole document usually has to be rewritten, only changes that don’t alter the encoded size can easily be done in place. Hence the standing recommendation: keep documents fairly small, and avoid writes that grow them. These limits significantly narrow the set of situations where document databases are useful.

Grouping related data for locality isn’t a document-model idea, though. Spanner offers the same property in a relational model by letting a schema declare that a table’s rows be interleaved within a parent table; Oracle has multi-table index cluster tables; Bigtable’s column families (used in Cassandra and HBase) serve the same purpose.

Convergence

Relational moving toward document

XML support since the mid-2000s in most systems other than MySQL, including local modification, indexing and querying inside documents. JSON support in PostgreSQL 9.3+, MySQL 5.7+, DB2 10.5+.

Document moving toward relational

RethinkDB supports relational-like joins. Some MongoDB drivers resolve database references automatically, effectively a client-side join, likely slower than one done in the database because of the extra round-trips and less optimization.

The models complement each other, and a hybrid looks like a good route for databases to take. Worth knowing: Codd’s original 1970 description allowed something quite like JSON documents inside a relational schema. He called them nonsimple domains, a value in a row needn’t be a primitive, it could be a nested relation, giving arbitrarily nested trees as values. SQL added roughly that, thirty years later.

Half two § Query Languages for Data

Declarative beats imperative, and here's the proof

The relational model arrived with a new way of querying: SQL is declarative, where IMS and CODASYL used imperative code. Find the sharks in a list of animals, three ways.

Imperative

function getSharks() {
  var sharks = [];
  for (var i = 0; i < animals.length; i++) {
    if (animals[i].family === "Sharks") {
      sharks.push(animals[i]);
    }
  }
  return sharks;
}

Relational algebra

sharks = σfamily = "Sharks"(animals)

-- σ is the selection
-- operator: keep only
-- tuples matching the
-- condition.

SQL

SELECT * FROM animals
WHERE family = 'Sharks';

-- SQL followed the
-- structure of the
-- relational algebra
-- fairly closely.

Imperative code tells the computer to perform certain operations in a certain order. A declarative language specifies only the pattern of the data you want, what conditions results must meet, how they should be transformed, and leaves the how to the optimizer.

Three reasons this matters, in increasing order of importance

  • Concision. Usually shorter and easier to work with than an imperative API.
  • It hides implementation details, so the database can introduce performance improvements without any query being rewritten. Concretely: if the engine wants to reclaim disk space by moving records around, the row order changes. SQL guarantees no ordering, so it doesn’t care. With imperative code the database can never be sure whether you depended on that order. SQL’s more limited functionality is what gives the database room to optimize.
  • Parallel execution. CPUs now get faster by adding cores, not clock speed. Imperative code is hard to parallelize because it specifies instructions that must run in a particular order. A declarative language specifies only the pattern of the results, so the database is free to use a parallel implementation.

The browser proof, CSS versus DOM manipulation

A site about ocean animals. The user is on the sharks page, so that nav item carries class="selected", and you want the title of the selected page to have a blue background. Declaratively that’s li.selected > p { background-color: blue }, all <p> elements whose direct parent is an <li> with class selected. Imperatively, in the core DOM API, it’s a nested loop. Both boxes below start correct. Then move the selection.

Declarative · CSS

  • Sharks

    • Great White Shark
    • Tiger Shark
    • Hammerhead Shark
  • Whales

    • Blue Whale
    • Humpback Whale
    • Fin Whale

Imperative · DOM API

  • Sharks

    • Great White Shark
    • Tiger Shark
    • Hammerhead Shark
  • Whales

    • Blue Whale
    • Humpback Whale
    • Fin Whale
Both correct, for nowSharks is selected and highlighted in both boxes. The CSS is one rule; the JavaScript is a nested loop with a node-type check. Now move the selection.
The imperative version, in full, and the XSL equivalent
var liElements = document.getElementsByTagName("li");
for (var i = 0; i < liElements.length; i++) {
  if (liElements[i].className === "selected") {
    var children = liElements[i].childNodes;
    for (var j = 0; j < children.length; j++) {
      var child = children[j];
      if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "P") {
        child.setAttribute("style", "background-color: blue");
      }
    }
  }
}
<!-- XSL: the XPath li[@class='selected']/p is equivalent
     to the CSS selector li.selected > p -->
<xsl:template match="li[@class='selected']/p">
  <fo:block background-color="blue">
    <xsl:apply-templates/>
  </fo:block>
</xsl:template>

Two concrete defects in the imperative version, beyond length. First, if the selected class is removed, the blue isn’t removed, even on re-running, so the item stays highlighted until a full page reload; CSS detects that the rule no longer applies and drops the background immediately. Second, to benefit from a newer, faster API such as getElementsByClassName or document.evaluate you must rewrite the code, whereas browser vendors can speed up CSS and XPath without breaking compatibility. Both defects have exact analogues in databases.

MapReduce, neither one thing nor the other

You’re a marine biologist logging an observation record every time you see animals. You want sharks sighted per month. MapReduce is neither declarative nor fully imperative but in between: query logic expressed as snippets of code that the framework calls repeatedly, built on map (collect) and reduce (fold, inject) from functional programming. A limited form is supported by MongoDB and CouchDB for read-only queries across many documents.

Two documents in the collection

{
  observationTimestamp: Date.parse(
    "Mon, 25 Dec 1995 12:34:56 GMT"),
  family:     "Sharks",
  species:    "Carcharodon carcharias",
  numAnimals: 3
}
{
  observationTimestamp: Date.parse(
    "Tue, 12 Dec 1995 16:17:18 GMT"),
  family:     "Sharks",
  species:    "Carcharias taurus",
  numAnimals: 4
}

Execution trace

ReadyPress the button to walk the execution one stage at a time.

PostgreSQL

SELECT
  date_trunc('month',
    observation_timestamp)
    AS observation_month,
  sum(num_animals)
    AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;
8 lines · declarative

MongoDB mapReduce

db.observations.mapReduce(
  function map() {
    var year = this
      .observationTimestamp
      .getFullYear();
    var month = this
      .observationTimestamp
      .getMonth() + 1;
    emit(year+"-"+month,
         this.numAnimals);
  },
  function reduce(key, values) {
    return Array.sum(values);
  },
  {
    query: { family: "Sharks" },
    out: "monthlySharkReport"
  }
);
18 lines · two coordinated functions

Aggregation pipeline

db.observations.aggregate([
  { $match: {
      family: "Sharks" } },
  { $group: {
      _id: {
        year:  { $year:
          "$observationTimestamp" },
        month: { $month:
          "$observationTimestamp" }
      },
      totalAnimals: {
        $sum: "$numAnimals" }
  } }
]);
13 lines · declarative again

Why map and reduce must be pure functions. They may only use the data passed to them as input, cannot perform additional database queries, and must have no side effects. That restriction is what lets the database run them anywhere, in any order, and rerun them on failure. Within it they’re still powerful: parse strings, call libraries, compute.

The usability problem is having to write two carefully coordinated functions where one query would do — and a declarative language gives the optimizer more room. Which is why MongoDB 2.2 added the aggregation pipeline: similar in expressiveness to a subset of SQL, differing mainly in wearing JSON syntax rather than SQL’s English-sentence style. The moral, in the chapter’s words: a NoSQL system may find itself accidentally reinventing SQL, in disguise.

Two corrections the chapter makes in passing: MapReduce is a fairly low-level model for distributed execution, and while SQL can be implemented as a pipeline of MapReduce operations, plenty of distributed SQL implementations don’t use it, nothing in SQL confines it to one machine, and MapReduce has no monopoly on distributed query execution. Also, running JavaScript mid-query isn’t a MapReduce exclusive; some SQL databases can be extended with JavaScript functions too.

Half three § Graph-Like Data Models

When anything might relate to everything

If your data is mostly one-to-many trees, or has no relationships between records, the document model fits. The relational model handles simple many-to-many. But as the connections get more complex, it becomes more natural to model the data as a graph: vertices (nodes, entities) and edges (relationships, arcs).

Social graphs

Vertices are people; edges indicate who knows whom.

The web graph

Vertices are pages; edges are HTML links. PageRank runs here.

Road / rail networks

Vertices are junctions; edges are roads or lines. Shortest-path search runs here.

In those three, every vertex is the same kind of thing. But an equally powerful use of graphs is storing completely different types of object in one datastore. Facebook keeps a single graph whose vertices are people, locations, events, checkins and comments, and whose edges say who is friends with whom, which checkin happened where, who commented on which post, who attended which event.

The working example, Lucy and Alain

Two people: Lucy from Idaho, Alain from Beaune, France. Married, living in London. Click any vertex to see how the property graph model stores it.

Lucy and Alain, and the locations they were born in and live in Eleven vertices. Nine are locations nested by WITHIN edges — Idaho inside the United States inside North America; London inside England, Beaune inside Bourgogne inside France, both inside Europe. Two are people: Lucy, born in Idaho and living in London, and Alain, born in Beaune and living in London. They are married to each other. WITHIN WITHIN WITHIN WITHIN WITHIN WITHIN WITHIN BORN_IN LIVES_IN BORN_IN LIVES_IN MARRIED_TO North America continent United States country Idaho state Europe continent England country France country London city Bourgogne région Beaune city Lucy Alain
Property graphClick any vertex to see it as rows in the vertices and edges tables, or run the traversal to watch a variable-length path unfold.

Each vertex consists of

  • A unique identifier
  • A set of outgoing edges
  • A set of incoming edges
  • A collection of properties (key-value pairs)

Each edge consists of

  • A unique identifier
  • The vertex where it starts, the tail
  • The vertex where it ends, the head
  • A label describing the kind of relationship
  • A collection of properties

You can think of a graph store as two relational tables, one for vertices, one for edges:

CREATE TABLE vertices (
  vertex_id   integer PRIMARY KEY,
  properties  json
);

CREATE TABLE edges (
  edge_id     integer PRIMARY KEY,
  tail_vertex integer REFERENCES vertices (vertex_id),
  head_vertex integer REFERENCES vertices (vertex_id),
  label       text,
  properties  json
);

CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);   -- both directions matter

Three properties that give this model its power

  • Any vertex can have an edge to any other vertex. No schema restricts which kinds of thing may be associated.
  • Given any vertex you can efficiently find both its incoming and outgoing edges, hence the two indexes above, so you can traverse the graph forward and backward.
  • Different labels for different kinds of relationship let you keep several kinds of information in one graph while the data model stays clean.

Look at what the example graph absorbs without complaint: different regional structures per country (France has départements and régions, the US has counties and states), historical quirks like a country within a country, and varying granularity, Lucy’s residence is a city, her birthplace only a state. Extend it with allergens as vertices and allergy edges and you can query what’s safe for each person to eat. Graphs are good for evolvability.

The same question, four query languages

Find the names of all the people who emigrated from the United States to Europe. Precisely: every vertex with a BORN_IN edge to a location within the US and a LIVES_IN edge to a location within Europe, returning each one’s name.

Cypher

MATCH
  (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
  (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
RETURN person.name
NotesA declarative language for property graphs, created for Neo4j, named after a character in The Matrix, not related to ciphers. The arrow notation does double duty: in CREATE it builds edges, in MATCH it finds patterns. (person) -[:BORN_IN]-> () matches any two vertices joined by a BORN_IN edge, binding the tail to person and leaving the head unnamed. :WITHIN*0.. means follow a WITHIN edge zero or more times.

Cypher does it in 4 lines; recursive SQL needs 29. The chapter’s reading of that gap is not that SQL is bad, it’s that different data models are designed to satisfy different use cases, so it matters that you pick one suited to your application.

Are graph databases CODASYL again?

At first glance the network model looks a lot like the graph model. The answer is no, in four specific ways.

DimensionCODASYL network modelGraph database
SchemaA schema specified which record type could be nested within which other record typeNo such restriction, any vertex can have an edge to any other, so applications adapt to changing requirements
Reaching a recordOnly by traversing one of the access paths to itRefer directly to any vertex by unique ID, or use an index to find vertices by value
OrderingA record’s children were an ordered set, so the database maintained the order, with storage-layout consequences, and inserts had to worry about positionVertices and edges are unordered; you sort results at query time if you want to
QueriesAll imperative, difficult to write, easily broken by schema changesImperative traversal if you want it, but most support declarative languages like Cypher or SPARQL

Triple-stores, RDF, and the semantic web

The triple-store model is mostly equivalent to the property graph model with different words. All information is stored as three-part statements: (subject, predicate, object). In (Jim, likes, bananas), Jim is the subject, likes the predicate, bananas the object. The subject is a vertex. The object is one of two things:

A primitive value

Then predicate and object are the key and value of a property on the subject vertex. (lucy, age, 33) is a vertex lucy with properties {"age":33}.

Another vertex

Then the predicate is an edge, the subject is the tail and the object is the head. In (lucy, marriedTo, alain) the predicate is the edge label.

Turtle · verbose

@prefix : <urn:example:>.
_:lucy     a       :Person.
_:lucy     :name   "Lucy".
_:lucy     :bornIn _:idaho.
_:idaho    a       :Location.
_:idaho    :name   "Idaho".
_:idaho    :type   "state".
_:idaho    :within _:usa.

Turtle · with semicolons

@prefix : <urn:example:>.
_:lucy  a :Person;   :name "Lucy";
        :bornIn _:idaho.
_:idaho a :Location; :name "Idaho";
        :type "state"; :within _:usa.
_:usa   a :Location;
        :name "United States";
        :type "country";
        :within _:namerica.

The _:someName form means nothing outside the file, it exists only so you can tell which triples refer to the same vertex. When the predicate is an edge, the object is a vertex; when it’s a property, the object is a string literal. RDF can also be written in XML, which does the same thing far more verbosely; Turtle/N3 is easier on the eyes, and tools like Apache Jena convert between formats.

The semantic web, briefly and fairly

The idea is simple and reasonable: sites already publish text and pictures for humans, so why not also publish machine-readable data? RDF was meant to be the consistent format that let data from different sites be combined into a web of data, an internet-wide database of everything. It was overhyped in the early 2000s and hasn’t shown signs of being realized, which has made people cynical, and it suffered from a plethora of acronyms, overly complex standards and hubris.

But the triple-store data model is completely independent of the semantic web, Datomic is a triple-store that claims no connection to it. Triples can be a good internal data model even if you never publish a byte of RDF. RDF’s one visible quirk comes from being designed for internet-wide exchange: subjects, predicates and objects are often URIs, so that your within and someone else’s within don’t collide when datasets are merged. Declare the prefix once at the top and forget it.

The foundation, Datalog

Much older than SPARQL or Cypher, studied extensively by academics in the 1980s, and less known among engineers, but it matters because it’s the foundation the later languages build on. In practice it’s the query language of Datomic, and Cascalog is a Datalog implementation for querying large datasets in Hadoop. Its data model generalizes the triple store slightly: instead of (subject, predicate, object) you write predicate(subject, object).

Where Cypher and SPARQL jump straight in with SELECT, Datalog takes a small step at a time: you define rules that teach the database new predicates, derived from data or from other rules. Rules can call other rules and recurse, so complex queries are built a piece at a time. Words starting with an uppercase letter are variables. A rule applies when the system finds a match for every predicate on the right-hand side of :-; when it applies, it’s as though the left-hand side were added to the database.

Rules

within_recursive(Location, Name) :-
  name(Location, Name).            /* Rule 1 */

within_recursive(Location, Name) :-
  within(Location, Via),
  within_recursive(Via, Name).     /* Rule 2 */

migrated(Name, BornIn, LivingIn) :-
  name(Person, Name),
  born_in(Person, BornLoc),
  within_recursive(BornLoc, BornIn),
  lives_in(Person, LivingLoc),
  within_recursive(LivingLoc, LivingIn).
                                   /* Rule 3 */

?- migrated(Who, 'United States', 'Europe').

Derived facts

ReadyRules aren't triples stored in the database, they're derived from data or from other rules. Press the button to apply them one at a time.

Datalog needs a different kind of thinking, and it’s less convenient for simple one-off queries, but rules can be combined and reused across queries, so it copes better when your data is complex.

Recall check 10 questions

Did it stick?

One attempt per question; the explanation appears either way.

1. What is the impedance mismatch, and what does an ORM do about it?

2. Why store region_id rather than the string “Greater Seattle Area”?

3. Normalizing data requires which relationship type, and why is that awkward for document databases?

4. In what single aspect did document databases revert to the hierarchical model?

5. What was the decisive advantage of the relational model over CODASYL?

6. Why is “schemaless” a misleading description of document databases?

7. When does the document model’s locality advantage stop paying off?

8. Which is the most important reason declarative query languages beat imperative ones?

9. Why must MapReduce’s map and reduce functions be pure?

10. Cypher’s :WITHIN*0.. has which SQL counterpart, and why is one needed at all?

Answered 0 of 10 · 0 correct

Vocabulary Click to flip

The terms this chapter installs


Chapter 2 in one breath. Data began as one big tree, the hierarchical model, which couldn’t represent many-to-many relationships, so the relational model was invented to fix that. More recently some applications didn’t fit relations either, and NoSQL diverged in two directions: document databases for self-contained documents with rare relationships between them, and graph databases for the opposite case where anything is potentially related to everything. All three are widely used and each is good in its domain; one model can be emulated in another, but the result is usually awkward, which is why there’s no one-size-fits-all system. Document and graph databases typically don’t enforce a schema, but your application still assumes a structure, so the real question is only whether the schema is explicit on write or implicit on read. Each model brings its own language: SQL, MapReduce, the aggregation pipeline, Cypher, SPARQL, Datalog, plus CSS and XPath as instructive non-database parallels.

And the models it names but doesn’t cover: genome sequence-similarity search, which none of these databases handle, hence specialised software like GenBank; particle physics at the LHC, working with hundreds of petabytes on custom solutions to keep hardware cost in check; and full-text search, arguably a data model in its own right, taken up again with search indexes in Chapter 3.

Study aid built for recall practice. Concepts, code examples and the Lucy/Alain graph are Martin Kleppmann’s, from Designing Data-Intensive Applications, Chapter 2, go back to the book for his wording and the full reference list.

← all lab notes