1. Queries#

Click here to run this notebook on Colab. For the exercises, use the version with blanks.

This is the first in a series of lessons about working with astronomical data.

As a running example, we will replicate parts of the analysis in a 2018 paper, “Off the beaten path: Gaia reveals GD-1 stars outside of the main stream” by Adrian Price-Whelan and Ana Bonaca.

Outline#

This lesson demonstrates the steps for selecting and downloading data from the Gaia Database:

  1. First we’ll make a connection to the Gaia server,

  2. We will explore information about the database and the tables it contains,

  3. We will write a query and send it to the server, and finally

  4. We will download the response from the server.

Query Language#

In order to select data from a database, you have to compose a query, which is a program written in a “query language”. The query language we’ll use is ADQL, which stands for “Astronomical Data Query Language”.

ADQL is a dialect of SQL (Structured Query Language), which is by far the most commonly used query language. Almost everything you will learn about ADQL also works in SQL.

The reference manual for ADQL is here. But you might find it easier to learn from this guide to writing ADQL queries for Gaia data.

Using Jupyter#

If you have not worked with Jupyter notebooks before, you might start with the tutorial on from Jupyter.org called “Try Classic Notebook”, or this tutorial from DataQuest.

There are two environments you can use to write and run notebooks:

  • “Jupyter Notebook” is the original, and

  • “Jupyter Lab” is a newer environment with more features.

For these lessons, you can use either one.

If you are too impatient for the tutorials, here are the most important things to know:

  1. Notebooks are made up of code cells and text cells (and a few other less common kinds). Code cells contain code; text cells, like this one, contain explanatory text written in Markdown.

  2. To run a code cell, click the cell to select it and press Shift-Enter. The output of the code should appear below the cell.

  3. In general, notebooks only run correctly if you run every code cell in order from top to bottom. If you run cells out of order, you are likely to get errors.

  4. You can modify existing cells, but then you have to run them again to see the effect.

  5. You can add new cells, but again, you have to be careful about the order you run them in.

  6. If you have added or modified cells and the behavior of the notebook seems strange, you can restart the “kernel”, which clears all of the variables and functions you have defined, and run the cells again from the beginning.

  • If you are using Jupyter notebook, open the Kernel menu and select “Restart and Run All”.

  • In Jupyter Lab, open the Kernel menu and select “Restart Kernel and Run All Cells”

  • In Colab, open the Runtime menu and select “Restart and run all”

Before you go on, you might want to explore the other menus and the toolbar to see what else you can do.

Connecting to Gaia#

The library we’ll use to get Gaia data is Astroquery. Astroquery provides Gaia, which is an object that represents a connection to the Gaia database.

We can connect to the Gaia database like this:

from astroquery.gaia import Gaia

This import statement creates a TAP+ connection; TAP stands for “Table Access Protocol”, which is a network protocol for sending queries to the database and getting back the results.

Databases and Tables#

What is a database, anyway? Most generally, it can be any collection of data, but when we are talking about ADQL or SQL:

  • A database is a collection of one or more named tables.

  • Each table is a 2-D array with one or more named columns of data.

We can use Gaia.load_tables to get the names of the tables in the Gaia database. With the option only_names=True, it loads information about the tables, called “metadata”, not the data itself.

tables = Gaia.load_tables(only_names=True)
INFO: Retrieving tables... [astroquery.utils.tap.core]
INFO: Parsing tables... [astroquery.utils.tap.core]
INFO: Done. [astroquery.utils.tap.core]

The following for loop prints the names of the tables.

for table in tables:
    print(table.name)

Hide code cell output

external.apassdr9
external.catwise2020
external.gaiadr2_astrophysical_parameters
external.gaiadr2_geometric_distance
external.gaiaedr3_distance
external.gaiaedr3_gcns_main_1
external.gaiaedr3_gcns_rejected_1
external.gaiaedr3_spurious
external.gaia_eso_survey
external.galex_ais
external.lamost_dr9_lrs
external.lamost_dr9_mrs
external.ravedr5_com
external.ravedr5_dr5
external.ravedr5_gra
external.ravedr5_on
external.ravedr6
external.sdssdr13_photoprimary
external.skymapperdr1_master
external.skymapperdr2_master
external.tmass_xsc
external.xgboost_table1
external.xgboost_table2
gaiadr1.aux_qso_icrf2_match
gaiadr1.ext_phot_zero_point
gaiadr1.allwise_best_neighbour
gaiadr1.allwise_neighbourhood
gaiadr1.gsc23_best_neighbour
gaiadr1.gsc23_neighbourhood
gaiadr1.ppmxl_best_neighbour
gaiadr1.ppmxl_neighbourhood
gaiadr1.sdss_dr9_best_neighbour
gaiadr1.sdss_dr9_neighbourhood
gaiadr1.tmass_best_neighbour
gaiadr1.tmass_neighbourhood
gaiadr1.ucac4_best_neighbour
gaiadr1.ucac4_neighbourhood
gaiadr1.urat1_best_neighbour
gaiadr1.urat1_neighbourhood
gaiadr1.cepheid
gaiadr1.phot_variable_time_series_gfov
gaiadr1.phot_variable_time_series_gfov_statistical_parameters
gaiadr1.rrlyrae
gaiadr1.variable_summary
gaiadr1.allwise_original_valid
gaiadr1.gsc23_original_valid
gaiadr1.ppmxl_original_valid
gaiadr1.sdssdr9_original_valid
gaiadr1.tmass_original_valid
gaiadr1.ucac4_original_valid
gaiadr1.urat1_original_valid
gaiadr1.gaia_source
gaiadr1.tgas_source
gaiadr2.aux_allwise_agn_gdr2_cross_id
gaiadr2.aux_iers_gdr2_cross_id
gaiadr2.aux_sso_orbit_residuals
gaiadr2.aux_sso_orbits
gaiadr2.dr1_neighbourhood
gaiadr2.allwise_best_neighbour
gaiadr2.allwise_neighbourhood
gaiadr2.apassdr9_best_neighbour
gaiadr2.apassdr9_neighbourhood
gaiadr2.gsc23_best_neighbour
gaiadr2.gsc23_neighbourhood
gaiadr2.hipparcos2_best_neighbour
gaiadr2.hipparcos2_neighbourhood
gaiadr2.panstarrs1_best_neighbour
gaiadr2.panstarrs1_neighbourhood
gaiadr2.ppmxl_best_neighbour
gaiadr2.ppmxl_neighbourhood
gaiadr2.ravedr5_best_neighbour
gaiadr2.ravedr5_neighbourhood
gaiadr2.sdssdr9_best_neighbour
gaiadr2.sdssdr9_neighbourhood
gaiadr2.tmass_best_neighbour
gaiadr2.tmass_neighbourhood
gaiadr2.tycho2_best_neighbour
gaiadr2.tycho2_neighbourhood
gaiadr2.urat1_best_neighbour
gaiadr2.urat1_neighbourhood
gaiadr2.sso_observation
gaiadr2.sso_source
gaiadr2.vari_cepheid
gaiadr2.vari_classifier_class_definition
gaiadr2.vari_classifier_definition
gaiadr2.vari_classifier_result
gaiadr2.vari_long_period_variable
gaiadr2.vari_rotation_modulation
gaiadr2.vari_rrlyrae
gaiadr2.vari_short_timescale
gaiadr2.vari_time_series_statistics
gaiadr2.panstarrs1_original_valid
gaiadr2.gaia_source
gaiadr2.ruwe
gaiadr3.gaia_source
gaiadr3.gaia_source_lite
gaiadr3.astrophysical_parameters
gaiadr3.astrophysical_parameters_supp
gaiadr3.oa_neuron_information
gaiadr3.oa_neuron_xp_spectra
gaiadr3.total_galactic_extinction_map
gaiadr3.total_galactic_extinction_map_opt
gaiadr3.commanded_scan_law
gaiadr3.allwise_best_neighbour
gaiadr3.allwise_neighbourhood
gaiadr3.apassdr9_best_neighbour
gaiadr3.apassdr9_join
gaiadr3.apassdr9_neighbourhood
gaiadr3.dr2_neighbourhood
gaiadr3.gsc23_best_neighbour
gaiadr3.gsc23_join
gaiadr3.gsc23_neighbourhood
gaiadr3.hipparcos2_best_neighbour
gaiadr3.hipparcos2_neighbourhood
gaiadr3.panstarrs1_best_neighbour
gaiadr3.panstarrs1_join
gaiadr3.panstarrs1_neighbourhood
gaiadr3.ravedr5_best_neighbour
gaiadr3.ravedr5_join
gaiadr3.ravedr5_neighbourhood
gaiadr3.ravedr6_best_neighbour
gaiadr3.ravedr6_join
gaiadr3.ravedr6_neighbourhood
gaiadr3.sdssdr13_best_neighbour
gaiadr3.sdssdr13_join
gaiadr3.sdssdr13_neighbourhood
gaiadr3.skymapperdr2_best_neighbour
gaiadr3.skymapperdr2_join
gaiadr3.skymapperdr2_neighbourhood
gaiadr3.tmass_psc_xsc_best_neighbour
gaiadr3.tmass_psc_xsc_join
gaiadr3.tmass_psc_xsc_neighbourhood
gaiadr3.tycho2tdsc_merge_best_neighbour
gaiadr3.tycho2tdsc_merge_neighbourhood
gaiadr3.urat1_best_neighbour
gaiadr3.urat1_neighbourhood
gaiadr3.galaxy_candidates
gaiadr3.galaxy_catalogue_name
gaiadr3.qso_candidates
gaiadr3.qso_catalogue_name
gaiadr3.nss_acceleration_astro
gaiadr3.nss_non_linear_spectro
gaiadr3.nss_two_body_orbit
gaiadr3.nss_vim_fl
gaiadr3.binary_masses
gaiadr3.chemical_cartography
gaiadr3.gold_sample_carbon_stars
gaiadr3.gold_sample_fgkm_stars
gaiadr3.gold_sample_oba_stars
gaiadr3.gold_sample_solar_analogues
gaiadr3.gold_sample_spss
gaiadr3.gold_sample_ucd
gaiadr3.sso_orbits
gaiadr3.synthetic_photometry_gspc
gaiadr3.vari_spurious_signals
gaiadr3.agn_cross_id
gaiadr3.frame_rotator_source
gaiadr3.gaia_crf3_xm
gaiadr3.alerts_mixedin_sourceids
gaiadr3.science_alerts
gaiadr3.gaia_source_simulation
gaiadr3.gaia_universe_model
gaiadr3.sso_observation
gaiadr3.sso_reflectance_spectrum
gaiadr3.sso_source
gaiadr3.xp_summary
gaiadr3.vari_agn
gaiadr3.vari_cepheid
gaiadr3.vari_classifier_class_definition
gaiadr3.vari_classifier_definition
gaiadr3.vari_classifier_result
gaiadr3.vari_compact_companion
gaiadr3.vari_eclipsing_binary
gaiadr3.vari_epoch_radial_velocity
gaiadr3.vari_long_period_variable
gaiadr3.vari_microlensing
gaiadr3.vari_ms_oscillator
gaiadr3.vari_planetary_transit
gaiadr3.vari_planetary_transit_13june2022
gaiadr3.vari_rad_vel_statistics
gaiadr3.vari_rotation_modulation
gaiadr3.vari_rrlyrae
gaiadr3.vari_short_timescale
gaiadr3.vari_summary
gaiadr3.tycho2tdsc_merge
gaiaedr3.gaia_source
gaiaedr3.agn_cross_id
gaiaedr3.commanded_scan_law
gaiaedr3.dr2_neighbourhood
gaiaedr3.frame_rotator_source
gaiaedr3.allwise_best_neighbour
gaiaedr3.allwise_neighbourhood
gaiaedr3.apassdr9_best_neighbour
gaiaedr3.apassdr9_join
gaiaedr3.apassdr9_neighbourhood
gaiaedr3.gsc23_best_neighbour
gaiaedr3.gsc23_join
gaiaedr3.gsc23_neighbourhood
gaiaedr3.hipparcos2_best_neighbour
gaiaedr3.hipparcos2_neighbourhood
gaiaedr3.panstarrs1_best_neighbour
gaiaedr3.panstarrs1_join
gaiaedr3.panstarrs1_neighbourhood
gaiaedr3.ravedr5_best_neighbour
gaiaedr3.ravedr5_join
gaiaedr3.ravedr5_neighbourhood
gaiaedr3.sdssdr13_best_neighbour
gaiaedr3.sdssdr13_join
gaiaedr3.sdssdr13_neighbourhood
gaiaedr3.skymapperdr2_best_neighbour
gaiaedr3.skymapperdr2_join
gaiaedr3.skymapperdr2_neighbourhood
gaiaedr3.tmass_psc_xsc_best_neighbour
gaiaedr3.tmass_psc_xsc_join
gaiaedr3.tmass_psc_xsc_neighbourhood
gaiaedr3.tycho2tdsc_merge_best_neighbour
gaiaedr3.tycho2tdsc_merge_neighbourhood
gaiaedr3.urat1_best_neighbour
gaiaedr3.urat1_neighbourhood
gaiaedr3.gaia_source_simulation
gaiaedr3.gaia_universe_model
gaiaedr3.tycho2tdsc_merge
gaiafpr.crowded_field_source
gaiafpr.lens_candidates
gaiafpr.lens_catalogue_name
gaiafpr.lens_observation
gaiafpr.lens_outlier
gaiafpr.sso_observation
gaiafpr.sso_source
gaiafpr.interstellar_medium_params
gaiafpr.interstellar_medium_spectra
gaiafpr.vari_epoch_radial_velocity
gaiafpr.vari_long_period_variable
gaiafpr.vari_rad_vel_statistics
public.hipparcos
public.hipparcos_newreduction
public.hubble_sc
public.igsl_source
public.igsl_source_catalog_ids
public.tycho2
public.dual
tap_config.coord_sys
tap_config.properties
tap_schema.columns
tap_schema.key_columns
tap_schema.keys
tap_schema.schemas
tap_schema.tables

So that’s a lot of tables. The ones we’ll use are:

  • gaiadr2.gaia_source, which contains Gaia data from data release 2,

  • gaiadr2.panstarrs1_original_valid, which contains the photometry data we’ll use from PanSTARRS, and

  • gaiadr2.panstarrs1_best_neighbour, which we’ll use to cross-match each star observed by Gaia with the same star observed by PanSTARRS.

We can use load_table (not load_tables) to get the metadata for a single table. The name of this function is misleading, because it only downloads metadata, not the contents of the table.

meta = Gaia.load_table('gaiadr2.gaia_source')
meta
<astroquery.utils.tap.model.taptable.TapTableMeta at 0x7994c607e190>

Jupyter shows that the result is an object of type TapTableMeta, but it does not display the contents.

To see the metadata, we have to print the object.

print(meta)
TAP Table name: gaiadr2.gaia_source
Description: This table has an entry for every Gaia observed source as listed in the
Main Database accumulating catalogue version from which the catalogue
release has been generated. It contains the basic source parameters,
that is only final data (no epoch data) and no spectra (neither final
nor epoch).
Size (bytes): 4906520690688
Num. columns: 96

Columns#

The following loop prints the names of the columns in the table.

for column in meta.columns:
    print(column.name)

Hide code cell output

solution_id
designation
source_id
random_index
ref_epoch
ra
ra_error
dec
dec_error
parallax
parallax_error
parallax_over_error
pmra
pmra_error
pmdec
pmdec_error
ra_dec_corr
ra_parallax_corr
ra_pmra_corr
ra_pmdec_corr
dec_parallax_corr
dec_pmra_corr
dec_pmdec_corr
parallax_pmra_corr
parallax_pmdec_corr
pmra_pmdec_corr
astrometric_n_obs_al
astrometric_n_obs_ac
astrometric_n_good_obs_al
astrometric_n_bad_obs_al
astrometric_gof_al
astrometric_chi2_al
astrometric_excess_noise
astrometric_excess_noise_sig
astrometric_params_solved
astrometric_primary_flag
astrometric_weight_al
astrometric_pseudo_colour
astrometric_pseudo_colour_error
mean_varpi_factor_al
astrometric_matched_observations
visibility_periods_used
astrometric_sigma5d_max
frame_rotator_object_type
matched_observations
duplicated_source
phot_g_n_obs
phot_g_mean_flux
phot_g_mean_flux_error
phot_g_mean_flux_over_error
phot_g_mean_mag
phot_bp_n_obs
phot_bp_mean_flux
phot_bp_mean_flux_error
phot_bp_mean_flux_over_error
phot_bp_mean_mag
phot_rp_n_obs
phot_rp_mean_flux
phot_rp_mean_flux_error
phot_rp_mean_flux_over_error
phot_rp_mean_mag
phot_bp_rp_excess_factor
phot_proc_mode
bp_rp
bp_g
g_rp
radial_velocity
radial_velocity_error
rv_nb_transits
rv_template_teff
rv_template_logg
rv_template_fe_h
phot_variable_flag
l
b
ecl_lon
ecl_lat
priam_flags
teff_val
teff_percentile_lower
teff_percentile_upper
a_g_val
a_g_percentile_lower
a_g_percentile_upper
e_bp_min_rp_val
e_bp_min_rp_percentile_lower
e_bp_min_rp_percentile_upper
flame_flags
radius_val
radius_percentile_lower
radius_percentile_upper
lum_val
lum_percentile_lower
lum_percentile_upper
datalink_url
epoch_photometry_url

You can probably infer what many of these columns are by looking at the names, but you should resist the temptation to guess. To find out what the columns mean, read the documentation.

If you want to know what can go wrong when you don’t read the documentation, you might like this article.

Exercise#

One of the other tables we’ll use is gaiadr2.panstarrs1_original_valid. Use load_table to get the metadata for this table. How many columns are there and what are their names?

Hide code cell content

# Solution

meta2 = Gaia.load_table('gaiadr2.panstarrs1_original_valid')
print(meta2)

for column in meta2.columns:
    print(column.name)
TAP Table name: gaiadr2.panstarrs1_original_valid
Description: The Panoramic Survey Telescope and Rapid Response System (Pan-STARRS) is
a system for wide-field astronomical imaging developed and operated by
the Institute for Astronomy at the University of Hawaii. Pan-STARRS1
(PS1) is the first part of Pan-STARRS to be completed and is the basis
for Data Release 1 (DR1). The PS1 survey used a 1.8 meter telescope and
its 1.4 Gigapixel camera to image the sky in five broadband filters (g,
r, i, z, y).

The current table contains a filtered subsample of the 10 723 304 629
entries listed in the original ObjectThin table.
We used only ObjectThin and MeanObject tables to extract
panstarrs1OriginalValid table, this means that objects detected only in
stack images are not included here. The main reason for us to avoid the
use of objects detected in stack images is that their astrometry is not
as good as the mean objects astrometry: “The stack positions (raStack,
decStack) have considerably larger systematic astrometric errors than
the mean epoch positions (raMean, decMean).” The astrometry for the
MeanObject positions uses Gaia DR1 as a reference catalog, while the
stack positions use 2MASS as a reference catalog.

In details, we filtered out all objects where:

-   nDetections = 1

-   no good quality data in Pan-STARRS, objInfoFlag 33554432 not set

-   mean astrometry could not be measured, objInfoFlag 524288 set

-   stack position used for mean astrometry, objInfoFlag 1048576 set

-   error on all magnitudes equal to 0 or to -999;

-   all magnitudes set to -999;

-   error on RA or DEC greater than 1 arcsec.

The number of objects in panstarrs1OriginalValid is 2 264 263 282.

The panstarrs1OriginalValid table contains only a subset of the columns
available in the combined ObjectThin and MeanObject tables. A
description of the original ObjectThin and MeanObjects tables can be
found at:
https://outerspace.stsci.edu/display/PANSTARRS/PS1+Database+object+and+detection+tables

Download:
http://mastweb.stsci.edu/ps1casjobs/home.aspx
Documentation:
https://outerspace.stsci.edu/display/PANSTARRS
http://pswww.ifa.hawaii.edu/pswww/
References:
The Pan-STARRS1 Surveys, Chambers, K.C., et al. 2016, arXiv:1612.05560
Pan-STARRS Data Processing System, Magnier, E. A., et al. 2016,
arXiv:1612.05240
Pan-STARRS Pixel Processing: Detrending, Warping, Stacking, Waters, C.
Z., et al. 2016, arXiv:1612.05245
Pan-STARRS Pixel Analysis: Source Detection and Characterization,
Magnier, E. A., et al. 2016, arXiv:1612.05244
Pan-STARRS Photometric and Astrometric Calibration, Magnier, E. A., et
al. 2016, arXiv:1612.05242
The Pan-STARRS1 Database and Data Products, Flewelling, H. A., et al.
2016, arXiv:1612.05243

Catalogue curator:
SSDC - ASI Space Science Data Center
https://www.ssdc.asi.it/
Size (bytes): 933802426368
Num. columns: 26
obj_name
obj_id
ra
dec
ra_error
dec_error
epoch_mean
g_mean_psf_mag
g_mean_psf_mag_error
g_flags
r_mean_psf_mag
r_mean_psf_mag_error
r_flags
i_mean_psf_mag
i_mean_psf_mag_error
i_flags
z_mean_psf_mag
z_mean_psf_mag_error
z_flags
y_mean_psf_mag
y_mean_psf_mag_error
y_flags
n_detections
zone_id
obj_info_flag
quality_flag

Writing queries#

By now you might be wondering how we download these tables. With tables this big, you generally don’t. Instead, you use queries to select only the data you want.

A query is a string written in a query language like SQL; for the Gaia database, the query language is a dialect of SQL called ADQL.

Here’s an example of an ADQL query.

query1 = """SELECT 
TOP 10
source_id, ra, dec, parallax 
FROM gaiadr2.gaia_source
"""

Python note: We use a triple-quoted string here so we can include line breaks in the query, which makes it easier to read.

The words in uppercase are ADQL keywords:

  • SELECT indicates that we are selecting data (as opposed to adding or modifying data).

  • TOP indicates that we only want the first 10 rows of the table, which is useful for testing a query before asking for all of the data.

  • FROM specifies which table we want data from.

The third line is a list of column names, indicating which columns we want.

In this example, the keywords are capitalized and the column names are lowercase. This is a common style, but it is not required. ADQL and SQL are not case-sensitive.

Also, the query is broken into multiple lines to make it more readable. This is a common style, but not required. Line breaks don’t affect the behavior of the query.

To run this query, we use the Gaia object, which represents our connection to the Gaia database, and invoke launch_job:

job = Gaia.launch_job(query1)
job
<Table length=10>
   name    dtype  unit                            description                             n_bad
--------- ------- ---- ------------------------------------------------------------------ -----
source_id   int64      Unique source identifier (unique within a particular Data Release)     0
       ra float64  deg                                                    Right ascension     0
      dec float64  deg                                                        Declination     0
 parallax float64  mas                                                           Parallax     1
<astroquery.utils.tap.model.job.Job at 0x7994c60563c0>

The result is an object that represents the job running on a Gaia server.

If you print it, it displays metadata for the forthcoming results.

print(job)
<Table length=10>
   name    dtype  unit                            description                             n_bad
--------- ------- ---- ------------------------------------------------------------------ -----
source_id   int64      Unique source identifier (unique within a particular Data Release)     0
       ra float64  deg                                                    Right ascension     0
      dec float64  deg                                                        Declination     0
 parallax float64  mas                                                           Parallax     1
Jobid: None
Phase: COMPLETED
Owner: None
Output file: 43f837a6-cd85-11f1-9d98-bc97e148b76b-O-result.vot.gz
Results: None

Don’t worry about Results: None. That does not actually mean there are no results.

However, Phase: COMPLETED indicates that the job is complete, so we can get the results like this:

results = job.get_results()
type(results)
astropy.table.table.Table

Note about limits: The Gaia archive limits what anonymous users can do: a query can run for at most one minute and return at most 50,000 rows. When the archive is busy, a query that usually works can time out, and astroquery reports an error like HTTPError: Error 500. If that happens, wait a minute and run the cell again.

If you use the archive often, you can register for a free account, which raises these limits. Then, before running queries, log in like this:

Gaia.login()

It asks for your user name and password.

The type function indicates that the result is an Astropy Table.

Optional detail: Why is table repeated three times? The first is the name of the module, the second is the name of the submodule, and the third is the name of the class. Most of the time we only care about the last one. It’s like the Linnean name for gorilla, which is Gorilla gorilla gorilla.

An Astropy Table is similar to a table in an SQL database except:

  • SQL databases are stored on disk drives, so they are persistent; that is, they “survive” even if you turn off the computer. An Astropy Table is stored in memory; it disappears when you turn off the computer (or shut down this Jupyter notebook).

  • SQL databases are designed to process queries. An Astropy Table can perform some query-like operations, like selecting columns and rows. But these operations use Python syntax, not SQL.

Jupyter knows how to display the contents of a Table.

results
Table length=10
source_idradecparallax
degdegmas
int64float64float64float64
4464185571489765376246.077157114993315.4753619481538980.3581981880000272
2021650318958976512294.9361368229388625.3203126311638340.4482071426928564
6003882045187270400229.28259303837524-42.978260915938110.3814393435896097
6680659523978470144306.5616715416606-40.54589081856055-2.227432561581064
6680733225618222592307.6583019373705-40.003792412434312.2595484953413174
5800583066773602560224.61633794146624-67.07005997802717--
5781838313353791232236.48568321530968-75.249154731490340.12674646527678737
4144471714989233408268.5454915814623-17.6824611471005330.12228975502535505
2047220836251855104294.829822599567434.110843527119030.22647934974275735
1872330283344244608316.1392282531856539.635831110233580.292845031820401

Each column has a name, units, and a data type.

For example, the units of ra and dec are degrees, and their data type is float64, which is a 64-bit floating-point number, used to store measurements with a fraction part.

This information comes from the Gaia database, and has been stored in the Astropy Table by Astroquery.

Exercise#

Read the documentation of this table and choose a column that looks interesting to you. Add the column name to the query and run it again. What are the units of the column you selected? What is its data type?

Hide code cell content

# Solution

# Let's add
#
# radial_velocity : Radial velocity (double, Velocity[km/s] )
#
# Spectroscopic radial velocity in the solar barycentric 
# reference frame.
#
# The radial velocity provided is the median value of the 
# radial velocity measurements at all epochs.

query = """SELECT 
TOP 10
source_id, ra, dec, parallax, radial_velocity
FROM gaiadr2.gaia_source
"""

Asynchronous queries#

launch_job asks the server to run the job “synchronously”, which normally means it runs immediately. But synchronous jobs are limited to 2000 rows. For queries that return more rows, you should run “asynchronously”, which mean they might take longer to get started.

If you are not sure how many rows a query will return, you can use the SQL command COUNT to find out how many rows are in the result without actually returning them. We’ll see an example in the next lesson.

The results of an asynchronous query are stored in a file on the server, so you can start a query and come back later to get the results. For anonymous users, files are kept for three days.

As an example, let’s try a query that’s similar to query1, with these changes:

  • It selects the first 3000 rows, so it is bigger than we should run synchronously.

  • It selects two additional columns, pmra and pmdec, which are proper motions along the axes of ra and dec.

  • It uses a new keyword, WHERE.

query2 = """SELECT 
TOP 3000
source_id, ra, dec, pmra, pmdec, parallax
FROM gaiadr2.gaia_source
WHERE parallax < 1
"""

A WHERE clause indicates which rows we want; in this case, the query selects only rows “where” parallax is less than 1. This has the effect of selecting stars with relatively low parallax, which are farther away. We’ll use this clause to exclude nearby stars that are unlikely to be part of GD-1.

WHERE is one of the most common clauses in ADQL/SQL, and one of the most useful, because it allows us to download only the rows we need from the database.

We use launch_job_async to submit an asynchronous query.

job = Gaia.launch_job_async(query2)
job
INFO: Query finished. [astroquery.utils.tap.core]
<Table length=3000>
   name    dtype    unit                              description                            
--------- ------- -------- ------------------------------------------------------------------
source_id   int64          Unique source identifier (unique within a particular Data Release)
       ra float64      deg                                                    Right ascension
      dec float64      deg                                                        Declination
     pmra float64 mas / yr                         Proper motion in right ascension direction
    pmdec float64 mas / yr                             Proper motion in declination direction
 parallax float64      mas                                                           Parallax
<astroquery.utils.tap.model.job.Job at 0x7994c6eddbd0>

And here are the results.

results = job.get_results()
results
Table length=3000
source_idradecpmrapmdecparallax
degdegmas / yrmas / yrmas
int64float64float64float64float64float64
2021675642089792384295.432966415707625.694747895432833-3.705910675783222-8.4350286709059610.1504022212411833
4144465530233812608268.53821863765097-17.795074200697346-1.4303474727014598-9.861545464502035-0.8908715834864402
6003865758671139840229.2542009248955-43.272600358081871.49669302552130227.6436070580368725-1.9908745008306592
5408471828453194496151.00917891019682-46.88423637662744-4.6368172764494571.74076960792951160.1055206828557775
5408458531225820288151.40576479580253-46.61343588999212-12.8958218118197673.1752137760813546-0.41882170015846476
5800602892358227072225.01887329936304-67.06130408344855-7.359523052877834-7.72393246678909050.07586289160158002
6003853178701555840227.90894692613932-42.11918047496137-5.386993898808422-3.3449216841996170.4962694722741371
6725836463927174144273.590974252965-39.752808132993491.6209277611794106-0.07351528958704044-0.06958704663385112
6680762117861851392306.6295194762143-40.2017335392175143.8324065852105091.23343737593403760.3892922878460933
..................
1872307635968286592315.96673029328339.313138972470206-4.250905288375864-4.7970182835558430.11053560924265925
6725825636291484672273.72897806692527-39.748234051990634.076797267355483-4.4176901754070790.12344089434679001
4464108403813508480246.391827375617115.112429714832048-2.87383658695071630.165764294159887220.7838151489625143
6680738444001666304306.9351629288942-40.5499866639835713.376738580469164-6.3145394082549070.28307323116788474
2065106420080252672313.266560848544240.43836437230535-3.667049422763947-0.363786628804102840.012780643906550843
2065101158752493824313.1092673548971640.30147894212878-1.3746603191168743-4.0580177135274680.1879915726290987
4144477968463747840268.8811420376856-17.745591462573721.818873310542059-0.26955611621288983-0.20618286581154596
2065103873160652160313.213193329522840.303588303391344-2.3291740320492487-4.5187786615061060.15829270857380764
2065080233672846976313.338262422301640.05197608784453-5.306470597353722-3.2867176497713810.44597760841999484

You might notice that some values of parallax are negative. As this FAQ explains, “Negative parallaxes are caused by errors in the observations.” They have “no physical meaning,” but they can be a “useful diagnostic on the quality of the astrometric solution.”

Exercise#

The clauses in a query have to be in the right order. Go back and change the order of the clauses in query2 and run it again. The modified query should fail, but notice that you don’t get much useful debugging information.

For this reason, developing and debugging ADQL queries can be really hard. A few suggestions that might help:

  • Whenever possible, start with a working query, either an example you find online or a query you have used in the past.

  • Make small changes and test each change before you continue.

  • While you are debugging, use TOP to limit the number of rows in the result. That will make each test run faster, which reduces your development time.

  • Launching test queries synchronously might make them start faster, too.

Hide code cell content

# Solution

# In this example, the WHERE clause is in the wrong place

query = """SELECT 
TOP 3000
WHERE parallax < 1
source_id, ref_epoch, ra, dec, parallax
FROM gaiadr2.gaia_source
"""

Operators#

In a WHERE clause, you can use any of the SQL comparison operators; here are the most common ones:

Symbol

Operation

>

greater than

<

less than

>=

greater than or equal

<=

less than or equal

=

equal

!= or <>

not equal

Most of these are the same as Python, but some are not. In particular, notice that the equality operator is =, not ==. Be careful to keep your Python out of your ADQL!

You can combine comparisons using the logical operators:

  • AND: true if both comparisons are true

  • OR: true if either or both comparisons are true

Finally, you can use NOT to invert the result of a comparison.

Exercise#

Read about SQL operators here and then modify the previous query to select rows where bp_rp is between -0.75 and 2.

Hide code cell content

# Solution

# Here's a solution using > and < operators

query = """SELECT 
TOP 10
source_id, ref_epoch, ra, dec, parallax
FROM gaiadr2.gaia_source
WHERE parallax < 1 
  AND bp_rp > -0.75 AND bp_rp < 2
"""

# And here's a solution using the BETWEEN operator

query = """SELECT 
TOP 10
source_id, ref_epoch, ra, dec, parallax
FROM gaiadr2.gaia_source
WHERE parallax < 1 
  AND bp_rp BETWEEN -0.75 AND 2
"""

bp_rp contains BP-RP color, which is the difference between two other columns, phot_bp_mean_mag and phot_rp_mean_mag. You can read about this variable here.

This Hertzsprung-Russell diagram shows the BP-RP color and luminosity of stars in the Gaia catalog (Copyright: ESA/Gaia/DPAC, CC BY-SA 3.0 IGO).

Selecting stars with bp-rp less than 2 excludes many class M dwarf stars, which are low temperature, low luminosity. A star like that at GD-1’s distance would be hard to detect, so if it is detected, it it more likely to be in the foreground.

Formatting queries#

The queries we have written so far are string “literals”, meaning that the entire string is part of the program. But writing queries yourself can be slow, repetitive, and error-prone.

It is often better to write Python code that assembles a query for you. One useful tool for that is the string format method.

As an example, we’ll divide the previous query into two parts; a list of column names and a “base” for the query that contains everything except the column names.

Here’s the list of columns we’ll select.

columns = 'source_id, ra, dec, pmra, pmdec, parallax'

And here’s the base; it’s a string that contains at least one format specifier in curly brackets (braces).

query3_base = """SELECT 
TOP 10 
{columns}
FROM gaiadr2.gaia_source
WHERE parallax < 1
  AND bp_rp BETWEEN -0.75 AND 2
"""

This base query contains one format specifier, {columns}, which is a placeholder for the list of column names we will provide.

To assemble the query, we invoke format on the base string and provide a keyword argument that assigns a value to columns.

query3 = query3_base.format(columns=columns)

In this example, the variable that contains the column names and the variable in the format specifier have the same name. That’s not required, but it is a common style.

The result is a string with line breaks. If you display it, the line breaks appear as \n.

query3
'SELECT \nTOP 10 \nsource_id, ra, dec, pmra, pmdec, parallax\nFROM gaiadr2.gaia_source\nWHERE parallax < 1\n  AND bp_rp BETWEEN -0.75 AND 2\n'

But if you print it, the line breaks appear as… line breaks.

print(query3)
SELECT 
TOP 10 
source_id, ra, dec, pmra, pmdec, parallax
FROM gaiadr2.gaia_source
WHERE parallax < 1
  AND bp_rp BETWEEN -0.75 AND 2

Notice that the format specifier has been replaced with the value of columns.

Let’s run it and see if it works:

job = Gaia.launch_job(query3)
print(job)
<Table length=10>
   name    dtype    unit                              description                            
--------- ------- -------- ------------------------------------------------------------------
source_id   int64          Unique source identifier (unique within a particular Data Release)
       ra float64      deg                                                    Right ascension
      dec float64      deg                                                        Declination
     pmra float64 mas / yr                         Proper motion in right ascension direction
    pmdec float64 mas / yr                             Proper motion in declination direction
 parallax float64      mas                                                           Parallax
<Table length=10>
   name    dtype    unit                              description                            
--------- ------- -------- ------------------------------------------------------------------
source_id   int64          Unique source identifier (unique within a particular Data Release)
       ra float64      deg                                                    Right ascension
      dec float64      deg                                                        Declination
     pmra float64 mas / yr                         Proper motion in right ascension direction
    pmdec float64 mas / yr                             Proper motion in declination direction
 parallax float64      mas                                                           Parallax
Jobid: None
Phase: COMPLETED
Owner: None
Output file: 463e572c-cd85-11f1-9d98-bc97e148b76b-O-result.vot.gz
Results: None
results = job.get_results()
results
Table length=10
source_idradecpmrapmdecparallax
degdegmas / yrmas / yrmas
int64float64float64float64float64float64
2021675642089792384295.432966415707625.694747895432833-3.705910675783222-8.4350286709059610.1504022212411833
4144465530233812608268.53821863765097-17.795074200697346-1.4303474727014598-9.861545464502035-0.8908715834864402
5408471828453194496151.00917891019682-46.88423637662744-4.6368172764494571.74076960792951160.1055206828557775
5408458531225820288151.40576479580253-46.61343588999212-12.8958218118197673.1752137760813546-0.41882170015846476
5800602892358227072225.01887329936304-67.06130408344855-7.359523052877834-7.72393246678909050.07586289160158002
6003853178701555840227.90894692613932-42.11918047496137-5.386993898808422-3.3449216841996170.4962694722741371
6725836463927174144273.590974252965-39.752808132993491.6209277611794106-0.07351528958704044-0.06958704663385112
6680762117861851392306.6295194762143-40.2017335392175143.8324065852105091.23343737593403760.3892922878460933
2047235542205871744294.8751956947062434.46349460671704-2.4712404805843873-6.5894371483276455-0.4509076357438277
5408473095460125440151.01578818261012-46.834780034507226-6.4966271327194856.730992959542870.5943950825234436

Good so far.

Exercise#

This query always selects sources with parallax less than 1. But suppose you want to take that upper bound as an input.

Modify query3_base to replace 1 with a format specifier like {max_parallax}. Now, when you call format, add a keyword argument that assigns a value to max_parallax, and confirm that the format specifier gets replaced with the value you provide.

Hide code cell content

# Solution

query_base = """SELECT 
TOP 10
{columns}
FROM gaiadr2.gaia_source
WHERE parallax < {max_parallax} AND 
bp_rp BETWEEN -0.75 AND 2
"""

query = query_base.format(columns=columns,
                          max_parallax=0.5)
print(query)
SELECT 
TOP 10
source_id, ra, dec, pmra, pmdec, parallax
FROM gaiadr2.gaia_source
WHERE parallax < 0.5 AND 
bp_rp BETWEEN -0.75 AND 2

Summary#

This notebook demonstrates the following steps:

  1. Making a connection to the Gaia server,

  2. Exploring information about the database and the tables it contains,

  3. Writing a query and sending it to the server, and finally

  4. Downloading the response from the server as an Astropy Table.

In the next lesson we will extend these queries to select a particular region of the sky.

Best practices#

  • If you can’t download an entire dataset (or it’s not practical) use queries to select the data you need.

  • Read the metadata and the documentation to make sure you understand the tables, their columns, and what they mean.

  • Develop queries incrementally: start with something simple, test it, and add a little bit at a time.

  • Use ADQL features like TOP and COUNT to test before you run a query that might return a lot of data.

  • If you know your query will return fewer than 2000 rows, you can run it synchronously, which might complete faster. If it might return more than 2000 rows, you should run it asynchronously.

  • ADQL and SQL are not case-sensitive, so you don’t have to capitalize the keywords, but you should.

  • ADQL and SQL don’t require you to break a query into multiple lines, but you should.

Jupyter notebooks can be good for developing and testing code, but they have some drawbacks. In particular, if you run the cells out of order, you might find that variables don’t have the values you expect.

To mitigate these problems:

  • Make each section of the notebook self-contained. Try not to use the same variable name in more than one section.

  • Keep notebooks short. Look for places where you can break your analysis into phases with one notebook per phase.

Copyright 2020–2026 Allen Downey

Code license: MIT License

Text license: Creative Commons Attribution 4.0 International