
SPARQL, Without the Semantic Web Sermon
What SPARQL actually is, the two things it does that SQL and Mongo can't, and the five places it will make you miserable. With figures you can drag.
Some questions take four seconds to ask and an afternoon to answer:
Which of our customers also sell to one of our customers?
In SQL that's a self-join through a couple of tables and a lot of squinting at foreign keys. In a document store it's often a rewrite, because you picked the wrong document shape eight months ago. In SPARQL it's a handful of lines that sit on the page roughly the way the sentence does.
That's the whole idea. The rest of this post is detail, including the parts where SPARQL is unpleasant and the part where I tell you to use something else.
The only idea in it
SPARQL queries RDF, and RDF has exactly one idea. Every fact is three words long.
Arrival — was directed by — Denis Villeneuve.
Subject, predicate, object. That's called a triple, and a database of them is just a big pile of triples. There are no tables, no columns, no schema you had to declare in advance. Drag through the figure below and watch a spreadsheet row get taken apart into them.
(The canonical version of this is the example in the W3C RDF Primer — Bob, the Mona Lisa, and a video about it. That's the graph on the cover of this post, redrawn; the Primer is still the clearest short thing written about RDF.)
| title | directed_by | studio | year |
|---|---|---|---|
| Arrival | Denis Villeneuve | Paramount | 2016 |
Four columns. To a database, this is one thing with four properties. To ask about Villeneuve you have to go through the movie.
At step two this looks like a downgrade. You took a tidy row and smashed it into gravel, and now you need three facts where you had one.
The payoff is step four. When the second movie shows up, it doesn't get a new "Denis Villeneuve" cell. It lands on the node that's already sitting there, because both facts point at the same thing. No join, no foreign key, no migration, no meeting about whether the director column should really be its own table. The connection is the storage format.
A query is a shape, not a set of instructions
This is the part that trips up everyone coming from SQL. It tripped me up for about a year.
A SQL query is a list of instructions: take this table, join it to that one on this key, filter, group, sort. A SPARQL query is a drawing of the shape you're looking for, with some of the words left blank. You hand the drawing to the database and it finds every way the blanks can be filled in.
SELECT ?actor WHERE { }
Find anything with a directedBy edge pointing at Villeneuve.
Anything starting with a ? is a blank. ?movie doesn't mean "the movies table," it means "whatever goes here." The database's job is to find every assignment of ?movie and ?actor that makes all three lines true at once. Reorder those three lines and the answer is identical, because you handed over a shape rather than a procedure. That holds for a plain pattern like this one; add OPTIONAL and order starts to matter again, which I'll come back to unhappily.
So how is that different from SQL, really?
The schema arrives after the data does. In SQL you decide the shape first. Adding a relationship later means ALTER TABLE, a migration, a backfill, and a conversation with whoever owns that service. In RDF you add a triple. If half your records have a "parent company" and half don't, that's not a nullable column you have to justify. It's a fact that exists for some things and not others.
You never write the join. Watch it grow:
Which movies did Villeneuve direct?
SELECT m.title FROM movies m JOIN people d ON d.id = m.director_id WHERE d.name = 'Denis Villeneuve';
SELECT ?movie WHERE {
?movie :directedBy :DenisVilleneuve .
}SPARQL marketing lies about this, so let me not. SPARQL absolutely does joins. Every time a variable appears in two lines, that's a join, and the engine still has to plan and execute it. The work didn't disappear. You just never had to name the plumbing — no roles.person_id = people.id — which makes getting a join wrong a much harder mistake to make. The mirror-image footgun: because joins happen wherever variable names match, reusing a name by accident silently creates a join you never wanted.
The flip side: relational query planners have had four decades and enormous commercial pressure to get good. Triple store planners have had less of both. There are plenty of queries where a well-indexed Postgres will embarrass a graph engine on the same data.
Identifiers are global. A row ID of 4471 means something inside your database and nothing outside it. RDF identifies things with URIs, so Denis Villeneuve is http://www.wikidata.org/entity/Q548823 on your machine, my machine, and a server in Bern. Merging two RDF datasets is closer to concatenating two files than to a data integration project, provided both sides reached for the same identifiers. When they didn't, you're back to reconciliation like everybody else. The difference is that in RDF the reconciliation is itself just more triples.
The schema is data too. You can ask a SPARQL endpoint what types of things it contains using the same language, in the same graph, as the questions about the things themselves. SQL has information_schema and it is a real ISO standard, not a vendor extension. But it's a separate, privileged namespace you can't join against your data in one breath, and Oracle and SQLite don't implement it anyway. In RDF the schema is just more triples. That sounds like trivia until you're pointing a program at a database it has never seen.
The one people oversell is inference: declaring that every Cardiologist is a Physician and letting the database work out the consequences. It works, and I have yet to see it decide a database choice. Don't pick one for it.
And NoSQL?
"NoSQL" is a category defined by what it isn't, which should always make you suspicious. The comparison that matters is with document stores, since that's what most people mean.
A document store makes you choose an access path at write time. You decide what a document contains, and reads along that path are wonderfully fast forever. Every question that cuts across the grain becomes a secondary index, an aggregation pipeline with a $lookup, or a re-modelling exercise. All of those work. None of them is the fast path you designed for, and you tend to find that out in production.
RDF makes the opposite bet: no access path is privileged, every attribute is equally a way in. That's why it's slower at the thing document stores are fast at, and why it can answer questions nobody anticipated when the data was loaded.
The competitor I take more seriously is property graphs — Neo4j, Cypher, and now GQL, which became an ISO standard in April 2024 and was the first new ISO database query language since SQL in 1987. Property graphs are nicer to write. Nodes hold properties directly instead of exploding into a dozen triples, and the syntax reads better.
So: if you're hand-writing the queries, your graph lives entirely inside your company, and no fact in it ever needs checking against the outside world, use a property graph.
I'd push on that last condition harder than most people do, though. "Internal" almost always describes where the rows came from, not where the facts live. A customer table is internal. Whether that customer still exists, still has that address, still belongs to that parent company: none of that is internal, and owning the table doesn't make it so. The moment something outside your walls is the authority on a value inside your graph, you're doing integration on a schedule, forever, and you need an identifier that means the same thing on both sides of it. Genuinely closed graphs exist. They're rarer than the word "internal" makes them sound.
The two tricks nothing else does
One query, databases you don't own. SPARQL has a keyword, SERVICE, that ships part of your query to somebody else's endpoint and merges what comes back into your results.
UniProt's endpoint held 232 billion triples of protein and related life-science data as of its 2026_02 release, and you can join against it from a laptop without downloading a byte. Wikidata is over 16 billion and growing by about a billion a year.
SQL has federation too — foreign data wrappers, Trino — so I don't want to oversell this. What SQL doesn't have is a stranger's endpoint you can point at this afternoon without first building a connector for their schema. What SPARQL doesn't have is any guarantee the stranger's endpoint is up, fast, or willing. Public endpoints are rate-limited and frequently down, Wikidata only permits SERVICE against an allow-listed set of hosts, and federated joins are typically planned as a naive fan-out. It's a real capability with a flaky floor.
Walking into a database blind. Because the schema is queryable data, a program — an agent, in our case, though it needn't be — can point itself at an endpoint it has never seen, ask what types exist and how they connect, and compose a sensible query from the answer. It also means the graph can change underneath that agent without redeploying it. The agent asks what's there now. Try this against an unfamiliar Postgres and you'll get there, but through a catalog that sits beside the data rather than in it, and nothing tells you which of those forty tables are real entities versus join tables versus a 2019 migration nobody deleted.
Now the part where I stop selling
I've written a lot of SPARQL. Five things about it are irritating.
It's verbose, and the URIs are a tax. Real queries open with a wall of PREFIX declarations, and the things inside them are hundred-character URIs. It's more typing than the equivalent SQL for the simple cases, and the simple cases are most cases.
OPTIONAL is a footgun. SPARQL's version of a left join composes in ways that surprise people, and nesting a couple of them can quietly change what your query means. Pérez, Arenas and Gutierrez showed that OPTIONAL alone is what pushes SPARQL evaluation to PSPACE-complete, then defined a restricted class of "well-designed patterns" to claw it back to something tractable. A query language needing peer-reviewed papers to explain when its left join behaves is not a great sign.
Performance at scale has an ending, and it isn't flattering. The Wikidata Query Service is the most visible SPARQL deployment on earth. It runs on Blazegraph, the engine Neptune is widely understood to be built on, and whose team Amazon hired outright, after which open-source development effectively stopped in 2018. Wikidata queries get a hard 60-second timeout, and a single client is throttled to 60 seconds of processing per minute: one long query and you're done for the minute. In May 2025, after years of trying to scale it, they split the graph in two, moving scholarly articles onto their own endpoint. Those are more than half of all the triples but under 10% of query traffic, and the damage was concentrated enough that five user agents accounted for over 90% of it. Those queries had to be rewritten to federate across the seam. Wikimedia has since picked QLever as the replacement engine and migration starts this year, which is the right ending. It took eight years to get there.
The ecosystem is small. Fewer engineers who know it, thin BI tool support, fewer libraries, and a much shorter tail of Stack Overflow answers when you're stuck at 11pm.
The standard has been sitting still. SPARQL 1.0 became a W3C Recommendation in January 2008 and 1.1 in March 2013. SPARQL 1.2 is still a Working Draft as I write this in 2026. Thirteen years of stability is a feature right up until it starts looking like something else.
Worth sorting those, because they don't all survive contact with a machine. The performance ceiling and the thin ecosystem are real no matter who writes the query. The verbosity, the OPTIONAL footgun and the hiring problem are costs of typing it. They cost me weeks, and they're also the three that mostly evaporate when something other than a person composes the query. I don't say that to rescue the language. Which of these you're actually buying depends entirely on which side of that line you're standing on.
Then the cultural thing, which is why this post is titled the way it is. RDF was born inside the Semantic Web project, and for two decades it was sold as a worldview — ontologies, reasoning, a machine-readable web — rather than as a tool. That framing drove away roughly everyone who just wanted to query a graph. You don't have to care about any of it. It's a graph database with global identifiers and a standard wire format. Take that and leave the sermon.
Why we bet on it anyway
We build Infona, which turns messy files into a knowledge graph, keeps that graph current against outside sources, and answers questions about it in plain English. Mostly for agents rather than people, which shapes every decision below.
So the question for us was never which data model is most elegant. It was which one a language model writes correctly.
When you ask a model to write SQL, it has to reconstruct the join structure from a flat schema on every single question: infer that person_id in one table points at id in another, and that "sponsor," a column value in the CSV, is a real-world entity in the question. Get one key wrong and you don't get an error. You get a confident, plausible, wrong answer.
Our internal holdout is 302 questions across 26 held-out knowledge graphs, judged against execution-verified answers, three seeds, majority vote, on Gemini 3 Flash Preview. The gap lands where the theory says it should. On the 68 join and relationship-traversal questions in that set, generating SPARQL over a typed graph scored 86.3% (95% CI 80.9–90.3). Our text-to-SQL baseline scored 39.7% on the same questions, same model, same seeds, same judge.
That SQL number is our reimplementation of a DAIL-SQL-style pipeline, not the published code, and it's missing techniques the real system has. Read it directionally, not as a claim about beating state-of-the-art text-to-SQL. What I'll defend is duller: the join structure gets modelled once at ingest, or rediscovered on every question. Only one of those is stable.
There's a second reason, and it's the one that actually decides what we build. A graph that answers questions about the world has to keep up with the world. Take a boring fact: a company's funding stage, its headquarters, whether it still exists. It was true when the CSV was exported. Nothing in that table tells you it was last true in November 2024. There's no column for who said so and none for when anyone last checked, and if you add them you've added them for that one attribute, in that one schema, and you'll do it again next quarter for the next one. So usually nobody adds them. The stale value sits in the row looking exactly as confident as a value verified this morning, an agent reads it, and the wrongness arrives with no signal attached. That failure is quieter than a bad join and I think it's more expensive, because a wrong join is usually wrong in a way somebody notices.
RDF handles this well for an unglamorous reason: a fact is already its own object. You can hang a source URL and a verified-at date on a single triple without widening a row or migrating anything, because there's no row to widen. And when a new fact about Acme arrives from outside, whether from a filing, a registry or a page, the global identifier means it lands on the node that's already there rather than beside it. That's the same property I called "closer to concatenating two files" a few sections up, pointed inward. Re-verification is federation on a schedule, run against a graph you already own. It's also why I don't fully believe the internal-data test I offered earlier: the rows are internal, the truth of them usually isn't, and the second one is what your agent is actually reporting.
That's the claim I'd defend hardest and it's also the one with no benchmark under it. I can show you a join-accuracy number. I can't show you a "still correct eighteen months later" number, because we haven't been running long enough to have one, and I'd be suspicious of anyone who says they can.
When it's worth it
Worth the tax
- Your data comes from many places and has to merge without a standing integration project.
- The facts have to stay current, and you need to know for each one when it was last checked and against what.
- The interesting questions are about how things connect, and they keep changing.
- Something other than a human is writing the queries — an agent, a pipeline, a UI.
- You need to point at a dataset you've never seen and work out what's in it.
Don't bother
- One dominant access path, high write volume, latency budget in milliseconds.
- Your graph is closed: hand-written queries, internal data only, and no value in it ever has to be checked against an outside source. Take a property graph.
- Someone on your team will hand-write every query, and nobody wants to learn a new language to do it.
- You mostly need aggregates over big flat tables. That's a columnar warehouse's job.
- Someone sold it to you on reasoning and inference. Ask them for a second reason.
SPARQL is a specialist tool with a bad reputation, half of it deserved and the other half inherited from the movement that produced it. It's clumsy for the easy questions. It's the only sane option I know for a certain kind of hard one: when the data keeps arriving from new places, the facts keep going out of date underneath you, the questions keep changing shape, and something that isn't a person has to write the query.
If you've run a graph database in production and it went badly, I want to hear about it. Those stories are more useful than the success ones and much harder to find. Email hi@infona.ai or leave it in the comments. If you'd rather talk it through, I keep office hours.