Skip to main content

DML reference

Querying extracted data. Schema statements are in the DDL reference.

SELECT does not require admin. It is bounded by the permission model instead.

Shape

Clause order is fixed:

[WITH [RECURSIVE] <cte> AS ( <select> ), ...]
SELECT [DISTINCT] <projection>, ...
FROM <source> [<alias>]
[ [LEFT [OUTER]] JOIN <source> [<alias>]
( VIA <alias>.<edge> | USING (<column>) | ON <a>.<c> = <b>.<d> [AND ...] ) ]...
[WHERE <predicate>]
[GROUP BY <column>, ...]
[HAVING <predicate>]
[ORDER BY <expr> [ASC | DESC], ...]
[LIMIT <n>] [OFFSET <n>]
[ (UNION [ALL] | INTERSECT | EXCEPT) <select> ]...

The corpus is named in FROM. There is no trailing scope clause.

Sources

SourceProduces
FROM <corpus>.<Entity> [alias]one row per extracted entity instance
FROM <corpus> [alias]one row per document
FROM SEARCH(<pipeline>, '<query>', TOP <k>) [alias]ranked rows: doc_id, score, rank, snippet
FROM ( <select> ) <alias>derived table, alias required
FROM <cte_name> [alias]an inlined CTE
FROM plugins.<operation> [alias]rows fetched live from an external API

System columns

Every entity scan carries provenance columns alongside your declared ones:

ColumnMeaning
_idcanonical node id, after alias resolution
_entity_typethe entity type
_doca source document id from lineage
_run_idthe extraction run that wrote this node
_confidenceextraction confidence
_folderfolder partition key, only when the corpus uses PARTITION BY FOLDER

_doc is the one worth knowing: it is what the permission filter keys on, and what you join through to reach document metadata.

SerchaQL
SELECT _id, _doc, title, published_on
FROM library.Paper
ORDER BY published_on DESC
LIMIT 3;

Predicates

FormExample
Comparisonamount > 5000, published_on >= DATE '2025-01-01'
Null tests_confidence IS NOT NULL
Listsrole IN ('lead','reviewer'), role NOT IN ('reviewer')
Subqueries_id IN (SELECT _id FROM …), NOT IN (…)
Existence[NOT] EXISTS (SELECT …)
Scalar subqueryamount > (SELECT AVG(amount) FROM …)
Correlateda.funder = b.funder
Search<alias> MATCHES ('query' USING <pipeline> TOP <k>)
BooleanAND, OR, NOT, parentheses

Operators: =, <>, !=, <, <=, >, >=. Precedence is OR < AND < NOT.

Joins

Three ways to relate two sources:

VIA traverses a declared edge. This is the graph join, and the one that makes the schema pay off.

JOIN <corpus>.<Entity> <alias> VIA <sourceAlias>.<edge_name>

USING (col) equi-joins on a shared column. Most often USING (_doc), to put two entities from the same document side by side.

ON a.col = b.col is explicit equality. Both sides must be alias-qualified; chain with AND.

Only JOIN and LEFT [OUTER] JOIN exist. There is no RIGHT or FULL.

SerchaQL
-- Which authors wrote which papers, following the edge.
SELECT p.title, a.full_name, a.affiliation
FROM library.Paper p
JOIN library.Author a VIA p.written_by
WHERE a.role = 'lead'
LIMIT 3;

One author, two papers. The edge resolved both mentions to the same node.

Two ways in, for two different questions.

SEARCH(...) as a table, when the ranked documents are the answer:

SELECT s.doc_id, s.snippet, s.score
FROM SEARCH(default, 'tidal forcing', TOP 5) s
ORDER BY s.score DESC;

MATCHES as a predicate, when you want entity rows whose source document matched. It is a semi-join, so your columns survive:

SELECT title, published_on
FROM library.Paper p
WHERE p MATCHES ('tidal forcing' USING default TOP 5);

Reach for MATCHES when the search is a filter and the entity is the answer.

Aggregation

COUNT, SUM, AVG, MIN, MAX and ARRAY_AGG, with GROUP BY and HAVING.

SerchaQL
SELECT a.affiliation,
COUNT(*) AS papers,
AVG(g.amount) AS avg_grant
FROM library.Paper p
JOIN library.Author a VIA p.written_by
JOIN library.Grant g USING (_doc)
GROUP BY a.affiliation
ORDER BY papers DESC;
Aliasing aggregates in HAVING

HAVING is parsed as an ordinary predicate, so HAVING COUNT(*) > 2 does not parse. Alias the aggregate and filter on the alias: SELECT …, COUNT(*) AS n … HAVING n > 2.

Window functions

ROW_NUMBER(), RANK() and DENSE_RANK(), all requiring OVER, plus aggregates used as window functions:

SELECT title, affiliation,
RANK() OVER (PARTITION BY affiliation ORDER BY published_on DESC) AS recency
FROM library.Paper p
JOIN library.Author a VIA p.written_by;

CTEs and set operations

WITH recent AS (
SELECT _doc, title FROM library.Paper WHERE published_on >= DATE '2025-01-01'
)
SELECT title FROM recent;

WITH RECURSIVE is supported for walking hierarchies. Set operations (UNION [ALL], INTERSECT, EXCEPT) are left-associative, and are not allowed inside a CTE body.

Generated columns

GENERATE runs an LLM per row as a projection. The template interpolates columns, and an AS alias is required:

SELECT title,
GENERATE('One sentence on why {title} matters to coastal planners') AS relevance
FROM library.Paper
LIMIT 5;

An optional second argument feeds a subquery's rows to the model as context.

External data

plugins.<operation> is a virtual table backed by an external API. Join it like any other source:

SELECT c.cited_title, x.identifier, x.year
FROM library.Citation c
LEFT JOIN plugins.registry_lookup x ON x.query = c.cited_title;

Every declared input column must be bound, by a join condition or a literal in WHERE. Results are cached. If a query would exceed the operation's soft call limit, the API returns 202 with an estimate; re-send with "confirm": true to proceed.

Permissions

Three checks, composed with AND, all fail-closed:

  1. Corpus grant: SELECT on the corpus
  2. Corpus membership: the documents in the corpus
  3. Per-document ACL: intersected with layer 2

The result is pushed into the scan. Deny-all short-circuits before touching storage. EXPLAIN shows all three named in the plan.

Explaining a query

EXPLAIN SELECT title FROM library.Paper LIMIT 5;
EXPLAIN ANALYZE SELECT ...;

stats.op_stats is returned on every query, with rows in/out and elapsed time per operator, so you do not need EXPLAIN ANALYZE to profile.

Not available

INSERT / UPDATE / DELETE: data arrives by extraction, not by statement. No transactions. No backend configuration through SQL. RESOLVE SEMANTIC, SEMANTIC JOIN, EMBED and CREATE SNAPSHOT are reserved and rejected.