6 Data Access
CalCOFI database releases are published as Parquet files on a public Google Cloud Storage (GCS) bucket. You can query them directly with DuckDB — from R, Python, the DuckDB CLI, or any DuckDB client — with no credentials, no API server, and no full download. DuckDB reads only the columns and row groups a query actually touches, straight over HTTPS.
This page covers querying the release Parquet directly. For convenience-wrapped biological ↔︎ environmental matching, see Matching Helpers. Looking for a particular dataset rather than a query? Every dataset has its own page at calcofi.io/datasets/{dataset_key}/ — coverage, every place it can be reached (this page’s Parquet included), its citation and its provider — see Portals.
The releases are read-only snapshots. The CTD team’s working database — multi-user PostgreSQL on the CalCOFI server, private, reached over SSH — is described in Server Access, including how to join it with the releases from DuckDB.
6.1 Where the data lives
A release is a versioned folder of sidecars — the record of what the release is — and its tables are Parquet objects that the release’s catalog.json points at:
gs://calcofi-db/ducklake/
├── releases/
│ ├── latest.txt # the promoted version, e.g. v2026.08.25
│ ├── versions.json # every version: date, tables, rows; consolidated / retired
│ ├── RELEASES.md # what changed between versions and why, newest first
│ └── {version}/
│ ├── catalog.json # table list, rows, partitioned/supplemental flags,
│ │ # and objects[] — where each table's bytes live
│ ├── relationships.json # primary keys + foreign keys
│ ├── metadata.json # table/column descriptions, units, datasets, measurement types
│ ├── RELEASE_NOTES.md # this version's RELEASES.md section + a generated appendix
│ └── parquet/ # legacy per-release copy — promoted + consolidated versions only
│ ├── {table}.parquet
│ └── {table}/dataset_key=.../*.parquet
└── tables/ # content-addressed objects, from the v2026.09 releases
├── {table}/{content_hash}/{table}.parquet
└── {table}/{col}={value}/{content_hash}/data_0.parquet
- Public HTTPS root of the bucket:
https://storage.googleapis.com/calcofi-db/— everypathin a catalog is relative to it.https://storage.calcofi.io/calcofi-db/serves the same bucket and redirects a legacyreleases/{version}/parquet/…URL to the canonical object where one still exists. - The promoted version is at
releases/latest.txt; the table list — and where each table’s bytes live — atreleases/{version}/catalog.json. - Resolve a table through the catalog; never build a
parquet/path by hand. From the v2026.09 releases each table entry incatalog.jsoncarriesobjects[]— one object for a single-file table, one per partition for a partitioned one — with a bucket-relativepath,bytes,sha256,content_hash,since(the first version that shipped that exact object) and its owncompat_path(the legacy per-release path of that one file). Objects are immutable and shared between versions: a release that changed three tables shares every other object with the release before it, sosinceis the per-table “what changed”. The legacyreleases/{version}/parquet/{table}.parquetcopy remains only for the promoted version and the consolidated ones (see Versions). The packages’ resolvers —calcofi4r::cc_release_sources(),calcofi4py.release_sources()— turn a catalog entry into URLs for any version, including those before v2026.09. - Most tables are single-file.
obsand the supplementalobs_ctd_full/obs_mets_fullare hive-partitioned bydataset_key(catalog.jsonflags these with"partitioned": true); a partition object’spathcarries itsdataset_key=…segment, sohive_partitioning = truerecovers the column. Supplemental tables ("supplemental": true) are hosted and cataloged but excluded fromcc_get_db()by default —obs_ctd_fullalone is ~270 M rows. - To see what’s in each table before writing a query, open the CalCOFI Schema explorer — ERD, sortable tables/columns with units and descriptions, dataset provenance, and the canonical measurement-type registry, all reading the same
metadata.jsonsidecar.
6.2 Versions
latestvs pinned.latest.txtnames the promoted version — a release is promoted only after its consumer-contract query suite passes, solatestis safe to follow interactively and is whatcc_get_db()defaults to. Anything that must be reproducible — a paper, an app build, a download bundle — pins avYYYY.MM.DD. Releases are immutable: the same version always returns the same rows.- Consolidated vs retired. Every version keeps its sidecars and notes, but only some keep their bytes. The consolidated versions (
"consolidated": trueinversions.json) mark a schema or data milestone and keep theirparquet/copy indefinitely —v2026.04.08(last of the per-dataset schema),v2026.05.14(firstctd_thin),v2026.06.26(zoodb and zooscan enter),v2026.07.17(unifiedtaxon),v2026.08.14(last before content-addressing) andv2026.08.25(cruise_keyby date span, depth ceiling, quality flags mapped) — plus, always, the promoted version and its predecessor. Every other version is retired by archive thinning once it is neither: itsversions.jsonentry gainsretired: {retired_utc, to, reason}, wheretois the consolidated version to use instead, andcc_get_db("v2026.08.10")errors naming it rather than failing table by table on 404s. When you pin, pin a consolidated version. - What changed.
RELEASES.mdis the running changelog across versions — one section per release, newest first, saying what was wrong, what is true now, and what it costs a consumer (**Consumers:**lines). Each version’sRELEASE_NOTES.mdis its section plus a generated appendix (tables and rows, datasets, the consumer-contract result, package versions):calcofi4r::cc_release_notes(version)prints it, andcc_list_versions()/calcofi4py.cc_list_versions()listversions.json. Per table,objects[].sincein the catalog says which version last changed its bytes.
6.3 Setup: the httpfs extension
DuckDB reads remote Parquet through its httpfs extension. Spatial queries (e.g. distances between casts and tows) also need spatial. Both are one-time installs, then loaded per session:
INSTALL httpfs; LOAD httpfs;
INSTALL spatial; LOAD spatial; -- only if you use ST_* functions6.4 Single-file tables
A single-file table is one HTTPS URL handed to read_parquet(). Take it from the catalog (the packages do this for you, below); typed by hand it must be a version whose parquet/ copy is kept — v2026.08.25 is consolidated, so this URL answers indefinitely:
SELECT taxon_key, scientific_name, common_name, worms_id, rank
FROM read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/taxon.parquet')
WHERE scientific_name = 'Sardinops sagax';Joins work the same way — name each table’s URL. The core model is small: obs holds one measured value per row (realm = bio | env) and points at the sample it came from (sample_key; parent_sample_key walks net → tow → site and bottle → cast) and, for biology, at taxon (taxon_key); event-level effort such as std_haul_factor is in sample_measurement:
SELECT s.sample_key, s.sample_type, s.datetime, s.latitude, s.longitude,
sm.measurement_type, sm.measurement_value
FROM read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/sample.parquet') s
JOIN read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/sample_measurement.parquet') sm ON sm.sample_key = s.sample_key
WHERE s.dataset_key = 'swfsc_ichthyo'
AND sm.measurement_type = 'std_haul_factor'
LIMIT 10;6.5 Hive-partitioned tables
obs (and the supplemental obs_ctd_full / obs_mets_full) is partitioned into one folder per dataset_key. From the v2026.09 releases the catalog lists each partition as its own object, so a partitioned table is read as an explicit list of HTTPS URLs — objects[].path prefixed with the bucket root — with hive_partitioning = true to recover dataset_key from the dataset_key=… path segment. That runs in every DuckDB, the browser included:
-- the list is catalog.json ▸ tables[name = 'obs'] ▸ objects[].path, one per partition
SELECT dataset_key, count(*) AS n
FROM read_parquet([
'https://storage.googleapis.com/calcofi-db/ducklake/tables/obs/dataset_key=calcofi_bottle/{hash}/data_0.parquet',
'https://storage.googleapis.com/calcofi-db/ducklake/tables/obs/dataset_key=calcofi_ctd-cast/{hash}/data_0.parquet',
-- … one URL per partition, in catalog order
'https://storage.googleapis.com/calcofi-db/ducklake/tables/obs/dataset_key=swfsc_ichthyo/{hash}/data_0.parquet'
], hive_partitioning = true)
GROUP BY dataset_key
ORDER BY dataset_key;Because dataset_key is the partition column, filtering on it lets DuckDB prune whole partitions — a single-dataset query never opens the other files. Nobody types that list: calcofi4r::cc_read_parquet_sql() / calcofi4py.read_parquet_sql() emit it from the catalog (next sections).
A partitioned table may also publish one whole-table file — a single-file twin — and obs does. In the catalog it is the objects[] entry without partition_by (ducklake/tables/obs/{content_hash}/obs.parquet); the resolvers leave it out of urls and return it separately as single_file (NA / None for a table that has none). It exists for https-only readers that cannot take a list — browser DuckDB-WASM, a read_parquet() that wants one URL — which read single_file instead of the partition list. Read one or the other, never both: the twin holds the same rows as the partitions, so a query over both counts every row twice. The price of the twin is partition pruning — a dataset_key filter still works, but scans the one file. On releases before v2026.09 it is the plain per-release copy, …/releases/{version}/parquet/obs.parquet:
-- the single-file twin of obs, for readers that cannot take a list or glob
SELECT dataset_key, count(*) AS n
FROM read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/obs.parquet')
GROUP BY dataset_key
ORDER BY dataset_key;Releases before v2026.09 (v2026.08.25 and the consolidated versions before it) have no objects[]: their partitioned table is a directory under parquet/, and plain HTTPS URLs cannot glob a directory. Reading one needs an s3-style glob, so configure DuckDB’s anonymous s3 access — GCS is s3-compatible:
INSTALL httpfs; LOAD httpfs;
SET s3_region = 'auto';
SET s3_endpoint = 'storage.googleapis.com';
SET s3_url_style = 'path';
SET s3_access_key_id = '';
SET s3_secret_access_key = '';
SELECT dataset_key, count(*) AS n
FROM read_parquet(
's3://calcofi-db/ducklake/releases/v2026.08.25/parquet/obs/**/*.parquet',
hive_partitioning = true)
GROUP BY dataset_key
ORDER BY dataset_key;The resolvers return this form for those versions automatically.
6.6 From R
Let the calcofi4r package register every release table as a view — it resolves each table through the catalog and handles httpfs, the partitioned tables, the supplemental opt-in and local caching (content-addressed, so a table unchanged between releases is cached once):
# remotes::install_github("calcofi/calcofi4r")
library(calcofi4r)
con <- cc_get_db() # promoted release, tables as views
con <- cc_get_db("v2026.08.25") # pinned; a retired version errors, naming the replacement
DBI::dbListTables(con)
# lazy dbplyr against the remote parquet
library(dplyr)
tbl(con, "taxon") |> filter(scientific_name == "Sardinops sagax")
# or a one-off SQL query
cc_query("SELECT dataset_key, count(*) AS n FROM obs GROUP BY 1 ORDER BY 2 DESC")To hand a table’s URLs to DuckDB (or anything else) yourself, resolve them through the catalog rather than building a path — cc_release_sources() is the one place a catalog entry becomes URLs, for any version:
library(DBI)
cat_ <- cc_catalog("latest") # or a pinned "vYYYY.MM.DD"
src <- cc_release_sources(cat_, "obs") # list(urls, hive, canonical, hashes, local_paths, single_file)
src$urls # one https URL per partition (the legacy path or s3 glob before v2026.09)
src$single_file # obs's whole-table twin — read it OR src$urls, never both
con <- dbConnect(duckdb::duckdb())
dbExecute(con, "INSTALL httpfs; LOAD httpfs;")
d <- dbGetQuery(con, sprintf(
"SELECT scientific_name, common_name, worms_id
FROM %s
WHERE common_name ILIKE '%%sardine%%'",
cc_read_parquet_sql(cc_release_sources(cat_, "taxon")))) # read_parquet('…') or read_parquet([...], hive_partitioning = true)6.7 From Python
calcofi4py registers every release table as a DuckDB view for you (partitioned + supplemental handling included), mirroring calcofi4r:
# pip install "calcofi4py @ git+https://github.com/CalCOFI/calcofi4py"
import calcofi4py as cc
con = cc.cc_get_db() # promoted release; cc_get_db("v2026.08.25") to pin
df = con.sql("SELECT * FROM taxon WHERE common_name ILIKE '%sardine%'").df()
cc.cc_query("SELECT count(*) FROM obs").fetchone()obs.measurement_qual carries each dataset’s own flag vocabulary, uninterpreted — bottle (6 = OK but from CTD, 8 = suspect, 9 = missing), CTD cast files (1/2 = use the primary/secondary sensor, 8 = questionable, 9 = bad/missing), DIC WOCE (2 = good, 3 = questionable, 4 = bad, 9 = missing); see metadata/measurement_qual.csv. A flagged value is still a row. Filter it yourself, or use the one predicate the apps use:
calcofi4r::cc_qual_ok_sql("o") # append to any WHERE over obs / obs_ctd_full / sample_measurementcalcofi4py.qual_ok_sql("o")-- what both expand to (NULL-safe: an unflagged row is kept)
AND COALESCE(NOT ((o.dataset_key IN ('calcofi_bottle', 'calcofi_ctd-cast')
AND regexp_replace(o.measurement_qual, '\.0+$', '') IN ('8', '9'))
OR (o.dataset_key = 'calcofi_dic' AND o.measurement_qual IN ('3', '4', '9'))), TRUE)Or plain duckdb, resolving the URLs through the catalog yourself (release_sources() mirrors calcofi4r::cc_release_sources(); a retired version raises RetiredVersionError, whose .to is the version to use instead):
import duckdb
import calcofi4py as cc
catalog = cc.cc_catalog("latest") # or "v2026.08.25"
src = cc.release_sources(catalog, "taxon") # {"urls", "hive", "canonical", "hashes", "local_paths", "single_file"}
con = duckdb.connect()
con.sql("INSTALL httpfs; LOAD httpfs;")
df = con.sql(f"""
SELECT scientific_name, common_name, worms_id
FROM {cc.read_parquet_sql(src)}
WHERE common_name ILIKE '%sardine%'
""").df()Without the package the same resolution is a few lines of json: fetch releases/{version}/catalog.json and prefix each table’s objects[].path with https://storage.googleapis.com/calcofi-db/, as the browser example below does.
6.8 From your browser
DuckDB compiles to WebAssembly, so the same engine runs client-side in your browser — no server, no install, no R or Python. CalCOFI ships a ready-made form at → Open CalCOFI Query that wraps the three retired bio↔︎env Plumber endpoints (and a free-form SQL shell): pick a function, fill the form, click Run, and a Chromium / Firefox / Safari worker thread fetches Parquet range-by-range over HTTPS and joins them in-browser.
To embed DuckDB-WASM in your own page, the boilerplate is about 15 lines — load the bundle from a CDN, instantiate, INSTALL httpfs and you’re ready:
<script type="module">
import * as duckdb from "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/+esm";
const bundles = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(bundles);
const worker = await duckdb.createWorker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
const conn = await db.connect();
await conn.query("INSTALL httpfs; LOAD httpfs;");
await conn.query("INSTALL spatial; LOAD spatial;");
const root = "https://storage.googleapis.com/calcofi-db";
const version = (await (await fetch(`${root}/ducklake/releases/latest.txt`)).text()).trim();
const catalog = await (await fetch(`${root}/ducklake/releases/${version}/catalog.json`)).json();
// resolve a table through the catalog: objects[].path → https URL. A partitioned
// table lists one object per partition (needs hive_partitioning) and may also
// publish a whole-table twin — the object WITHOUT partition_by (obs does); a
// browser reads the twin instead of the list, never both (that doubles every row).
// catalogs before v2026.09 have no objects[] — fall back to the per-release path
const readParquet = (name) => {
const t = catalog.tables.find(t => t.name === name);
const objs = t.objects ?? [{ path: `ducklake/releases/${version}/parquet/${name}.parquet` }];
const twin = t.partitioned ? objs.find(o => !o.partition_by) : null;
if (twin) return `read_parquet('${root}/${twin.path}')`;
const urls = objs.map(o => `'${root}/${o.path}'`).join(", ");
return `read_parquet([${urls}]${t.partitioned ? ", hive_partitioning = true" : ""})`;
};
const result = await conn.query(`
SELECT scientific_name, common_name, worms_id
FROM ${readParquet("taxon")}
WHERE common_name ILIKE '%sardine%'
`);
console.log(result.toArray());
</script>For arbitrary one-off SQL with no setup at all, paste a query into shell.duckdb.org — DuckDB’s official DuckDB-WASM shell. Same WebAssembly engine, same public Parquet, identical rows.
6.9 Run it from anywhere
The same portable SQL — what calcofi4r::cc_match_*() emits as attr(d, "sql"), what the Integrated App download bundle ships in its query/ folder, what is shown in the worked example below — runs in any DuckDB client:
| Where | How |
|---|---|
| R, on your laptop | calcofi4r::cc_match_ichthyo_by_name(...) — emits and runs the SQL; attr(d, "sql") hands it back |
| Python, on a notebook server | duckdb.connect().sql(open("query.sql").read()).df() — see From Python |
| shell, on the command line | duckdb < query.sql |
| your web browser, no install | CalCOFI Query — point-and-click form, runs DuckDB-WASM client-side |
6.10 Reproducibility
Because every query is plain SQL against immutable, versioned, public Parquet, a CalCOFI result is reproducible by anyone — pin the {version} and re-run the SQL. Pin a consolidated version: its bytes are kept indefinitely, and a later release that shares an object with it shares the same bytes.
The Integrated App builds on this: its data download bundle ships a query/ folder alongside the data:
data/original/{bio,env}.csv ← query/{bio,env}.sql
data/integrated/integrated_*.csv ← query/integrated_*.sql
query/manifest.json release version, filters, GCS source URLs,
per-file row counts + md5 checksums
query/REPRODUCE.md DuckDB-CLI / Python / R re-run snippets
Each *.sql file is fully interpolated, GCS-URL-based, and copy-paste runnable (prefixed with the INSTALL/LOAD it needs). Re-run query/integrated_*.sql in DuckDB and you get back exactly the rows in the matching .csv — the manifest.json md5 checksums let you confirm it. The same SQL is what calcofi4r::cc_match_bio_env() executes and attaches as attr(x, "sql"), what the browser-based CalCOFI Query generates as its SQL panel, and what a hand-written query produces — a single source of truth across R, Python, the CLI and JavaScript.
6.11 Worked example: sardine larvae + temperature
The recurring example through these pages is Pacific sardine (Sardinops sagax) larvae matched to CTD-bottle temperature, Q1 2018, with relaxed (5 km / 72 hr) matching.
Q1 2018 — not a more recent year — because CTD-bottle environmental data in the current release ends 2021-05, while net-tow biological data runs later. Q1 2018 has ample overlap of both.
Done as direct SQL, the query is a temporal interval join plus a spatial ST_Distance_Sphere filter. After the INSTALL/LOAD setup above, run the query below. This block is character-for-character what calcofi4r::cc_match_ichthyo_by_name() returns as attr(d, "sql") with version = "v2026.08.25" — the two produce the identical 13 rows. Every table in it was resolved through that release’s catalog, and because v2026.08.25 is consolidated the URLs stay valid. On this pre-v2026.09 release the partitioned obs resolves to an s3:// glob, so the query needs the anonymous-s3 settings; from v2026.09 the same call emits an explicit HTTPS list that runs anywhere, the browser included. The Integrated App download bundle builds query/integrated_*.sql with the same cc_match_bio_env() engine (its filter set is taxa + quarters + dates rather than life_stage, so a sardine / Q1 2018 bundle returns the egg + larva superset — same matching mechanics, same reproducibility):
WITH bio AS (
SELECT
o.obs_id::VARCHAR AS bio_id,
o.datetime AS bio_datetime,
o.longitude AS bio_lon,
o.latitude AS bio_lat,
o.measurement_value * shf.measurement_value / nullif(ps.measurement_value, 0) AS bio_value,
o.measurement_value AS tally,
o.taxon_key,
t.scientific_name,
t.worms_id,
o.life_stage
FROM read_parquet('s3://calcofi-db/ducklake/releases/v2026.08.25/parquet/obs/**/*.parquet', hive_partitioning = true) o
JOIN read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/taxon.parquet') t ON t.taxon_key = o.taxon_key
LEFT JOIN read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/sample_measurement.parquet') shf ON shf.sample_key = o.sample_key AND shf.measurement_type = 'std_haul_factor'
LEFT JOIN read_parquet('https://storage.googleapis.com/calcofi-db/ducklake/releases/v2026.08.25/parquet/sample_measurement.parquet') ps ON ps.sample_key = o.sample_key AND ps.measurement_type = 'prop_sorted'
WHERE o.realm = 'bio'
AND o.dataset_key = 'swfsc_ichthyo'
AND o.measurement_type = 'abundance'
AND o.measurement_value IS NOT NULL
AND COALESCE(NOT ((o.dataset_key = 'calcofi_bottle' AND regexp_replace(o.measurement_qual, '\.0+$', '') IN ('8', '9')) OR (o.dataset_key = 'calcofi_ctd-cast' AND regexp_replace(o.measurement_qual, '\.0+$', '') IN ('8', '9')) OR (o.dataset_key = 'calcofi_dic' AND regexp_replace(o.measurement_qual, '\.0+$', '') IN ('3', '4', '9'))), TRUE)
AND o.datetime IS NOT NULL
AND o.longitude IS NOT NULL
AND o.latitude IS NOT NULL
AND t.scientific_name IN ('Sardinops sagax')
AND o.life_stage IN ('larva')
AND o.datetime >= TIMESTAMP '2018-01-01'
AND o.datetime <= TIMESTAMP '2018-03-31'
),
env AS (
SELECT
obs_id AS env_id,
datetime AS env_datetime,
longitude AS env_lon,
latitude AS env_lat,
measurement_value AS env_value,
depth_min_m AS env_depth_m,
measurement_type AS measurement_type
FROM read_parquet('s3://calcofi-db/ducklake/releases/v2026.08.25/parquet/obs/**/*.parquet', hive_partitioning = true)
WHERE realm = 'env'
AND measurement_type = 'temperature'
AND measurement_value IS NOT NULL
AND COALESCE(NOT ((dataset_key = 'calcofi_bottle' AND regexp_replace(measurement_qual, '\.0+$', '') IN ('8', '9')) OR (dataset_key = 'calcofi_ctd-cast' AND regexp_replace(measurement_qual, '\.0+$', '') IN ('8', '9')) OR (dataset_key = 'calcofi_dic' AND regexp_replace(measurement_qual, '\.0+$', '') IN ('3', '4', '9'))), TRUE)
AND datetime IS NOT NULL
AND longitude IS NOT NULL
AND latitude IS NOT NULL
AND datetime >= TIMESTAMP '2018-01-01' - INTERVAL '72 hours'
AND datetime <= TIMESTAMP '2018-03-31' + INTERVAL '72 hours'
),
matched AS (
-- temporal interval join: every env observation within ± max_time_hr
SELECT
bio.*,
env.* EXCLUDE (env_lon, env_lat),
abs(epoch(bio.bio_datetime) - epoch(env.env_datetime)) / 3600.0 AS time_diff_hr,
ST_Distance_Sphere(
ST_Point(bio.bio_lon, bio.bio_lat),
ST_Point(env.env_lon, env.env_lat)) / 1000.0 AS dist_km
FROM bio
JOIN env
ON env.env_datetime BETWEEN bio.bio_datetime - INTERVAL '72 hours'
AND bio.bio_datetime + INTERVAL '72 hours'
),
within AS (
-- spatial filter: keep pairs within max_dist_km
SELECT * FROM matched
WHERE dist_km <= 5
),
ranked AS (
SELECT
*,
min(time_diff_hr) OVER (PARTITION BY bio_id) AS mn_time_diff_hr,
min(dist_km) OVER (PARTITION BY bio_id) AS mn_dist_km
FROM within
)
-- one row per bio observation (× measurement_type): env values aggregated
SELECT
* EXCLUDE (
env_id, env_value, env_datetime, env_depth_m,
time_diff_hr, dist_km, mn_time_diff_hr, mn_dist_km),
count(*) AS n_env,
avg(env_value) AS env_value,
CASE WHEN count(*) = 1 THEN 0
ELSE coalesce(stddev_samp(env_value), 0) END AS env_value_sd,
avg(env_depth_m) AS env_depth_m,
min(env_datetime) AS env_datetime_min,
max(env_datetime) AS env_datetime_max,
avg(dist_km) AS dist_km,
avg(time_diff_hr) AS time_diff_hr
FROM ranked
WHERE time_diff_hr = mn_time_diff_hr
GROUP BY ALL
ORDER BY bio_idThe next page, Matching Helpers, shows this same query as a one-liner.