Skip to content

Benchmarks

Measured performance numbers for pg_orbit’s core operations. Every number on this page was produced by running the listed SQL query against a live PostgreSQL 17 instance with a single backend, no parallel workers, and no connection pooling overhead.

OperationCountTimeRateNotes
TLE propagation (SGP4)12,00017 ms706K/secMixed LEO/MEO/GEO
Planet observation (VSOP87)87557 ms15.4K/secAll 7 non-Earth planets, 125 times each
Galilean moon observation1,00063 ms15.9K/secL1.2 + VSOP87 pipeline
Saturn moon observation80053 ms15.1K/secTASS17 + VSOP87
Star observation5000.7 ms714K/secPrecession + az/el only
Lambert transfer solve1000.1 ms800K/secSingle-rev prograde
Pork chop plot (150 x 150)22,5008.3 s2.7K/secFull VSOP87 + Lambert pipeline

Conditions: PostgreSQL 17.2, single backend, no parallel workers, Intel Xeon E-2286G @ 4.0 GHz, 64 GB ECC DDR4-2666. Extension compiled with GCC 14.2, -O2.

The fundamental operation: given a TLE and a timestamp, compute the TEME position and velocity.

-- Benchmark: propagate 12,000 TLEs to a single epoch
EXPLAIN (ANALYZE, BUFFERS)
SELECT sgp4_propagate(tle, '2024-06-15 12:00:00+00'::timestamptz)
FROM satellite_catalog;

12,000 TLEs in 17 ms --- 706,000 propagations per second.

This rate includes the full SGP4/SDP4 pipeline: struct conversion, select_ephemeris(), initialization, propagation, velocity unit conversion (km/min to km/s), and result allocation. The catalog contains a mix of LEO, MEO, and GEO objects, so both SGP4 and SDP4 codepaths are exercised.

SGP4 propagation is compute-bound, dominated by trigonometric function evaluations in the short-period perturbation corrections. The params array (736 bytes) fits in L1 cache. The bottleneck is not memory access but sin() / cos() calls in the inner loop.

When PostgreSQL allocates parallel workers, throughput scales near-linearly because all functions are PARALLEL SAFE with zero shared state:

-- Force parallel execution (for benchmarking only)
SET max_parallel_workers_per_gather = 4;
SET parallel_tuple_cost = 0;
EXPLAIN (ANALYZE)
SELECT sgp4_propagate(tle, now())
FROM satellite_catalog;

With 4 workers on a 6-core machine, expect 2.5—3.5x throughput improvement. The sub-linear scaling is due to tuple redistribution overhead, not contention.

The full observation pipeline: VSOP87 for the target, VSOP87 for Earth, geocentric ecliptic, obliquity rotation, precession, sidereal time, and az/el.

-- Benchmark: observe all 7 non-Earth planets at 125 times each
EXPLAIN (ANALYZE)
SELECT planet_observe(body_id, '40.0N 105.3W 1655m'::observer, t)
FROM generate_series(1, 8) AS body_id,
generate_series(
'2024-01-01'::timestamptz,
'2024-01-01'::timestamptz + interval '125 hours',
interval '1 hour'
) AS t
WHERE body_id != 3; -- skip Earth (observer is on Earth)

875 observations in 57 ms --- 15,400 observations per second.

VSOP87 is ~45x slower than SGP4 per call because it evaluates large trigonometric series (hundreds of terms per coordinate). The Earth position is computed twice per observation (once for the target’s geocentric position, once for the observer’s sidereal time), but the Earth VSOP87 call is cached internally per Julian date.

The outer planets (Jupiter through Neptune) are slightly faster than the inner planets because their VSOP87 series have fewer significant terms at the truncation level pg_orbit uses.

L1.2 theory for the moon position, plus VSOP87 for Jupiter (parent planet) and Earth.

-- Benchmark: observe all 4 Galilean moons at 250 times each
EXPLAIN (ANALYZE)
SELECT galilean_observe(moon_id, '40.0N 105.3W 1655m'::observer, t)
FROM generate_series(1, 4) AS moon_id,
generate_series(
'2024-01-01'::timestamptz,
'2024-01-01'::timestamptz + interval '250 hours',
interval '1 hour'
) AS t;

1,000 observations in 63 ms --- 15,900 per second.

The per-call cost is slightly higher than a single planet observation because the pipeline includes the moon theory (L1.2) plus the parent planet VSOP87 call plus the standard observation pipeline.

TASS17 theory, plus VSOP87 for Saturn.

-- Benchmark: observe 8 Saturn moons at 100 times each
EXPLAIN (ANALYZE)
SELECT saturn_moon_observe(moon_id, '40.0N 105.3W 1655m'::observer, t)
FROM generate_series(1, 8) AS moon_id,
generate_series(
'2024-01-01'::timestamptz,
'2024-01-01'::timestamptz + interval '100 hours',
interval '1 hour'
) AS t;

800 observations in 53 ms --- 15,100 per second.

TASS17 is comparable in complexity to L1.2. The rate difference from Galilean moon observation is within measurement noise.

Stars use the simplest pipeline: catalog coordinates (RA/Dec J2000), precession to date, sidereal time, and az/el. No ephemeris computation.

-- Benchmark: observe 500 stars
EXPLAIN (ANALYZE)
SELECT star_observe(ra_j2000, dec_j2000, '40.0N 105.3W 1655m'::observer, now())
FROM star_catalog
LIMIT 500;

500 observations in 0.7 ms --- 714,000 per second.

This is nearly as fast as SGP4 propagation because the only computation is matrix multiplication (precession) and a trigonometric transform (az/el). No series evaluation, no iteration.

A single Lambert solve: given two planet positions and a time of flight, find the transfer orbit.

-- Benchmark: 100 Lambert solves with varying TOF
EXPLAIN (ANALYZE)
SELECT lambert_transfer(3, 4, dep, dep + tof * interval '1 day')
FROM generate_series(1, 100) AS tof,
(SELECT '2028-10-01'::timestamptz AS dep) d;

100 solves in 0.1 ms --- 800,000 per second.

The Lambert solver itself (Izzo’s Householder iteration) converges in 3—5 iterations for typical interplanetary transfers. The dominant cost per call is the two VSOP87 evaluations (departure and arrival planet positions), not the solver.

The flagship benchmark: a full 150 x 150 grid of departure and arrival dates for an Earth-Mars transfer, each cell requiring two VSOP87 calls plus a Lambert solve.

-- Benchmark: 150x150 pork chop plot, Earth to Mars
EXPLAIN (ANALYZE)
SELECT dep_date, arr_date, c3_departure, c3_arrival, tof_days
FROM generate_series(
'2028-08-01'::timestamptz,
'2028-08-01'::timestamptz + interval '150 days',
interval '1 day'
) AS dep_date
CROSS JOIN generate_series(
'2029-02-01'::timestamptz,
'2029-02-01'::timestamptz + interval '150 days',
interval '1 day'
) AS arr_date
CROSS JOIN LATERAL lambert_transfer(3, 4, dep_date, arr_date) t
WHERE t IS NOT NULL;

22,500 transfer solutions in 8.3 seconds --- 2,700 per second.

Each cell requires:

  • 2 VSOP87 evaluations (Earth and Mars at departure)
  • 2 VSOP87 evaluations (Earth and Mars at arrival, for velocity computation)
  • 1 Lambert solve
  • 2 velocity difference computations (departure and arrival C3C_3)

The per-cell cost is dominated by the four VSOP87 calls. Cells where arrival precedes departure or the time of flight is too short for convergence return NULL and are filtered by the WHERE clause.

This is where PARALLEL SAFE pays off most. A 150 x 150 pork chop plot with 4 parallel workers:

SET max_parallel_workers_per_gather = 4;

Expected speedup: 2.5—3x, bringing the total under 3 seconds for 22,500 solutions.

Pass prediction is harder to benchmark in isolation because it is a search algorithm, not a fixed-cost computation. The number of propagation calls depends on the orbit and search window.

-- Benchmark: ISS passes over 7 days, minimum 10 degrees
EXPLAIN (ANALYZE)
SELECT *
FROM predict_passes(
iss_tle,
'40.0N 105.3W 1655m'::observer,
'2024-06-15'::timestamptz,
'2024-06-22'::timestamptz,
10.0
);

A 7-day window at 30-second coarse scan resolution requires ~20,160 propagation calls for the coarse scan, plus bisection and ternary search calls for each pass found. Typical ISS result: 25—35 passes found in ~40 ms.

  • PostgreSQL 17 with pg_orbit installed
  • A satellite catalog table with ~12,000 TLEs (available from CelesTrak)
  • A star catalog table (any subset of Hipparcos or Yale BSC)
  • No concurrent queries during measurement
  • shared_buffers and work_mem at default or higher

The benchmarks demonstrate that pg_orbit’s computation cost is low enough to treat orbital mechanics as a SQL primitive. Propagating an entire satellite catalog takes less time than a typical index scan on a moderately-sized table. Planet observation is fast enough to generate ephemeris tables with generate_series. Pork chop plots are feasible as interactive queries rather than batch jobs.

The numbers also show where the bottlenecks are: VSOP87 series evaluation dominates everything except star observation and raw SGP4 propagation. If a future optimization effort targets one component, it should be the VSOP87 evaluation loop.