Database Access

Talks about: , and

How long a generated repository takes to run a statement, measured across every logging configuration YoSQL can generate.

What is being measured is the overhead YoSQL adds on top of JDBC — not how fast your database is. The numbers are dominated by the database in any real application, which is rather the point: there is nothing between your query and the driver except the code you can read in target/generated-sources.

The scenarios

Each configuration runs the same set, so the numbers are comparable across them:

Eleven of them, declared as the Read, Write and Call interfaces in yosql-benchmarks-dao, so that an implementation either covers all of them or does not compile.

Reading

Writing

Calling stored procedures

The published results

The results are in microseconds, against an in-process H2, with every logging implementation configured for maximum output so the cost of each can be read against the no-op baseline.

Read them with their provenance in mind. They were measured on a shared GitHub Actions runner, one fork, three warmup and five measurement iterations — enough to see the shape, not enough to separate two implementations that are close. Several have a confidence interval wider than the score itself.

If you are making a decision, run them on your own hardware with your own statements. That is not a disclaimer; it is the only way a number like this means anything for your project.

mvn --projects yosql-benchmarks/yosql-benchmarks-dao --also-make \
  --activate-profiles benchmarks verify

The run writes target/benchmark/yosql-benchmarks-dao.json, which jmh.morethan.io will render against the published baseline.

Against JDBI

yosql-benchmarks-vs-jdbi runs the eleven scenarios twice — once through generated repositories, once through JDBI — in one JMH run.

That matters more than it sounds. Numbers from two runs on two machines are not comparable at all; numbers from one run share the JVM, the warmup, the schema and the hardware, so what is left is the difference between the two libraries. Both take a connection per call and give it back, both read rows into Map<String, Object>, and both send the database the same SQL — the statements come from this module’s .sql files, so neither side can be measured running a query the other did not.

Each implementation gets its own in-memory database, because the write scenarios insert and delete and a shared one would make each side’s numbers depend on how often the other had already run.

Both implementations are in the repository. If you suspect one was written to lose, read it — that is the answer a chart cannot give you.

What it measures

µs per operation, lower is better. JDK 25, H2 in process, one fork, three warmup and five measurement iterations, on an otherwise idle 16-core machine.

ScenarioYoSQLJDBIDifference
callStoredProcedure7.07 ± 0.4417.63 ± 5.81+10.56
readManyToOneRelation7.85 ± 0.6818.62 ± 2.52+10.77
readMultipleEntitiesBasedOnCondition7.87 ± 0.6718.17 ± 3.98+10.30
deleteSingleEntityByPrimaryKey7.87 ± 1.2817.85 ± 7.13+9.98
readOneToManyRelation8.00 ± 0.7417.40 ± 1.10+9.40
readSingleEntityByPrimaryKey8.03 ± 1.1917.62 ± 2.32+9.59
updateOneToManyRelation8.06 ± 1.1418.57 ± 4.09+10.51
readMultipleEntities8.07 ± 0.7916.39 ± 1.50+8.32
writeSingleEntity12.15 ± 1.3621.54 ± 4.81+9.39
updateSingleEntity13.60 ± 2.0522.11 ± 5.96+8.52
writeMultipleEntities56.47 ± 12.9660.72 ± 6.61+4.25

What it means, and what it does not

The ratios run from 1.08× to 2.49×, and quoting any of them would be misleading. Read the last column instead: the difference is about 9.6 µs on every scenario, whatever that scenario does. A fixed cost per call, not a proportional one — JDBI builds a Handle, with its configuration and its mapper registry, every time you ask for one. Generated code has no such step because it has nothing to configure.

writeMultipleEntities is the one that proves it. It does roughly 50 µs of real database work, and there the two are indistinguishable: +4.25 µs with error bars that overlap. Once actual work dominates, the constant disappears into it.

So the honest claim is narrow: JDBI’s per-call machinery costs around 10 µs, and YoSQL has no per-call machinery. Whether that matters to you is arithmetic. Against an in-process H2, where a query costs 8 µs, it doubles your time. Against a database on the other side of a socket, where a query costs hundreds of microseconds or milliseconds, 10 µs is a rounding error you will never measure — which is the same reason this page opens by saying it measures what the layer costs and not what your application will do.

Two caveats worth keeping in view. JDBI’s error bars are wide — ±5.81 µs on one scenario — so no single row settles anything; it is the same result appearing in all eleven that carries it. And this is one fork on one machine.

mvn --projects yosql-benchmarks/yosql-benchmarks-vs-jdbi \
  --activate-profiles benchmarks verify

Leave --also-make off once the dependencies are installed. With it, the DAO benchmarks run too — including the variant that logs every statement — which takes far longer and writes a very large log.

Against an ORM

There is none, and there is unlikely to be one.

Read the same entity twice and Hibernate answers the second from its identity map, so it wins by a distance; turn that off and you are measuring a Hibernate nobody deploys. Whichever you pick, the number argues about the configuration rather than about the tools, and the honest version of that argument is prose. The comparison of alternatives says where an ORM is the better choice without pretending to a measurement.

The JDBI result above also suggests what such a comparison would find between two SQL-first libraries: a fixed per-call cost, invisible the moment a real database is involved. That is worth knowing once. It is not worth knowing four times.