Le GeoPackage sous le capot : SQLite et SQL spatial

Ouvrir un .gpkg comme une base de données, comprendre ses tables système et l'interroger en SQL avec les fonctions spatiales.

Un GeoPackage n'est pas un format mystérieux : c'est une base de données SQLite qui respecte quelques conventions définies par l'OGC. Comprendre sa structure permet de l'inspecter, de le réparer et surtout de l'interroger en SQL, sans serveur.

Ouvrir un GeoPackage comme une base

Renommez mentalement donnees.gpkg en donnees.sqlite : n'importe quel outil SQLite l'ouvre. Par exemple :

  • DB Browser for SQLite (interface graphique) ;
  • l'outil en ligne de commande sqlite3 ;
  • le Gestionnaire BD de QGIS (menu Base de données), qui ajoute l'affichage des géométries ;
  • Python : import sqlite3, sans aucune dépendance.

Les tables système

Trois tables obligatoires décrivent le contenu :

TableRôle
gpkg_spatial_ref_sysLes systèmes de coordonnées utilisés (code EPSG + définition WKT)
gpkg_contentsL'inventaire des couches : nom, type (features, tiles), emprise, SCR
gpkg_geometry_columnsPour chaque couche vecteur : la colonne géométrie et son type
-- Quelles couches contient ce fichier, et dans quel système ?
SELECT c.table_name, c.data_type, g.geometry_type_name, s.organization || ':' || s.organization_coordsys_id AS scr
FROM gpkg_contents c
LEFT JOIN gpkg_geometry_columns g USING (table_name)
LEFT JOIN gpkg_spatial_ref_sys s ON s.srs_id = c.srs_id;

Chaque couche est ensuite une table ordinaire : une clé fid, une colonne géométrie (souvent geom) et vos attributs.

Le format binaire des géométries

La colonne géométrie contient un BLOB : un petit en-tête GeoPackage suivi de la géométrie en WKB (Well-Known Binary) :

47 50          « GP » : signature
00             version
03             drapeaux : petit-boutiste + enveloppe [minx, maxx, miny, maxy]
6A 08 00 00    srs_id = 2154
… 32 octets    enveloppe (4 nombres flottants)
01 01 00 00 00 WKB : petit-boutiste, type 1 = Point
… 16 octets    X et Y

C'est exactement ce que lisent et écrivent la visionneuse et le convertisseur DXF de CartoForge, directement dans votre navigateur grâce à SQLite compilé en WebAssembly.

Requêtes attributaires

Tout le SQL de SQLite fonctionne sur les attributs :

-- Les 10 communes les plus peuplées
SELECT nom, population FROM communes ORDER BY population DESC LIMIT 10;

-- Population par département
SELECT substr(code_insee, 1, 2) AS dep, SUM(population) AS pop, COUNT(*) AS nb_communes
FROM communes GROUP BY dep ORDER BY pop DESC;

-- Ajouter et remplir un champ calculé
ALTER TABLE communes ADD COLUMN categorie TEXT;
UPDATE communes SET categorie = CASE WHEN population >= 10000 THEN 'urbaine' ELSE 'rurale' END;

Requêtes spatiales avec SpatiaLite

Pour les fonctions géométriques (surface, intersection, distance), il faut l'extension SpatiaLite, intégrée à QGIS et à GDAL. Dans le Gestionnaire BD de QGIS ou via ogr2ogr -dialect SQLite, les fonctions ST_* sont disponibles directement sur les géométries GeoPackage :

-- Surface de chaque commune en km² (couche en Lambert 93)
SELECT nom, ST_Area(geom) / 1e6 AS km2 FROM communes;

-- Nombre d'arrêts de bus par commune (jointure spatiale)
SELECT c.nom, COUNT(a.fid) AS nb_arrets
FROM communes c
LEFT JOIN arrets a ON ST_Within(a.geom, c.geom)
GROUP BY c.fid;

-- Écoles à moins de 300 m d'une route principale
SELECT DISTINCT e.nom
FROM ecoles e, routes r
WHERE r.importance <= 2 AND ST_Distance(e.geom, r.geom) < 300;

Sur une grosse table, une jointure spatiale sans index est très lente (chaque entité est comparée à toutes les autres). QGIS et GDAL créent un index spatial R-Tree (rtree_<table>_geom) : utilisez-le pour présélectionner les candidats par emprise.

Utiliser l'index spatial

-- Entités dont l'emprise touche un rectangle (très rapide)
SELECT t.*
FROM batiments t
JOIN rtree_batiments_geom r ON t.fid = r.id
WHERE r.maxx >= 940000 AND r.minx <= 945000
  AND r.maxy >= 6535000 AND r.miny <= 6540000;

Depuis Python, sans rien installer

import sqlite3

con = sqlite3.connect("donnees.gpkg")
for nom, pop in con.execute("SELECT nom, population FROM communes ORDER BY population DESC LIMIT 5"):
    print(f"{nom:30} {pop:>10,}")
con.close()

Pour décoder les géométries, passez à GeoPandas, qui lit les GeoPackage avec gpd.read_file().

Entretien

  • VACUUM; récupère la place après de grosses suppressions (le fichier ne rétrécit pas tout seul).
  • PRAGMA integrity_check; vérifie que le fichier n'est pas corrompu.
  • Évitez de placer un GeoPackage en cours d'édition sur un partage réseau ou un dossier synchronisé (OneDrive…) : SQLite supporte mal les écritures concurrentes à distance.

Démonstration

Une vraie base SQLite construite dans votre navigateur (sql.js, SQLite compilé en WebAssembly), organisée comme un GeoPackage : tables système et couche communes (Métropole de Lyon).

À essayer : exécutez les requêtes proposées (tables système, classement, champ calculé avec ALTER TABLE et UPDATE), puis écrivez les vôtres (Ctrl + Entrée pour exécuter). Les communes du résultat s'allument en rouge sur la carte. Les fonctions spatiales ST_* demandent SpatiaLite et ne sont pas disponibles ici.