9bNine Bits

Apache AGE: Graph Database Functionality for PostgreSQL

Apache AGE is an open-source PostgreSQL extension that adds graph database functionality to a regular Postgres installation. It lets you model data as vertices and edges and query it with Cypher, while still using standard SQL and Postgres transactions for everything else.
Apache AGE: Graph Database Functionality for PostgreSQL

What Apache AGE Is

Apache AGE (the name stands for A Graph Extension) is a PostgreSQL extension developed under the Apache Software Foundation's open-source incubator program. It does not replace PostgreSQL or run as a separate service: it loads into an existing Postgres cluster and adds the ability to store and query graph-shaped data using Cypher, the declarative query language originally popularized by graph databases.

The core idea is that many applications have both relational data (users, orders, invoices) and graph data (social connections, recommendation paths, dependency chains) and shouldn't need two separate database systems to handle both.

How Apache AGE Stores Graph Data Internally

When you call create_graph(), AGE creates a new PostgreSQL schema to hold that graph. Each vertex label and each edge label you define becomes its own table inside that schema.

  • Every row gets an internal graphid identifier.
  • Properties for that vertex or edge are stored in a single column of type agtype, a structured type AGE adds that can hold JSON-like scalars, objects, arrays, and the vertex/edge/path objects Cypher returns.
  • Edge tables additionally store the start and end graphid values linking them to vertex rows.

Because labels are ordinary tables, standard PostgreSQL mechanics apply directly: B-tree or GIN indexes, foreign keys, constraints, vacuum, write-ahead logging, and replication all work the same way they do for any other table.

Querying with Cypher Inside SQL

AGE exposes a set-returning function, cypher(), that takes a graph name and a Cypher query written as a dollar-quoted string. Because PostgreSQL doesn't know the result shape of an arbitrary Cypher query ahead of time, you must declare the output columns yourself and cast each one to agtype using an AS clause.

This means every graph query is really a SQL statement with a Cypher fragment embedded inside it, which is also why you can nest cypher() calls inside larger SELECT statements, filter their output with ordinary WHERE clauses, or join the result against other tables.

A Minimal Example

CREATE EXTENSION IF NOT EXISTS age;
LOAD 'age';
SET search_path = ag_catalog, "$user", public;

SELECT create_graph('shop');

SELECT * FROM cypher('shop', $$
  CREATE (a:Customer {name: 'Alice'})-[:PLACED]->(o:Order {id: 1001, total: 42.50})
$$) AS (result agtype);

SELECT * FROM cypher('shop', $$
  MATCH (c:Customer)-[:PLACED]->(o:Order)
  WHERE o.total > 20
  RETURN c.name, o.id
$$) AS (customer agtype, order_id agtype);

The LOAD 'age' and search_path statements are required once per session before any cypher() call will resolve. The final query mixes a graph pattern match with a standard property comparison and returns results as agtype values.

Combining Graph and Relational Data

Since vertex and edge labels are plain Postgres tables, you can join them with tables that have nothing to do with the graph model. A common pattern is keeping billing, inventory, or user-account data in conventional relational tables while modeling only the connected part of the domain (referrals, friendships, dependency graphs) as a graph.

You can also write a single query that joins the output of a cypher() call to a regular table using the graphid or a property value extracted from agtype, without exporting data between systems.

Trade-offs Compared to a Dedicated Graph Database

Running graph workloads inside Postgres has real advantages: there's no second system to deploy, back up, patch, or monitor, and graph writes share transactions with your relational writes, so you get consistency across both without distributed-transaction complexity.

The trade-offs are also real. AGE implements a subset of openCypher, not the full specification, and some conveniences found in dedicated graph engines, such as extended procedure libraries, are not part of core AGE. Very deep multi-hop traversals on large graphs can also perform worse than in engines built around native graph storage and traversal-specific indexing, since AGE's execution still goes through Postgres's general-purpose query planner. Because supported Cypher clauses and functions change between AGE releases, check the project's current documentation for exact coverage before relying on a specific clause.

Common Mistakes

  • Forgetting LOAD 'age'; on a new connection. The extension can be installed cluster-wide, but each session still needs to load it before cypher() and related functions resolve.
  • Not adding ag_catalog to search_path, which causes "function does not exist" errors even though AGE is installed.
  • Mismatching the AS (col agtype, ...) clause with the actual columns returned by the Cypher query, or forgetting it entirely.
  • Assuming AGE supports the entire openCypher language or vendor-specific procedure extensions found in other graph engines, then being surprised when a clause isn't recognized.
  • Not indexing the properties actually filtered on. Properties live inside a single agtype column, so a filter on a property key benefits from a targeted index (for example a GIN index) rather than relying on a default index on the id column alone.

Common questions

Is Apache AGE a separate database I need to install?

No. It is a PostgreSQL extension that loads into an existing Postgres cluster with CREATE EXTENSION. There is no separate server process to run.

Can I use SQL and Cypher together in one query?

Yes. Cypher queries run through the cypher() function, which returns rows you can filter, join, or combine with regular SQL in the same statement.

Does Apache AGE support the full openCypher language?

It supports a subset of openCypher rather than the complete specification. Supported clauses and functions change between releases, so check the current project documentation before depending on a specific feature.

How are vertex and edge properties stored?

In a column of type agtype, a structured type AGE provides that holds JSON-like scalar, object, and array values as well as graph-specific objects like vertices, edges, and paths.

Why does my cypher() query fail with a function-not-found error?

This almost always means LOAD 'age' wasn't run in the current session, or ag_catalog isn't in the search_path. Both are required before AGE's functions are visible to the query.

Sources

  1. Apache AGE (official project site)
  2. Apache AGE source and documentation (GitHub)
  3. PostgreSQL Documentation: Extensions

About this page

Last reviewed November 7, 2026 by the Nine Bits editors. Read our editorial policy or report an error.