DuckDB
DuckDB er den hurtigste vej ind. Den læser GeoParquet direkte fra S3, forstår geometrikolonnen og kan joine datasæt fra forskellige kilder i én forespørgsel.
1. Indlæs udvidelser og makroen dk()
Filnavnene skifter ved hvert snapshot. Makroen finder selv det nyeste, så du kan skrive dk('bbr', 'bygning') i stedet for en lang sti. Læg blokken i ~/.duckdbrc, så indlæses den hver gang, DuckDB starter.
INSTALL spatial; LOAD spatial;
INSTALL httpfs; LOAD httpfs;
SET s3_region = 'eu-west-1';
-- union_by_name: snapshots matches på kolonnenavn, så omdøbte eller nye kolonner i ét
-- snapshot ikke får læsningen til at fejle (kolonner, der mangler i et snapshot, bliver NULL)
-- dk('schema', 'relation') læser det nyeste snapshot af et datasæt
CREATE OR REPLACE MACRO dk(s, r) AS TABLE
SELECT * FROM read_parquet(
's3://gc2-parquet/centia-io/dk/schema=' || s || '/relation=' || r || '/*/*.parquet',
union_by_name = true)
WHERE _gc2_snapshot_date = (
SELECT max(regexp_extract(file, '_gc2_snapshot_date=([0-9-]+)', 1))::DATE
FROM glob('s3://gc2-parquet/centia-io/dk/schema=' || s || '/relation=' || r || '/*/*.parquet'));
-- dk_at('schema', 'relation', 'YYYY-MM-DD') læser datasættet, som det så ud på en given dato
CREATE OR REPLACE MACRO dk_at(s, r, dato) AS TABLE
SELECT * FROM read_parquet(
's3://gc2-parquet/centia-io/dk/schema=' || s || '/relation=' || r || '/*/*.parquet',
union_by_name = true)
WHERE _gc2_snapshot_date = (
SELECT max(regexp_extract(file, '_gc2_snapshot_date=([0-9-]+)', 1))::DATE
FROM glob('s3://gc2-parquet/centia-io/dk/schema=' || s || '/relation=' || r || '/*/*.parquet')
WHERE regexp_extract(file, '_gc2_snapshot_date=([0-9-]+)', 1)::DATE <= dato::DATE);
-- dk_alle('schema', 'relation') læser alle snapshots; _gc2_snapshot_date er en kolonne
CREATE OR REPLACE MACRO dk_alle(s, r) AS TABLE
SELECT * FROM read_parquet(
's3://gc2-parquet/centia-io/dk/schema=' || s || '/relation=' || r || '/*/*.parquet',
union_by_name = true);2. Forespørg
SELECT navn, kommunekode FROM dk('dagi', 'kommuneinddeling') ORDER BY navn;Du kan også læse stierne direkte uden makroen:
-- Uden makro: læs alle snapshots og vælg det nyeste
SELECT *
FROM 's3://gc2-parquet/centia-io/dk/schema=dar/relation=postnummer/*/*.parquet'
QUALIFY _gc2_snapshot_date = max(_gc2_snapshot_date) OVER ();3. Gem resultatet
-- Gem et udtræk lokalt som GeoPackage, GeoJSON eller Parquet
SET geometry_always_xy = true;
COPY (SELECT navn, the_geom FROM dk('dagi', 'regionsinddeling'))
TO 'regioner.gpkg' WITH (FORMAT gdal, DRIVER 'GPKG', SRS 'EPSG:25832');
COPY (SELECT * FROM dk('bbr', 'bygning') WHERE kommunekode = '0101')
TO 'bygninger_kbh.parquet';Gå tilbage i tid
Hvert datasæt høstes løbende, og hvert snapshot gemmes i sin egen mappe med datoen i navnet:_gc2_snapshot_date=2026-09-17/. Gamle snapshots slettes ikke, så du kan læse et register, som det så ud på en bestemt dag, følge det over tid og finde ud af, hvad der er ændret.
dk_at() fra init.sql giver dig det seneste snapshot på eller før en dato.dk_alle() giver dig alle snapshots med datoen som kolonne.
-- Datasættet, som det så ud på en bestemt dato
SELECT count(*) FROM dk_at('plandatadk', 'pdk_lokalplan_vedtaget_v', '2026-09-17');
-- Alle snapshots på én gang: datoen er en almindelig kolonne
SELECT _gc2_snapshot_date AS dato, count(*) AS rækker
FROM dk_alle('plandatadk', 'pdk_lokalplan_vedtaget_v')
GROUP BY ALL
ORDER BY dato;Når datasættet har en stabil nøgle, fx planid, id_lokalid eller cvrnummer, kan to snapshots sammenlignes med et anti-join:
-- Hvad er kommet til, og hvad er forsvundet mellem to datoer?
SELECT 'ny' AS ændring, planid, kommunenavn, plannavn
FROM dk('plandatadk', 'pdk_lokalplan_vedtaget_v')
ANTI JOIN dk_at('plandatadk', 'pdk_lokalplan_vedtaget_v', '2026-09-17') USING (planid)
UNION ALL
SELECT 'fjernet', planid, kommunenavn, plannavn
FROM dk_at('plandatadk', 'pdk_lokalplan_vedtaget_v', '2026-09-17')
ANTI JOIN dk('plandatadk', 'pdk_lokalplan_vedtaget_v') USING (planid);Makroerne er kun genveje. Snapshot-mapperne følger hive-konventionen, så DuckDB, Polars, Spark og pyarrow giver dig datoen som kolonne, når du læser med jokertegn:
-- Uden makroer: læs alle snapshots via stien
SELECT _gc2_snapshot_date, count(*)
FROM 's3://gc2-parquet/centia-io/dk/schema=geodanmark/relation=kyst/*/*.parquet'
GROUP BY ALL;Historikken begynder 17. september 2026. Hvor ofte et datasæt høstes, afhænger af kilden. Se det enkelte datasæts side for de snapshots, der findes. Bemærk også, at mange grunddataregistre selv er bitemporale (registreringfra/virkningfra) og derfor har deres egen historik i kolonnerne. Snapshots supplerer den med et øjebliksbillede af, hvad kilden udstillede den dag.
QGIS
Brug FlatGeobuf-filerne i QGIS. De har et rumligt indeks, så QGIS kun henter de features, der er i kortudsnittet. Selv store lag som matrikelskel og BBR-bygninger åbner hurtigt.
- Find datasættet i kataloget og kopiér FlatGeobuf-URL'en.
- Vælg Lag › Tilføj lag › Tilføj vektorlag i QGIS.
- Vælg kildetypen Protokol: HTTP(S), cloud osv., indsæt URL'en og tryk Tilføj.
Vil du hellere arbejde med GeoParquet, kan QGIS 3.34+ åbne filerne, når GDAL er bygget med Parquet-driveren. Det er den i OSGeo4W-installationen på Windows og i conda-forge. Vælg kildetypen Fil og indsæt /vsicurl/ foran HTTPS-URL'en.
Husk, at URL'en peger på et bestemt snapshot. Når datasættet høstes igen, ligger det nye snapshot under et nyt filnavn, mens det gamle bliver liggende.
Python
Med DuckDB i Python kan du genbruge SQL'en fra eksemplerne og få resultatet som en DataFrame:
import duckdb
con = duckdb.connect()
con.sql(open("init.sql").read()) # samme init.sql som i DuckDB-afsnittet
df = con.sql("""
SELECT k.navn, count(*) AS fredede_bygninger
FROM dk('kulturarvsstyrelsen', 'bygning_fredede') b
JOIN dk('dagi', 'kommuneinddeling') k ON ST_Intersects(b.the_geom, k.the_geom)
GROUP BY ALL ORDER BY 2 DESC
""").df()GeoPandas kan læse begge formater direkte:
import geopandas as gpd
from pyarrow import fs
s3 = fs.S3FileSystem(anonymous=True, region="eu-west-1")
# GeoParquet: stien uden "s3://", og et anonymt filsystem
gdf = gpd.read_parquet("gc2-parquet/centia-io/dk/schema=.../data-....parquet", filesystem=s3)
# FlatGeobuf over HTTPS, kun et udsnit (EPSG:25832)
gdf = gpd.read_file("https://gc2-parquet.s3.amazonaws.com/.../data-....fgb",
bbox=(720000, 6170000, 730000, 6180000))STAC og automatisering
Bucketen er beskrevet som et STAC-katalog. catalog.json linker til en collection.json pr. datasæt, og hvert snapshot har en item.json med udstrækning, rækketal, skemaversion og links til filerne. Du kan bruge værktøjer som pystac og STAC Browser, eller du kan hente JSON'en selv.
# Hele kataloget: ét link pr. datasæt
curl -s https://gc2-parquet.s3.amazonaws.com/centia-io/dk/catalog.json
# Nyeste snapshot af et datasæt, med stier til filerne
curl -s https://gc2-parquet.s3.amazonaws.com/centia-io/dk/schema=dar/relation=adresse/latest.json
# Eller list bucketen direkte
aws s3 ls --no-sign-request s3://gc2-parquet/centia-io/dk/schema=dar/Den nemmeste måde at finde de aktuelle filer på er latest.json, som ligger i hvert datasæts mappe:
{
"collection": "dar.adressepunkt",
"snapshot_date": "2026-09-22",
"item": "./_gc2_snapshot_date=2026-09-22/item.json",
"assets": {
"data": { "href": "./_gc2_snapshot_date=2026-09-22/data-….parquet" },
"flatgeobuf": { "href": "./_gc2_snapshot_date=2026-09-22/data-….fgb" },
"metadata": { "href": "./_gc2_snapshot_date=2026-09-22/metadata-….json" }
}
}Hurtige forespørgsler
- Vælg kun de kolonner, du skal bruge. Parquet er kolonnebaseret, så
SELECT navnhenter en brøkdel af, hvadSELECT *gør. - Filtrér på bbox før geometri. Hver fil har en
the_geom_bbox-kolonne, som DuckDB kan bruge til at springe rækkegrupper over. - Gem mellemresultater lokalt. Skal du bruge det samme udtræk mange gange, så kopier det til en lokal tabel med
CREATE TABLE … AS. - Koordinatsystemet er EPSG:25832. Afstande og arealer er i meter, så
ST_AreaogST_DWithinvirker direkte uden transformation.
-- the_geom_bbox er en GeoParquet 1.1-kolonne med hver features bbox.
-- Filtrér på den først, så springer DuckDB hele rækkegrupper over.
SELECT count(*)
FROM dk('dar', 'adressepunkt')
WHERE the_geom_bbox.xmin BETWEEN 720000 AND 730000
AND the_geom_bbox.ymin BETWEEN 6170000 AND 6180000;