· Aseem Savio · —

The Document Hippies vs. The Relational Police vs. the Graph Nerds - and the People Who Just Use Postgres

A practical look at relational, document, and graph data models - when each one fits, and why picking the wrong one makes everything harder.

The Document Hippies vs. The Relational Police vs. the Graph Nerds - and the People Who Just Use Postgres

I wanted to write about something that gives us engineers a powerful way to think about the problem we’re solving: data models. How we model the data at hand is vital.

All software is layered. Generally, we layer data models on top of other data models. They’re separated by APIs that allow us to manipulate them. APIs are a clean way to hide implementation details behind a neat facade. These abstractions allow us engineers from unrelated areas to work on a problem together without needing to think about details that don’t really concern us or our problem.

When we think about data modelling, these models come to mind:

  • Relational
  • Document
  • Graph

And where do vector databases fit in? We’ll answer this at the end of this post.

Engineers sometimes piggyback on one of these models and try to fit their problem into it. There is a high chance that you have come across this colleague, or it may have even been you (in which case, I’m sorry :p): the document DB enthusiast wanting to promote MongoDB for everything he does, the relational police trying to fit into Postgres a complex many-to-many schema that would have been better modelled as a graph, etc. While these are technically possible, it doesn’t mean we should do it. We have different data models at our disposal for solving these specialised problems with as little complexity as possible.

And there are engineers who believe the relational model is dead. In a recent interview with a candidate for our team at work, they said relational databases are a thing of the past as they don’t scale, and that only legacy systems use them (wut?). There are also engineers who (still) believe non-relational databases aren’t ACID compliant. I said “still” because non-relational databases only became broadly ACID compliant in recent years.

While emulating a data model that is less suitable for your problem is technically possible, it would result in a very awkward design.

What you need to know when picking the relational model

The relational model is usually the default, and it’s a natural fit when we have a rather simple many-to-many relationship. Note the emphasis on the simple. For example, students and courses, where a single student can enrol in multiple courses, and a single course can have multiple students enrolled.

The DB can join the normalised data (data stored in separate, related tables - slower to read because it needs joins, but consistent and easy to maintain) for you.

{
  "students": [
    { "student_id": 1, "name": "Jane Doe", "major": "Computer Science" },
    { "student_id": 2, "name": "John Smith", "major": "Data Science" }
  ],
  "courses": [
    { "course_id": "CS-101", "title": "Intro to Programming", "credits": 4 },
    { "course_id": "MATH-201", "title": "Linear Algebra", "credits": 3 }
  ],
  "enrollments": [
    { "student_id": 1, "course_id": "CS-101", "enrollment_date": "2026-09-01", "grade": "A" },
    { "student_id": 1, "course_id": "MATH-201", "enrollment_date": "2026-09-01", "grade": "B+" },
    { "student_id": 2, "course_id": "CS-101", "enrollment_date": "2026-09-02", "grade": "A-" }
  ]
}

A SQL JOIN can pull together data stored neatly in separate tables and present it as a single result like the one below.

[
  {
    "student_id": 1,
    "name": "Jane Doe",
    "major": "Computer Science",
    "course_id": "CS-101",
    "title": "Intro to Programming",
    "credits": 4,
    "enrollment_date": "2026-09-01",
    "grade": "A"
  },
  {
    "student_id": 1,
    "name": "Jane Doe",
    "major": "Computer Science",
    "course_id": "MATH-201",
    "title": "Linear Algebra",
    "credits": 3,
    "enrollment_date": "2026-09-01",
    "grade": "B+"
  },
  {
    "student_id": 2,
    "name": "John Smith",
    "major": "Data Science",
    "course_id": "CS-101",
    "title": "Intro to Programming",
    "credits": 4,
    "enrollment_date": "2026-09-02",
    "grade": "A-"
  }
]

In this model, the application layer doesn’t have to work as hard to keep data consistent. Normalisation removes duplication, and the database enforces the relationships through foreign keys.

Schema changes here are straightforward. In most cases, it’s a migration. Some migrations can take longer, and a few can even require downtime, though.

The old story that relational databases don’t scale no longer holds. Instagram built its data layer on sharded Postgres, and Shopify serves millions of queries per second from a sharded MySQL fleet. At work, we migrated a data-intensive, highly volatile database that stores petabytes of customer vulnerability data from a text search database engine to CockroachDB (a RDBMS) so it could operate at our scale, and it proved effective for our use cases — frequent writes, huge queries with many filters.

What to know when picking the document model

Use this when there is a one-to-many relationship in your data, where the “many” side belongs to the parent and doesn’t change that often. Think an e-commerce site’s product and its variants.

{
  "product_id": "PROD-1024",
  "name": "Wireless Headphones",
  "category": "Electronics",
  "specs": {
    "battery_life": "30h",
    "water_resistance": "IPX4"
  },
  "variants": [
    { "color": "Matte Black", "sku": "HP-BLK-01", "stock": 45 },
    { "color": "Silver", "sku": "HP-SLV-02", "stock": 12 }
  ]
}

The claim that this model is “schema-less” is misleading. It may be so during writes, but your application still has to assume some structure when it reads the data back. That flexibility makes it well suited to heterogeneous data. i.e., data that comes from different sources (think sensor readings from different providers in your IoT system), where you cannot control (for any reason) the incoming structure.

Use this when it is okay to store de-normalised data. i.e., when your data is tree-like and it is acceptable to duplicate sub-JSON trees inside a document. Since data is duplicated, your application has to work harder to keep those copies consistent when data changes.

When you have more nested components like the variants shown above, you can technically emulate this in a relational model, but it quickly becomes far too complex.

The old stories about document databases not being ACID compliant no longer hold. Most document DBs ARE ACID compliant today.

What to know when picking the Graph model

Use this when modelling data with many-to-many relationships ON STEROIDS. Think social networks like Facebook, or rail and road networks. Graph algorithms like shortest paths, page rank, community detection, etc. let us traverse these databases, which are made up of vertices (nodes) and edges (relationships).

%%{init: {"themeVariables": {"fontSize": "14px"}, "flowchart": {"useMaxWidth": false, "nodeSpacing": 45, "rankSpacing": 60}}}%%
flowchart LR
    %% Nodes
    A((User: Alice))
    B((Product: Laptop))
    C((User: Bob))

    %% Edges / Relationships
    A -- "PURCHASED<br>(date: 2026-10-01)" --> B
    C -- "PURCHASED<br>(date: 2026-10-25)" --> B
    A -- "FRIEND_OF" --> C

    %% Styling
    style A fill:#f9f,stroke:#333,stroke-width:2px,color:#111
    style B fill:#bbf,stroke:#333,stroke-width:2px,color:#111
    style C fill:#f9f,stroke:#333,stroke-width:2px,color:#111

These databases don’t restrict the kinds of relationships that can exist between vertices. They can technically be modelled in the relational world, but doing so is far too complex, so avoid it. Graph schemas can evolve quickly because there are no restrictions on the relationships.

Multi-model engines

These are the new kids in town. SurrealDB, ArangoDB, Cosmos DB, and Spanner Graph are single engines that support all three models. You can look into these if you want to cut costs by maintaining only one DB, model your data in the model it was meant to be modelled in, and keep your sanity.

Where vector databases fit, and where they don’t

Vector search is an index, a new access pattern, not the fourth new data model. It’s an index over embeddings, the same way B-trees are an index over rows. Your data is still a row, a document, or a node. You’ve just added a new column to store a few hundred floats. That means you could store your embeddings in your existing database, whatever its model. Postgres has pgvector, MongoDB has Atlas Vector Search, and Neo4j, Elasticsearch, and Redis all ship vector indexes. A dedicated store earns its place at 100M+ vectors.

%%{init: {"themeVariables": {"fontSize": "14px"}, "flowchart": {"useMaxWidth": false}}}%%
flowchart TD
    %% Input Pipeline
    Raw["Raw Data<br>('Puppy playing')"] --> Embed["Embedding Model<br>(AI/LLM)"]
    Embed --> Vec["Vector Embedding<br>[0.23, -0.89, 0.41, ...]"]

    %% Storage & Query
    Vec --> DB[("Vector Database<br>(Stored Coordinates)")]

    %% Similarity Search Concept
    Query["Search Query<br>('Happy dog')"] --> QVec["Query Vector"]
    QVec --> DB
    DB --> Match["Nearest Neighbors Search<br>(Cosine Similarity / Distance)"]

    %% Styling
    style DB fill:#bfb,stroke:#333,stroke-width:2px,color:#111
    style Match fill:#ffd,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5,color:#111

Something to remember: embeddings are derived data. So storing the embedding model and its version alongside each vector is crucial for your application to manage them.

I hope you got something to take away from this :)