1# PostgreSQL benchmarks
2
3## PgClient
4
5`PgClient.ts` measures complete queries through `PgClient`, including Effect execution, pool checkout, the PostgreSQL
6wire protocol, and result decoding. It covers a parameterized one-row query, a 100-row result at three and at twenty
7columns, a single-statement transaction, and 20 concurrent queries through a ten-connection pool. The transaction is
8there because `BEGIN`, `COMMIT`, and `SAVEPOINT` reach the connection through a different path from the statements
9between them, so a change to that path is invisible to every other workload here. Its numbers are dominated by the
10commit, so read it for regressions rather than for throughput. The two row-count-matched widths are there because
11per-column work and per-row work scale differently: a change that barely moves a three-column result can dominate a
12twenty-column one. A PostgreSQL Testcontainer starts automatically, so Docker is the only external
13requirement.
14
15Run it from the repository root:
16
17```sh
18nix develop -c pnpm --filter @effect/sql-pg benchmark:client
19```
20
21Set `PGCLIENT_BENCHMARK_MULTIPLEX=1` to run the same workloads on a multiplexed pool, where concurrent statements are
22pipelined rather than sent one at a time. It only changes the concurrent workload; the sequential ones submit one
23statement at a time and have nothing to pipeline.
24
25To use an existing PostgreSQL server instead, set `PGCLIENT_BENCHMARK_URL` to its connection URI. Keep the same server,
26Node version, and machine load when comparing revisions.
27
28Two numbers from different machines are not comparable, and on anything but Linux the container is the reason. Docker
29runs the server inside a virtual machine on macOS and Windows, so every round trip crosses a virtualised network and
30the workloads that wait on one - the single-row query and the transaction - measure that boundary as much as they
31measure the client. Point `PGCLIENT_BENCHMARK_URL` at a natively installed server there, or at a unix socket
32(`postgres:///postgres?host=/tmp`), and compare two revisions against the same server rather than comparing platforms.
33
34The benchmark file is deliberately valid against both the working branch and `main`. Since `main` does not contain the
35new file yet, pipe it from the benchmark branch while checked out on `main`:
36
37```sh
38BENCHMARK_REF=origin/eff-854/pg-connection-startup
39nix develop -c pnpm install
40git show "$BENCHMARK_REF:packages/sql/pg/benchmark/PgClient.ts" \
41 | nix develop -c pnpm --dir packages/sql/pg exec node --input-type=module
42```
43
44This runs the benchmark source from `BENCHMARK_REF`, but resolves `@effect/sql-pg` from the current checkout.
45
46## PgCodec
47
48This benchmark compares the `@effect/sql-pg` binary codecs with the native codec paths used by `postgres.js`. It runs
49entirely in one Node process. It does not open a database connection or include socket, TLS, query, or server latency.
50
51Run it from the repository root:
52
53```sh
54nix develop -c pnpm --filter @effect/sql-pg benchmark:codec
55```
56
57Every suite but one uses the same six-column semantic row: `int4`, `bool`, `float8`, `text`, `jsonb`, and `bytea`;
58`int4[] decode` uses a 16-element `int4` array instead, because an array is where a value's cost is paid per element.
59Each sample processes 100 rows. `@effect/sql-pg` uses its binary wire representation and `postgres.js` uses its normal
60text-oriented codecs, so the byte layouts differ even where the values match.
61
62## The end-to-end suites
63
64`Bind frame from JavaScript values` and `DataRow frames to JavaScript values` measure the native codec end to end:
65JavaScript values in, a complete `Bind` frame out, and `DataRow` frames in, JavaScript values out.
66
67The `Bind` suite runs `@effect/sql-pg` twice. `value sink` is `PgProtocol.makeBindEncoder(PgTypes.writeParameter)`,
68which writes each value straight into the frame; it is what a client should use. `encoded parameters` is
69`PgProtocol.encodeBind` over `PgTypes.encode` output, which allocates an array per parameter and copies it into the
70frame. Both produce the same bytes.
71
72`Bind frame from array parameters` is the same comparison for a row of two arrays, a 16-element `int4[]` and an
738-element `text[]`. Arrays are where writing values in place pays most, because the old path encoded every element
74into an array of its own before copying all of them into the frame. Its rows are per parameter row, not per element,
75so they are not comparable with the six-column suites.
76
77## The component suites
78
79The remaining suites split that work up, which is useful for finding hot spots but not for ranking the libraries:
80
81- `type encode` and `type decode` compare the public codec paths for the shared scalar types.
82- `int4[] decode` only measures `@effect/sql-pg`; `postgres.js` ships no array parser and leaves an array column as
83 text.
84- The two parser suites feed a block of 100 `DataRow` frames to the native parser, first as one buffer and then in
85 64-byte chunks. `postgres.js` is absent from those tables because its protocol parser is private and fused to
86 live connection and query state. Wrapping that state machine in a fake socket would measure client orchestration as
87 well as parsing, so it would not be an equivalent offline parser benchmark. Its exported serializers and parsers are
88 still included in the encode and decode tables.
89
90Tinybench warms each task for 250 ms and measures it for one second. Results depend on the Node version, CPU, power
91management, and other local load. Compare results from the same machine and runtime rather than treating one run as a
92portable score.