Skip to content

Observing the Solar System

pg_orbit computes positions for all eight planets (VSOP87), the Sun, and the Moon (ELP2000-82B). Every observation returns the same topocentric type: azimuth, elevation, range, and range rate from a given observer at a given time. The solar system becomes queryable with standard SQL.

Knowing where planets are involves one of a few approaches:

  • Stellarium gives you a beautiful real-time sky view. You scrub time, click objects, read coordinates. Not scriptable, not batch-queryable.
  • JPL Horizons computes high-precision ephemerides via web form or API. Accurate to milliarcseconds. One object per request, rate-limited.
  • Skyfield (Python) loads JPL DE441 ephemerides and computes positions with sub-arcsecond accuracy. Excellent for one-off scripts; batch processing over large time ranges or many observers means writing loops.
  • Astropy provides coordinate frames, time systems, and ERFA wrappers. Powerful, but computing “what’s above the horizon right now” requires assembling several components.

All of these produce results that live outside your database. If you want to correlate planet positions with weather data, observation logs, or satellite passes, you export, import, and join.

All planets, the Sun, and the Moon are available as SQL function calls. The functions take an observer and a timestamp, and return topocentric coordinates. You can sweep all eight planets, generate time series, filter by elevation, and join with other tables in the same query.

Key functions:

FunctionWhat it computes
planet_observe(body_id, observer, time)Topocentric az/el/range for a planet
planet_heliocentric(body_id, time)Heliocentric ecliptic J2000 position (AU)
sun_observe(observer, time)Topocentric Sun position
moon_observe(observer, time)Topocentric Moon position (ELP2000-82B)

Body IDs follow the VSOP87 convention: 1=Mercury, 2=Venus, 3=Earth, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune. Body 0 returns the Sun at the heliocentric origin (all zeros).

  • VSOP87 accuracy is about 1 arcsecond. JPL DE441 (used by Skyfield and SPICE) achieves 0.001 arcsecond. For visual observation planning, 1 arcsecond is more than sufficient. For pointing a dish at GHz frequencies or precision astrometry, use SPICE.
  • ELP2000-82B accuracy is about 10 arcseconds for the Moon. Good enough for knowing when the Moon is up, what phase it is in, and whether it will interfere with observations. Not sufficient for occultation timing.
  • No light-time iteration. pg_orbit computes geometric positions, not apparent positions. The difference matters at the milliarcsecond level.
  • No atmospheric refraction. Objects near the horizon appear slightly higher than their geometric position. pg_orbit does not apply refraction corrections.

The simplest possible observation query:

SELECT topo_azimuth(t) AS azimuth,
topo_elevation(t) AS elevation,
topo_range(t) / 149597870.7 AS distance_au
FROM planet_observe(5, '40.0N 105.3W 1655m'::observer, now()) t;

Body ID 5 is Jupiter. The range comes back in km; dividing by 149,597,870.7 converts to AU.

Sweep all eight planets plus the Sun and Moon. Filter to objects above the horizon:

SELECT CASE body_id
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
END AS planet,
round(topo_azimuth(obs)::numeric, 1) AS az,
round(topo_elevation(obs)::numeric, 1) AS el,
round((topo_range(obs) / 149597870.7)::numeric, 3) AS dist_au
FROM generate_series(1, 8) AS body_id,
LATERAL planet_observe(body_id, '40.0N 105.3W 1655m'::observer,
'2024-06-21 04:00:00+00') obs
WHERE body_id != 3 -- cannot observe Earth from Earth
AND topo_elevation(obs) > 0
ORDER BY topo_elevation(obs) DESC;

Solar system status: heliocentric distances

Section titled “Solar system status: heliocentric distances”

See where every planet is relative to the Sun:

SELECT body_id,
CASE body_id
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
END AS planet,
round(helio_distance(planet_heliocentric(body_id, now()))::numeric, 4) AS dist_au,
round(helio_x(planet_heliocentric(body_id, now()))::numeric, 4) AS x_au,
round(helio_y(planet_heliocentric(body_id, now()))::numeric, 4) AS y_au,
round(helio_z(planet_heliocentric(body_id, now()))::numeric, 4) AS z_au
FROM generate_series(1, 8) AS body_id;

The heliocentric coordinates are in the ecliptic J2000 frame. X points toward the vernal equinox, Z toward the north ecliptic pole.

Track Jupiter’s elevation from sunset to sunrise:

SELECT t,
round(topo_elevation(
planet_observe(5, '40.0N 105.3W 1655m'::observer, t)
)::numeric, 1) AS jupiter_el,
round(topo_azimuth(
planet_observe(5, '40.0N 105.3W 1655m'::observer, t)
)::numeric, 1) AS jupiter_az
FROM generate_series(
'2024-06-21 02:00:00+00'::timestamptz, -- ~8pm MDT
'2024-06-21 12:00:00+00'::timestamptz, -- ~6am MDT
interval '30 minutes'
) AS t
WHERE topo_elevation(
planet_observe(5, '40.0N 105.3W 1655m'::observer, t)
) > 0;

This produces a time series of Jupiter’s position through the night, filtered to only the hours it is above the horizon. Replace body ID 5 with any other planet.

Useful for solar panel analysis, sunrise/sunset approximation, or photography planning:

SELECT t,
round(topo_azimuth(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS az,
round(topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS el
FROM generate_series(
'2024-06-21 12:00:00+00'::timestamptz, -- ~6am MDT
'2024-06-22 03:00:00+00'::timestamptz, -- ~9pm MDT
interval '15 minutes'
) AS t;

At the summer solstice from Boulder, the Sun reaches about 73 degrees elevation at local noon, rising in the northeast and setting in the northwest.

The Moon’s distance varies between about 356,000 km (perigee) and 407,000 km (apogee):

SELECT t::date AS date,
round(topo_range(moon_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS range_km
FROM generate_series(
'2024-01-01'::timestamptz,
'2024-02-01'::timestamptz,
interval '1 day'
) AS t;

Compare planet visibility from two different locations:

WITH observers AS (
SELECT 'Boulder, CO' AS location, '40.0N 105.3W 1655m'::observer AS obs
UNION ALL
SELECT 'Sydney, AU', '33.9S 151.2E 58m'::observer
)
SELECT o.location,
CASE body_id
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
END AS planet,
round(topo_elevation(
planet_observe(body_id, o.obs, '2024-06-21 10:00:00+00')
)::numeric, 1) AS elevation
FROM observers o,
generate_series(5, 6) AS body_id
ORDER BY o.location, body_id;

Earth’s distance from the Sun should be about 0.983 AU at perihelion (early January) and 1.017 AU at aphelion (early July):

SELECT 'perihelion' AS point,
round(helio_distance(
planet_heliocentric(3, '2024-01-03 12:00:00+00')
)::numeric, 4) AS earth_au
UNION ALL
SELECT 'aphelion',
round(helio_distance(
planet_heliocentric(3, '2024-07-05 12:00:00+00')
)::numeric, 4);

This is a useful sanity check when verifying the extension is installed correctly.

When does the Sun cross specific elevation thresholds? Find solar noon and the elevation at specific times:

-- Sample the Sun every minute around local noon to find peak elevation
SELECT t,
round(topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 2) AS el
FROM generate_series(
'2024-06-21 17:30:00+00'::timestamptz,
'2024-06-21 18:30:00+00'::timestamptz,
interval '1 minute'
) AS t
ORDER BY topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t)) DESC
LIMIT 5;

The highest elevation reading approximates solar noon. For Boulder at the summer solstice, expect about 73 degrees.