Star Catalogs
pg_orbit computes topocentric positions for any star given its J2000 right ascension and declination. Feed it a star catalog table and you can observe hundreds of thousands of stars in a single query. The function applies IAU 1976 precession to bring J2000 coordinates to the observation epoch, then transforms to horizon coordinates for a given observer.
How you do it today
Section titled “How you do it today”Computing where a star appears in the sky involves:
- Stellarium: Type a star name, get its current position. Not queryable, not batchable.
- Astropy + catalog: Load the Hipparcos or Tycho-2 catalog, apply precession/nutation/aberration, transform to alt-az. Accurate, but per-object Python calls.
- Skyfield: Wraps the Hipparcos catalog with high-precision coordinate transforms. Clean API, but processing a full catalog means iterating over rows.
- SIMBAD/VizieR: Query astronomical databases for catalog data. Returns J2000 coordinates; you still need to transform to local horizon coordinates yourself.
The bottleneck is the same as with planets: the computation happens outside your database. If your observation log, scheduling system, or data pipeline lives in PostgreSQL, you export catalog data, compute positions externally, and import the results.
What changes with pg_orbit
Section titled “What changes with pg_orbit”star_observe() takes J2000 RA (in hours) and Dec (in degrees), an observer, and a time. It returns a topocentric with azimuth, elevation, and zero range (stars are treated as infinitely distant). The function applies IAU 1976 precession and the standard equatorial-to-horizontal transform.
star_observe_safe() does the same but returns NULL for invalid inputs (RA outside 0-24 hours, Dec outside +/-90 degrees). Use it for batch queries over catalog tables that might contain bad rows.
The key performance characteristic: star observation processes at about 714,000 observations per second. A 100,000-star catalog can be fully observed from any location at any time in under 150ms.
What pg_orbit does not replace
Section titled “What pg_orbit does not replace”- No nutation. IAU 1976 precession alone introduces errors up to ~10 arcseconds over a few decades. For visual observation planning, this is negligible. For sub-arcsecond work, use SOFA/ERFA routines.
- No proper motion. Barnard’s Star moves 10 arcseconds/year. pg_orbit treats catalog coordinates as fixed. If your catalog includes proper motion columns, you can pre-apply the correction in SQL before calling
star_observe(). - No aberration. Annual aberration displaces star positions by up to ~20 arcseconds. This matters for precision pointing but not for finding stars at the eyepiece.
- No parallax. Stellar parallax is at most ~0.8 arcseconds (Proxima Centauri). Not a concern for observation planning.
- Range is zero. Stars are treated as infinitely far. The
topo_range()accessor returns 0 for star observations.
Try it
Section titled “Try it”Observe well-known stars
Section titled “Observe well-known stars”The bright navigational stars and their J2000 coordinates:
-- Polaris: RA 2h 31m 49s = 2.530303h, Dec +89.2641 degSELECT 'Polaris' AS star, round(topo_azimuth(star_observe( 2.530303, 89.2641, '40.0N 105.3W 1655m'::observer, now()))::numeric, 1) AS az, round(topo_elevation(star_observe( 2.530303, 89.2641, '40.0N 105.3W 1655m'::observer, now()))::numeric, 1) AS el;From Boulder (latitude ~40 N), Polaris should be at roughly 40 degrees elevation, near due north (azimuth ~0/360).
-- Observe several bright stars at onceWITH stars(name, ra_h, dec_deg) AS (VALUES ('Polaris', 2.530303, 89.2641), ('Sirius', 6.752478, -16.7161), ('Vega', 18.615650, 38.7837), ('Betelgeuse', 5.919529, 7.4070), ('Rigel', 5.242299, -8.2016), ('Arcturus', 14.261027, 19.1824), ('Capella', 5.278155, 46.0076), ('Procyon', 7.655033, 5.2250))SELECT name, round(topo_azimuth(obs)::numeric, 1) AS az, round(topo_elevation(obs)::numeric, 1) AS elFROM stars, LATERAL star_observe(ra_h, dec_deg, '40.0N 105.3W 1655m'::observer, '2024-01-15 03:00:00+00') obsWHERE topo_elevation(obs) > 0ORDER BY topo_elevation(obs) DESC;This observes eight bright stars and filters to those above the horizon. The LATERAL keyword lets PostgreSQL call star_observe() once per star.
Build a star catalog table
Section titled “Build a star catalog table”For batch operations, store catalog data in a table. Here is a minimal schema using Hipparcos-style data:
CREATE TABLE star_catalog ( hip_id integer PRIMARY KEY, name text, ra_hours float8 NOT NULL, dec_deg float8 NOT NULL, vmag float8, -- visual magnitude spectral text);
-- Insert a few bright stars for demonstrationINSERT INTO star_catalog VALUES (11767, 'Polaris', 2.530303, 89.2641, 1.98, 'F7Ib'), (32349, 'Sirius', 6.752478, -16.7161, -1.46, 'A1V'), (91262, 'Vega', 18.615650, 38.7837, 0.03, 'A0V'), (27989, 'Betelgeuse', 5.919529, 7.4070, 0.42, 'M1Ia'), (24436, 'Rigel', 5.242299, -8.2016, 0.13, 'B8Ia'), (69673, 'Arcturus', 14.261027, 19.1824, -0.05, 'K1III'), (24608, 'Capella', 5.278155, 46.0076, 0.08, 'G8III'), (37279, 'Procyon', 7.655033, 5.2250, 0.34, 'F5IV'), (7588, 'Achernar', 1.628556, -57.2367, 0.46, 'B3V'), (80763, 'Antares', 16.490128, -26.4320, 0.96, 'M1Ib');Batch observe the catalog
Section titled “Batch observe the catalog”Observe every star in the catalog from a given location and time:
SELECT name, vmag, round(topo_azimuth(obs)::numeric, 1) AS az, round(topo_elevation(obs)::numeric, 1) AS elFROM star_catalog, LATERAL star_observe_safe(ra_hours, dec_deg, '40.0N 105.3W 1655m'::observer, '2024-01-15 03:00:00+00') obsWHERE obs IS NOT NULL AND topo_elevation(obs) > 0ORDER BY vmag;star_observe_safe() returns NULL if the catalog contains invalid coordinates, so the query runs cleanly over the full table. The WHERE obs IS NOT NULL clause filters those out.
What is visible tonight, brighter than magnitude 2?
Section titled “What is visible tonight, brighter than magnitude 2?”SELECT name, vmag, spectral, round(topo_azimuth(obs)::numeric, 1) AS az, round(topo_elevation(obs)::numeric, 1) AS elFROM star_catalog, LATERAL star_observe_safe(ra_hours, dec_deg, '40.0N 105.3W 1655m'::observer, '2024-07-15 04:00:00+00') obsWHERE obs IS NOT NULL AND topo_elevation(obs) > 10 AND vmag < 2.0ORDER BY vmag;This finds all bright stars above 10 degrees elevation from Boulder on a July evening. Replace the time and observer for your own conditions.
Track a star through the night
Section titled “Track a star through the night”Watch Vega rise, culminate, and set:
SELECT t, round(topo_azimuth(star_observe( 18.615650, 38.7837, '40.0N 105.3W 1655m'::observer, t ))::numeric, 1) AS az, round(topo_elevation(star_observe( 18.615650, 38.7837, '40.0N 105.3W 1655m'::observer, t ))::numeric, 1) AS elFROM generate_series( '2024-07-15 02:00:00+00'::timestamptz, '2024-07-15 12:00:00+00'::timestamptz, interval '30 minutes') AS tWHERE topo_elevation(star_observe( 18.615650, 38.7837, '40.0N 105.3W 1655m'::observer, t)) > 0;Vega culminates at nearly 89 degrees elevation from Boulder — it passes almost directly overhead on summer nights.
Precession demonstration
Section titled “Precession demonstration”The same star at J2000.0 epoch vs. 25 years later. IAU 1976 precession shifts the apparent position:
SELECT 'J2000.0 epoch' AS epoch, round(topo_elevation(star_observe( 2.530303, 89.2641, '40.0N 105.3W 1655m'::observer, '2000-01-01 12:00:00+00' ))::numeric, 2) AS polaris_el
UNION ALL
SELECT '2025 epoch', round(topo_elevation(star_observe( 2.530303, 89.2641, '40.0N 105.3W 1655m'::observer, '2025-06-15 04:00:00+00' ))::numeric, 2);The elevation changes by a fraction of a degree over 25 years. This is precession in action: the Earth’s rotational axis slowly traces a circle in space.
Cross-match with observation logs
Section titled “Cross-match with observation logs”If you keep an observation log in PostgreSQL, you can join it with star positions:
-- Hypothetical observation log tableCREATE TABLE obs_log ( id serial PRIMARY KEY, target_hip integer REFERENCES star_catalog(hip_id), obs_time timestamptz NOT NULL, observer observer NOT NULL, notes text);
-- What was the elevation of each target at the time of observation?SELECT l.obs_time, s.name, round(topo_elevation( star_observe(s.ra_hours, s.dec_deg, l.observer, l.obs_time) )::numeric, 1) AS actual_el, l.notesFROM obs_log lJOIN star_catalog s ON s.hip_id = l.target_hipORDER BY l.obs_time;This retroactively computes the sky position of every logged target at the time it was observed. Useful for data quality checks — an observation logged at 5 degrees elevation might be suspect.
Full catalog performance
Section titled “Full catalog performance”With a full Hipparcos catalog loaded (118,218 stars), a full-catalog observation runs at about 714,000 stars per second:
-- Time a full catalog sweep (for benchmarking)EXPLAIN ANALYZESELECT count(*)FROM star_catalog, LATERAL star_observe_safe(ra_hours, dec_deg, '40.0N 105.3W 1655m'::observer, now()) obsWHERE obs IS NOT NULL AND topo_elevation(obs) > 0;The exact throughput depends on hardware, but the function is PARALLEL SAFE, so PostgreSQL will distribute the work across available cores on large catalogs.