Skip to content

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.

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.

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.

  • 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.

The bright navigational stars and their J2000 coordinates:

-- Polaris: RA 2h 31m 49s = 2.530303h, Dec +89.2641 deg
SELECT '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).

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 demonstration
INSERT 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');

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 el
FROM star_catalog,
LATERAL star_observe_safe(ra_hours, dec_deg,
'40.0N 105.3W 1655m'::observer,
'2024-01-15 03:00:00+00') obs
WHERE obs IS NOT NULL
AND topo_elevation(obs) > 0
ORDER 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 el
FROM star_catalog,
LATERAL star_observe_safe(ra_hours, dec_deg,
'40.0N 105.3W 1655m'::observer,
'2024-07-15 04:00:00+00') obs
WHERE obs IS NOT NULL
AND topo_elevation(obs) > 10
AND vmag < 2.0
ORDER 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.

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 el
FROM generate_series(
'2024-07-15 02:00:00+00'::timestamptz,
'2024-07-15 12:00:00+00'::timestamptz,
interval '30 minutes'
) AS t
WHERE 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.

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.

If you keep an observation log in PostgreSQL, you can join it with star positions:

-- Hypothetical observation log table
CREATE 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.notes
FROM obs_log l
JOIN star_catalog s ON s.hip_id = l.target_hip
ORDER 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.

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 ANALYZE
SELECT count(*)
FROM star_catalog,
LATERAL star_observe_safe(ra_hours, dec_deg,
'40.0N 105.3W 1655m'::observer,
now()) obs
WHERE 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.