Environment · Public data
Air quality in Europe, daily
PM2.5, PM10, NO2 and ozone at Europe's monitoring stations, day by day.
European Environment Agency, Air Quality download service (E2a, up-to-date data) · EEA standard re-use policy: CC BY 4.0 · commons_pubdata.environment.air_quality_daily
- 2,436,953rows
- The 39 countries reporting to the EEAcoverage
- Weeklyrefresh
- 10 Oct 2026last refreshed
- 30.8 MBtable size

| date | sampling_point_id | pollutant | pollutant_code | mean | max |
|---|---|---|---|---|---|
| 2025-03-05 | XK/SPO-XK0001A_00005_100 | PM10 | 5 | 39.45744545454546 | 65.1729 |
| 2025-03-05 | XK/SPO-XK0001A_00007_100 | O3 | 7 | 15.391271818181815 | 38.6456 |
| 2025-03-05 | XK/SPO-XK0001A_00008_100 | NO2 | 8 | 70.2291 | 102.667 |
| 2025-03-05 | XK/SPO-XK0001A_06001_100 | PM2.5 | 6001 | 23.180236363636364 | 42.6806 |
| 2025-03-05 | XK/SPO-XK0002A_00005_100 | PM10 | 5 | 48.1777 | 81.4314 |
| 2025-03-05 | XK/SPO-XK0002A_00007_100 | O3 | 7 | 55.26038888888889 | 87.5597 |
| 2025-03-05 | XK/SPO-XK0002A_00008_100 | NO2 | 8 | 58.663980000000016 | 88.7377 |
| 2025-03-05 | XK/SPO-XK0002A_06001_100 | PM2.5 | 6001 | 31.438830000000003 | 56.8934 |
The first rows of environment.air_quality_daily as of 10 Oct 2026.
Query it with DuckDB.
lhbox duckdb --catalog commons/pubdata
SELECT d.date, s.station_name, s.municipality, s.country_code, round(d.mean, 1) AS pm25 FROM commons_pubdata.environment.air_quality_daily AS d JOIN commons_pubdata.environment.air_quality_stations AS s USING (sampling_point_id) WHERE d.pollutant = 'PM2.5' AND d.valid_hours >= 18 ORDER BY d.mean DESC LIMIT 20;
INSTALL iceberg; LOAD iceberg; INSTALL httpfs; LOAD httpfs; CREATE SECRET pubdata (TYPE S3, ENDPOINT 'lakehousebox-public-data.b-cdn.net', URL_STYLE 'path', REGION 'us-east-1'); ATTACH ':memory:' AS pubdata; CREATE SCHEMA pubdata.environment; CREATE VIEW pubdata.environment.air_quality_daily AS SELECT * FROM iceberg_scan('s3://commons--pubdata/environment/air_quality_daily'); CREATE VIEW pubdata.environment.air_quality_stations AS SELECT * FROM iceberg_scan('s3://commons--pubdata/environment/air_quality_stations');
SELECT d.date, s.station_name, s.municipality, s.country_code, round(d.mean, 1) AS pm25 FROM pubdata.environment.air_quality_daily AS d JOIN pubdata.environment.air_quality_stations AS s USING (sampling_point_id) WHERE d.pollutant = 'PM2.5' AND d.valid_hours >= 18 ORDER BY d.mean DESC LIMIT 20;
Read it where you work.
Public, read-only data. With an account, lhbox duckdb attaches the catalog as commons_pubdata; an agent with the LakehouseBox connector opens it with open_public_catalog("commons/pubdata"). Without one, DuckDB reads the public files directly.
Example queries
The worst PM2.5 days this year, with the station behind them
SELECT d.date, s.station_name, s.municipality, s.country_code, round(d.mean, 1) AS pm25 FROM commons_pubdata.environment.air_quality_daily AS d JOIN commons_pubdata.environment.air_quality_stations AS s USING (sampling_point_id) WHERE d.pollutant = 'PM2.5' AND d.valid_hours >= 18 ORDER BY d.mean DESC LIMIT 20;
Days above the WHO 2021 daily guideline for NO2 (25 µg/m³) per country, traffic stations only
SELECT s.country_code, count(*) AS station_days FROM commons_pubdata.environment.air_quality_daily AS d JOIN commons_pubdata.environment.air_quality_stations AS s USING (sampling_point_id) WHERE d.pollutant = 'NO2' AND s.station_type = 'traffic' AND d.valid_hours >= 18 AND d.mean > 25 GROUP BY 1 ORDER BY 2 DESC;
Know your columns.
| Column | Type | Meaning |
|---|---|---|
date | date | the day (the measurement hours' start dates) |
sampling_point_id | string | the EEA sampling point, <country>/SPO-… (joins air_quality_stations) |
pollutant | string | PM2.5, PM10, NO2 or O3 |
pollutant_code | long | the EEA pollutant vocabulary code (6001, 5, 8, 7) |
mean | double | mean of the day's valid hourly values |
max | double | highest valid hourly value of the day |
View all 8 columns
valid_hours | long | how many valid hourly values the day had (24 = complete) |
unit | string | unit as reported, usually ug.m-3 (micrograms per cubic metre) |
Join keys: sampling_point_id.
Companion tables
commons_pubdata.environment.air_quality_stations: one row per sampling point: station code and name, country, pollutant, coordinates, altitude, area (urban, suburban, rural) and type (traffic, industrial, background), municipality
Before you query.
- Values are unverified (E2a): an isolated spike can be an instrument fault the verified release later removes.
- Require
valid_hours >= 18(75 % of the day, the EU's own completeness rule) before comparing days or stations. - Join
air_quality_stationsfor coordinates and forstation_type(traffic, industrial, background) andarea; comparing a traffic station with a rural background one is comparing different things. - Ozone peaks in summer afternoons, so its daily mean understates it: use
maxor a running 8-hour mean on the hourly source for health thresholds.
Source & licence.
- Source
- European Environment Agency, Air Quality download service (E2a, up-to-date data)
- Licence
- EEA standard re-use policy: CC BY 4.0
- Attribution
- © European Environment Agency (EEA), Air Quality e-Reporting (E2a, up-to-date, unverified).
- Coverage
- the 39 countries reporting to the EEA (EU-27, EFTA, the UK, the Western Balkans, Andorra, Gibraltar, Kosovo); 1 January of the current year to the latest reported day (the table restarts with each new year)
- Refresh
- weekly, the whole table replaced; source version eea e2a to 2026-10-10
- Table
commons/pubdata·environment.air_quality_daily· Iceberg v2 · browse the catalog- Recipe
pubdata/recipes/air_quality.py, registry entrypubdata/registry/air_quality.yaml
Source: European Environment Agency, Air Quality download service (E2a, up-to-date data) · EEA standard re-use policy: CC BY 4.0 · Snapshot shown: 10 Oct 2026HTML for people. Markdown for agents.
# Air quality in Europe, daily
*PM2.5, PM10, NO2 and ozone at Europe's monitoring stations, day by day.*
Daily mean and maximum concentrations of fine particles (PM2.5), particles (PM10), nitrogen dioxide (NO2) and ozone (O3) at every station the European countries report to the European Environment Agency, from 1 January of the current year to the latest reported day, with the number of valid hours behind each value. A second table places every sampling point: station, coordinates, altitude, municipality and whether it measures traffic, industry or background.
## At a glance
| | |
|---|---|
| Table | `pubdata.environment.air_quality_daily` |
| Category | Environment · Environment, Air quality, Europe, Health |
| Rows | 2,436,953 |
| Size | 30.8 MB in 1 data file(s) |
| Iceberg format | v2 |
| Last refreshed | 2026-10-10T10:48:41Z |
| Source version | eea e2a to 2026-10-10 |
| Refresh cadence | weekly (replace) |
| Coverage | the 39 countries reporting to the EEA (EU-27, EFTA, the UK, the Western Balkans, Andorra, Gibraltar, Kosovo); 1 January of the current year to the latest reported day (the table restarts with each new year) |
## Origin
- **Source:** [European Environment Agency, Air Quality download service (E2a, up-to-date data)](https://eeadmz1-downloads-webapp.azurewebsites.net/)
- **Licence:** EEA standard re-use policy: CC BY 4.0
- **Attribution:** © European Environment Agency (EEA), Air Quality e-Reporting (E2a, up-to-date, unverified).
- **Note:** E2a data are reported by the countries hourly and are not yet validated (the verified E1a set follows the year after). ~10,000 sampling-point files (~2.3 GB) are read each refresh and reduced to daily values (a few tens of MB).
- **Recipe:** `pubdata/recipes/air_quality.py` in the LakehouseBox repository; registry entry `pubdata/registry/air_quality.yaml`.
## Schema
| column | type | description |
|---|---|---|
| `date` | date | the day (the measurement hours' start dates) |
| `sampling_point_id` | string | the EEA sampling point, <country>/SPO-… (joins air_quality_stations) |
| `pollutant` | string | PM2.5, PM10, NO2 or O3 |
| `pollutant_code` | long | the EEA pollutant vocabulary code (6001, 5, 8, 7) |
| `mean` | double | mean of the day's valid hourly values |
| `max` | double | highest valid hourly value of the day |
| `valid_hours` | long | how many valid hourly values the day had (24 = complete) |
| `unit` | string | unit as reported, usually ug.m-3 (micrograms per cubic metre) |
**Join keys:** `sampling_point_id`.
## Companion tables
- `pubdata.environment.air_quality_stations`: one row per sampling point: station code and name, country, pollutant, coordinates, altitude, area (urban, suburban, rural) and type (traffic, industrial, background), municipality (34,184 rows)
## For agents
Values are unverified (E2a): an isolated spike can be an instrument fault the verified release later removes. Require
`valid_hours >= 18` (75 % of the day, the EU's own completeness rule) before comparing days or stations. Join
`air_quality_stations` for coordinates and for `station_type` (traffic, industrial, background) and `area`; comparing
a traffic station with a rural background one is comparing different things. Ozone peaks in summer afternoons, so its
daily mean understates it: use `max` or a running 8-hour mean on the hourly source for health thresholds.
## Example SQL (DuckDB)
```sql
-- the worst PM2.5 days this year, with the station behind them
SELECT d.date, s.station_name, s.municipality, s.country_code, round(d.mean, 1) AS pm25
FROM pubdata.environment.air_quality_daily AS d
JOIN pubdata.environment.air_quality_stations AS s USING (sampling_point_id)
WHERE d.pollutant = 'PM2.5' AND d.valid_hours >= 18
ORDER BY d.mean DESC LIMIT 20;
-- days above the WHO 2021 daily guideline for NO2 (25 µg/m³) per country, traffic stations only
SELECT s.country_code, count(*) AS station_days
FROM pubdata.environment.air_quality_daily AS d
JOIN pubdata.environment.air_quality_stations AS s USING (sampling_point_id)
WHERE d.pollutant = 'NO2' AND s.station_type = 'traffic' AND d.valid_hours >= 18 AND d.mean > 25
GROUP BY 1 ORDER BY 2 DESC;
```
## How to read it
Without an account, over HTTPS (the bucket is public):
```sql
INSTALL iceberg; LOAD iceberg; INSTALL httpfs; LOAD httpfs;
-- anonymous: the bucket is public, requests go unsigned, through the public-data cache
CREATE OR REPLACE SECRET pubdata (TYPE S3, ENDPOINT 'lakehousebox-public-data.b-cdn.net', URL_STYLE 'path', REGION 'us-east-1');
CREATE OR REPLACE VIEW air_quality_daily AS SELECT * FROM iceberg_scan('s3://commons--pubdata/environment/air_quality_daily');
SELECT * FROM air_quality_daily LIMIT 10;
```
With a LakehouseBox account:
```bash
# with a LakehouseBox account (any organisation): the public catalog by name, read-only
lhbox duckdb --catalog commons/pubdata # opens DuckDB attached as `commons_pubdata`
SELECT * FROM commons_pubdata.environment.air_quality_daily LIMIT 10;
```
Public URL of the table's storage: `https://s3.lakehousebox.com/commons--pubdata/environment/air_quality_daily/`. This card: `https://lakehousebox.com/pubdata/cards/air_quality.md`; the page: https://lakehousebox.com/public-data/air_quality. Every dataset as a table: `commons_pubdata.meta.datasets`; as Markdown: https://lakehousebox.com/public-data.md.