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.
Summary table
Section titled “Summary table”| Operation | Count | Time | Rate | Notes |
|---|---|---|---|---|
| TLE propagation (SGP4) | 12,000 | 17 ms | 706K/sec | Mixed LEO/MEO/GEO |
| Planet observation (VSOP87) | 875 | 57 ms | 15.4K/sec | All 7 non-Earth planets, 125 times each |
| Galilean moon observation | 1,000 | 63 ms | 15.9K/sec | L1.2 + VSOP87 pipeline |
| Saturn moon observation | 800 | 53 ms | 15.1K/sec | TASS17 + VSOP87 |
| Star observation | 500 | 0.7 ms | 714K/sec | Precession + az/el only |
| Lambert transfer solve | 100 | 0.1 ms | 800K/sec | Single-rev prograde |
| Pork chop plot (150 x 150) | 22,500 | 8.3 s | 2.7K/sec | Full 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.
TLE propagation
Section titled “TLE propagation”The fundamental operation: given a TLE and a timestamp, compute the TEME position and velocity.
-- Benchmark: propagate 12,000 TLEs to a single epochEXPLAIN (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.
What limits the rate
Section titled “What limits the rate”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.
Scaling with parallel workers
Section titled “Scaling with parallel workers”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.
Planet observation
Section titled “Planet observation”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 eachEXPLAIN (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 tWHERE 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.
Per-planet breakdown
Section titled “Per-planet breakdown”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.
Galilean moon observation
Section titled “Galilean moon observation”L1.2 theory for the moon position, plus VSOP87 for Jupiter (parent planet) and Earth.
-- Benchmark: observe all 4 Galilean moons at 250 times eachEXPLAIN (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.
Saturn moon observation
Section titled “Saturn moon observation”TASS17 theory, plus VSOP87 for Saturn.
-- Benchmark: observe 8 Saturn moons at 100 times eachEXPLAIN (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.
Star observation
Section titled “Star observation”Stars use the simplest pipeline: catalog coordinates (RA/Dec J2000), precession to date, sidereal time, and az/el. No ephemeris computation.
-- Benchmark: observe 500 starsEXPLAIN (ANALYZE)SELECT star_observe(ra_j2000, dec_j2000, '40.0N 105.3W 1655m'::observer, now())FROM star_catalogLIMIT 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.
Lambert transfer
Section titled “Lambert transfer”A single Lambert solve: given two planet positions and a time of flight, find the transfer orbit.
-- Benchmark: 100 Lambert solves with varying TOFEXPLAIN (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.
Pork chop plot
Section titled “Pork chop plot”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 MarsEXPLAIN (ANALYZE)SELECT dep_date, arr_date, c3_departure, c3_arrival, tof_daysFROM generate_series( '2028-08-01'::timestamptz, '2028-08-01'::timestamptz + interval '150 days', interval '1 day' ) AS dep_dateCROSS JOIN generate_series( '2029-02-01'::timestamptz, '2029-02-01'::timestamptz + interval '150 days', interval '1 day' ) AS arr_dateCROSS JOIN LATERAL lambert_transfer(3, 4, dep_date, arr_date) tWHERE 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 )
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.
Parallelization
Section titled “Parallelization”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
Section titled “Pass prediction”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 degreesEXPLAIN (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.
Reproducing these benchmarks
Section titled “Reproducing these benchmarks”- 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_buffersandwork_memat default or higher
CREATE EXTENSION pg_orbit;
-- Load a TLE catalogCREATE TABLE satellite_catalog (tle tle);-- (COPY from CelesTrak bulk TLE file)
-- Verify catalog sizeSELECT count(*) FROM satellite_catalog;-- Expected: ~12,000 rows
-- Disable parallel workers for baseline measurementSET max_parallel_workers_per_gather = 0;-- Run each benchmark query three times-- Discard the first run (cold start)-- Report the median of runs 2 and 3
-- Example:EXPLAIN (ANALYZE, BUFFERS, TIMING)SELECT sgp4_propagate(tle, now())FROM satellite_catalog;Use EXPLAIN (ANALYZE) rather than client-side timing to exclude network latency and result serialization overhead. The Execution Time line in the EXPLAIN output is the number to report.
What these numbers mean
Section titled “What these numbers mean”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.