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.
Summary table
Section titled “Summary table”| Operation | Count | Time | Rate | Notes |
|---|---|---|---|---|
| TLE propagation (SGP4) | 12,000 | 17 ms | 706K/sec | Mixed LEO/MEO/GEO |
Visibility cone filter (&?) | 66,440 | 12.1 ms | 5.5M/sec | 84% pruned (2h, 10°), no SGP4 |
Conjunction screening (&&) | 66,440 | 4.6 ms | — | ISS: 9 co-orbital objects found |
KNN orbital distance (<->) | 66,440 | 2.1 ms | — | 10 nearest to ISS, 2-D index-ordered |
| 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 |
| Star with proper motion + parallax | 8,761 | 343 ms | 25.5K/sec | Proper motion + annual parallax (VSOP87 Earth) |
| Planet equatorial + apparent | 1,414 | 107 ms | 13.2K/sec | RA/Dec geometric + light-time + aberration |
Angular distance (<-> equatorial) | 10,000 | 7 ms | 1.43M/sec | Vincenty formula on equatorial pairs |
| Atmospheric refraction | 18,200 | 3.5 ms | 5.2M/sec | Bennett (1982) + P/T correction |
| 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 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.
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_orrery 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.
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 epochsEXPLAIN (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.
Planet equatorial and apparent positions
Section titled “Planet equatorial and apparent positions”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 planetsEXPLAIN (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 tWHERE 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.
Angular distance (<-> on equatorial)
Section titled “Angular distance (<-> on equatorial)”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 pairsEXPLAIN (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.
Atmospheric refraction
Section titled “Atmospheric refraction”Bennett’s (1982) empirical formula for atmospheric refraction, with optional pressure/temperature correction.
-- Benchmark: refraction + P/T-corrected refraction for elevation rangeEXPLAIN (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.
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.
Visibility cone filtering (&? operator)
Section titled “Visibility cone filtering (&? operator)”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 elevationEXPLAIN (ANALYZE, BUFFERS)SELECT count(*)FROM satellite_catalogWHERE 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.
Pruning rate by query pattern
Section titled “Pruning rate by query pattern”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.
| Query | Candidates | Pruned | Notes |
|---|---|---|---|
| 2h, Eagle ID (43.7°N), 10° | 10,763 | 83.8% | Typical mid-latitude evening |
| 2h, Equator (0°N), 10° | 10,174 | 84.7% | All inclinations pass latitude check; RAAN filter dominates |
| 2h, Eagle ID, 45° | 6,796 | 89.8% | Higher elevation: altitude filter tighter |
| 24h, Eagle ID, 10° | 61,426 | 7.5% | RAAN filter bypassed (full Earth rotation) |
SP-GiST index performance
Section titled “SP-GiST index performance”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.
| Query | Seqscan | SP-GiST | Candidates | Pruned |
|---|---|---|---|---|
| 2h, Eagle ID, 10° | 12.1 ms | 16.1 ms | 10,763 | 83.8% |
| 2h, Equator, 10° | 12.1 ms | 16.8 ms | 10,174 | 84.7% |
| 2h, Eagle ID, 45° | 11.9 ms | 16.9 ms | 6,796 | 89.8% |
| 24h, Eagle ID, 10° | 12.5 ms | 23.3 ms | 61,426 | 7.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,440predict_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
Conjunction screening (&& operator)
Section titled “Conjunction screening (&& operator)”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 catalogEXPLAIN (ANALYZE, BUFFERS)SELECT b.nameFROM satellite_catalog aJOIN satellite_catalog b ON a.tle && b.tle AND a.norad_id != b.norad_idWHERE 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.
GiST && performance by orbital regime
Section titled “GiST && performance by orbital regime”| Probe satellite | GiST | Seqscan | Matches | Notes |
|---|---|---|---|---|
| ISS (LEO, 51.6°) | 4.6 ms | 63.3 ms | 9 | Co-orbital vehicles |
| Starlink-230369 (LEO, 53°) | 9.5 ms | 14.9 ms | 0 | Dense LEO shell |
| SYNCOM 2 (GEO, 33°) | 4.0 ms | 7.2 ms | 0 | Sparse 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.
Index characteristics
Section titled “Index characteristics”| Metric | Value |
|---|---|
| Build time | 93 ms (66,440 TLEs) |
| Index size | 15 MB (237 bytes/object) |
| Consistency | 0 false positives, 0 false negatives (verified against seqscan) |
KNN orbital distance (<-> operator)
Section titled “KNN orbital distance (<-> operator)”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 distanceEXPLAIN (ANALYZE, BUFFERS)SELECT name, round((tle <-> (SELECT tle FROM satellite_catalog WHERE tle_norad_id(tle) = 25544 LIMIT 1))::numeric, 1) AS orbital_dist_kmFROM satellite_catalogORDER 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).
KNN performance by scenario
Section titled “KNN performance by scenario”| Query | Time | Buffers | Notes |
|---|---|---|---|
| 10 nearest to ISS (LEO) | 2.1 ms | 982 | Dense regime, 2-D distance breaks altitude ties |
| 10 nearest to SYNCOM 2 (GEO) | 0.2 ms | 40 | Sparse regime, fewer nodes |
| 100 nearest to ISS | 1.4 ms | 1,062 | Marginal cost per additional neighbor |
| All within 50 km of ISS | 16.0 ms | 4,014 | 12,496 matches |
Reproducing these benchmarks
Section titled “Reproducing these benchmarks”- 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
tlecolumn for index benchmarks - 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_orrery;
-- Load a TLE catalog (pg-orrery-catalog handles this)-- pg-orrery-catalog build --table satellite_catalog | psql -d mydbCREATE TABLE satellite_catalog (name text, tle tle);-- (or COPY from CelesTrak bulk TLE file)
-- Create both indexes for full benchmark coverageCREATE INDEX idx_tle_gist ON satellite_catalog USING gist (tle tle_ops);CREATE INDEX idx_tle_spgist ON satellite_catalog USING spgist (tle tle_spgist_ops);
-- Verify catalog sizeSELECT count(*) FROM satellite_catalog;-- The numbers on this page use 66,440 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_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.