Your Database Is Already a Graph Database
I turned SQLite, DuckDB, Postgres, MongoDB, Qdrant and LanceDB into a knowledge graph for RAG. No graph database installed. Here is what happened.
When the answer is spread across chunks from several documents, but the token limit lets you send only a few, what happens to the facts that don't fit?
Anyone who has built a RAG system knows the budget problem. The LLM has a token limit, and you pay for every token you send. So you can't hand it every chunk of every document. You retrieve, maybe re-rank, and send the top five, ten or fifteen chunks, whatever your model and your bill allow.
Most of the time that works. But every now and then the answer needs one fact that lives in a chunk ranked 12th, or 22nd, and it never makes the cut. The model either says "not in context" or fills the gap with something that sounds right.
This is where a knowledge graph helps. Instead of more paragraphs, it gives the model the facts and how they connect: Sarabhai founded ISRO. Thumba was renamed after Sarabhai. No stop words, no surrounding filler, just the relationships. A person can read those lines and follow how things flow, and they cost a fraction of the tokens.
So far, so standard. Every GraphRAG tutorial agrees a graph is useful. And almost every one of them then says the same thing: install a graph database.
That step always bothered me. I work on RAG and knowledge graph systems, and in every project I've seen there is already a main database holding the client data. Postgres, MongoDB, something. Bringing in one more database just for the graph means one more thing to deploy, secure, back up and keep in sync, forever.
So I started wondering whether the databases we already use could be turned into graph databases. While looking around, I saw Azure Cosmos DB listed as a graph database. That confused me. As far as I knew, Cosmos DB is a NoSQL database. How is it a graph database too?
I went digging, and slowly it made sense. Cosmos DB doesn't have a separate graph engine. It puts a wrapper on its NoSQL storage, and the wrapper makes it behave like a graph. Then the obvious thought: if it works for Cosmos DB, why not for the databases we already have? Someone just has to try.
So I did. Six databases from three families, the same knowledge graph, the same questions, and every single answer checked. By the end I had a clear answer, and a couple of real surprises. One database's built-in graph feature turned out about 300 times slower than four lines of plain SQL.
First, what is a knowledge graph?
The simplest definition is also the best one. A knowledge graph is a set of entities, the relations between them, and facts, where every fact is a triple: subject, predicate, object.
(Chandrayaan-1, released, Moon Impact Probe)
(Moon Impact Probe, impacted near, Shackleton crater)
That's really the whole idea. Once you see it like this, a knowledge graph stops looking like a special technology. It's another way of writing down the same information you would otherwise put in a table.
Take a table of missions with columns for launch vehicle, destination and lander. Every filled cell can become a triple. You don't need the column headers anymore, because the predicate carries that meaning. And you don't need empty cells either. In a table, a mission without a lander still gets a blank "lander" cell. In a graph, that fact simply isn't there.
This is why graphs are often called a universal format. Tables, JSON, XML and CSV can all be flattened into triples. Trees and nested documents fit naturally, and so do loops, like a social network where A knows B, B knows C and C knows A. A new kind of fact doesn't need a schema change; you just add triples with a new predicate. And merging two graphs is far easier than merging two tables with different columns: put the triples together and merge the entities that are the same.
For RAG, the part that matters is that triples connect documents. The Chandrayaan-1 article mentions the probe. Another article mentions the crater. Vector search sees two unrelated chunks. The graph sees one path.
And here's the thing that made the whole experiment possible. If a knowledge graph is just triples, then storing it is just storing rows. Every database on earth knows how to store rows.
Storing it in a normal database
I kept three tables: entities (id, name, type, a short description), relations (subject, predicate, object, and the chunk it came from), and mentions (which chunk talks about which entity).
At query time the retriever does two jobs. It embeds the question and finds the closest entities by vector search. Then it walks out from those entities, one or two hops, and collects their relations for the LLM.
One or two hops really is enough. When I checked on my data, three hops from an ordinary entity already reached 72 to 93 percent of the whole graph. Start at Chandrayaan-3, go three hops, and you're holding almost everything about Indian space research. That's not context anymore, that's a data dump.
How the wrapper works
A graph database answers questions like "who is connected to ISRO, up to two hops away?". The wrapper's whole job is to turn that kind of question into lookups an ordinary database is already good at. It takes four steps.
- Store every triple as a row. One edges table with subject id, predicate and object id. Store each edge in both directions, so the walk can go either way.
- Index the subject column. Now "who is connected to X?" is one indexed lookup instead of a scan through the whole table.
- Turn each hop into a query. Hop 1 looks up X. Hop 2 looks up every id that hop 1 returned. Keep the lowest hop count for each entity, and stop at two.
- Join back for the content. Use the ids you reached to fetch the triples, or the chunks that mention them, and hand those to the LLM.
That is everything a graph database does for a RAG retriever. Figure 4 walks through it for ISRO.
Each database family does these four steps in its own way. In SQL, with every edge stored in both directions, steps 3 and 4 fit into one recursive query:
CREATE TABLE edges(src INT, dst INT, weight INT);
CREATE INDEX e_src ON edges(src);
-- everything within 2 hops of entity 42
WITH RECURSIVE walk(id, depth) AS (
SELECT 42, 0
UNION
SELECT e.dst, w.depth + 1
FROM edges e JOIN walk w ON e.src = w.id
WHERE w.depth < 2
)
SELECT id, MIN(depth) FROM walk WHERE id <> 42 GROUP BY id;
That query is the whole "wrapper" for SQLite, Postgres and DuckDB. MongoDB has its own recursive lookup, $graphLookup:
db.entities.aggregate([
{ $match: { _id: 42 } },
{ $graphLookup: {
from: "edges", startWith: "$_id",
connectFromField: "dst", connectToField: "src",
maxDepth: 1, depthField: "hops", as: "reached" } }
])
The vector databases, Qdrant and LanceDB, were the ones I doubted most. No joins, no recursion. But they store metadata with every point, so I kept each entity's neighbour list in its payload:
{ "id": 42, "payload": { "name": "ISRO", "neighbors": [7, 19, 311], "weights": [12, 9, 4] } }
One hop becomes "fetch these ids", and finding chunks for some entities becomes a payload filter. A small Python loop ties the hops together. Honestly, that same loop, one WHERE src IN (...) per hop, works on any database that has an index. Which is every database.
Where do the vectors go?
The first step, finding entities from the question, needs entity embeddings somewhere. Keyword matching won't survive production; the first time a user rephrases, it breaks. So I tried both setups people actually use.
If your database supports vectors (Postgres with pgvector, SQLite with sqlite-vec, DuckDB), the embeddings sit right next to the graph. If it doesn't, or you already run a vector store, keep the embeddings there with the entity id as the key. The vector store hands over the top entity ids, and your main database walks the graph from them.
Running it
I crawled 156 Wikipedia articles around ISRO and Indian space missions, cut them into 958 chunks, ran spaCy to pick out entities, and linked entities that appeared in the same chunk. About 2,700 entities and 17,000 edges.
spaCy's small model was its own entertainment. It tagged the Moon as a PERSON. SpaceX became a person too, and so did Andhra Pradesh. Thankfully none of my queries used the labels.
The setup had its moments as well. I installed Docker inside WSL, it worked, and then I ran the uninstall commands I'd kept ready for the end of the project, right after. So I installed it again. Then Claude Code, which ran the experiments for me, couldn't reach Docker because its session had started before my user joined the docker group. One wsl --shutdown later, we were in business.
The rule was simple. For every query I computed the correct answer separately in Python with networkx, and every database had to match it exactly. A wrong answer counts as a failure, not a timing.
All six databases matched on every query. Both wiring patterns worked. The full flow, question to final context, took 24 to 54 ms. Then I grew the data about ten times, to 10,000 chunks and 204,000 edges, and everything still matched, at 44 to 87 ms. On a laptop.
I won't make much of the timings, since everything stayed well under a second. But one thing did surprise me: the built-in graph features were the slow ones.
As far as I can tell, Cypher's variable-length paths enumerate every route through the graph, while the recursive SQL only tracks every node. Two other things worth knowing: very popular entities ("India" ended up with 6,631 neighbours) make two-hop lookups grow with the data, so cap neighbours per entity. And on Postgres, a one-hop lookup took 0.05 ms inside the database but 0.66 ms by the time it reached my code. The network was 13 times the actual work. SQLite, running inside the process, skips that cost entirely.
I also wanted to try SQL/PGQ, the graph syntax in the SQL standard, on PostgreSQL 19. The release candidate I pulled didn't support it yet; CREATE PROPERTY GRAPH was a syntax error. Next time.
What the graph adds to an answer
The main goal was proving the databases can do it. But I wanted a quick look at the payoff from the opening too: does the graph fill gaps the chunks leave?
Then the same model answered 7 questions that need facts from more than one article, with three kinds of context:
| Context given to the model | Tokens | Correct | Partial | Wrong |
|---|---|---|---|---|
| Top 5 chunks from vector search | ~2,200 | 6 | 0 | 1 |
| Top 80 triples from the graph | ~1,300 | 3 | 3 | 1 |
| Chunks and triples together | ~3,400 | 5 | 2 | 0 |
Triples alone answered fully or partly in 6 of 7 cases, with about 40 percent fewer tokens than the chunks. And the one question vector search got wrong was exactly the case from the opening. It asked about Vikram Sarabhai and what happened to the Thumba launch station after his death. The chunk with the answer ranked 22nd (not reranked), so it never reached the model, and the model said "not in context". With the triples added, it connected Sarabhai to Thumba and came back partly right.
A fair warning about this test. These were Wikipedia articles, where each answer sits in one or two clean paragraphs. That is vector search's home ground. Graphs earn their keep on scattered data: one idea per slide across a long deck, a contract and its amendments, tickets that refer to each other. Seven questions, marked by me, is also a small sample. Even so, on data that favours vectors, the graph filled a gap the chunks left, with less text.
What I took away
If your goal is KG RAG, you most likely don't need a separate graph database. The database you already run can hold the KG all six I tried did it correctly.
Graph databases still make sense when the graph itself is the product: long path queries like fraud rings or supply chains, or heavy graph algorithms on huge graphs that change all the time. A RAG retriever asks a much smaller question, "what's connected to these few entities?", and any database with a graph wrapper can answer that.
github.com/dhruvbhatnagar9548/graph-wrapper-bench →Data from English Wikipedia (CC BY-SA 4.0). Code under Apache-2.0.