# European air quality (EEA) > Europe's air quality archive, as the European Environment Agency publishes it through the Air Quality Download Service: one row per sampling point, time interval and aggregation type, from 2013 to now, for the 39 reporting countries. Both of the EEA’s quality tiers are included and flagged in a `tier` column — E1a, the verified data each country reports by 30 September for the previous year, and E2a ("Up-To-Date"), the unverified stream transmitted continuously and running about two hours behind live. The data is kept in the source’s own long form, so every pollutant a country reports is present rather than a curated handful: alongside PM10, PM2.5, NO2, O3, SO2 and CO there are benzene, the heavy metals and the rest of the Eionet vocabulary. Every value carries its validity and verification status, and the companion tables turn the codes into words: stations, sampling points, pollutants and the three code vocabularies. For Romania this is the same national monitoring network (RNMCA/ANPM) reached through the official European channel: no credentials, full history back to 2013, and verified numbers where they exist. - Dataset id: `eea-air-quality` - Update cadence: hourly - Upstream source: https://eeadmz1-downloads-webapp.azurewebsites.net/ - Tags: air-quality, environment, pollution, europe, eea, romania, pm10, pm25, no2, ozone, calitatea-aerului - Current version: ingest `01M54NT0AJ8DMD5HDRVTX72KXW`, snapshot 2026-10-17, generated 2026-10-17T10:30:06.011Z ## Licence and attribution - Licence: Creative Commons Attribution 4.0 International (`CC-BY-4.0`) - Licence text: https://www.eea.europa.eu/en/legal-notice - Required attribution: Air quality data from the European Environment Agency (EEA) Air Quality Download Service, reported by the EEA member and cooperating countries under Directive 2008/50/EC. Licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); copyright holder: European Environment Agency. - Notes: THE TERMS. The EEA legal notice (https://www.eea.europa.eu/en/legal-notice, modified 2026-07-02) states: "Unless otherwise indicated, the European Environment Agency (EEA) is the owner of copyrights and database rights in this website and its contents, and EEA materials are published under the CC-BY license" and "Information, documents and material available on this website and for which the EEA holds the rights of use are public and may be re-used without prior permission, free of charge, for commercial or non-commercial purposes, provided that the EEA is always acknowledged as the original source of the material and that the original meaning or message of the content is not distorted. Such acknowledgment must be included in each copy of the material. The re-use of the content on the EEA website covers the reproduction, adaptation and/or distribution, irrespective of the means and/or the format used." The EEA re-use policy follows Directive 2003/98/EC, the Commission Decision of 12 December 2011 on the re-use of Commission documents (2011/833/EU) and Directive 2013/37/EU. This specific dataset carries the same licence at the record level: the INSPIRE metadata for "Air Quality download service for verified and Up To Date data, 2013-now" in the EEA spatial data catalogue (https://sdi.eea.europa.eu/catalogue/datahub/api/records/fe809728-9cec-41c0-a9be-3a8f04600974) records its legal constraints verbatim as "License CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/). Copyright holder: European Environment Agency (EEA)." CAVEATS A REUSER MUST READ. (1) THIRD-PARTY CONTENT. The same legal notice says the permission above "do[es] not apply to content supplied by third parties". The measurements here are reported to the EEA by national environmental agencies under the Air Quality Directive (2008/50/EC) and its Implementing Decision 2011/850/EU, and the EEA publishes the aggregate; a reuser redistributing one country’s subset should name that country’s agency as well — for Romania, ANPM / Rețeaua Națională de Monitorizare a Calității Aerului. The EEA logo is separately restricted and is not republished here. (2) THE E2a TIER IS NOT THE OFFICIAL RECORD. Rows with tier = "E2a" are the Up-To-Date stream: unverified, mostly verification = 2 or 3, and subject to revision or withdrawal. The official, ratified record is the E1a tier, which arrives up to twenty-one months later. Presenting an E2a number as a country’s official air quality figure misrepresents the source. (3) INVALID VALUES ARE PUBLISHED, NOT REMOVED. A negative `validity` means the value failed its check or the station was being calibrated; those rows are kept so the record is complete, and an analysis that does not filter them is wrong. (4) WHAT IS CHANGED. This is a derived work: the source’s own columns, renamed to snake_case, with the no-data sentinel -9900 stored as NULL, a UTC instant added beside the source’s UTC+1 wall clock, and the Eionet vocabularies joined in as lookup tables. No value is converted, rounded or removed. Every measurement column description quotes the EEA’s own "Parquet schema definition" section verbatim, from https://eeadmz1-downloads-webapp.azurewebsites.net/content/documentation/How_To_Downloads.pdf. Reproduce the attribution line verbatim anywhere you republish this data. ## Manifest - Start here for how this directory works: https://data.bunnytech.app/llms.txt - This dataset: https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/manifest.json - All datasets: https://data-directory.fsn1.your-objectstorage.com/public/index.json - Human page: https://data.bunnytech.app/datasets/eea-air-quality The manifest is the contract: file URLs, row counts, column types, min/max statistics and example queries all live there. Re-read it to discover a new version. ## Fastest path — Parquet exports Immutable, physically sorted snapshot exports. No catalog fetch, best row-group pruning, safe to cache forever. Register one view per table: ```sql INSTALL httpfs; LOAD httpfs; CREATE OR REPLACE VIEW measurements AS SELECT * FROM read_parquet([ 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2012/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2013/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2014/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2015/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2016/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2017/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2018/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2019/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2020/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2021/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2022/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2023/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2024/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2025/data_0.parquet', 'https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2026/data_0.parquet' ]); CREATE OR REPLACE VIEW stations AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/stations.parquet'); CREATE OR REPLACE VIEW sampling_points AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/sampling_points.parquet'); CREATE OR REPLACE VIEW sampling_point_index AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/sampling_point_index.parquet'); CREATE OR REPLACE VIEW similar_stations AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/similar_stations.parquet'); CREATE OR REPLACE VIEW pollutants AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/pollutants.parquet'); CREATE OR REPLACE VIEW validity_codes AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/validity_codes.parquet'); CREATE OR REPLACE VIEW verification_codes AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/verification_codes.parquet'); CREATE OR REPLACE VIEW aggregation_types AS SELECT * FROM read_parquet('https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/aggregation_types.parquet'); ``` Example queries: -- One station’s NO2 series, verified data only: The sort order is built for exactly this: tier, country and pollutant prune first, the sampling point lands on one contiguous run, and start_time keeps it in reading order. validity IN (1, 2, 3, 4) keeps only the values the reporting country stands behind. ```sql ATTACH IF NOT EXISTS 'ducklake:https://data-directory.fsn1.your-objectstorage.com/catalog/catalog.ducklake' AS "datadirectory" (READ_ONLY); SELECT m.start_utc, m.value, m.unit FROM datadirectory.eea_air_quality.measurements m WHERE m.tier = 'E1a' AND m.country_code = 'RO' AND m.pollutant_code = 8 AND m.sampling_point_id = 'SPO-RO0122A_00008_100' AND m.agg_type = 'hour' AND m.validity IN (1, 2, 3, 4) ORDER BY m.start_time; ``` -- Daily PM2.5 average per station, most recent month: obs_year is the partition column, so constraining it means the read never opens the other years’ files. Note the DISTINCT subquery: sampling_points has one row per sampling-point CONFIGURATION, so joining it directly would multiply every measurement row by the number of configurations that sampling point has had. ```sql ATTACH IF NOT EXISTS 'ducklake:https://data-directory.fsn1.your-objectstorage.com/catalog/catalog.ducklake' AS "datadirectory" (READ_ONLY); SELECT sp.station_code, sp.station_type, date_trunc('day', m.start_time) AS day, round(avg(m.value), 1) AS pm25 FROM datadirectory.eea_air_quality.measurements m JOIN (SELECT DISTINCT sampling_point_id, station_code, station_type FROM datadirectory.eea_air_quality.sampling_points) sp USING (sampling_point_id) WHERE m.obs_year = 2026 AND m.pollutant_code = 6001 AND m.country_code = 'RO' AND m.agg_type = 'hour' AND m.validity > 0 GROUP BY 1, 2, 3 ORDER BY day DESC, pm25 DESC; ``` ## Live lake — DuckLake catalog with time travel Attach the published catalog read-only over plain https. This works in native DuckDB AND in duckdb-wasm in the browser — the reader catalog resolves its data files through public https URLs, so no credentials and no backend are involved. ```sql INSTALL ducklake; LOAD ducklake; ATTACH 'ducklake:https://data-directory.fsn1.your-objectstorage.com/catalog/eea-air-quality/catalog.ducklake' AS eea_air_quality (READ_ONLY); SELECT * FROM eea_air_quality.main.measurements LIMIT 5; USE eea_air_quality; ``` - Latest catalog: https://data-directory.fsn1.your-objectstorage.com/catalog/eea-air-quality/catalog.ducklake - This exact version: https://data-directory.fsn1.your-objectstorage.com/catalog/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/catalog.ducklake (sha256 a45138cb9a6fc1fd8a26919584ba7eac14aabc455237ba47a90bc5902952d461) - Written by DuckDB v1.5.5 - Time travel: this ingest is snapshot 32 — e.g. `SELECT * FROM eea_air_quality.measurements AT (VERSION => 32)` ## Wholesale — S3 access For bulk downloads, read the bucket prefix directly over S3 with httpfs: ```sql INSTALL httpfs; LOAD httpfs; CREATE SECRET hetzner ( TYPE s3, ENDPOINT 'fsn1.your-objectstorage.com', URL_STYLE 'path', KEY_ID '', SECRET '' ); SELECT * FROM read_parquet('s3://data-directory/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/**/*.parquet'); ``` ## Schema ### measurements One row per sampling point, time interval and aggregation type — the whole of the EEA air quality archive in the source’s own long form, both the quality-verified E1a tier and the unverified near-real-time E2a tier, with `tier` saying which. `pollutant_code` says what was measured (join `pollutants`), `sampling_point_id` says where (join `sampling_points` for the station, its coordinates and its classification), and `value` is the number in the unit `unit` names. Values that FAILED their quality check are present, not removed: `validity = 1` (or 2, 3, 4) is the filter almost every analysis wants, and `validity < 0` is how the source marks a value it does not stand behind. - 46.9M rows · 272 MB · write mode `snapshot_history` - Physically sorted by: `tier`, `country_code`, `pollutant_code`, `sampling_point_id`, `start_time` - Primary key: `tier`, `sampling_point_id`, `start_time`, `agg_type` - Files: 15 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2012/data_0.parquet (303 rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2013/data_0.parquet (2.7M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2014/data_0.parquet (2M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2015/data_0.parquet (2.7M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2016/data_0.parquet (3.2M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2017/data_0.parquet (4.4M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2018/data_0.parquet (5.5M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2019/data_0.parquet (3.5M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2020/data_0.parquet (5.1M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2021/data_0.parquet (5.4M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2022/data_0.parquet (2.7M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2023/data_0.parquet (1.8M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2024/data_0.parquet (2.3M rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2025/data_0.parquet (796 rows) - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/measurements/obs_year=2026/data_0.parquet (5.7M rows) Columns: - `tier` VARCHAR NOT NULL — Which EEA delivery this row came from. "E1a" is the quality-verified dataset a country reports by 30 September for the previous year; "E2a" is the Up-To-Date/UTD stream it transmits continuously and has not yet verified. The two are never mixed silently — this column is the reason, and it is the leading sort column so filtering on it is free. (range: "E1a" … "E2a") (~2 distinct) - `country_code` VARCHAR NOT NULL — ISO 3166-1 alpha-2 reporting country code, taken from the prefix the source puts on every sampling point identifier ("RO/SPO-RO0122A_00008_100"). It is the unit in which this table is reloaded — re-running a country replaces exactly that country — and the second sort column, so "everything in Romania" is a range rather than a pattern match. (range: "RO" … "RO") (~1 distinct) - `pollutant_code` INTEGER NOT NULL — Numeric pollutant identifier from the Eionet vocabulary at http://dd.eionet.europa.eu/vocabulary/aq/pollutant — 1 is SO2, 5 is PM10, 7 is O3, 8 is NO2, 10 is CO, 6001 is PM2.5, and so on for 678 codes. Joins to pollutants.pollutant_code for the symbol, the name and the mandatory unit. The EEA documentation calls the source column simply "the pollutant identifier". (range: 1 … 6001) - `sampling_point_id` VARCHAR NOT NULL — The sampling point this row was measured at, without the country prefix. The EEA documents it as "identifier known as 'local id' of the sampling point … created by national reporters, therefore there is no vocabulary for this parameter", so its exact shape differs by country. Joins to sampling_points.sampling_point_id and to sampling_point_index.sampling_point_id, which are where the station, its coordinates and its classification live. Deliberately NOT parsed into a station code here: only about a third of Europe’s sampling point ids embed one, and 4,380 of them embed a string that is not the station’s EoI code at all. (range: "SPO-RO0008R_00001_100" … "SPO-RO0280A_00007_100") (~1595 distinct) - `start_time` TIMESTAMP NOT NULL — Beginning of the time interval the value is reported for, exactly as the source writes it — a wall clock with no offset attached. The EEA converts hourly and variable-interval measurements to UTC+1 before publishing them; daily values are instead left in the time zone the reporting country uses (sampling_points.time_zone). Use start_utc when you need a true instant, and this column when you need the number the source published. (range: "2012-12-31 23:00:00" … "2026-10-17 10:00:00") (~107725 distinct) - `obs_year` INTEGER NOT NULL — Calendar year of start_time. Redundant with start_time on purpose: it is the column the published parquet is partitioned on, so a query constrained to a year or a range of years skips the other years’ files without opening them. - `start_utc` TIMESTAMPTZ — start_time as a true instant in UTC. Filled for the hourly and variable-interval rows, which the EEA states are published in UTC+1; NULL for daily rows, whose clock is the reporting country’s own and is not carried in the measurement file — join sampling_points.time_zone if you need to place those on a UTC axis. - `end_time` TIMESTAMP — End of the time interval the value is reported for, on the same clock as start_time — UTC+1 for hourly and variable-interval rows, the reporting country’s own zone for daily rows (see start_time). For hourly rows it is start_time plus one hour. - `agg_type` VARCHAR NOT NULL — The primary observation the value is: "hour", "day" or "var" (a variable interval — weekly, monthly, anything that is not one of the first two). Joins to aggregation_types.code. Part of the primary key, because one sampling point publishes both hourly values and daily aggregates for the same day. (~2 distinct) - `value` DOUBLE [the unit named by this row’s unit column] — The measurement, in the unit `unit` names. The source writes -9900 where there is no measurement at all; that sentinel is stored as NULL here, because a -9900 left in place silently ruins every average taken over the column. Nothing else is altered: no unit conversion, no rounding, no removal of values that failed a quality check — `validity` is what tells you about those. - `unit` VARCHAR — Unit of measurement, from the Eionet vocabulary at http://dd.eionet.europa.eu/vocabulary/uom/concentration. "ug.m-3" is µg/m³, "mg.m-3" is mg/m³, "ng.m-3" is ng/m³. It varies by pollutant — CO is reported in mg/m³ and heavy metals in ng/m³ — so never aggregate across pollutants without grouping by it. - `validity` INTEGER — Validity of the measurement, from http://dd.eionet.europa.eu/vocabulary/aq/observationvalidity. 1 valid, 2 valid but below the detection limit, 3 the same with the number replaced by half the detection limit, 4 valid ozone using CCQM.O3.2019; -1 not valid (or simply missing) and -99 not valid because the station was being calibrated or maintained. Joins to validity_codes.code. Invalid rows are published rather than dropped, so filter them out deliberately. - `verification` INTEGER — Verification status, from http://dd.eionet.europa.eu/vocabulary/aq/observationverification: 1 verified, 2 preliminary verified, 3 not verified. Joins to verification_codes.code. Independent of validity — a value can be verified and invalid, or valid and unverified. E1a rows are overwhelmingly 1; E2a rows are 2 or 3. - `result_time` TIMESTAMP — When the file that carried this value was generated, in the EEA’s words "the date and time in which the information of the file that contained the reported data was generated". It is the closest thing the source gives to "when was this number last restated", and it moves when a country re-delivers a period. - `data_capture` DOUBLE [%] — Data capture associated with the primary observation: the percentage of the interval for which a sample or observation was actually taken. Mostly NULL — the countries that report it do so for aggregated values rather than for hourly ones. - `snapshot_date` DATE NOT NULL — The UTC run day on which this tier and country were last read from the EEA containers. The source republishes each sampling point’s full series, and a value can be revised or withdrawn, so this column says how fresh our copy of it is. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### stations One row per air quality monitoring station in the EEA’s pan-European metadata — 6,716 of them across 42 reporting countries — with its coordinates, altitude, network, station type and area classification. The metadata file describes a station once per sampling-point configuration and a station that moved or was re-classified appears more than once, so this table keeps the most recently commissioned description of each station; sampling_points has every row of the file, unaggregated. - 6,716 rows · 200 KB · write mode `upsert` - Physically sorted by: `country_code`, `station_code` - Primary key: `station_code` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/stations.parquet (6,716 rows) Columns: - `country_code` VARCHAR NOT NULL — ISO 3166-1 alpha-2 code of the reporting country. Leads the sort order, so "every station in Romania" is a range on a sort column. (range: "AD" … "XK") - `station_code` VARCHAR NOT NULL — The station’s EoI code — the pan-European identifier, two letters of country followed by a number and a letter. This is the code EEA maps and reports use, and the key sampling_points.station_code and similar_stations.station_code join to. (range: "4101422" … "XK0012A") (~6801 distinct) - `station_id` VARCHAR — The station’s INSPIRE local identifier as reported ("STA-" plus the national code). Not the same string as station_code, and it is the one that appears inside the reporting country’s own XML deliveries. - `station_national_code` VARCHAR — The station’s identifier in the reporting country’s own national system, as the country declared it. Its format is entirely national — a number in some countries, a mnemonic in others. - `network_id` VARCHAR — The monitoring network the station belongs to, as an INSPIRE local identifier ("NET-" plus a national code). In most countries this is the regional or agency-level network that operates the station. - `namespace` VARCHAR — The reporting namespace that owns these identifiers, e.g. "RO.ANPM.AQ" for Romania’s national environmental protection agency. It is what makes a national code unique across Europe. - `time_zone` VARCHAR — The time zone the reporting country delivers this station’s data in, as the last segment of the Eionet timezone vocabulary URI. It is what daily measurement rows are timed in (hourly and variable rows are converted to UTC+1 by the EEA before publication). - `projection` VARCHAR — The coordinate reference system the longitude, latitude and altitude are given in. Every row of the source file says EPSG:4979 — WGS 84 in three dimensions. - `longitude` DOUBLE [degrees] — Station longitude in decimal degrees. - `latitude` DOUBLE [degrees] — Station latitude in decimal degrees. - `altitude` DOUBLE [m] — Station altitude above sea level. Genuinely negative values occur (192 rows of the source file are between -4 and -1 m, in countries that lie below sea level) and are kept as they are; the file carries no "unknown" sentinel in this column. - `station_type` VARCHAR — What dominates the air the station measures: "background", "traffic" or "industrial". This is the classification the Air Quality Directive requires, and comparing a traffic station with a background one without saying so is the single most common way to misread this dataset. - `station_area` VARCHAR — The kind of area the station stands in: "urban", "suburban", "rural", or one of the three finer rural classes "rural-regional", "rural-nearcity" and "rural-remote". - `sampling_point_count` INTEGER — How many distinct sampling points the metadata file lists for this station. Derived here rather than read from the source, so "which stations measure more than one thing" is answerable without a join. - `pollutant_count` INTEGER — How many distinct pollutants the metadata file lists for this station. Derived here for the same reason as sampling_point_count. - `observation_begin` TIMESTAMP — The earliest ObservationDateBegin across the station’s sampling points — when the station first started reporting anything, as the metadata file records it. - `observation_end` TIMESTAMP — The latest ObservationDateEnd across the station’s sampling points. NULL while at least one of its sampling points is still open, which is the normal case for an active station. - `first_seen_date` DATE NOT NULL — The first UTC run day on which this station appeared in the EEA metadata. Maintained by the framework: it is never moved forward. - `last_seen_date` DATE NOT NULL — The most recent UTC run day on which this station still appeared in the EEA metadata. A station that stops being listed keeps its old value rather than being deleted, so its measurements never lose their dimension row. - `snapshot_date` DATE NOT NULL — The UTC run day this row was last read from the metadata file. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that last wrote this row started (UTC). ### sampling_points The EEA pan-European metadata export, landed row for row: one row per sampling-point configuration — the instrument, its inlet, the pollutant it measures and the period it measured it for. A sampling point that was re-equipped or re-classified has more than one row, which is why this table declares no primary key: all 69,342 rows of the file are distinct, and collapsing them would throw away the only record the source publishes of that change. It is the authoritative way to get from measurements.sampling_point_id to a station, a location and a pollutant — but JOIN IT THROUGH A DISTINCT: `JOIN (SELECT DISTINCT sampling_point_id, station_code, station_type FROM sampling_points) USING (sampling_point_id)`, or a sampling point with several configurations multiplies every measurement row it matches. - 69.3K rows · 1.1 MB · write mode `replace` - Physically sorted by: `country_code`, `station_code`, `sampling_point_id`, `pollutant_code` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/sampling_points.parquet (69.3K rows) Columns: - `country_code` VARCHAR NOT NULL — ISO 3166-1 alpha-2 code of the reporting country. (range: "AD" … "XK") - `station_code` VARCHAR NOT NULL — EoI code of the station this sampling point belongs to; joins to stations.station_code. (range: "4101422" … "XK0012A") - `sampling_point_id` VARCHAR NOT NULL — The sampling point identifier, in exactly the form measurements.sampling_point_id carries it. NOT UNIQUE in this table: 69,342 rows cover 48,717 sampling points, so join through a DISTINCT subquery — see the table description. (range: "GB_SamplingPoint_105" … "SP_52001002_9_A") - `pollutant_code` INTEGER — Numeric pollutant identifier, taken from the trailing segment of the source’s AirPollutantCode URI. Joins to pollutants.pollutant_code and to measurements.pollutant_code. (range: 1 … 7910) - `pollutant_uri` VARCHAR — The full Eionet vocabulary URI the source writes for the pollutant, kept verbatim alongside the code parsed out of it. - `sampling_process_id` VARCHAR — Identifier of the sampling process — the measurement method plus the instrument, as the country declared it. Two rows for the same sampling point usually differ here. - `sample_id` VARCHAR — Identifier of the sample the process feeds, as the country declared it. Part of the INSPIRE model the EEA’s e-Reporting is built on. - `station_id` VARCHAR — The station’s INSPIRE local identifier ("STA-" plus a national code); joins to stations.station_id. - `station_national_code` VARCHAR — The station’s identifier in the reporting country’s own national system. - `network_id` VARCHAR — The monitoring network this sampling point reports through. - `namespace` VARCHAR — The reporting namespace that owns these identifiers. - `time_zone` VARCHAR — The time zone the reporting country delivers this sampling point’s data in, as the last segment of the Eionet timezone vocabulary URI. This is the clock daily measurement rows are on. - `observation_begin` TIMESTAMP — When this configuration started producing observations, as the country declared it. - `observation_end` TIMESTAMP — When it stopped. NULL while the configuration is still in use, which is the normal case for an operating sampling point. - `projection` VARCHAR — Coordinate reference system of longitude, latitude and altitude. EPSG:4979 throughout the file — WGS 84 in three dimensions. - `longitude` DOUBLE [degrees] — Longitude of the sampling point, in decimal degrees. - `latitude` DOUBLE [degrees] — Latitude of the sampling point, in decimal degrees. - `altitude` DOUBLE [m] — Altitude of the sampling point above sea level. - `measurement_type` VARCHAR — How the measurement is made: "automatic" (a continuous analyser), "active" (a pump drawing air through a filter that is later analysed), "passive" (a diffusive sampler) or "remote" (an optical technique measuring at a distance). - `station_type` VARCHAR — Station classification for this configuration: "background", "traffic" or "industrial". It sits on the sampling-point row because a station can be re-classified over its life. - `station_area` VARCHAR — Area classification for this configuration: "urban", "suburban", "rural", "rural-regional", "rural-nearcity" or "rural-remote". - `equivalence_demonstrated` VARCHAR — Whether the method has been shown equivalent to the reference method the Air Quality Directive prescribes: "ref" the reference method itself, "yes" demonstrated equivalent, "no" not demonstrated, "noRef" no reference method exists for this pollutant, "inprogress" a demonstration under way. - `measurement_equipment` VARCHAR — The instrument model, as the last segment of the Eionet measurement-equipment vocabulary URI the country reported. NULL when the country reported no equipment. - `inlet_height` DOUBLE [m] — Height of the sampling inlet above the ground. The source writes a negative sentinel (-99, -999, -9900 or -9999, depending on the country) where the value is unknown; since a height cannot be negative, anything below zero is stored as NULL here. - `building_distance` DOUBLE [m] — Distance from the inlet to the nearest building. Same negative-sentinel handling as inlet_height. - `kerb_distance` DOUBLE [m] — Distance from the inlet to the kerb of the nearest road — the number that makes a traffic station’s readings interpretable. Same negative-sentinel handling as inlet_height. - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the metadata file. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### sampling_point_index The station-to-sampling-point index the download service keeps beside the data itself (station-spo-metadata.csv, in the same container as the parquet files). Thinner than sampling_points — station, pollutant label and coordinates, nothing else — but considerably fresher: it covered 1,445 of Romania’s 1,536 near-real-time sampling points on 2026-09-02, where the pan-European metadata export, last regenerated in March 2024, covered 1,194. Use it when a sampling point in measurements has no row in sampling_points. - 59.7K rows · 645 KB · write mode `replace` - Physically sorted by: `station_code`, `sampling_point_id` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/sampling_point_index.parquet (59.7K rows) Columns: - `station_code` VARCHAR NOT NULL — EoI code of the station, from the file’s own "eucode" column; joins to stations.station_code. (range: "AD0942A" … "XK0013") - `sampling_point_id` VARCHAR NOT NULL — The sampling point identifier in the normalised form the parquet files use, from the file’s own "spo_norm" column; joins to measurements.sampling_point_id. (range: "GB_SamplingPoint_105" … "SP_52001002_9_A") - `sampling_point_id_reported` VARCHAR — The sampling point identifier as originally reported, from the file’s own "spo" column. Identical to sampling_point_id for most countries; kept because it is the string the reporting country actually uses. - `station_id` VARCHAR — The station’s INSPIRE local identifier, from the file’s own "sta" column. - `pollutant_label` VARCHAR — The pollutant this sampling point measures, as the short label the file writes in its "polu" column ("PM10", "NO2", "Cd in PM10"). It is the Notation of the Eionet pollutant vocabulary, so it joins to pollutants.notation. - `latitude` DOUBLE [degrees] — Latitude of the station, in decimal degrees, from the file’s "Lat". - `longitude` DOUBLE [degrees] — Longitude of the station, in decimal degrees, from the file’s "Lon". - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the index file. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### similar_stations The similarity ranking the EEA stores beside the measurement data (similar-stations.csv, in the same container as the parquet files): for each target station and pollutant, the other stations its own model considers comparable. The EEA publishes no schema for this companion file, so every column below is described from the file’s own header and from what the file demonstrably contains — that is stated on each column rather than dressed up as documentation. It is landed rather than linked because house rule 12 requires every machine-readable file a source exposes to be read. - 65.9K rows · 660 KB · write mode `replace` - Physically sorted by: `target_station_code`, `pollutant_label`, `similarity_rank` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/similar_stations.parquet (65.9K rows) Columns: - `target_station_code` VARCHAR NOT NULL — The station the ranking is for, from the file’s "Target" column. An EoI code, so it joins to stations.station_code. (range: "AL0203A" … "XK0012A") - `pollutant_label` VARCHAR — The pollutant the ranking is for, from the file’s "AirPollutant" column, written as the Eionet pollutant Notation — so it joins to pollutants.notation. Similarity is per-pollutant: the same two stations rank differently for NO2 and for PM10. (range: "NO2" … "PM2.5") - `similarity_rank` INTEGER — The file’s own "RowNumber" column, verbatim: the position of this station in the target’s ranking, counting from 1. Lower is more similar. (range: 1 … 6) - `station_code` VARCHAR — The comparable station, from the file’s "AirQualityStationEoICode" column; joins to stations.station_code. - `similarity_score` DOUBLE — The file’s own "EucDist" column, verbatim. Its header names a Euclidean distance, but the values in the file are between 0 and 1 and DECREASE as similarity_rank grows, so in practice it reads as a similarity score where higher is more alike. The EEA publishes no definition of the feature space it is computed in; treat it as the source’s own ordering rather than as a distance in any named unit. - `group_hash` VARCHAR — The file’s own "Group" column, verbatim: a 32-character hexadecimal digest that is constant across all rows of one (target, pollutant) ranking. The EEA publishes no definition of it; it behaves as an opaque identifier for the ranking itself. - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the similarity file. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### pollutants The Eionet Data Dictionary vocabulary "AQD - Pollutant", landed as a lookup table: the 678 codes measurements.pollutant_code can take, with the symbol, the full name, the definition and the unit the Air Quality Directive makes mandatory for each. Without this table a reader has a column of bare integers; with it, `pollutant_code = 6001` is PM2.5. - 684 rows · 17 KB · write mode `replace` - Physically sorted by: `pollutant_code` - Primary key: `pollutant_code` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/pollutants.parquet (684 rows) Columns: - `pollutant_code` INTEGER NOT NULL — The numeric code, parsed from the trailing segment of the concept URI. This is the value measurements.pollutant_code and sampling_points.pollutant_code carry. (range: 1 … 7910) (~802 distinct) - `notation` VARCHAR — The vocabulary’s Notation: the chemical symbol or short label the EEA uses everywhere else — "SO2", "PM10", "PM2.5", "NOX as NO2", "Cd in PM10". It is also the string sampling_point_index.pollutant_label and similar_stations.pollutant_label carry. - `label` VARCHAR — The vocabulary’s full English label, including the medium in brackets — "Particulate matter < 2.5 µm (aerosol)". - `definition` TEXT — The vocabulary’s own definition of the concept, in full. Empty for many of the rarer codes, which the vocabulary defines by their label alone. - `uri` VARCHAR — The concept URI. It is what the source’s AirPollutantCode column contains verbatim, so it is the string to match on when reading EEA data from anywhere else. - `status` VARCHAR — Registration status of the concept in the Data Dictionary — "valid" for a code in current use, "deprecated" or "superseded" for one that has been retired. Retired codes still appear in historical measurements, which is why they are kept here. - `accepted_date` DATE — The day the concept was accepted into the vocabulary. - `mandatory_unit` VARCHAR — The unit the vocabulary makes mandatory for this pollutant, as the last segment of its unit-of-measure URI — "ug.m-3", "mg.m-3". NULL where the vocabulary prescribes none, in which case measurements.unit is the only statement of the unit. - `mandatory_unit_uri` VARCHAR — The full unit-of-measure URI, kept verbatim beside the segment parsed out of it. - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the vocabulary. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### validity_codes The Eionet Data Dictionary vocabulary "AQD - Observation Validity", landed as a lookup table: what each measurements.validity value means. This is the table that tells a reader that -1 is "not valid" and 2 is "valid, but below detection limit" — the difference between an analysis and a wrong answer. - 6 rows · 2.9 KB · write mode `replace` - Physically sorted by: `code` - Primary key: `code` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/validity_codes.parquet (6 rows) Columns: - `code` INTEGER NOT NULL — The validity code, from the vocabulary’s Notation. Joins to measurements.validity. (range: -99 … 4) (~5 distinct) - `label` VARCHAR — The vocabulary’s English label for the code. - `definition` TEXT — The vocabulary’s own definition, in full — including the separate rules it gives for primary data and for aggregated data. - `uri` VARCHAR — The concept URI. - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the vocabulary. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### verification_codes The Eionet Data Dictionary vocabulary "AQD - Observation Verification", landed as a lookup table: what each measurements.verification value means. Verification is independent of validity — a value can be verified and invalid, or valid and not yet verified — and this table is where the source says so in its own words. - 3 rows · 2.5 KB · write mode `replace` - Physically sorted by: `code` - Primary key: `code` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/verification_codes.parquet (3 rows) Columns: - `code` INTEGER NOT NULL — The verification code, from the vocabulary’s Notation. Joins to measurements.verification. (range: 1 … 3) (~3 distinct) - `label` VARCHAR — The vocabulary’s English label for the code. - `definition` TEXT — The vocabulary’s own definition, in full. - `uri` VARCHAR — The concept URI. - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the vocabulary. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ### aggregation_types The Eionet Data Dictionary vocabulary "AQD - Primary Observations Time", landed as a lookup table: what each measurements.agg_type value means. The measurement files use only "hour", "day" and "var" today, but the vocabulary defines ten, and a country that starts reporting weekly or monthly values will land rows this table already explains. - 10 rows · 2.8 KB · write mode `replace` - Physically sorted by: `code` - Primary key: `code` - Files: 1 - https://data-directory.fsn1.your-objectstorage.com/public/eea-air-quality/v/01M54NT0AJ8DMD5HDRVTX72KXW/aggregation_types.parquet (10 rows) Columns: - `code` VARCHAR NOT NULL — The aggregation code, from the vocabulary’s Notation. Joins to measurements.agg_type. (range: "day" … "year") (~10 distinct) - `label` VARCHAR — The vocabulary’s English label for the code. - `definition` TEXT — The vocabulary’s own definition, where it gives one. - `uri` VARCHAR — The concept URI. - `snapshot_date` DATE NOT NULL — The UTC run day this row was read from the vocabulary. - `ingested_at` TIMESTAMP NOT NULL — When the ingest run that produced this row started (UTC). ## Performance notes - There are no indexes in a lakehouse. The physical sort order plus Parquet row-group min/max statistics ARE the index. Predicates on the leading sort columns prune whole row groups; everything else is a full column scan. - `measurements` prunes on `tier` → `country_code` → `pollutant_code` → `sampling_point_id` → `start_time`. - `stations` prunes on `country_code` → `station_code`. - `sampling_points` prunes on `country_code` → `station_code` → `sampling_point_id` → `pollutant_code`. - `sampling_point_index` prunes on `station_code` → `sampling_point_id`. - `similar_stations` prunes on `target_station_code` → `pollutant_label` → `similarity_rank`. - `pollutants` prunes on `pollutant_code`. - `validity_codes` prunes on `code`. - `verification_codes` prunes on `code`. - `aggregation_types` prunes on `code`. - `ILIKE '%needle%'` cannot prune: it decompresses that whole column. Prefer prefix predicates on the lowercased search companion columns where they exist. - Every file URL under `/v//` is immutable. Cache it as aggressively as you like. - Files are never rewritten in place — a new ingest writes a new version path. Re-read the manifest to pick up updates rather than polling the file URLs. - Pull only the columns you need. Parquet is columnar and the reader range-GETs what it reads. ## Do not - Do not scrape https://data.bunnytech.app HTML. Every number on those pages comes from the manifest above. - Do not hit the upstream source (https://eeadmz1-downloads-webapp.azurewebsites.net/) in bulk. This mirror exists so you do not have to. - Do not read DuckLake internal data files with a plain Parquet reader. They come with positional delete files, and ignoring those silently returns deleted rows. Use the ducklake extension, or use the published exports above. - Do not republish without the attribution line, and do not imply endorsement.