Skip to content

Benchmarks

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

OperationCountTimeRateNotes
TLE propagation (SGP4)12,00017 ms706K/secMixed LEO/MEO/GEO
Visibility cone filter (&?)66,44012.1 ms5.5M/sec84% pruned (2h, 10°), no SGP4
Conjunction screening (&&)66,4404.6 msISS: 9 co-orbital objects found
KNN orbital distance (<->)66,4402.1 ms10 nearest to ISS, 2-D index-ordered
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
Star with proper motion + parallax8,761343 ms25.5K/secProper motion + annual parallax (VSOP87 Earth)
Planet equatorial + apparent1,414107 ms13.2K/secRA/Dec geometric + light-time + aberration
Angular distance (<-> equatorial)10,0007 ms1.43M/secVincenty formula on equatorial pairs
Atmospheric refraction18,2003.5 ms5.2M/secBennett (1982) + P/T correction
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 18.1, 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_orrery 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.

Star observation with proper motion and parallax

Section titled “Star observation with proper motion and parallax”

The full stellar pipeline: proper motion correction (RA/Dec rates), annual parallax displacement from VSOP87 Earth position, IAU 1976 precession, sidereal time, and equatorial output.

-- Benchmark: proper motion + parallax for stars across a year of epochs
EXPLAIN (ANALYZE)
SELECT star_equatorial_pm(
mod(a * 1.5, 24.0), mod(a * 0.7, 180.0) - 90.0,
a * 0.1, a * 0.05, a * 10.0, a * 0.01,
'2024-01-01'::timestamptz + (a || ' hours')::interval
)
FROM generate_series(1, 8761) AS a;

8,761 evaluations in 343 ms --- 25,500 per second.

When parallax_mas > 0, each call adds a GetVsop87Coor() evaluation for Earth’s heliocentric position to compute the annual parallax displacement. This makes it ~28x slower than basic star observation (which skips VSOP87 entirely). The cost is dominated by the single VSOP87 Earth call per star --- the parallax displacement math itself is negligible.

For catalogs where parallax is zero or unknown (most survey catalogs), star_equatorial() without proper motion runs at the full 714K/sec rate.

Equatorial output (RA/Dec) for planets, including the _apparent() pipeline that adds light-time correction and annual stellar aberration.

-- Benchmark: equatorial + apparent for all non-Earth planets
EXPLAIN (ANALYZE)
SELECT planet_equatorial(body_id, t),
planet_equatorial_apparent(body_id, t)
FROM generate_series(1, 8) AS body_id,
generate_series(
'2024-01-01'::timestamptz,
'2024-01-01'::timestamptz + interval '100 hours',
interval '1 hour'
) AS t
WHERE body_id != 3;

1,414 evaluations (707 geometric + 707 apparent) in 107 ms --- 13,200 per second.

The _apparent() variant adds two operations beyond the geometric pipeline: iterating on light-travel time (1—2 iterations to converge) and applying annual stellar aberration from Earth’s VSOP87 velocity vector. The aberration calculation itself is cheap (~20 ns) but the extra VSOP87 evaluation for the retarded position roughly doubles the cost per call.

The <-> operator on the equatorial type computes angular separation in degrees using the Vincenty formula, which is numerically stable at all separations including 0° and 180°.

-- Benchmark: angular distance between 10,000 star pairs
EXPLAIN (ANALYZE)
SELECT eq_angular_distance(
star_equatorial(mod(a * 1.3, 24.0), mod(a * 0.7, 180.0) - 90.0, now()),
star_equatorial(mod(a * 2.1, 24.0), mod(a * 0.9, 180.0) - 90.0, now())
)
FROM generate_series(1, 10000) AS a;

10,000 angular distances in 7 ms --- 1.43 million per second.

The Vincenty formula involves four trigonometric evaluations and an atan2 --- comparable cost to a single coordinate transform. The eq_within_cone() predicate is even faster because it uses a cosine shortcut (cos(dist) >= cos(radius)) that avoids the atan2.

Bennett’s (1982) empirical formula for atmospheric refraction, with optional pressure/temperature correction.

-- Benchmark: refraction + P/T-corrected refraction for elevation range
EXPLAIN (ANALYZE)
SELECT atmospheric_refraction(a * 0.01),
atmospheric_refraction_ext(a * 0.01, 1013.25, 15.0)
FROM generate_series(1, 9100) AS a;

18,200 evaluations (9,100 base + 9,100 extended) in 3.5 ms --- 5.2 million per second.

Refraction is pure arithmetic --- a single cot() approximation with a polynomial correction term. No series evaluation, no iteration. The _ext() variant adds a pressure/temperature scaling factor (one division). This makes refraction essentially free when layered onto the observation pipeline.

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.

The &? operator answers “could this satellite possibly be visible from this observer?” using three geometric filters (altitude, inclination, RAAN) without any SGP4 propagation. This is the first stage of the pass prediction pipeline, reducing the number of satellites that need full propagation.

-- Benchmark: filter a 66,440-object catalog
-- Eagle, Idaho: 2-hour window, 10 deg minimum elevation
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM satellite_catalog
WHERE tle &? ROW(
observer('43.6977N 116.3535W 760m'),
'2024-01-01 02:00:00+00'::timestamptz,
'2024-01-01 04:00:00+00'::timestamptz,
10.0
)::observer_window;

66,440 TLEs filtered in 12.1 ms --- 83.8% pruned, 10,763 candidates survive.

The operator evaluates three geometric conditions per TLE: perigee altitude vs. maximum visible altitude, inclination + ground footprint vs. observer latitude, and RAAN alignment via J2 secular precession. Each check is a few floating-point operations --- no SGP4 initialization, no Kepler equation, no trigonometric series.

Measured against a 66,440-object catalog merged from Space-Track, CelesTrak, SatNOGS, and CelesTrak SupGP. The pruning rate depends on observer latitude, query window duration, and minimum elevation. Shorter windows and higher minimum elevations prune more aggressively.

QueryCandidatesPrunedNotes
2h, Eagle ID (43.7°N), 10°10,76383.8%Typical mid-latitude evening
2h, Equator (0°N), 10°10,17484.7%All inclinations pass latitude check; RAAN filter dominates
2h, Eagle ID, 45°6,79689.8%Higher elevation: altitude filter tighter
24h, Eagle ID, 10°61,4267.5%RAAN filter bypassed (full Earth rotation)

The optional SP-GiST index (tle_spgist_ops) builds a 2-level trie partitioned by semi-major axis and inclination. At 66,440 objects, sequential evaluation of the &? operator (12 ms) is faster than the SP-GiST index scan (16—23 ms). The tree traversal overhead exceeds the pruning benefit at this catalog size because the &? operator itself is so cheap --- three floating-point comparisons per TLE.

QuerySeqscanSP-GiSTCandidatesPruned
2h, Eagle ID, 10°12.1 ms16.1 ms10,76383.8%
2h, Equator, 10°12.1 ms16.8 ms10,17484.7%
2h, Eagle ID, 45°11.9 ms16.9 ms6,79689.8%
24h, Eagle ID, 10°12.5 ms23.3 ms61,4267.5%

The SP-GiST index achieves zero heap fetches (pure Index Only Scan), but page traversal through 11 MB of index data (4,964 buffer hits) exceeds the cost of a 1,338-buffer sequential scan.

What the pruning means for predict_passes()

Section titled “What the pruning means for predict_passes()”

For a 66,440-object catalog and a 2-hour window from Eagle, Idaho:

  • Without &?: 66,440 predict_passes() calls (each ~1 ms for a 7-day window)
  • With &?: 10,763 calls --- 55,677 unnecessary propagations avoided
  • Time saved: ~56 seconds per query at typical propagation cost

The GiST index on the tle type enables indexed conjunction screening using the && (overlap) operator. The index stores altitude band and inclination for each TLE, allowing PostgreSQL to skip entire subtrees of non-overlapping orbits.

-- Benchmark: find ISS conjunction candidates in a 66,440-object catalog
EXPLAIN (ANALYZE, BUFFERS)
SELECT b.name
FROM satellite_catalog a
JOIN satellite_catalog b ON a.tle && b.tle AND a.norad_id != b.norad_id
WHERE tle_norad_id(a.tle) = 25544;

9 co-orbital objects found in 4.6 ms (vs. 63.3 ms sequential scan --- 5.8x speedup).

The GiST index scan hits 237 buffers compared to 1,338 for a sequential scan. The 9 objects returned are all ISS-visiting vehicles or co-orbital modules: PROGRESS MS-31, PROGRESS MS-32, SOYUZ MS-28, DRAGON FREEDOM 3, DRAGON CRS-33, CYGNUS NG-23, HTV-X1, ISS (NAUKA), and OBJECT E.

Probe satelliteGiSTSeqscanMatchesNotes
ISS (LEO, 51.6°)4.6 ms63.3 ms9Co-orbital vehicles
Starlink-230369 (LEO, 53°)9.5 ms14.9 ms0Dense LEO shell
SYNCOM 2 (GEO, 33°)4.0 ms7.2 ms0Sparse regime

The GiST index provides the largest speedup for queries that return few matches, where the index prunes most of the tree without reading leaf pages. Dense LEO shells produce more candidates and reduce the speedup ratio.

MetricValue
Build time93 ms (66,440 TLEs)
Index size15 MB (237 bytes/object)
Consistency0 false positives, 0 false negatives (verified against seqscan)

The <-> operator computes 2-D orbital distance in km, combining altitude-band separation with inclination gap (converted to km via Earth radius). With a GiST index, it supports index-ordered KNN queries --- PostgreSQL traverses the tree by increasing distance without computing all distances upfront.

-- Benchmark: 10 nearest orbits to the ISS by 2-D orbital distance
EXPLAIN (ANALYZE, BUFFERS)
SELECT name,
round((tle <-> (SELECT tle FROM satellite_catalog
WHERE tle_norad_id(tle) = 25544 LIMIT 1))::numeric, 1)
AS orbital_dist_km
FROM satellite_catalog
ORDER BY tle <-> (SELECT tle FROM satellite_catalog
WHERE tle_norad_id(tle) = 25544 LIMIT 1)
LIMIT 10;

10 nearest in 2.1 ms, index-ordered (982 buffer hits).

QueryTimeBuffersNotes
10 nearest to ISS (LEO)2.1 ms982Dense regime, 2-D distance breaks altitude ties
10 nearest to SYNCOM 2 (GEO)0.2 ms40Sparse regime, fewer nodes
100 nearest to ISS1.4 ms1,062Marginal cost per additional neighbor
All within 50 km of ISS16.0 ms4,01412,496 matches
  • PostgreSQL 18 with pg_orrery installed
  • A satellite catalog table (the numbers on this page use a 66,440-object catalog merged from Space-Track, CelesTrak, SatNOGS, and CelesTrak SupGP; see Building TLE Catalogs)
  • GiST and SP-GiST indexes on the tle column for index benchmarks
  • 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_orrery’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 v0.10.0 additions --- aberration, angular distance, refraction --- range from negligible overhead (refraction at 5.2M/sec) to moderate (apparent positions at 13.2K/sec, roughly half the geometric rate due to the extra VSOP87 call for light-time iteration). Angular distance at 1.43M/sec means cone-search predicates over star catalogs are fast even without index support.

The visibility cone filter (&?) is the fastest operation per evaluation --- three floating-point comparisons vs. the full SGP4 pipeline --- and its 84—90% pruning rate means the most expensive operation in a pass prediction pipeline (SGP4 propagation) only runs on the small fraction of the catalog that could actually produce a visible pass.

The GiST index provides the clearest speedup for conjunction screening: 5.8x faster than sequential scan for ISS && queries, with 0 false positives or negatives verified against exhaustive sequential evaluation. KNN queries find the nearest orbits in 2 ms via index-ordered traversal using 2-D orbital distance (altitude + inclination), which would otherwise require computing and sorting all 66,440 distances.

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