Guides

Graph vs. SQL

Most data problems are solved by a relational database. Some are solved better by a graph database. A few require both at the same time. Ontology Workbench is designed to handle all three cases from a single canonical model.

The core difference

Relational (SQL) databases organize data into tables with rows and columns. Relationships between tables are expressed through foreign keys and joins. SQL databases excel at structured, uniform data with well-defined schemas — financial records, product catalogs, user accounts.

Graph databases store data as nodes and edges. Every relationship is a first-class citizen with its own identity and (optionally) its own properties. Graph databases excel at highly connected data where the relationships themselves carry meaning — social networks, knowledge graphs, supply chains, fraud detection.

The distinction isn't about which is "better." It's about which structure fits the shape of your data.

Where they diverge

ConcernRelational (SQL)Graph
Data shapeUniform rows in tablesHeterogeneous nodes and edges
Relationship storageForeign keys + join tablesNative edges with properties
Query styleJoins across tablesTraversals across edges
Best forStructured, tabular recordsConnected, relationship-heavy data
ExamplesPostgreSQL, MySQL, SQLiteNeo4j, Amazon Neptune, ArangoDB
ORM/query layerPrisma, Drizzle, SQLAlchemyCypher, Gremlin, AQL

Relationships as data

In a relational schema, a many-to-many relationship between User and Project requires a join table (UserProject) with foreign keys. Querying who worked on what requires an explicit join.

In a graph schema, that same relationship is an edge — (User)-[:WORKED_ON]->(Project) — and the edge can carry its own properties like role or startDate. Traversing "all projects this user contributed to" is a single path query, not a multi-table join.

OWB supports relationship properties natively across all export targets. When you define a relationship in the workbench and attach properties to it:

  • Graph exports (Neo4j, Neptune, ArangoDB, DGraph) emit them as native edge attributes.
  • SQL exports (Prisma, Drizzle, SQLAlchemy) handle them based on cardinality:
    • N:1 and 1:N — property columns are added alongside the foreign key on the FK-holding side of the relationship.
    • 1:1 — property columns are added alongside the FK on the target side.
    • N:M — an explicit join table (or association object in SQLAlchemy) is generated with the property columns included.

Modeling for both

The OWB data model is database-agnostic by design. You define entity types, properties, and relationships once. The same model then exports to your target environment.

SQL export path

Export to Prisma, Drizzle, or SQLAlchemy for use with PostgreSQL, MySQL, or SQLite. Entity types become tables. N:1 and 1:N relationships become FK columns; N:M relationships always produce an explicit join table. Relationship properties are emitted as columns alongside the FK or in the join table, so no schema data is lost. Abstract types are skipped — their properties are inherited by concrete subtypes.

Graph export path

Export to Neo4j (Cypher), Amazon Neptune (Gremlin), ArangoDB (JSON Schema), or DGraph (DQL) for native graph databases. Entity types become node labels. Relationships become typed edges. Relationship properties become edge attributes.

Semantic web path

Export to OWL/XML for knowledge graph platforms, reasoners, or linked data pipelines. Entity types become OWL classes. Relationships become object properties. Abstract types and inheritance are expressed natively as owl:SubClassOf axioms.

Using both at the same time

Some architectures deliberately split storage across a relational database and a graph database. A common pattern:

  • SQL layer: canonical records, transactions, financial data — anywhere ACID compliance and structured queries matter
  • Graph layer: relationship traversal, recommendations, fraud detection, lineage — anywhere the connections between records are the point

OWB supports this directly. Export your model to Prisma and Neo4j from the same source. The entity types stay consistent; only the target syntax changes. If your team's schema evolves, you update one model and re-export both targets — no manual synchronization between two separate schema files.

Example

Given an Author and Book entity type connected by a WROTE relationship with a year property:

Prisma (SQL)WROTE is N:M with a year property, so an explicit join model is generated

model Author {
  id    String  @id @default(cuid())
  name  String
 
  wrotes  Wrote[] @relation("WroteSource")
}
 
model Book {
  id      String  @id @default(cuid())
  title   String
 
  wrotes  Wrote[] @relation("WroteTarget")
}
 
model Wrote {
  authorId  String
  bookId    String
 
  year      Int?
 
  author    Author  @relation("WroteSource", fields: [authorId], references: [id])
  book      Book    @relation("WroteTarget", fields: [bookId], references: [id])
 
  @@id([authorId, bookId])
}

Neo4j Cypher — emits the relationship as a native edge with the year property

CREATE CONSTRAINT FOR (n:Author) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT FOR (n:Book) REQUIRE n.id IS UNIQUE;
 
// WROTE (Author → Book)
// Properties: year (Integer)
// Pattern:
//   MATCH (s:Author {id: $sourceId}), (t:Book {id: $targetId})
//   MERGE (s)-[r:WROTE {year: $year}]->(t)
//   RETURN r

Same model. Two targets. One source of truth — year is available in both the SQL join table and as a graph edge attribute.

Inheritance and abstract types

OWB supports entity inheritance — you can define an abstract Asset type with shared properties and have Image, Video, and Document extend it.

  • SQL exports (Prisma, Drizzle, SQLAlchemy): abstract types are not mapped to tables. Concrete subtypes include all inherited properties directly. SQLAlchemy generates abstract types as Python mixin classes that concrete models inherit from.
  • Neo4j: concrete nodes carry multiple labels — a Car that extends Vehicle gets labels :Car:Vehicle, preserving the inheritance chain in the graph.
  • Neptune, ArangoDB, DGraph: inheritance is flattened — inherited fields are written directly on the concrete type, with a comment noting the source.
  • OWL/XML: inheritance is expressed natively as owl:SubClassOf axioms, which is the semantically richest representation.

Abstract types are never instantiated as rows or nodes — they exist solely to share property definitions across concrete subtypes.

Choosing your export

If you're unsure which path to use:

  • Starting a new project with a relational database? Use Prisma or Drizzle. Both generate type-safe query clients from the schema.
  • Need Python / Django / FastAPI? Use SQLAlchemy for a mature, production-ready ORM.
  • Working with highly connected data or building recommendations / graph search? Use Neo4j or Amazon Neptune (Gremlin).
  • Using ArangoDB or DGraph? Export directly to their native schema formats.
  • Building on top of a knowledge graph platform or need semantic reasoning? Use OWL/XML.
  • Running both SQL and graph? Export both from the same model and keep them in sync.

All exports are available from the Export button in the workbench toolbar, or via the REST API.