Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ to roles you would trust with the server's OS account.

- [Language reference](doc/plruby.md)
- [Cookbook: tested recipes](doc/cookbook.md)
- [Performance benchmarks](doc/benchmark.md)
- [Installation](INSTALL)
- [Changelog](CHANGELOG.md)
- [Feature comparison: PL/Ruby vs PL/php vs PL/Perl vs PL/Tcl](doc/comparison.md)
Expand Down
38 changes: 38 additions & 0 deletions bench/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/sh
# Benchmark PL/Ruby against PL/pgSQL, PL/Perl, PL/Python and PL/Tcl.
#
# Usage: PGPORT=5432 [PGBENCH=/usr/lib/postgresql/18/bin/pgbench] sh bench/run.sh
# Requires: a running cluster you can create a "plruby_bench" database in, with
# plruby installed and (optionally) plperl / plpython3u / pltcl available.
#
# Each workload runs one function per language under `pgbench -c 1` for $SECS
# seconds and reports transactions per second (higher is better).
set -e
PGBENCH=${PGBENCH:-pgbench}
DB=plruby_bench
SECS=${SECS:-8}
LANGS=${LANGS:-"ruby pgsql perl python tcl"}

# A 100-element int[] literal for the array workload, built once.
ARR=$(seq -s, 1 100)

dropdb --if-exists $DB 2>/dev/null || true
createdb $DB
psql -qX -d $DB -f "$(dirname "$0")/setup.sql"

for fn in call str spi arr; do
case $fn in
call) body='\set a random(1,1000)
SELECT FN(:a);' ;;
str) body='SELECT FN(chr(97+(random()*20)::int) || repeat(chr(98), 30));' ;;
spi) body='SELECT FN();' ;;
arr) body="SELECT FN('{$ARR}'::int[]);" ;;
esac
for lang in $LANGS; do
printf '%s\n' "$body" | sed "s/FN/${fn}_${lang}/" > /tmp/plruby_bench_$$.sql
tps=$($PGBENCH -n -c 1 -T $SECS -f /tmp/plruby_bench_$$.sql $DB 2>/dev/null \
| awk '/^tps/ {printf "%.0f", $3}')
printf '%-12s %10s tps\n' "${fn}_${lang}" "${tps:-skipped}"
done
done
rm -f /tmp/plruby_bench_$$.sql
99 changes: 99 additions & 0 deletions bench/setup.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
-- Benchmark functions: identical logic in PL/Ruby, PL/pgSQL, PL/Perl,
-- PL/Python and PL/Tcl. Loaded by bench/run.sh. Each of the four workloads
-- has one function per language with matching semantics so the numbers compare
-- like for like.
CREATE EXTENSION IF NOT EXISTS plruby;
CREATE EXTENSION IF NOT EXISTS plperl;
CREATE EXTENSION IF NOT EXISTS plpython3u;
CREATE EXTENSION IF NOT EXISTS pltcl;

-- 1. Call overhead: a trivial body that returns its argument unchanged, so the
-- measurement is dominated by the per-invocation interpreter entry cost.
CREATE OR REPLACE FUNCTION call_ruby(a int) RETURNS int LANGUAGE plruby AS $$
args[0]
$$;
CREATE OR REPLACE FUNCTION call_pgsql(a int) RETURNS int LANGUAGE plpgsql AS $$
BEGIN RETURN a; END;
$$;
CREATE OR REPLACE FUNCTION call_perl(a int) RETURNS int LANGUAGE plperl AS $$
return $_[0];
$$;
CREATE OR REPLACE FUNCTION call_python(a int) RETURNS int LANGUAGE plpython3u AS $$
return a
$$;
CREATE OR REPLACE FUNCTION call_tcl(a int) RETURNS int LANGUAGE pltcl AS $$
return $1
$$;

-- 2. String / numeric ops: reverse, upper-case and append the length. In-
-- language compute with no database access.
CREATE OR REPLACE FUNCTION str_ruby(t text) RETURNS text LANGUAGE plruby AS $$
s = args[0]; s.reverse.upcase + s.length.to_s
$$;
CREATE OR REPLACE FUNCTION str_pgsql(t text) RETURNS text LANGUAGE plpgsql AS $$
BEGIN RETURN upper(reverse(t)) || length(t); END;
$$;
CREATE OR REPLACE FUNCTION str_perl(t text) RETURNS text LANGUAGE plperl AS $$
return uc(reverse($_[0])) . length($_[0]);
$$;
CREATE OR REPLACE FUNCTION str_python(t text) RETURNS text LANGUAGE plpython3u AS $$
return t[::-1].upper() + str(len(t))
$$;
CREATE OR REPLACE FUNCTION str_tcl(t text) RETURNS text LANGUAGE pltcl AS $$
return "[string toupper [string reverse $1]][string length $1]"
$$;

-- 3. SPI queries: sum a column over a 1,000-row table. Exercises the database-
-- access path and the per-row C-to-language value conversion.
DROP TABLE IF EXISTS bench_rows;
CREATE TABLE bench_rows AS SELECT g AS id, g * 2 AS val FROM generate_series(1, 1000) g;
CREATE OR REPLACE FUNCTION spi_ruby() RETURNS bigint LANGUAGE plruby AS $$
r = spi_exec("select val from bench_rows")
s = 0
while (row = spi_fetch_row(r)); s += row['val']; end
s
$$;
CREATE OR REPLACE FUNCTION spi_pgsql() RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE s bigint := 0; r record;
BEGIN
FOR r IN SELECT val FROM bench_rows LOOP s := s + r.val; END LOOP;
RETURN s;
END;
$$;
CREATE OR REPLACE FUNCTION spi_perl() RETURNS bigint LANGUAGE plperl AS $$
my $rv = spi_exec_query("select val from bench_rows");
my $s = 0;
$s += $_->{val} for @{$rv->{rows}};
return $s;
$$;
CREATE OR REPLACE FUNCTION spi_python() RETURNS bigint LANGUAGE plpython3u AS $$
rv = plpy.execute("select val from bench_rows")
return sum(row["val"] for row in rv)
$$;
CREATE OR REPLACE FUNCTION spi_tcl() RETURNS bigint LANGUAGE pltcl AS $$
set s 0
spi_exec "select val from bench_rows" { set s [expr {$s + $val}] }
return $s
$$;

-- 4. Array / composite marshaling: take an int[] and return each element + 1.
-- Exercises the type-conversion layer in both directions.
CREATE OR REPLACE FUNCTION arr_ruby(a int[]) RETURNS int[] LANGUAGE plruby AS $$
args[0].map { |x| x + 1 }
$$;
CREATE OR REPLACE FUNCTION arr_pgsql(a int[]) RETURNS int[] LANGUAGE plpgsql AS $$
BEGIN RETURN ARRAY(SELECT x + 1 FROM unnest(a) AS x); END;
$$;
CREATE OR REPLACE FUNCTION arr_perl(a int[]) RETURNS int[] LANGUAGE plperl AS $$
return [ map { $_ + 1 } @{$_[0]} ];
$$;
CREATE OR REPLACE FUNCTION arr_python(a int[]) RETURNS int[] LANGUAGE plpython3u AS $$
return [x + 1 for x in a]
$$;
-- PL/Tcl passes an array argument as its PostgreSQL text form ("{1,2,3}"),
-- not a Tcl list, so parse and rebuild the literal explicitly.
CREATE OR REPLACE FUNCTION arr_tcl(a int[]) RETURNS int[] LANGUAGE pltcl AS $$
set out {}
foreach x [split [string trim $1 "{}"] ","] { lappend out [expr {$x + 1}] }
return "{[join $out ,]}"
$$;
77 changes: 77 additions & 0 deletions doc/benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# PL/Ruby performance

How fast is PL/Ruby compared to the built-in procedural languages and its
scripting-language peers? These are the first published numbers for the
extension. Reproduce them any time with the committed suite:

```sh
PGPORT=5432 sh bench/run.sh
```

The harness loads `bench/setup.sql` — the same four workloads written in
PL/Ruby, PL/pgSQL, PL/Perl, PL/Python and PL/Tcl with matching semantics — and
times each function under `pgbench -c 1`.

## Results

PostgreSQL 18.4, Ruby 3.2 (MRI, embedded), single client, `pgbench -T 8`, one
warm session, Ubuntu 24.04 container on x86-64. Peers: PL/Perl (Perl 5.38),
PL/Python (Python 3.12), PL/Tcl (Tcl 8.6). Transactions per second; higher is
better. Treat ±5% as noise (the SPI and array rows are the noisiest).

| Benchmark | PL/Ruby | PL/pgSQL | PL/Perl | PL/Python | PL/Tcl |
|---|---:|---:|---:|---:|---:|
| Call overhead (return argument) | 46,000 | 55,000 | 53,000 | 51,000 | 53,000 |
| String ops (reverse+upper+length) | 34,500 | 36,000 | 36,000 | 36,000 | 35,000 |
| SPI loop over 1,000 rows | 2,600 | 6,600 | 2,400 | 3,800 | 2,500 |
| Array marshaling (int[100] + 1) | 18,600 | 25,000 | 15,900 | 19,600 | 17,600 |

## Reading the numbers

- **Compute is call-overhead-bound, and PL/Ruby is in the pack.** On the string
workload all five languages sit within a few percent of each other: the
executor's function-call machinery dominates, not the interpreter. On the
trivial return-the-argument call, PL/Ruby is ~15% behind PL/pgSQL and
PL/Perl. That gap is the MRI entry path — every call crosses into the
interpreter through a protected `rb_eval`/`rb_protect` trampoline so that Ruby
exceptions become catchable PostgreSQL errors. It is a fixed per-call cost
that the string workload's actual work already amortizes away.

- **Row iteration is PL/pgSQL's home turf.** Its `FOR ... IN SELECT` loop
iterates natively without crossing a language boundary per row, so it leads
the SPI workload by more than 2x. Among the interpreted languages PL/Python
is fastest here because `plpy.execute` returns one materialized result whose
rows are read by cached column mapping; PL/Ruby, PL/Perl and PL/Tcl cluster
together (PL/Ruby marginally ahead of the other two). PL/Ruby's
`spi_fetch_row` builds a fresh `Hash` per row — one output-function call and
one String per column — which is the per-cell conversion cost, not anything
in the loop itself.

- **Array marshaling favors the languages with native array conversion.**
PL/pgSQL wins by doing `unnest`/`array_agg` in C. Among the scripting
languages PL/Ruby is second only to PL/Python and comfortably ahead of
PL/Perl and PL/Tcl: arguments arrive as a native Ruby `Array` and a returned
`Array` converts straight back, with no text round-trip. PL/Tcl pays to parse
the array's text form itself (it does not auto-convert array arguments to Tcl
lists), and PL/Perl trails despite its arrayref conversion.

## Guidance

- For pure computation, use whichever language reads best; the overhead
differences are small and the string-level work erases them.
- For tight loops over large results, prefer a set-based SQL statement (or
PL/pgSQL) when the logic allows. When you need Ruby's expressiveness per row,
`spi_query` with a block or `Cursor#each` keeps memory flat while paying the
same per-row conversion cost measured here.
- For array- and composite-heavy work, PL/Ruby's native `Array`/`Hash`
conversion is a genuine strength — the fastest of the interpreted PLs after
PL/Python, and without the text-parsing tax PL/Tcl carries.

## Notes on method

Each workload is a single prepared-ish statement driven by `pgbench -c 1` for a
fixed wall-clock window; the reported figure is `pgbench`'s own TPS. One client
keeps the comparison about per-call language cost rather than concurrency or
lock behavior. The functions are validated to return identical results across
all five languages before timing (see `bench/setup.sql`); if a language's
extension is absent, `bench/run.sh` reports it as `skipped` rather than failing.
Loading