DEV Community

Philip Z
Philip Z

Posted on Originally published at teaql.io

TeaQL Was 2,000x Faster Than the Obvious SQLx Query—Here’s What Actually Happened

We recently measured two implementations of the same application request over
the MusicBrainz dataset:

Load the newest 100 recordings that have linked works, and load at most ten
work relations for each recording.

The controlled SQLx test returned the same 100 recordings, 103 relation rows,
103 links, 103 link types, and Work-ID checksum through both paths. One took
5,871.169 milliseconds. The other took 2.469 milliseconds—a 2,378×
difference inside SQLx itself
.

TeaQL Rust previously completed the corresponding typed graph workload in
2.864 milliseconds. That does not mean TeaQL has a PostgreSQL driver 2,000×
faster than SQLx. The difference was the amount of work requested from the
database. The expert SQLx control proves it.

What TeaQL is

TeaQL is a model-driven application runtime. A semantic model generates
language-native Q APIs for queries, E APIs for loaded expressions, and governed
graph mutation APIs. The same model can target seven runtimes: Rust, Java,
TypeScript, Go, Swift, .NET, and Python.

Queries carry more than SQL structure. They retain relation bounds, loaded
state, tenant and authorization scope, version policy, and an operational
comment and purpose. That additional intent is what made this optimization
possible without turning application code into handcrafted SQL.

The obvious single-statement SQLx solution

A capable SQL developer may reach for a window function to express Top-N per
parent:

WITH ranked AS (
  SELECT relation.*,
         row_number() OVER (
           PARTITION BY relation.entity0
           ORDER BY relation.link_order ASC, relation.id DESC
         ) AS rn
  FROM l_recording_work relation
  WHERE relation.version > 0
), roots AS (
  SELECT recording.id
  FROM recording
  WHERE recording.version > 0
    AND EXISTS (
      SELECT 1 FROM ranked WHERE ranked.entity0 = recording.id
    )
  ORDER BY recording.id DESC
  LIMIT 100
)
SELECT ...
FROM roots
LEFT JOIN ranked
  ON ranked.entity0 = roots.id AND ranked.rn <= 10
LEFT JOIN link ...
LEFT JOIN link_type ...
LEFT JOIN work ...;
Enter fullscreen mode Exit fullscreen mode

This is readable, set-oriented SQL. It is not an obviously careless query. It
also asks PostgreSQL to rank the complete relation population—about 2.7 million
rows in this fixture—before reducing the graph to 100 roots and 103 relations.

In the Rust SQLx control, the PostgreSQL median was 5,871.169 ms. An earlier
raw JDBC run measured 5,579.224 ms, confirming that changing the client library
does not remove the database work. DuckDB, whose vectorized analytical engine
fits this global-ranking shape better, completed the earlier query in
808.158 ms.

The business request carried a stronger bound

The corresponding TeaQL request describes a bounded object graph. In an
application, we can name and reuse each meaningful graph fragment instead of
repeating the whole nested request at every call site:

fn link_type_details() -> LinkTypeRequest {
    Q::link_types_minimal()
        .select_name()
        .select_description()
        .select_link_phrase()
}

fn link_details() -> LinkRequest {
    Q::links_minimal()
        .select_ended()
        .select_link_type_with(link_type_details())
}

fn recording_work_details() -> LRecordingWorkRequest {
    Q::l_recording_works_minimal()
        .select_link_order()
        .select_link_with(link_details())
        .select_entity1_with(Q::works_minimal().select_name())
        .order_by_link_order_asc()
        .order_by_id_desc()
        .limit(10)
}

Q::recordings_minimal()
    .select_name()
    .select_length()
    .have_l_recording_works()
    .select_l_recording_work_list_with(recording_work_details())
    .order_by_id_desc()
    .limit(100)
    .comment("what: load the MB03 recording-work graph")
    .purpose("why: render bounded recording-work details")
    .execute_for_list(&context)
    .await?;
Enter fullscreen mode Exit fullscreen mode

The main request now reads as a composition of business-relevant graph
fragments. The helpers are ordinary typed Rust functions: they can be reused,
tested, extended, and combined like building blocks. They do not hide raw SQL
or switch to a second query system; each still returns a generated TeaQL query
selection that the runtime can govern as one request.

See the same TeaQL request fully expanded

Q::recordings_minimal()
    .select_name()
    .select_length()
    .have_l_recording_works()
    .select_l_recording_work_list_with(
        Q::l_recording_works_minimal()
            .select_link_order()
            .select_link_with(
                Q::links_minimal()
                    .select_ended()
                    .select_link_type_with(
                        Q::link_types_minimal()
                            .select_name()
                            .select_description()
                            .select_link_phrase(),
                    ),
            )
            .select_entity1_with(Q::works_minimal().select_name())
            .order_by_link_order_asc()
            .order_by_id_desc()
            .limit(10),
    )
    .order_by_id_desc()
    .limit(100)
    .comment("what: load the MB03 recording-work graph")
    .purpose("why: render bounded recording-work details")
    .execute_for_list(&amp;context)
    .await?;
Enter fullscreen mode Exit fullscreen mode

The important information is not the Rust syntax. It is the request shape:

  1. choose 100 matching roots;
  2. load no more than ten ordered relations for each selected root;
  3. hydrate only the referenced Link, LinkType, and Work objects;
  4. preserve version, policy, comment, and purpose semantics throughout.

TeaQL can select the roots first and constrain relation loading to those roots.
It does not need to rank relations belonging to recordings that cannot appear
on this page.

The SQLx control explains the result

We then wrote the optimization explicitly in ordinary SQLx:

  1. fetch the 100 root recording IDs;
  2. pass those IDs to a second parameterized query;
  3. rank only relations whose entity0 belongs to that root set;
  4. join Link, LinkType, and Work for the bounded rows.

Using one initialized SQLx pool connection, three warmups, and ten sequential
measurements, the medians were:

Implementation PostgreSQL median
SQLx, natural global-window statement 5,871.169 ms
SQLx, expert root-first two-stage plan 2.469 ms
TeaQL Rust, typed governed graph (separate retained run) 2.864 ms

The first two rows are the controlled 2,378× comparison. The TeaQL row comes
from a separate retained run and is context, not part of that ratio. It includes
generated typed requests, relation hydration, and identity-graph assembly; the
SQLx control decodes aggregate tuples.

How much code did each plan require?

We counted the physical non-blank lines that declare and execute each query.
Imports, connection setup, timing, result gates, and reporting were excluded;
embedded SQL was included because the application owns it.

Implementation Query LOC
SQLx, natural global window 26
SQLx, expert root-first 34
TeaQL, typed graph request (fully expanded) 27

The numbers are deliberately unexciting: TeaQL is not winning through a tiny
code-golf example. Its fully expanded typed request is about the same size as
the natural SQL. The composed version moves reusable graph fragments out of the
call site; it improves local readability without pretending that their
definitions vanished. The important difference is what those lines preserve.
The expert SQLx version owns two SQL statements, transfers root IDs, binds the
array, and must keep both stages semantically aligned. TeaQL declares the root
and relation bounds once inside the graph request.

LOC is formatting-sensitive, and the SQLx benchmark returns aggregate tuples
while TeaQL hydrates entities. It should be read as maintenance surface, not as
a universal productivity score. A fuller hand-written implementation would
also need typed SQLx rows, graph assembly, authorization, tenant isolation,
loaded-state handling, and observability.

Performance is only half of the problem

An expert can—and in this benchmark did—write the fast SQLx plan. TeaQL's
advantage is not that manual optimization is impossible. It is that application
developers do not have to discover, implement, and repeatedly preserve it
themselves.

Every handcrafted optimization creates another enforcement point. The root
query may contain a tenant predicate while the child query accidentally omits
it. The same can happen to authorization scope, soft-delete/version policy,
privacy masking, or trace metadata. Such mistakes are especially easy when an
AI coding agent rewrites a query for performance: the output looks faster and
can still leak another tenant's children.

TeaQL keeps both stages inside the same governed execution model. This is part
of TeaQL Harness Engineering: reduce the space in which generated code can be
fast but semantically or operationally wrong.

What the benchmark proves—and what it does not

It proves that API semantics can give a runtime enough information to avoid a
large amount of unnecessary database work. It also shows why query-count rules
are incomplete: one elegant statement can do much more work than several
bounded statements.

It does not prove that:

  • TeaQL has an intrinsically faster PostgreSQL driver than SQLx;
  • every window query is slow;
  • multiple queries are always preferable;
  • these timings generalize to other hardware, datasets, indexes, or databases;
  • 2,378× is a general TeaQL-versus-SQLx performance ratio.

The controlled result is narrower and more useful:

The obvious SQLx query ranked 2.7 million rows for a page containing 103
relations. Once the business bounds were applied before ranking, most of that
work disappeared.

Reproduce it

The public
TeaQL runtime benchmark repository
retains four related evidence packages:

  1. B001: Rust TeaQL, Diesel, and SeaORM typed graph workloads;
  2. B002: raw JDBC across PostgreSQL and DuckDB;
  3. B003: Java TeaQL across PostgreSQL and DuckDB;
  4. B004: natural and expert root-first SQLx plans with a correctness gate.

Each package records source, query shape, environment, warmups, measurements,
cardinalities, and checksums. B004 also contains an executable LOC counter.

The 2,378× headline is therefore reproducible, but deliberately narrow. The
broader TeaQL claim is about making the good plan declarative, typed, reusable,
and governed.

Top comments (0)