diff --git a/404.html b/404.html index a95b9e8..f3e8981 100644 --- a/404.html +++ b/404.html @@ -16,7 +16,7 @@ - + diff --git a/check_topology/index.html b/check_topology/index.html index 022a740..a6953fe 100644 --- a/check_topology/index.html +++ b/check_topology/index.html @@ -22,7 +22,7 @@ - + diff --git a/fdw/index.html b/fdw/index.html index c03ea4d..fc347e7 100644 --- a/fdw/index.html +++ b/fdw/index.html @@ -22,7 +22,7 @@ - + diff --git a/filter_data/index.html b/filter_data/index.html index a972b27..5af6531 100644 --- a/filter_data/index.html +++ b/filter_data/index.html @@ -22,7 +22,7 @@ - + diff --git a/grant/index.html b/grant/index.html index 68add2c..6d1de4e 100644 --- a/grant/index.html +++ b/grant/index.html @@ -22,7 +22,7 @@ - + diff --git a/group_data/index.html b/group_data/index.html index 65d3b6d..a0eb4cc 100644 --- a/group_data/index.html +++ b/group_data/index.html @@ -22,7 +22,7 @@ - + diff --git a/import_data/index.html b/import_data/index.html index 2b9cf04..d75ad5c 100644 --- a/import_data/index.html +++ b/import_data/index.html @@ -22,7 +22,7 @@ - + @@ -355,39 +355,6 @@ - - -
Pour la formation, on doit importer des données pour pouvoir travailler. QGIS possède plusieurs outils pour réaliser cette importation dans PostgreSQL.
+Pour la formation, on doit importer des données pour pouvoir travailler.
On doit charger au préalable la couche source dans QGIS (SHP, TAB, etc.), puis on doit vérifier :
+On doit charger au préalable la couche source dans QGIS (SHP, TAB, etc.), puis on doit vérifier :
EPSG:2154
UTF-8
, ISO-8859-15
, etc. Il faut ouvrir la table attributaire, et vérifier si les accents sont bien affichés. Sinon choisir le bon encodage dans l'onglet Général des propriétés de la couchePour importer, on utilise le bouton Import de couche/fichier du gestionnaire de bdd. On choisit par exemple le fichier des communes:
-Après l'import, on peut cliquer, dans le panneau de gauche, sur le nom de la couche créée et parcourir les données avec l'onglet Table. Si on souhaite comparer avec la couche d'origine, il suffit de charger la table, en double-cliquant dessus dans l'arbre (ou via les autres outils de QGIS)
-NB: si un champ s'appelle déjà id dans la donnée source, et qu'il contient des valeurs dupliquées, ou des valeurs textuelles, alors il faut cocher la case Clé primaire dans l'outil d'import, puis choisir un nom différent pour que QGIS crée ce nouvel identifiant dans le bon format (entier auto-incrémenté via une séquence, qu'on appelle aussi serial). Par ex: id_commune
-Il suffit d'utiliser le même outil d'import via le gestionnaire de bdd, et cocher la case Remplacer la table de destination si existante.
-Attention, cela supprime la table avant de la recréer et de la remplir, ce qui peut entraîner des effets de bord (par exemple, on perd les droits définis)
-Imaginons qu'on ait donné tous les droits sur les tables du schéma, par exemple via cette requête
-1 -2 -3 -4 -5 |
|
Ensuite, on souhaite réimporter le SHP, sans perdre les droits: on doit d'abord vider la table puis réimporter les données, sans cocher la case Remplacer la table de destination si existante
-1 -2 -3 |
|
Ensuite, on importe via l'outil spécifique du menu Traitement / Boîte à outils. Chercher "export" dans le champ du haut (Rechercher...), et lancer l'algorithme Exporter vers PostgreSQL (connexions disponibles) de GDAL. Il faut choisir les options suivantes:
+Pour importer, il existe plusieurs manières dans QGIS. La plus performante pour des gros volumes de données est l'utilisation de l'algorithme de la boîte à outils
du menu Traitement
appelé Exporter vers PostgreSQL (Connexions disponibles
.
Pour trouver cet algorithme, chercher PosgreSQL
dans le champ du haut, et lancer l'algorithme Exporter vers PostgreSQL (connexions disponibles) de GDAL. Il faut choisir les options suivantes :
z_formation
commune
id
dans le champ Clef primaire si aucun champ entier auto-incrémenté existe, ou choisir le champ appropriéLancer l'algorithme, et vérifier une fois les données importées que les nouvelles données ont bien été ajoutées à la table.
+ +Après l'import, on peut charger la table comme une couche via l'explorateur de QGIS :
+Rafraîchir
Il est possible d'utiliser l'outil Importer un vecteur vers une base de données PostGIS (connexions disponibles) par lot. Pour cela, une fois la boîte de dialogue de cet algorithme ouverte, cliquer sur le bouton Exécuter comme processus de lot. Cela affiche un tableau, ou chaque ligne représente les variables d'entrée d'un algorithme.
Vous pouvez créer manuellement chaque ligne, ou choisir directement les couches depuis votre projet QGIS. Voir la documentation QGIS pour plus de détail: https://docs.qgis.org/latest/fr/docs/user_manual/processing/batch.html
-Continuer vers Sélectionner des données: SELECT
+Continuer vers Sélectionner des données : SELECT
@@ -878,7 +787,7 @@Nous présupposons qu'une base de données est accessible pour la formation, via un utilisateur PostgreSQL avec des droits élevés (notamment pour créer des schémas et des tables). L'extension PostGIS doit aussi être activée sur cette base de données.
+Nous présupposons qu'une base de données est accessible pour la formation, via un rôle PostgreSQL avec des droits élevés (notamment pour créer des schémas et des tables). L'extension PostGIS doit aussi être activée sur cette base de données.
Pour cette formation, nous utilisons des données libres de droit :
Il peut est chargé en base avec cette commande : pg_restore -d "NOM_BASE" data_formation.dump
Il peut est chargé en base avec cette commande : +
1 |
|
Ce jeu de données a pour sources :
Extraction de données d'OpenStreetMap dans un format SIG, sous licence ODBL ( site https://github.com/igeofr/osm2igeo ). On utilisera par exemple les données de l'ancienne région Haute-Normandie: -https://www.data.data-wax.com/OSM2IGEO/FRANCE/202103_OSM2IGEO_23_HAUTE_NORMANDIE_SHP_L93_2154.zip
+Extraction de données d'OpenStreetMap dans un format SIG, sous licence "ODBL" (site https://github.com/igeofr/osm2igeo ). On utilisera par exemple les données de l'ancienne région Haute-Normandie.
Données cadastrales (site https://cadastre.data.gouv.fr ), sous licence Par exemple pour la Seine-Maritime: -https://cadastre.data.gouv.fr/data/etalab-cadastre/2019-01-01/shp/departements/76/
+Données cadastrales (site https://cadastre.data.gouv.fr ), sous licence "Licence Ouverte 2.0" Par exemple pour la Seine-Maritime : +https://cadastre.data.gouv.fr/data/etalab-cadastre/2024-10-01/shp/departements/76/
PLU (site https://www.geoportail-z_formation.gouv.fr/map/ ). Par exemple les données de la ville du Havre: -https://www.geoportail-z_formation.gouv.fr/map/#tile=1&lon=0.13496041707835396&lat=49.49246433172931&zoom=12&mlon=0.117760&mlat=49.502918 -Cliquer sur la commune, et utiliser le lien de téléchargement, actuellement:
+PLU (site https://www.geoportail-urbanisme.gouv.fr/map/ ). Par exemple les données de la ville du Havre. Cliquer sur la commune, et utiliser le lien de téléchargement.
Ces données peuvent aussi être importées dans la base de formation via les outils de QGIS.
-Un rappel sur les concepts de table, champs, relations.
Lorsqu'on travaille avec des données PostgreSQL, QGIS n'accède pas à la donnée en lisant un ou plusieurs fichiers, mais fait des requêtes à la base, à chaque fois qu'il en a besoin: déplacement de carte, zoom, ouverture de la table attributaire, sélection par expression, etc.
La base de données fournit donc un lieu de stockage des données centralisé. On peut gérer les droits d'accès ou d'écriture sur les schémas et les tables.
Dans QGIS, il faut créer une nouvelle connexion à PostgreSQL, via l'outil "Éléphant" : menu Couches / Ajouter une couche / Ajouter une couche PostgreSQL . Configurer les options suivantes:
+Dans QGIS, il faut créer une nouvelle connexion à PostgreSQL, via l'outil "Éléphant" : menu Couches / Ajouter une couche / Ajouter une couche PostgreSQL. Configurer les options suivantes :
Attention Pour plus de sécurité, privilégier l'usage d'un service PostgreSQL: -https://docs.qgis.org/latest/fr/docs/user_manual/managing_data_source/opening_data.html#pg-service-file
-Il est aussi intéressant pour les performances d'accès aux données PostgreSQL de modifier une option dans les options de QGIS, onglet Rendu: il faut cocher la case Réaliser la simplification par le fournisseur de données lorsque c'est possible. Cela permet de télécharger des versions allégées des données aux petites échelles. Documentation
-NB Pour les couches PostGIS qui auraient déjà été ajoutées avant d'avoir activé cette option, vous pouvez manuellement changer dans vos projets via l'onglet Rendu de la boîte de dialogue des propriétés de chaque couche PostGIS.
+https://docs.qgis.org/latest/fr/docs/user_manual/managing_data_source/opening_data.html#pg-service-file (plugin QGIS intéressant : PG Service Parser) +Il est aussi intéressant pour les performances d'accès aux données PostgreSQL de modifier une option dans les options de QGIS, onglet Rendu : il faut cocher la case Réaliser la simplification par le fournisseur de données lorsque c'est possible. Cela permet de télécharger des versions allégées des données aux petites échelles. Documentation QGIS
+ +NB Pour les couches PostGIS qui auraient déjà été ajoutées avant d'avoir activé cette option, vous pouvez manuellement changer dans vos projets via l'onglet Rendu de la boîte de dialogue des propriétés de chaque couche PostGIS.
Trois solutions sont possibles:
+Trois solutions sont possibles :
schémas
, puis les tables
ou vues
exploitables. Une icône devant chaque table/vue indique si une table est géométrique ou non ainsi que le type de géométrie, point, ligne ou polygone. On peut utiliser le menu Clic-Droit
sur les objets de l'arbre.On travaille via QGIS, avec le gestionnaire de bases de données : menu Base de données > Gestionnaire BD (sinon via l'icône de la barre d’outil base de données).
-Dans l'arbre qui se présente à gauche du gestionnaire de bdd, on peut choisir sa connexion, puis double-cliquer, ce qui montre l'ensemble des schémas, et l'ouverture d'un schéma montre la liste des tables et vues. Les menus du gestionnaire permettent de créer ou d'éditer des objets (schémas, tables).
-Une fenêtre SQL permet de lancer manuellement des requêtes SQL. Nous allons principalement utiliser cet outil : menu Base de données / Fenêtre SQL (on peut aussi le lancer via F2). :
-Depuis QGIS: dans le gestionnaire de base de données, menu ** Table / Créer une table**:
+On peut travailler avec le gestionnaire de bases de données de QGIS : menu Base de données > Gestionnaire BD (sinon via l'icône de la barre d’outil base de données) ou avec l'explorateur (recommandé).
+Dans l'arbre qui se présente, on peut choisir sa connexion, puis double-cliquer, ce qui montre l'ensemble des schémas, et l'ouverture d'un schéma montre la liste des tables et vues. Les menus permettent de créer ou d'éditer des objets (schémas, tables).
+Une fenêtre SQL permet de lancer manuellement des requêtes SQL. Nous allons principalement utiliser cet outil : menu Base de données / Fenêtre SQL (on peut aussi le lancer via F2).
+NB: C'est possible aussi d'utiliser le fenêtre SQL de l'explorateur via clic-droit Exécuter le SQL ...
, mais elle ne permet pas encore de ne lancer que le texte surligné, ce qui est pourtant très pratique pendant une formation.
Les schémas dans une base PostgreSQL sont utiles pour regrouper les tables.
+On recommande de ne pas créer de tables dans le schéma public
, mais d'utiliser des schémas (par thématique, pour la gestion des droits, etc.).
Pour la formation, nous allons créer un schéma z_formation
:
Créer un schéma
.Ensuite, on peut créer une table dans ce schéma : dans l'explorateur, faire un clic-droit sur le schéma z_formation
, puis Nouvelle table...
:
NB: on a créé une table dans cet exemple z_formation.borne_incendie
avec les champs id_borne (text), code (text), debit (real) et geom (géométrie de type Point, code SRID 2154)
Créer une table en SQL
+ +NB: on a créé une table dans cet exemple z_formation.borne_incendie
avec les champs code (text), debit (real) et geom (géométrie de type Point, code SRID 2154)
id
de type entier auto-incrémenté a été créé automatiquement par QGIS en tant que clé primaire de la table.On peut aussi utiliser du SQL pour créer des objets dans la base :
On peut bien sûr charger la table dans QGIS, puis utiliser les outils d'édition classique pour créer des nouveaux objets.
-En SQL, il est aussi possible d'insérer des données ( https://sql.sh/cours/insert-into ). Par exemple pour les bornes à incendie:
+On peut bien sûr charger la table dans QGIS, puis utiliser les outils d'édition classique pour créer des nouveaux objets ou les modifier.
+En SQL, il est aussi possible d'insérer des données ( https://sql.sh/cours/insert-into ). Par exemple pour les bornes à incendie :
1 2 3 @@ -945,41 +980,10 @@ |
NB: Nous verrons plus loin l'utlisation de fonctions de création de géométrie, comme ST_MakePoint
-1 |
|
1 -2 |
|
1 -2 -3 -4 -5 |
|
1 |
|
NB: Nous verrons plus loin l'utilisation de fonctions de création de géométrie, comme ST_MakePoint
On peut vérifier si chaque table contient un index spatial via le gestionnaire de base de données de QGIS, en cliquant sur la table dans l'arbre, puis en regardant les informations de l'onglet Info. On peut alors créer l'index spatial via le lien bleu Aucun index spatial défini (en créer un).
-Sinon, il est possible de le faire en SQL via la requête suivante:
+Sinon, il est possible de le faire en SQL via la requête suivante :
1 |
|
Si on souhaite automatiser la création des indexes pour toutes les tables qui n'en ont pas, on peut utiliser une fonction, décrite dans la partie Fonctions utiles
@@ -1004,7 +1008,7 @@Cette formation concerne des utilisateurs de QGIS, g\u00e9omaticiens, qui souhaitent comprendre l'apport de l'utilisation de PostgreSQL comme outil de centralisation de la donn\u00e9es spatiale (et non spatiale):
Avant de v\u00e9rifier la topologie, il faut au pr\u00e9alable avoir des g\u00e9om\u00e9tries valides (cf. chapitre pr\u00e9c\u00e9dent).
Certaines micro-erreurs de topologie peuvent peuvent \u00eatre corrig\u00e9es en r\u00e9alisant une simplification des donn\u00e9es \u00e0 l'aide d'une grille, par exemple pour corriger des soucis d'arrondis. Pour cela, PostGIS a une fonction ST_SnapToGrid.
On peut utiliser conjointement ST_Simplify et ST_SnapToGrid pour effectuer une premi\u00e8re correction sur les donn\u00e9es. Attention, ces fonctions modifient la donn\u00e9e. A vous de choisir la bonne tol\u00e9rance, par exemple 5 cm, qui d\u00e9pend de votre donn\u00e9e et de votre cas d'utilisation.
Tester la simplification en lan\u00e7ant la requ\u00eate suivante, et en chargeant le r\u00e9sultat comme une nouvelle couche dans QGIS
SELECT\n ST_Multi(\n ST_CollectionExtract(\n ST_MakeValid(\n ST_SnapToGrid(\n st_simplify(geom,0),\n 0.05 -- 5 cm\n )\n ),\n 3\n )\n )::geometry(multipolygon, 2154)\nFROM z_formation.parcelle_havre\n;\n
Une fois le r\u00e9sultat visuellement test\u00e9 dans QGIS, par comparaison avec la table source, on peut choisir de modifier la g\u00e9om\u00e9trie de la table avec la version simplifi\u00e9e des donn\u00e9es:
-- Parcelles\nUPDATE z_formation.parcelle_havre\nSET geom =\nST_Multi(\n ST_CollectionExtract(\n ST_MakeValid(\n ST_SnapToGrid(\n st_simplify(geom,0),\n 0.05 -- 5 cm\n )\n ),\n 3\n )\n)\n;\n;\n
Attention: Si vous avez d'autres tables avec des objets en relation spatiale avec cette table, il faut aussi effectuer le m\u00eame traitement pour que les g\u00e9om\u00e9tries de toutes les couches se calent sur la m\u00eame grille. Par exemple la table des zonages.
UPDATE z_formation.zone_urba\nSET geom =\nST_Multi(\n ST_CollectionExtract(\n ST_MakeValid(\n ST_SnapToGrid(\n st_simplify(geom,0),\n 0.05 -- 5 cm\n )\n ),\n 3\n )\n)\n;\n
"},{"location":"check_topology/#reperer-certaines-erreurs-de-topologies","title":"Rep\u00e9rer certaines erreurs de topologies","text":"PostGIS poss\u00e8de de nombreuses fonctions de relations spatiales qui permettent de trouver les objets qui se chevauchent, qui se touchent, etc. Ces fonctions peuvent \u00eatre utilis\u00e9es pour comparer les objets d'une m\u00eame table, ou de deux tables diff\u00e9rentes. Voir: https://postgis.net/docs/reference.html#Spatial_Relationships_Measurements
Par exemple, trouver les parcelles voisines qui se recouvrent: on utilise la fonction ST_Overlaps. On peut cr\u00e9er une couche listant les recouvrements:
DROP TABLE IF EXISTS z_formation.recouvrement_parcelle_voisines;\nCREATE TABLE z_formation.recouvrement_parcelle_voisines AS\nSELECT DISTINCT ON (geom)\nparcelle_a, parcelle_b, aire_a, aire_b, ST_Area(geom) AS aire, geom\nFROM (\n SELECT\n a.id_parcelle AS parcelle_a, ST_Area(a.geom) AS aire_a,\n b.id_parcelle AS parcelle_b, ST_Area(b.geom) AS aire_b,\n (ST_Multi(\n st_collectionextract(\n ST_MakeValid(ST_Intersection(a.geom, b.geom))\n , 3)\n ))::geometry(MultiPolygon,2154) AS geom\n FROM z_formation.parcelle_havre AS a\n JOIN z_formation.parcelle_havre AS b\n ON a.id_parcelle != b.id_parcelle\n --ON ST_Intersects(a.geom, b.geom)\n AND ST_Overlaps(a.geom, b.geom)\n) AS voisin\nORDER BY geom\n;\n\nCREATE INDEX ON z_formation.recouvrement_parcelle_voisines USING GIST (geom);\n
On peut alors ouvrir cette couche dans QGIS pour zoomer sur chaque objet de recouvrement.
R\u00e9cup\u00e9rer la liste des identifiants de ces parcelles:
SELECT string_agg( parcelle_a::text, ',') FROM z_formation.recouvrement_parcelle_voisines;\n
On peut utiliser le r\u00e9sultat de cette requ\u00eate pour s\u00e9lectionner les parcelles probl\u00e9matiques: on s\u00e9lectionne le r\u00e9sultat dans le tableau du gestionnaire de base de donn\u00e9es, et on copie (CTRL + C). On peut alors utiliser cette liste dans une s\u00e9lection par expression dans QGIS, avec par exemple l'expression
\"id_parcelle\" IN (\n729091,742330,742783,742513,742514,743114,742992,742578,742991,742544,743009,744282,744378,744378,744281,744199,743646,746445,743680,744280,\n743653,743812,743208,743812,743813,744199,694298,694163,721712,707463,744412,707907,707069,721715,721715,696325,696372,746305,722156,722555,\n722195,714500,715969,722146,722287,723526,720296,720296,722296,723576,723572,723572,723571,724056,723570,723568,740376,722186,724055,714706,\n723413,723988,721808,721808,723413,724064,723854,723854,724063,723518,720736,720653,741079,741227,740932,740932,740891,721259,741304,741304,\n741501,741226,741812)\n
Une fois les parcelles s\u00e9lectionn\u00e9es, on peut utiliser certains outils de QGIS pour faciliter la correction:
Dans PostGIS, on peut utiliser la fonction ST_Snap dans une requ\u00eate SQL pour d\u00e9placer les n\u0153uds d'une g\u00e9om\u00e9trie et les coller sur ceux d'une autre.
Par exemple, coller les g\u00e9om\u00e9tries choisies (via identifiants dans le WHERE) de la table de zonage sur les parcelles choisies (via identifiants dans le WHERE):
WITH a AS (\n SELECT DISTINCT z.id_zone_urba,\n st_force2d(\n ST_Multi(\n ST_Snap(\n ST_Simplify(z.geom, 1),\n ST_Collect(p.geom),\n 0.5\n )\n )\n ) AS geom\n FROM z_formation.parcelle_havre AS p\n INNER JOIN z_formation.zone_urba AS z\n ON st_dwithin(z.geom, p.geom, 0.5)\n WHERE TRUE\n AND z.id_zone_urba IN (113,29)\n AND p.id_parcelle IN (711337,711339,711240,711343)\n GROUP BY z.id_zone_urba\n)\nUPDATE z_formation.zone_urba pz\nSET geom = a.geom\nFROM a\nWHERE pz.id_zone_urba = a.id_zone_urba\n
Attention: Cette fonction ne sait coller qu'aux n\u0153uds de la table de r\u00e9f\u00e9rence, pas aux segments. Il serait n\u00e9anmoins possible de cr\u00e9er automatiquement les n\u0153uds situ\u00e9s sur la projection du n\u0153ud \u00e0 d\u00e9placer sur la g\u00e9om\u00e9trie de r\u00e9f\u00e9rence.
Dans la pratique, il est tr\u00e8s souvent fastidieux de corriger les erreurs de topologie d'une couche. Les outils automatiques ( V\u00e9rifier les g\u00e9om\u00e9tries de QGIS ou outil v.clean de Grass) ne permettent pas toujours de bien voir ce qui a \u00e9t\u00e9 modifi\u00e9.
Au contraire, une modification manuelle est plus pr\u00e9cise, mais prend beaucoup de temps.
Le Minist\u00e8re du D\u00e9veloppement Durable a mis en ligne un document int\u00e9ressant sur les outils disponibles dans QGIS, OpenJump et PostgreSQL pour valider et corriger les g\u00e9om\u00e9tries: http://www.geoinformations.developpement-durable.gouv.fr/verification-et-corrections-des-geometries-a3522.html
"},{"location":"fdw/","title":"Acc\u00e9der \u00e0 des donn\u00e9es externes : les Foreign Data Wrapper (FDW)","text":"L'utilisation d'un FDW permet de consulter des donn\u00e9es externes \u00e0 la base comme si elles \u00e9taient stock\u00e9es dans des tables. On peut lancer des requ\u00eates pour r\u00e9cup\u00e9rer seulement certains champs, filtrer les donn\u00e9es, etc.
Des tables \u00e9trang\u00e8res sont cr\u00e9\u00e9es, qui pointent vers les donn\u00e9es externes. A chaque requ\u00eate sur ces tables, PostgreSQL r\u00e9cup\u00e8re les donn\u00e9es depuis la connexion au serveur externe.
On passe classiquement par les \u00e9tapes suivantes:
postgres_fdw
(bases PostgreSQL externes), ogr_fdw
(donn\u00e9es vectorielles via ogr2ogr), etc.Avec ce Foreign Data Wrapper ogr_fdw, on peut appeler n'importe quelle source de donn\u00e9es externe compatible avec la librairie ogr2ogr et les exploiter comme des tables: fichiers GeoJSON ou Shapefile, GPX, CSV, mais aussi les protocoles comme le WFS.
Voir la documentation officielle de ogr_fdw.
"},{"location":"fdw/#installation","title":"Installation","text":"Pour l'installer sur une machine Linux, il suffit d'installer le paquet correspondant \u00e0 la version de PostgreSQL, par exemple postgresql-11-ogr-fdw
.
Sous Windows, il est disponible avec le paquet PostGIS via l'outil StackBuilder.
"},{"location":"fdw/#exemple-dutilisation-recuperer-des-couches-dun-serveur-wfs","title":"Exemple d'utilisation: r\u00e9cup\u00e9rer des couches d'un serveur WFS","text":"Nous allons utiliser le FDW pour r\u00e9cup\u00e9rer des donn\u00e9es mises \u00e0 disposition sur le serveur de l'INPN via le protocole WFS.
Vous pouvez d'abord tester dans QGIS quelles donn\u00e9es sont disponibles sur ce serveur en cr\u00e9ant une nouvelle connexion WFS avec l'URL http://ws.carmencarto.fr/WFS/119/fxx_inpn?
Via QGIS ou un autre client \u00e0 la base de donn\u00e9es, nous pouvons maintenant montrer comment r\u00e9cup\u00e9rer ces donn\u00e9es:
ogr_fdw
:-- Ajouter l'extension pour lire des fichiers SIG\n-- Cette commande doit \u00eatre lanc\u00e9e par un super utilisateur (ou un utilisateur ayant le droit de le faire)\nCREATE EXTENSION IF NOT EXISTS ogr_fdw;\n
-- Cr\u00e9er le serveur\nDROP SERVER IF EXISTS fdw_ogr_inpn_metropole;\nCREATE SERVER fdw_ogr_inpn_metropole FOREIGN DATA WRAPPER ogr_fdw\nOPTIONS (\n datasource 'WFS:http://ws.carmencarto.fr/WFS/119/fxx_inpn?',\n format 'WFS'\n);\n
-- Cr\u00e9er un sch\u00e9ma pour la dreal\nCREATE SCHEMA IF NOT EXISTS inpn_metropole;\n
IMPORT SCHEMA
:-- R\u00e9cup\u00e9rer l'ensemble des couches WFS comme des tables dans le sch\u00e9ma ref_dreal\nIMPORT FOREIGN SCHEMA ogr_all\nFROM SERVER fdw_ogr_inpn_metropole\nINTO inpn_metropole\nOPTIONS (\n -- mettre le nom des tables en minuscule et sans caract\u00e8res bizarres\n launder_table_names 'true',\n -- mettre le nom des champs en minuscule\n launder_column_names 'true'\n)\n;\n
SELECT foreign_table_schema, foreign_table_name\nFROM information_schema.foreign_tables\nWHERE foreign_table_schema = 'inpn_metropole'\nORDER BY foreign_table_schema, foreign_table_name;\n
ce qui montre:
foreign_table_schema foreign_table_name inpn_metropole arretes_de_protection_de_biotope inpn_metropole arretes_de_protection_de_geotope inpn_metropole bien_du_patrimoine_mondial_de_l_unesco inpn_metropole geoparcs inpn_metropole ospar inpn_metropole parc_naturel_marin inpn_metropole parcs_nationaux inpn_metropole parcs_naturels_regionaux inpn_metropole reserves_biologiques inpn_metropole reserves_de_la_biosphere inpn_metropole reserves_integrales_de_parcs_nationaux inpn_metropole reserves_nationales_de_chasse_et_faune_sauvage inpn_metropole reserves_naturelles_nationales inpn_metropole reserves_naturelles_regionales inpn_metropole rnc inpn_metropole sites_d_importance_communautaire inpn_metropole sites_d_importance_communautaire_joue__zsc_sic_ inpn_metropole sites_ramsar inpn_metropole terrains_des_conservatoires_des_espaces_naturels inpn_metropole terrains_du_conservatoire_du_littoral inpn_metropole zico inpn_metropole znieff1 inpn_metropole znieff1_mer inpn_metropole znieff2 inpn_metropole znieff2_mer inpn_metropole zones_de_protection_speciale-- Tester\nSELECT *\nFROM inpn_metropole.zico\nLIMIT 1;\n
Attention, lorsqu'on acc\u00e8de depuis PostgreSQL \u00e0 un serveur WFS, on est tributaire
Nous d\u00e9conseillons fortement dans ce cas de charger le serveur externe en r\u00e9alisant des requ\u00eates complexes (ou trop fr\u00e9quentes) sur ces tables \u00e9trang\u00e8res, surtout lorsque les donn\u00e9es \u00e9voluent peu.
Au contraire, nous conseillons de cr\u00e9er des vues mat\u00e9rialis\u00e9es \u00e0 partir des tables \u00e9trang\u00e8res pour \u00e9viter des requ\u00eates lourdes en stockant les donn\u00e9es dans la base:
-- Pour \u00e9viter de requ\u00eater \u00e0 chaque fois le WFS, on peut cr\u00e9er des vues mat\u00e9rialis\u00e9es\n\n-- suppression de la vue si elle existe d\u00e9j\u00e0\nDROP MATERIALIZED VIEW IF EXISTS inpn_metropole.vm_zico;\n\n-- cr\u00e9ation de la vue: on doit parfois forcer le type de g\u00e9om\u00e9trie attendue\nCREATE MATERIALIZED VIEW inpn_metropole.vm_zico AS\nSELECT *, \n(ST_multi(msgeometry))::geometry(multipolygon, 2154) AS geom\nFROM inpn_metropole.zico\n;\n\n-- Ajout d'un index spatial sur la g\u00e9om\u00e9trie\nCREATE INDEX ON inpn_metropole.vm_zico USING GIST (geom);\n
Une fois la vue cr\u00e9\u00e9e, vous pouvez faire vos requ\u00eates sur cette vue, avec des performances bien meilleures et un all\u00e8gement de la charge sur le serveur externe.
Pour rafra\u00eechir les donn\u00e9es \u00e0 partir du serveur WFS, il suffit de rafra\u00eechir la ou les vues mat\u00e9rialis\u00e9es:
-- Rafra\u00eechir la vue, par exemple \u00e0 lancer une fois par mois\nREFRESH MATERIALIZED VIEW inpn_metropole.vm_zico;\n
"},{"location":"fdw/#le-fdw-postgres_fdw-pour-acceder-aux-tables-dune-autre-base-de-donnees-postgresql","title":"Le FDW postgres_fdw pour acc\u00e9der aux tables d'une autre base de donn\u00e9es PostgreSQL","text":"-- Cr\u00e9ation du serveur externe\nDROP SERVER IF EXISTS foreign_server_test CASCADE;\nCREATE SERVER IF NOT EXISTS foreign_server_test\nFOREIGN DATA WRAPPER postgres_fdw\nOPTIONS (host 'mon_serveur_postgresql_externe.com', port '5432', dbname 'external_database')\n;\n\n-- on d\u00e9clare se connecter en tant qu'utilisateur `mon_utilisateur_externe` lorsqu'on r\u00e9cup\u00e8re des donn\u00e9es\n-- depuis une connexion avec l'utilisateur interne `mon_utilisateur`\nCREATE USER MAPPING FOR \"mon_utilisateur\"\nSERVER foreign_server_test\nOPTIONS (user 'mon_utilisateur_externe', password '***********');\n\n-- on stocke les tables \u00e9trang\u00e8res dans un sch\u00e9ma sp\u00e9cifique pour isoler des autres sch\u00e9mas en dur\nDROP SCHEMA IF EXISTS fdw_test_schema CASCADE;\nCREATE SCHEMA IF NOT EXISTS fdw_test_schema;\n\n-- importer automatiquement les tables d'un sch\u00e9ma de la base distante\nIMPORT FOREIGN SCHEMA \"un_schema\"\nLIMIT TO (\"une_table\", \"une_autre_table\")\nFROM SERVER foreign_server_test\nINTO fdw_test_schema;\n\n-- Tester\nSELECT * FROM fdw_test_schema.une_table LIMIT 1;\n
Continuer vers Tutoriels en ligne
"},{"location":"filter_data/","title":"Filtrer les donn\u00e9es : la clause WHERE","text":"R\u00e9cup\u00e9rer les donn\u00e9es \u00e0 partir de la valeur exacte d'un champ. Ici le nom de la commune
-- R\u00e9cup\u00e9rer seulement la commune du Havre\nSELECT id_commune, code_insee, nom,\npopulation\nFROM z_formation.commune\nWHERE nom = 'Le Havre'\n
On peut chercher les lignes dont le champ correspondant \u00e0 plusieurs valeurs
-- R\u00e9cup\u00e9rer la commune du Havre et de Rouen\nSELECT id_commune, code_insee, nom,\npopulation\nFROM z_formation.commune\nWHERE nom IN ('Le Havre', 'Rouen')\n
On peut aussi filtrer sur des champs de type entier ou nombres r\u00e9els, et faire des conditions comme des in\u00e9galit\u00e9s.
-- Filtrer les donn\u00e9es, par exemple par d\u00e9partement et population\nSELECT *\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\nAND population > 1000\n;\n
On peut chercher des lignes dont un champ commence et/ou se termine par un texte
-- Filtrer les donn\u00e9es, par exemple par d\u00e9partement et d\u00e9but et/ou fin de nom\nSELECT *\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\n-- commence par C\nAND nom LIKE 'C%'\n-- se termine par ville\nAND nom ILIKE '%ville'\n;\n
On peut utiliser les calculs sur les g\u00e9om\u00e9tries pour filtrer les donn\u00e9es. Par exemple filtrer par longueur de lignes
-- Les routes qui font plus que 10km\n-- on peut utiliser des fonctions dans la clause WHERE\nSELECT id_route, id, geom\nFROM z_formation.route\nWHERE True\nAND ST_Length(geom) > 10000\n
Continuer vers Regrouper des donn\u00e9es: GROUP BY
"},{"location":"filter_data/#quiz","title":"Quiz","text":"\u00c9crire une requ\u00eate retournant toutes les communes de Seine-Maritime qui contiennent la cha\u00eene de caract\u00e8res 'saint'-- Toutes les communes de Seine-Maritime qui contiennent le mot saint\nSELECT *\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\nAND nom ILIKE '%saint%';\n
\u00c9crire une requ\u00eate retournant les nom et centro\u00efde des communes de Seine-Maritime avec une population inf\u00e9rieure ou \u00e9gale \u00e0 50 -- Nom et centro\u00efde des communes de Seine-Maritime avec une population <= 50\nSELECT nom, ST_Centroid(geom) as geom\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\nAND population <= 50\n
"},{"location":"grant/","title":"Gestion des droits","text":"Dans PostgreSQL, on peut cr\u00e9er des r\u00f4les (des utilisateurs) et g\u00e9rer les droits sur les diff\u00e9rents objets : base, sch\u00e9mas, tables, fonctions, etc.
La documentation officielle de PostgreSQL est compl\u00e8te, et propose plusieurs exemples.
Nous montrons ci-dessous quelques utilisations possibles. Attention, pour pouvoir r\u00e9aliser certaines op\u00e9rations, vous devez :
Cr\u00e9ation d'un sch\u00e9ma de test et d'un r\u00f4le de connexion, en tant qu'utilisateur avec des droits forts sur la base de donn\u00e9es (cr\u00e9ation de sch\u00e9mas, de tables, etc.).
-- cr\u00e9ation d'un sch\u00e9ma de test\nCREATE SCHEMA IF NOT EXISTS nouveau_schema;\n\n-- cr\u00e9ation de tables pour tester\nCREATE TABLE IF NOT EXISTS nouveau_schema.observation (id serial primary key, nom text, geom geometry(point, 2154));\nCREATE TABLE IF NOT EXISTS nouveau_schema.nomenclature (id serial primary key, code text, libelle text);\n
Cr\u00e9ation d'un r\u00f4le de connexion (en tant que super-utilisateur, ou en tant qu'utilisateur ayant le droit de cr\u00e9er des r\u00f4les)
-- cr\u00e9ation d'un r\u00f4le nomm\u00e9 invite\nCREATE ROLE invite WITH PASSWORD 'mot_de_passe_a_changer' LOGIN;\n
On donne le droit de connexion sur la base (nomm\u00e9e ici qgis)
-- on donne le droit de connexion sur la base\nGRANT CONNECT ON DATABASE qgis TO invite;\n
Exemple de requ\u00eates pratiques pour donner ou retirer des droits (en tant qu'utilisateur propri\u00e9taire de la base et des objets)
-- on donne le droit \u00e0 invite d'utiliser les sch\u00e9ma public et nouveau_schema\n-- Utile pour pouvoir lister les tables\n-- Si un r\u00f4le n'a pas le droit USAGE sur un sch\u00e9ma,\n-- il ne peut pas lire les donn\u00e9es des tables\n-- m\u00eame si des droits SELECT on \u00e9t\u00e9 donn\u00e9es sur ces tables\nGRANT USAGE ON SCHEMA public, nouveau_schema TO \"invite\", \"autre_role\";\n\n-- on permet \u00e0 invite de lire les donn\u00e9es (SELECT)\n-- de toutes les tables du sch\u00e9ma nouveau_schema\nGRANT SELECT ON ALL TABLES IN SCHEMA nouveau_schema TO \"invite\", \"autre_role\";\n\n-- On permet l'ajout et la modification de donn\u00e9es sur la table observation seulement\nGRANT INSERT OR UPDATE ON TABLE nouveau_schema.observation TO \"invite\";\n\n-- On peut aussi enlever des droits avec REVOKE.\n-- Cela enl\u00e8ve seulement les droits donn\u00e9s pr\u00e9c\u00e9demment avec GRANT\n-- Ex: On pourrait donner tous les droits sur une table\n-- puis retirer la possibilit\u00e9 de faire des suppressions\nGRANT ALL ON TABLE nouveau_schema.observation TO \"autre_role\";\n-- on retire les droits DELETE et TRUNCATE\nREVOKE DELETE, TRUNCATE ON TABLE nouveau_schema.observation FROM \"autre_role\";\n\n-- On peut aussi par exemple retirer tous les privil\u00e8ges sur les tables du sch\u00e9ma public\nREVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"invite\";\n
"},{"location":"grant/#droits-par-defaut-sur-les-nouveaux-objets-crees-par-un-utilisateur","title":"Droits par d\u00e9faut sur les nouveaux objets cr\u00e9\u00e9s par un utilisateur.","text":"Lorsqu'un utilisateur cr\u00e9e un sch\u00e9ma, une table ou une vue, aucun droit n'est donn\u00e9 sur cet objet aux autres utilisateurs. Par d\u00e9faut les autres utilisateurs ne peuvent donc pas par exemple lire les donn\u00e9es de ce nouvel objet.
PostgreSQL fournit un moyen de d\u00e9finir en quelque sorte: Donner ce(s) droit(s) sur tous ces objets cr\u00e9\u00e9s par cet utilisateur \u00e0 ces autres utilisateurs
Documentation officielle : https://docs.postgresql.fr/current/sql-alterdefaultprivileges.html
-- Donner le droit SELECT pour toutes les nouvelles tables cr\u00e9\u00e9es \u00e0 l'avenir\n-- dans le sch\u00e9ma nouveau_schema\nALTER DEFAULT PRIVILEGES IN SCHEMA \"nouveau_schema\" GRANT SELECT ON TABLES TO \"invite\", \"autre_role\";\n
"},{"location":"grant/#lister-tous-les-droits-donnes-sur-tous-les-objets-de-la-base","title":"Lister tous les droits donn\u00e9s sur tous les objets de la base","text":"Une requ\u00eate SQL peut \u00eatre utilis\u00e9e pour lister tous les droits accord\u00e9s sur plusieurs types d'objets : sch\u00e9ma, tables, fonctions, types, aggr\u00e9gats, etc.
Un exemple de r\u00e9sultat :
object_schema object_type object_name object_owner grantor grantee privileges is_grantable urbanisme schema urbanisme role_sig role_sig role_urba CREATE, USAGE f urbanisme table zone_urba role_sig role_sig role_urba INSERT, SELECT, UPDATE f cadastre schema cadastre role_sig role_sig role_lecteur USAGE f cadastre table commune role_sig role_sig role_lecteur SELECT f cadastre table parcelle role_sig role_sig role_lecteur SELECT fSi un objet n'est pas retourn\u00e9 par cette requ\u00eate, c'est qu'aucun droit sp\u00e9cifique ne lui a \u00e9t\u00e9 accord\u00e9.
Requ\u00eate SQL permettant de r\u00e9cup\u00e9rer les droits accord\u00e9s sur tous les objets de la base, ainsi que les propri\u00e9taires et les r\u00f4les qui ont accord\u00e9 ces privil\u00e8ges-- Adapted from https://dba.stackexchange.com/a/285632\nWITH rol AS (\n SELECT oid,\n rolname::text AS role_name\n FROM pg_roles\n UNION\n SELECT 0::oid AS oid,\n 'public'::text\n),\nschemas AS ( -- Schemas\n SELECT oid AS schema_oid,\n n.nspname::text AS schema_name,\n n.nspowner AS owner_oid,\n 'schema'::text AS object_type,\n coalesce ( n.nspacl, acldefault ( 'n'::\"char\", n.nspowner ) ) AS acl\n FROM pg_catalog.pg_namespace n\n WHERE n.nspname !~ '^pg_'\n AND n.nspname <> 'information_schema'\n),\nclasses AS ( -- Tables, views, etc.\n SELECT schemas.schema_oid,\n schemas.schema_name AS object_schema,\n c.oid,\n c.relname::text AS object_name,\n c.relowner AS owner_oid,\n CASE\n WHEN c.relkind = 'r' THEN 'table'\n WHEN c.relkind = 'v' THEN 'view'\n WHEN c.relkind = 'm' THEN 'materialized view'\n WHEN c.relkind = 'c' THEN 'type'\n WHEN c.relkind = 'i' THEN 'index'\n WHEN c.relkind = 'S' THEN 'sequence'\n WHEN c.relkind = 's' THEN 'special'\n WHEN c.relkind = 't' THEN 'TOAST table'\n WHEN c.relkind = 'f' THEN 'foreign table'\n WHEN c.relkind = 'p' THEN 'partitioned table'\n WHEN c.relkind = 'I' THEN 'partitioned index'\n ELSE c.relkind::text\n END AS object_type,\n CASE\n WHEN c.relkind = 'S' THEN coalesce ( c.relacl, acldefault ( 's'::\"char\", c.relowner ) )\n ELSE coalesce ( c.relacl, acldefault ( 'r'::\"char\", c.relowner ) )\n END AS acl\n FROM pg_class c\n JOIN schemas\n ON ( schemas.schema_oid = c.relnamespace )\n WHERE c.relkind IN ( 'r', 'v', 'm', 'S', 'f', 'p' )\n),\ncols AS ( -- Columns\n SELECT c.object_schema,\n null::integer AS oid,\n c.object_name || '.' || a.attname::text AS object_name,\n 'column' AS object_type,\n c.owner_oid,\n coalesce ( a.attacl, acldefault ( 'c'::\"char\", c.owner_oid ) ) AS acl\n FROM pg_attribute a\n JOIN classes c\n ON ( a.attrelid = c.oid )\n WHERE a.attnum > 0\n AND NOT a.attisdropped\n),\nprocs AS ( -- Procedures and functions\n SELECT schemas.schema_oid,\n schemas.schema_name AS object_schema,\n p.oid,\n p.proname::text AS object_name,\n p.proowner AS owner_oid,\n CASE p.prokind\n WHEN 'a' THEN 'aggregate'\n WHEN 'w' THEN 'window'\n WHEN 'p' THEN 'procedure'\n ELSE 'function'\n END AS object_type,\n pg_catalog.pg_get_function_arguments ( p.oid ) AS calling_arguments,\n coalesce ( p.proacl, acldefault ( 'f'::\"char\", p.proowner ) ) AS acl\n FROM pg_proc p\n JOIN schemas\n ON ( schemas.schema_oid = p.pronamespace )\n),\nudts AS ( -- User defined types\n SELECT schemas.schema_oid,\n schemas.schema_name AS object_schema,\n t.oid,\n t.typname::text AS object_name,\n t.typowner AS owner_oid,\n CASE t.typtype\n WHEN 'b' THEN 'base type'\n WHEN 'c' THEN 'composite type'\n WHEN 'd' THEN 'domain'\n WHEN 'e' THEN 'enum type'\n WHEN 't' THEN 'pseudo-type'\n WHEN 'r' THEN 'range type'\n WHEN 'm' THEN 'multirange'\n ELSE t.typtype::text\n END AS object_type,\n coalesce ( t.typacl, acldefault ( 'T'::\"char\", t.typowner ) ) AS acl\n FROM pg_type t\n JOIN schemas\n ON ( schemas.schema_oid = t.typnamespace )\n WHERE ( t.typrelid = 0\n OR ( SELECT c.relkind = 'c'\n FROM pg_catalog.pg_class c\n WHERE c.oid = t.typrelid ) )\n AND NOT EXISTS (\n SELECT 1\n FROM pg_catalog.pg_type el\n WHERE el.oid = t.typelem\n AND el.typarray = t.oid )\n),\nfdws AS ( -- Foreign data wrappers\n SELECT null::oid AS schema_oid,\n null::text AS object_schema,\n p.oid,\n p.fdwname::text AS object_name,\n p.fdwowner AS owner_oid,\n 'foreign data wrapper' AS object_type,\n coalesce ( p.fdwacl, acldefault ( 'F'::\"char\", p.fdwowner ) ) AS acl\n FROM pg_foreign_data_wrapper p\n),\nfsrvs AS ( -- Foreign servers\n SELECT null::oid AS schema_oid,\n null::text AS object_schema,\n p.oid,\n p.srvname::text AS object_name,\n p.srvowner AS owner_oid,\n 'foreign server' AS object_type,\n coalesce ( p.srvacl, acldefault ( 'S'::\"char\", p.srvowner ) ) AS acl\n FROM pg_foreign_server p\n),\nall_objects AS (\n SELECT schema_name AS object_schema,\n object_type,\n schema_name AS object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM schemas\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM classes\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM cols\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n calling_arguments,\n owner_oid,\n acl\n FROM procs\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM udts\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM fdws\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM fsrvs\n),\nacl_base AS (\n SELECT object_schema,\n object_type,\n object_name,\n calling_arguments,\n owner_oid,\n ( aclexplode ( acl ) ).grantor AS grantor_oid,\n ( aclexplode ( acl ) ).grantee AS grantee_oid,\n ( aclexplode ( acl ) ).privilege_type AS privilege_type,\n ( aclexplode ( acl ) ).is_grantable AS is_grantable\n FROM all_objects\n),\nungrouped AS (\n SELECT acl_base.object_schema,\n acl_base.object_type,\n acl_base.object_name,\n --acl_base.calling_arguments,\n owner.role_name AS object_owner,\n grantor.role_name AS grantor,\n grantee.role_name AS grantee,\n acl_base.privilege_type,\n acl_base.is_grantable\n FROM acl_base\n JOIN rol owner\n ON ( owner.oid = acl_base.owner_oid )\n JOIN rol grantor\n ON ( grantor.oid = acl_base.grantor_oid )\n JOIN rol grantee\n ON ( grantee.oid = acl_base.grantee_oid )\n WHERE acl_base.grantor_oid <> acl_base.grantee_oid\n)\nSELECT\n object_schema, object_type, object_name, object_owner,\n grantor, grantee,\n -- The same function name can be used many times\n -- Since we do not include the calling_arguments field, we should add a DISTINCT below\n string_agg(DISTINCT privilege_type, ' - ' ORDER BY privilege_type) AS privileges,\n is_grantable\nFROM ungrouped\nWHERE True\n-- Simplify objects returned\n-- You can comment the following line to get these types too\nAND object_type NOT IN ('function', 'window', 'aggregate', 'base type', 'composite type')\n-- You can also filter for specific schemas or object names by uncommenting and adapting the following lines\n-- AND object_schema IN ('cadastre', 'environment')\n-- AND object_type = 'table'\n-- AND object_name ILIKE '%parcelle%'\nGROUP BY object_schema, object_type, object_name, object_owner, grantor, grantee, is_grantable\nORDER BY object_schema, object_type, grantor, grantee, object_name\n;\n
Continuer vers Acc\u00e9der \u00e0 des donn\u00e9es externes: Foreign Data Wrapper
"},{"location":"group_data/","title":"Grouper des donn\u00e9es et calculer des statistiques","text":"Les fonctions d'agr\u00e9gat dans PostgreSQL
"},{"location":"group_data/#valeurs-distinctes-dun-champ","title":"Valeurs distinctes d'un champ","text":"On souhaite r\u00e9cup\u00e9rer toutes les valeurs possibles d'un champ
-- V\u00e9rifier les valeurs distinctes d'un champ: table commune\nSELECT DISTINCT depart\nFROM z_formation.commune\nORDER BY depart\n\n-- idem sur la table lieu_dit_habite\nSELECT DISTINCT nature\nFROM z_formation.lieu_dit_habite\nORDER BY nature\n
"},{"location":"group_data/#regrouper-des-donnees-en-specifiant-les-champs-de-regroupement","title":"Regrouper des donn\u00e9es en sp\u00e9cifiant les champs de regroupement","text":"Certains calculs n\u00e9cessitent le regroupement de lignes, comme les moyennes, les sommes ou les totaux. Pour cela, il faut r\u00e9aliser un regroupement via la clause GROUP BY
Compter les communes par d\u00e9partement et calculer la population totale
-- Regrouper des donn\u00e9es\n-- Compter le nombre de communes par d\u00e9partement\nSELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nWHERE True\nGROUP BY depart\nORDER BY nb_commune DESC\n
Calculer des statistiques sur l'aire des communes pour chaque d\u00e9partement
SELECT depart,\ncount(id_commune) AS nb,\nmin(ST_Area(geom)/10000)::int AS min_aire_ha,\nmax(ST_Area(geom)/10000)::int AS max_aire_ha,\navg(ST_Area(geom)/10000)::int AS moy_aire_ha,\nsum(ST_Area(geom)/10000)::int AS total_aire_ha\nFROM z_formation.commune\nGROUP BY depart\n
Compter le nombre de routes par nature
-- Compter le nombre de routes par nature\nSELECT count(id_route) AS nb_route, nature\nFROM z_formation.route\nWHERE True\nGROUP BY nature\nORDER BY nb_route DESC\n
Compter le nombre de routes par nature et par sens
SELECT count(id_route) AS nb_route, nature, sens\nFROM z_formation.route\nWHERE True\nGROUP BY nature, sens\nORDER BY nature, sens DESC\n
Les caculs sur des ensembles group\u00e9s peuvent aussi \u00eatre r\u00e9alis\u00e9 sur les g\u00e9om\u00e9tries.. Les plus utilis\u00e9s sont
ST_Collect
qui regroupe les g\u00e9om\u00e9tries dans une multi-g\u00e9om\u00e9trie,ST_Union
qui fusionne les g\u00e9om\u00e9tries.Par exemple, on peut souhaiter trouver l'enveloppe convexe autour de points (\u00e9lastique tendu autour d'un groupe de points). Ici, nous regroupons les lieux-dits par nature (ce qui n'a pas beaucoup de sens, mais c'est pour l'exemple). Dans ce cas, il faut faire une sous-requ\u00eate pour filtrer seulement les r\u00e9sultats de type polygone (car s'il y a seulement 1 ou 2 objets par nature, alors on ne peut cr\u00e9er de polygone)
SELECT *\nFROM (\n SELECT\n nature,\n -- ST_Convexhull renvoie l'enveloppe convexe\n ST_Convexhull(ST_Collect(geom)) AS geom\n FROM z_formation.lieu_dit_habite\n GROUP BY nature\n) AS source\n-- GeometryType renvoie le type de g\u00e9om\u00e9trie\nWHERE Geometrytype(geom) = 'POLYGON'\n
Attention, on doit donner un alias \u00e0 la sous-requ\u00eate (ici source
)
Un autre exemple sur les bornes. Ici, on groupe les bornes par identifiant pair ou impair, et on calcule l'enveloppe convexe
SELECT count(id_borne), ((id_borne % 2) = 0) AS pair,\n(st_convexhull(ST_Collect(geom))) AS geom\nFROM z_formation.borne_incendie\nGROUP BY pair\n
On peut r\u00e9aliser l'\u00e9quivalent d'un DISSOLVE
de QGIS en regroupant les g\u00e9om\u00e9tries via ST_Union
. Par exemple fusionner l'ensemble des communes pour construire les g\u00e9om\u00e9tries des d\u00e9partements:
SELECT\ndepart,\ncount(id_commune) AS nb_com,\n-- ST_Union cr\u00e9e une seule g\u00e9om\u00e9trie en fusionnant les g\u00e9om\u00e9tries.\nST_Union(geom) AS geom\n\nFROM z_formation.commune\n\nGROUP BY depart\n
Attention, cette requ\u00eate est lourde, et devra \u00eatre enregistr\u00e9e comme une table.
"},{"location":"group_data/#filtrer-sur-les-regroupements","title":"Filtrer sur les regroupements","text":"Si on souhaite compter les communes par d\u00e9partement, calculer la population totale et aussi filter celles qui ont plus de 500 000 habitants, il peut para\u00eetre logique d'\u00e9crire cette requ\u00eate :
SELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nGROUP BY depart\nWHERE sum(population) > 500000\nORDER BY nb_commune DESC\n
ou bien encore :
SELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nGROUP BY depart\nWHERE total_population > 500000\nORDER BY nb_commune DESC\n
Ces deux requ\u00eates renvoient une erreur. La bonne requ\u00eate est :
SELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nGROUP BY depart\nHAVING sum(population) > 500000\nORDER BY nb_commune DESC\n
Il faut savoir que la clause WHERE
est ex\u00e9cut\u00e9e avant la clause GROUP BY
, il n'est donc pas possible de filtrer sur des regroupements avec celle-ci. C'est le r\u00f4le de la clause HAVING
.
Aussi la clause SELECT
est ex\u00e9cut\u00e9e apr\u00e8s les clauses WHERE
et HAVING
, il n'est donc pas possible d'utiliser des alias d\u00e9clar\u00e9s avec celle-ci.
Un sch\u00e9ma illustrant ceci est disponible sur le site postgresqltutorial.com.
Continuer vers Rassembler des donn\u00e9es: UNION ALL
"},{"location":"group_data/#quiz","title":"Quiz","text":"\u00c9crire une requ\u00eate retournant, pour le/les d\u00e9partement(s) dont la population moyenne des villes est sup\u00e9rieure ou \u00e9gale \u00e0 1500 habitants, le nom du/des d\u00e9partement(s) ainsi que cette moyenne.SELECT depart,\navg(population) AS moyenne_population\nFROM z_formation.commune\nGROUP BY depart\nHAVING avg(population) >= 1500\n
\u00c9crire une requ\u00eate retournant pour les d\u00e9partements 'SEINE-MARITIME' et 'EURE', leur nom, le nombre de communes ainsi que la surface et la surface de l'enveloppe convexe en m\u00e8tre carr\u00e9 sous forme d'entier. SELECT depart,\ncount(id_commune) AS nb_commune,\nST_Area(ST_Collect(geom))::int8 AS surface,\nST_Area(ST_Convexhull(ST_Collect(geom)))::int8 AS surface_enveloppe_convexe\nFROM z_formation.commune\nWHERE depart IN ('SEINE-MARITIME', 'EURE')\nGROUP BY depart\n
"},{"location":"import_data/","title":"Importer des donn\u00e9es","text":"Pour la formation, on doit importer des donn\u00e9es pour pouvoir travailler. QGIS poss\u00e8de plusieurs outils pour r\u00e9aliser cette importation dans PostgreSQL.
"},{"location":"import_data/#import-dune-couche-depuis-qgis","title":"Import d'une couche depuis QGIS","text":"On doit charger au pr\u00e9alable la couche source dans QGIS (SHP, TAB, etc.), puis on doit v\u00e9rifier :
Pour importer, on utilise le bouton Import de couche/fichier du gestionnaire de bdd. On choisit par exemple le fichier des communes:
Apr\u00e8s l'import, on peut cliquer, dans le panneau de gauche, sur le nom de la couche cr\u00e9\u00e9e et parcourir les donn\u00e9es avec l'onglet Table. Si on souhaite comparer avec la couche d'origine, il suffit de charger la table, en double-cliquant dessus dans l'arbre (ou via les autres outils de QGIS)
NB: si un champ s'appelle d\u00e9j\u00e0 id dans la donn\u00e9e source, et qu'il contient des valeurs dupliqu\u00e9es, ou des valeurs textuelles, alors il faut cocher la case Cl\u00e9 primaire dans l'outil d'import, puis choisir un nom diff\u00e9rent pour que QGIS cr\u00e9e ce nouvel identifiant dans le bon format (entier auto-incr\u00e9ment\u00e9 via une s\u00e9quence, qu'on appelle aussi serial). Par ex: id_commune
"},{"location":"import_data/#reimporter-une-donnee-dans-une-table-existante","title":"R\u00e9importer une donn\u00e9e dans une table existante.","text":""},{"location":"import_data/#avec-suppression-de-la-table-puis-recreation","title":"Avec suppression de la table puis recr\u00e9ation.","text":"Il suffit d'utiliser le m\u00eame outil d'import via le gestionnaire de bdd, et cocher la case Remplacer la table de destination si existante.
Attention, cela supprime la table avant de la recr\u00e9er et de la remplir, ce qui peut entra\u00eener des effets de bord (par exemple, on perd les droits d\u00e9finis)
"},{"location":"import_data/#avec-vidage-puis-ajout-des-nouvelles-donnees","title":"Avec vidage puis ajout des nouvelles donn\u00e9es","text":"Imaginons qu'on ait donn\u00e9 tous les droits sur les tables du sch\u00e9ma, par exemple via cette requ\u00eate
-- Ajout des droits un sch\u00e9ma et sur toutes les tables d'un sch\u00e9ma\nGRANT ALL ON SCHEMA z_formation TO \"unutilisateur\";\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA z_formation TO \"unutilisateur\";\nGRANT ALL ON SCHEMA z_formation TO \"unepersonne\";\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA z_formation TO \"unepersonne\";\n
Ensuite, on souhaite r\u00e9importer le SHP, sans perdre les droits: on doit d'abord vider la table puis r\u00e9importer les donn\u00e9es, sans cocher la case Remplacer la table de destination si existante
-- Vider une table en remettant \u00e0 z\u00e9ro la s\u00e9quence\n-- qui permet d'auto-incr\u00e9menter le champ id (la cl\u00e9 primaire)\nTRUNCATE TABLE z_formation.commune RESTART IDENTITY;\n
Ensuite, on importe via l'outil sp\u00e9cifique du menu Traitement / Bo\u00eete \u00e0 outils. Chercher \"export\" dans le champ du haut (Rechercher...), et lancer l'algorithme Exporter vers PostgreSQL (connexions disponibles) de GDAL. Il faut choisir les options suivantes:
Lancer l'algorithme, et v\u00e9rifier une fois les donn\u00e9es import\u00e9es que les nouvelles donn\u00e9es ont bien \u00e9t\u00e9 ajout\u00e9es \u00e0 la table.
"},{"location":"import_data/#importer-plusieurs-couches-en-batch","title":"Importer plusieurs couches en batch","text":"Il est possible d'utiliser l'outil Importer un vecteur vers une base de donn\u00e9es PostGIS (connexions disponibles) par lot. Pour cela, une fois la bo\u00eete de dialogue de cet algorithme ouverte, cliquer sur le bouton Ex\u00e9cuter comme processus de lot. Cela affiche un tableau, ou chaque ligne repr\u00e9sente les variables d'entr\u00e9e d'un algorithme.
Vous pouvez cr\u00e9er manuellement chaque ligne, ou choisir directement les couches depuis votre projet QGIS. Voir la documentation QGIS pour plus de d\u00e9tail: https://docs.qgis.org/latest/fr/docs/user_manual/processing/batch.html
Continuer vers S\u00e9lectionner des donn\u00e9es: SELECT
"},{"location":"join_data/","title":"Les jointures","text":"Les jointures permettent de r\u00e9cup\u00e9rer des donn\u00e9es en relation les unes par rapport aux autres.
"},{"location":"join_data/#les-jointures-attributaires","title":"Les jointures attributaires","text":"La condition de jointure est faite sur des champs non g\u00e9om\u00e9triques. Par exemple une \u00e9galit\u00e9 (code, identifiant).
"},{"location":"join_data/#exemple-1-parcelles-et-communes","title":"Exemple 1: parcelles et communes","text":"R\u00e9cup\u00e9ration des informations de la commune pour un ensemble de parcelles
-- Jointure attributaire: r\u00e9cup\u00e9ration du nom de la commune pour un ensemble de parcelles\nSELECT c.nom, p.*\nFROM z_formation.parcelle as p\nJOIN z_formation.commune as c\nON p.commune = c.code_insee\nLIMIT 100\n-- IMPORTANT: ne pas oublier le ON cad le crit\u00e8re de jointure,\n-- sous peine de \"produit cart\u00e9sien\" (calcul co\u00fbteux de tous les possibles)\n;\n
Il est souvent int\u00e9ressant, pour des donn\u00e9es volumineuses, de cr\u00e9er un index sur le champ de jointure (par exemple ici sur les champs commune
et code_insee
.
-- cr\u00e9ation\nCREATE TABLE z_formation.observation (\n id serial NOT NULL PRIMARY KEY,\n date date DEFAULT (now())::date NOT NULL,\n description text,\n geom public.geometry(Point,2154),\n code_insee character varying(5)\n);\nCREATE INDEX sidx_observation_geom ON z_formation.observation USING gist (geom);\n\n-- on y met des donn\u00e9es\nINSERT INTO z_formation.observation VALUES (1, '2020-07-08', 'un', '01010000206A080000D636D95AFB832141279BD2C8FEA65A41', '76618');\nINSERT INTO z_formation.observation VALUES (2, '2020-07-08', 'deux', '01010000206A08000010248E173E37224156920AEA21525A41', '27213');\nINSERT INTO z_formation.observation VALUES (3, '2020-07-08', 'trois', '01010000206A08000018BF3048EA112341183933F6CC885A41', NULL);\n
On fait une jointure attributaire entre les points des observations et les communes
SELECT\n -- tous les champs de la table observation\n o.*,\n -- le nom de la commune\n c.nom,\n -- l'aire enti\u00e8re en hectares\n ST_area(c.geom)::integer/10000 AS surface_commune\nFROM z_formation.observation AS o\nJOIN z_formation.commune AS c ON o.code_insee = c.code_insee\nWHERE True\n
R\u00e9sultat:
id date description geom code_insee nom surface_commune 2 2020-07-08 deux .... 27213 Vexin-sur-Epte 11434 1 2020-07-08 un .... 76618 Petit-Caux 9243On ne r\u00e9cup\u00e8re ici que 2 lignes alors qu'il y a bien 3 observations dans la table.
Pour r\u00e9cup\u00e9rer les 3 lignes, on doit faire une jointure LEFT
. On peut utiliser un CASE WHEN
pour tester si la commune est trouv\u00e9e sous chaque point
SELECT\n o.*, c.nom, ST_area(c.geom)::integer/10000 AS surface_commune,\n CASE\n WHEN c.code_insee IS NULL THEN 'pas de commune'\n ELSE 'ok'\n END AS test_commune\nFROM z_formation.observation AS o\nLEFT JOIN z_formation.commune AS c ON o.code_insee = c.code_insee\nWHERE True\n
R\u00e9sultat
id date description geom code_insee nom surface_commune test_commune 2 2020-07-08 deux .... 27213 Vexin-sur-Epte 11434 ok 1 2020-07-08 un .... 76618 Petit-Caux 9243 ok 3 2020-07-08 trois .... Null Null Null pas de commune"},{"location":"join_data/#les-jointures-spatiales","title":"Les jointures spatiales","text":"Le crit\u00e8re de jointure peut \u00eatre une condition spatiale. On r\u00e9alise souvent une jointure par intersection ou par proximit\u00e9.
"},{"location":"join_data/#joindre-des-points-avec-des-polygones","title":"Joindre des points avec des polygones","text":"Un exemple classique de r\u00e9cup\u00e9ration des donn\u00e9es de la table commune (nom, etc.) depuis une table de points.
-- Pour chaque lieu-dit, on veut le nom de la commune\nSELECT\nl.id_lieu_dit_habite, l.nom,\nc.nom AS nom_commune, c.code_insee,\nl.geom\nFROM \"z_formation\".lieu_dit_habite AS l\nJOIN \"z_formation\".commune AS c\n ON st_intersects(c.geom, l.geom)\nORDER BY l.nom\n
id_lieu_dit_habite nom nom_commune code_insee geom 58 Abbaye du Valasse Gruchet-le-Valasse 76329 .... 1024 Ablemont Bacqueville-en-Caux 76051 .... 1043 Agranville Douvrend 76220 .... 1377 All des Artisans Mesnils-sur-Iton 27198 .... 1801 All\u00e9e des Maronniers Heudebouville 27332 .... 1293 Alliquerville Trouville 76715 .... 507 Alventot Sainte-H\u00e9l\u00e8ne-Bondeville 76587 .... 555 Alvinbuc Veauville-l\u00e8s-Baons 76729 .... 69 Ancien h\u00f4tel de ville Rouen 76540 .... On peut facilement inverser la table principale pour afficher les lignes ordonn\u00e9es par commune.
SELECT\nc.nom, c.code_insee,\nl.id_lieu_dit_habite, l.nom\nFROM \"z_formation\".commune AS c\nJOIN \"z_formation\".lieu_dit_habite AS l\n ON st_intersects(c.geom, l.geom)\nORDER BY c.nom\n
nom code_insee id_lieu_dit_habite nom Aclou 27001 107 Manoir de la Haule Acquigny 27003 106 Manoir de Becdal Ailly 27005 596 Quaizes Ailly 27005 595 Ingremare Ailly 27005 594 Gruchet Alizay 27008 667 Le Solitaire Ambenay 27009 204 Les Siaules Ambenay 27009 201 Les Renardieres Ambenay 27009 202 Le Culoron On a plusieurs lignes par commune, autant que de lieux-dits pour cette commune. Par contre, comme ce n'est pas une jointure LEFT
, on ne trouve que des r\u00e9sultats pour les communes qui ont des lieux-dits.
On pourrait aussi faire des statistiques, en regroupant par les champs de la table principale, ici les communes.
SELECT\nc.nom, c.code_insee,\ncount(l.id_lieu_dit_habite) AS nb_lieu_dit,\nc.geom\nFROM \"z_formation\".commune AS c\nJOIN \"z_formation\".lieu_dit_habite AS l\n ON st_intersects(c.geom, l.geom)\nGROUP BY c.nom, c.code_insee, c.geom\nORDER BY nb_lieu_dit DESC\nLIMIT 10\n
nom code_insee nb_lieu_dit geom Heudebouville 27332 61 .... Mesnils-sur-Iton 27198 52 .... Rouen 76540 20 .... Saint-Sa\u00ebns 76648 19 .... Les Grandes-Ventes 76321 19 .... Mesnil-en-Ouche 27049 18 .... Quincampoix 76517 18 ...."},{"location":"join_data/#joindre-des-lignes-avec-des-polygones","title":"Joindre des lignes avec des polygones","text":"R\u00e9cup\u00e9rer le code commune de chaque chemin, par intersection entre le chemin et la commune.
"},{"location":"join_data/#jointure-spatiale-simple-entre-les-geometries-brutes","title":"Jointure spatiale simple entre les g\u00e9om\u00e9tries brutes","text":"-- Ici, on peut r\u00e9cup\u00e9rer plusieurs fois le m\u00eame chemin\n-- s'il passe par plusieurs communes\nSELECT\nv.*,\nc.nom, c.code_insee\nFROM \"z_formation\".chemin AS v\nJOIN \"z_formation\".commune AS c\n ON ST_Intersects(v.geom, c.geom)\nORDER BY id_chemin, nom\n
Cela peut renvoyer plusieurs lignes par chemin, car chaque chemin peut passer par plusieurs communes.
"},{"location":"join_data/#jointure-spatiale-entre-le-centroide-des-chemins-et-la-geometrie-des-communes","title":"Jointure spatiale entre le centro\u00efde des chemins et la g\u00e9om\u00e9trie des communes","text":"On peut utiliser le centro\u00efde de chaque chemin pour avoir un seul objet par chemin comme r\u00e9sultat.
-- cr\u00e9ation de l'index\nCREATE INDEX ON z_formation.chemin USING gist (ST_Centroid(geom));\n-- Jointure spatiale\n-- On ne veut qu'une seule ligne par chemin\n-- Donc on fait l'intersection entre le centro\u00efde des chemins (pour avoir un point) et les communes\nSELECT\nv.*,\nc.nom, c.code_insee\nFROM \"z_formation\".chemin AS v\nJOIN \"z_formation\".commune AS c\n ON ST_Intersects(ST_Centroid(v.geom), c.geom)\n
NB: Attention, dans ce cas, l'index spatial sur la g\u00e9om\u00e9trie des chemins n'est pas utilis\u00e9. C'est pour cela que nous avons cr\u00e9\u00e9 un index spatial sur ST_Centroid(geom)
pour la table des chemins.
A l'inverse, on peut vouloir faire des statistiques pour chaque commune via jointure spatiale. Par exemple le nombre de chemins et le total des longueurs par commune.
-- A l'inverse, on veut r\u00e9cup\u00e9rer des statistiques par commune\n -- On veut une ligne par commune, avec des donn\u00e9es sur les voies\nSELECT\nc.id_commune, c.nom, c.code_insee,\ncount(v.id_chemin) AS nb_chemin,\nsum(st_length(v.geom)) AS somme_longueur_chemins_entiers\nFROM z_formation.commune AS c\nJOIN z_formation.chemin AS v\n ON st_intersects(c.geom, st_centroid(v.geom))\nGROUP BY c.id_commune, c.nom, c.code_insee\n;\n
"},{"location":"join_data/#utilisation-dune-jointure-left-pour-garder-les-communes-sans-chemins","title":"Utilisation d'une jointure LEFT pour garder les communes sans chemins","text":"La requ\u00eate pr\u00e9c\u00e9dente ne renvoie pas de lignes pour les communes qui n'ont pas de chemin dont le centro\u00efde est dans une commune. C'est une jointure de type INNER JOIN
Si on veut quand m\u00eame r\u00e9cup\u00e9rer ces communes, on fait une jointure LEFT JOIN
: pour les lignes sans chemins, les champs li\u00e9s \u00e0 la table des chemins seront mis \u00e0 NULL
.
SELECT\nc.id_commune, c.nom, c.code_insee,\ncount(v.id_chemin) AS nb_chemin,\nsum(st_length(v.geom)) AS somme_longueur_chemins_entiers\nFROM z_formation.commune AS c\nLEFT JOIN z_formation.chemin AS v\n ON st_intersects(c.geom, st_centroid(v.geom))\nGROUP BY c.id_commune, c.nom, c.code_insee\n;\n
C'est beaucoup plus long, car la requ\u00eate n'utilise pas d'abord l'intersection, donc l'index spatial des communes, mais fait un parcours de toutes les lignes des communes, puis un calcul d'intersection. Pour acc\u00e9l\u00e9rer la requ\u00eate, on doit cr\u00e9er l'index sur les centro\u00efdes des chemins
CREATE INDEX ON z_formation.chemin USING GIST(ST_Centroid(geom))\n
puis la relancer. Dans cet exemple, on passe de 100 secondes \u00e0 1 seconde, gr\u00e2ce \u00e0 ce nouvel index spatial.
"},{"location":"join_data/#affiner-le-resultat-en-decoupant-les-chemins","title":"Affiner le r\u00e9sultat en d\u00e9coupant les chemins","text":"Dans la requ\u00eate pr\u00e9c\u00e9dente, on calculait la longueur totale de chaque chemin, pas le morceau exacte qui est sur chaque commune. Pour cela, on va utiliser la fonction ST_Intersection
. La requ\u00eate va \u00eatre plus co\u00fbteuse, car il faut r\u00e9aliser le d\u00e9coupage des lignes des chemins par les polygones des communes.
On va d\u00e9couper exactement les chemins par commune et r\u00e9cup\u00e9rer les informations
CREATE TABLE z_formation.decoupe_chemin_par_commune AS\n-- D\u00e9couper les chemins par commune\nSELECT\n-- id unique\n-- infos du chemin\nl.id AS id_chemin,\n-- infos de la commune\nc.nom, c.code_insee,\nST_Multi(st_collectionextract(ST_Intersection(c.geom, l.geom), 2))::geometry(multilinestring, 2154) AS geom\nFROM \"z_formation\".commune AS c\nJOIN \"z_formation\".chemin AS l\n ON st_intersects(c.geom, l.geom)\n;\nCREATE INDEX ON z_formation.decoupe_chemin_par_commune USING GIST (geom);\n
NB: Attention \u00e0 ne pas confondre ST_Intersects
qui renvoie vrai ou faux, et ST_Intersection
qui renvoie la g\u00e9om\u00e9trie issue du d\u00e9coupage d'une g\u00e9om\u00e9trie par une autre.
On peut bien s\u00fbr r\u00e9aliser des jointures spatiales entre 2 couches de polygones, et d\u00e9couper les polygones par intersection. Attention, les performances sont forc\u00e9ment moins bonnes qu'avec des points.
Trouver l'ensemble des zonages PLU pour les parcelles du Havre.
On va r\u00e9cup\u00e9rer plusieurs r\u00e9sultats pour chaque parcelle si plusieurs zonages chevauchent une parcelle.
-- Jointure spatiale\nSELECT\np.id_parcelle,\nz.libelle, z.libelong, z.typezone\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.zone_urba AS z\n ON st_intersects(z.geom, p.geom)\nWHERE True\n
Compter pour chaque parcelle le nombre de zonages en intersection: on veut une seule ligne par parcelle.
SELECT\np.id_parcelle,\ncount(z.libelle) AS nombre_zonage\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.zone_urba AS z\n ON st_intersects(z.geom, p.geom)\nWHERE True\nGROUP BY p.id_parcelle\nORDER BY nombre_zonage DESC\n
D\u00e9couper les parcelles par les zonages, et pouvoir calculer les surfaces des zonages, et le pourcentage par rapport \u00e0 la surface de chaque parcelle. On essaye le SQL suivant:
SELECT\np.id_parcelle,\nz.libelle, z.libelong, z.typezone,\n-- d\u00e9couper les g\u00e9om\u00e9tries\nst_intersection(z.geom, p.geom) AS geom\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.zone_urba AS z\n ON st_intersects(z.geom, p.geom)\nWHERE True\nORDER BY p.id_parcelle\n
Il renvoie l'erreur
ERREUR: Error performing intersection: TopologyException: Input geom 1 is invalid: Self-intersection at or near point 492016.26000489673 6938870.663846286 at 492016.26000489673 6938870.663846286\n
On a ici des soucis de validit\u00e9 de g\u00e9om\u00e9trie. Il nous faut donc corriger les g\u00e9om\u00e9tries avant de poursuivre. Voir chapitre sur la validation des g\u00e9om\u00e9tries.
Une fois les g\u00e9om\u00e9tries valid\u00e9es, la requ\u00eate fonctionne. On l'utilise dans une sous-requ\u00eate pour cr\u00e9er une table et calculer les surfaces
-- suppression de la table\nDROP TABLE IF EXISTS z_formation.decoupe_zonage_parcelle;\n-- cr\u00e9ation de la table avec calcul de pourcentage de surface\nCREATE TABLE z_formation.decoupe_zonage_parcelle AS\nSELECT row_number() OVER() AS id,\nsource.*,\nST_Area(geom) AS aire,\n100 * ST_Area(geom) / aire_parcelle AS pourcentage\nFROM (\nSELECT\n p.id_parcelle, p.id AS idpar, ST_Area(p.geom) AS aire_parcelle,\n z.id_zone_urba, z.libelle, z.libelong, z.typezone,\n -- d\u00e9couper les g\u00e9om\u00e9tries\n (ST_Multi(st_intersection(z.geom, p.geom)))::geometry(MultiPolygon,2154) AS geom\n FROM z_formation.parcelle_havre AS p\n JOIN z_formation.zone_urba AS z ON st_intersects(z.geom, p.geom)\n WHERE True\n) AS source;\n\n-- Ajout de la cl\u00e9 primaire\nALTER TABLE z_formation.decoupe_zonage_parcelle ADD PRIMARY KEY (id);\n\n-- Ajout de l'index spatial\nCREATE INDEX ON z_formation.decoupe_zonage_parcelle USING GIST (geom);\n
"},{"location":"join_data/#faire-un-rapport-des-surfaces-intersectees-de-zonages-sur-une-table-principale","title":"Faire un rapport des surfaces intersect\u00e9es de zonages sur une table principale","text":"Par exemple, pour chacune des communes, on souhaite calculer la somme des surfaces intersect\u00e9e par chaque type de zone (parcs, znieff, etc.).
Afin d'avoir \u00e0 disposition des donn\u00e9es de test pour cet exemple de rapport, nous allons cr\u00e9er 2 tables z_formation.parc_national
et z_formation.znieff
, et y ins\u00e9rer des fausses donn\u00e9es:
-- Table des parcs nationaux\nCREATE TABLE IF NOT EXISTS z_formation.parc_national (\n id serial primary key,\n nom text,\n geom geometry(multipolygon, 2154)\n);\nCREATE INDEX ON z_formation.parc_national USING GIST (geom);\n\n-- Table des znieff\nCREATE TABLE IF NOT EXISTS z_formation.znieff(\n id serial primary key,\n nom_znieff text,\n geom geometry(multipolygon, 2154)\n);\nCREATE INDEX ON z_formation.znieff USING GIST (geom);\n
On ins\u00e8re des polygones dans ces deux tables:
-- donn\u00e9es de test\n-- parcs\nINSERT INTO z_formation.parc_national VALUES (1, 'un', '01060000206A0800000100000001030000000100000008000000C3F7DE73553D20411B3DC1FB0C625A410531F757E93D2041BAECB21FA85E5A41F35B09978081204195F05B9787595A41D61E4865A1A7204147BC8A3AC0605A41ED76A806317F2041A79F7E4876605A41B80752433C832041037846623A655A41E10ED595BA6120413CC1D1C18C685A41C3F7DE73553D20411B3DC1FB0C625A41');\nINSERT INTO z_formation.parc_national VALUES (2, 'deux', '01060000206A080000010000000103000000010000000900000024D68B4AE0412141AAAAAA3C685B5A4130642ACBD01421413A85AE4B72585A41CA08F0240E382141746C4BD107535A41FA30F7A78A4A2141524A29E544555A414796BF5CE63621414DD2E222A4565A416B92160F9B5D2141302807F981575A4130DC700B2E782141DC0ED50B6B5C5A4106FBB8C8294F214150AC17BF015E5A4124D68B4AE0412141AAAAAA3C685B5A41');\nINSERT INTO z_formation.parc_national VALUES (3, 'trois', '01060000206A0800000100000001030000000100000006000000918DCFE7E0861F4137AB79AF14515A411AE56040588A1F41642A43EEC74F5A41DF2EBB3CEBA41F418C31C66ADA4F5A4168864C9562A81F416E87EA40B8505A415CBC8A74C3A31F410FA4F63202515A41918DCFE7E0861F4137AB79AF14515A41');\nINSERT INTO z_formation.parc_national VALUES (4, 'quatre', '01060000206A080000010000000103000000010000000500000004474FE81DBA2041269A684EFD625A41AB17C51223C9204120B507BEAD605A4116329539BBF22041A3273886D5615A416F611F0FB6E32041FA1A9F0F4A645A4104474FE81DBA2041269A684EFD625A41');\nINSERT INTO z_formation.parc_national VALUES (5, 'cinq', '01060000206A0800000100000001030000000100000005000000F2E3C256231E2041E0ACE631AE535A41F7C823E772202041E89C73B6EF505A41B048BCC266362041DAC785A15E515A419E999911782F204180C9F223F8535A41F2E3C256231E2041E0ACE631AE535A41');\nSELECT pg_catalog.setval('z_formation.parc_national_id_seq', 5, true);\n\n-- znieff\nINSERT INTO z_formation.znieff VALUES (1, 'uno', '01060000206A08000001000000010300000001000000050000004039188C39D12041770A5DF74A4A5A413A54B7FBE9CE20410C5DA7C8F5455A41811042C0A4EA204130ECE38267475A416F611F0FB6E320417125FC66FB475A414039188C39D12041770A5DF74A4A5A41');\nINSERT INTO z_formation.znieff VALUES (2, 'dos', '01060000206A080000010000000103000000010000000500000076BEC6DF62492141513FFDF0525A5A417CA32770B24B21411EDBD22150595A419437ABB1F05421410F06E50CBF595A419437ABB1F0542141B022F1FE085A5A4176BEC6DF62492141513FFDF0525A5A41');\nINSERT INTO z_formation.znieff VALUES (3, 'tres', '01060000206A0800000100000001030000000100000005000000A6E6CD62DF5B2141B607528F585C5A41ACCB2EF32E5E2141C5DC3FA4E95B5A414CB7438DE46A2141C5DC3FA4E95B5A41B895F013CE62214189888850A55D5A41A6E6CD62DF5B2141B607528F585C5A41');\nINSERT INTO z_formation.znieff VALUES (4, 'quatro', '01060000206A0800000100000001030000000100000005000000CE857DF445102041985D7665365D5A41DA4F3F15E5142041339521C7305B5A41C2F7DE73553D2041927815D5E65A5A410393E50712252041B607528F585C5A41CE857DF445102041985D7665365D5A41');\nINSERT INTO z_formation.znieff VALUES (5, 'cinco', '01060000206A080000010000000103000000010000000500000045A632DC2B702041FD25CB033C5F5A41CEFDC334A373204115EB459D0E5C5A41F25B099780812041397A8257805D5A415755558D1A7720419E42D7F5855F5A4145A632DC2B702041FD25CB033C5F5A41');\nSELECT pg_catalog.setval('z_formation.znieff_id_seq', 5, true);\n
Pour chaque commune, on souhaite calculer la somme des surfaces intersect\u00e9es par chaque type de zone. On doit donc utiliser toutes les tables de zonage (ici seulement 2 tables, mais c'est possible d'en ajouter)
R\u00e9sultat attendu:
id_commune code_insee nom surface_commune_ha somme_surface_parcs somme_surface_znieff 1139 27042 Barville 275.138028733401 87.2237204013011 None 410 27057 Bernienville 779.74546553394 None 5.26504189468878 1193 27061 Berthouville 757.19696570046 19.9975421896336 None 495 27074 Boisney 576.995877227961 0.107059260396721 None 432 27077 Boissey-le-Ch\u00e2tel 438.373848703835 434.510197417769 83.9289621127432SELECT\n c.id_commune, c.code_insee, c.nom,\n ST_Area(c.geom) / 10000 AS surface_commune_ha,\n (SELECT sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) FROM z_formation.parc_national AS p WHERE ST_Intersects(p.geom, c.geom) ) AS surface_parc_national,\n (SELECT sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) FROM z_formation.znieff AS p WHERE ST_Intersects(p.geom, c.geom) ) AS surface_znieff\nFROM z_formation.commune AS c\nORDER BY c.nom\n
LEFT
SELECT\n -- champs choisis dans la table commune\n c.id_commune, c.code_insee, c.nom,\n -- surface en ha\n ST_Area(c.geom) / 10000 AS surface_commune_ha,\n -- somme des d\u00e9coupages des parcs par commune\n sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) AS somme_surface_parcs,\n -- somme des d\u00e9coupages des znieff par commune\n sum(ST_Area(ST_Intersection(c.geom, z.geom)) / 10000 ) AS somme_surface_znieff\n\nFROM z_formation.commune AS c\n-- jointure spatiale sur les parcs\nLEFT JOIN z_formation.parc_national AS p\n ON ST_Intersects(c.geom, p.geom)\n-- jointure spatiale sur les znieff\nLEFT JOIN z_formation.znieff AS z\n ON ST_Intersects(c.geom, z.geom)\n\n-- clause WHERE optionelle\n-- WHERE p.id IS NOT NULL OR z.id IS NOT NULL\n\n-- on regroupe sur les champs des communes\nGROUP BY c.id_commune, c.code_insee, c.nom, c.geom\n\n-- on ordonne par nom\nORDER BY c.nom\n
Avantages:
WHERE
des conditions sur les champs des tables jointes. Par exemple ne r\u00e9cup\u00e9rer que les lignes qui sont concern\u00e9es par un parc ou une znieff, via WHERE p.id IS NOT NULL OR z.id IS NOT NULL
(comment\u00e9 ci-dessus pour le d\u00e9sactiver)ATTENTION:
ATTENTION:
geom
de toutes les tablesPour chaque objets d'une table, on souhaite r\u00e9cup\u00e9rer des informations sur les objets proches d'une autre table. Au lieu d'utiliser un tampon puis une intersection, on utilise la fonction ST_DWithin
On prend comme exemple la table des bornes \u00e0 incendie cr\u00e9\u00e9e pr\u00e9c\u00e9demment (remplie avec quelques donn\u00e9es de test).
Trouver toutes les parcelles \u00e0 moins de 200m d'une borne \u00e0 incendie
SELECT\np.id_parcelle, p.geom,\nb.id_borne, b.code,\nST_Distance(b.geom, p.geom) AS distance\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.borne_incendie AS b\n ON ST_DWithin(p.geom, b.geom, 200)\nORDER BY id_parcelle, id_borne\n
Attention, elle peut renvoyer plusieurs fois la m\u00eame parcelle si 2 bornes sont assez proches. Pour ne r\u00e9cup\u00e9rer que la borne la plus proche, on peut faire la requ\u00eate suivante. La clause DISTINCT ON
permet de dire quel champ doit \u00eatre unique (ici id_parcelle).
On ordonne ensuite par ce champ et par la distance pour prendre seulement la ligne correspondant \u00e0 la parcelle la plus proche
SELECT DISTINCT ON (p.id_parcelle)\np.id_parcelle, p.geom,\nb.id_borne, b.code,\nST_Distance(b.geom, p.geom) AS distance\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.borne_incendie AS b\n ON ST_DWithin(p.geom, b.geom, 200)\nORDER BY id_parcelle, distance\n
Pour information, on peut v\u00e9rifier en cr\u00e9ant les tampons
-- Tampons non dissous\nSELECT id_borne, ST_Buffer(geom, 200) AS geom\nFROM z_formation.borne_incendie\n\n-- Tampons dissous\nSELECT ST_Union(ST_Buffer(geom, 200)) AS geom\nFROM z_formation.borne_incendie\n
Un article int\u00e9ressant de Paul Ramsey sur le calcul de distance via l'op\u00e9rateur <->
pour trouver le plus proche voisin d'un objet.
Continuer vers Fusionner des g\u00e9om\u00e9tries
"},{"location":"links_and_data/","title":"Liens utiles","text":""},{"location":"links_and_data/#documentation","title":"Documentation","text":"Documentation de PostgreSQL : https://docs.postgresql.fr/current/
Documentation des fonctions PostGIS:
Nous pr\u00e9supposons qu'une base de donn\u00e9es est accessible pour la formation, via un utilisateur PostgreSQL avec des droits \u00e9lev\u00e9s (notamment pour cr\u00e9er des sch\u00e9mas et des tables). L'extension PostGIS doit aussi \u00eatre activ\u00e9e sur cette base de donn\u00e9es.
"},{"location":"links_and_data/#jeux-de-donnees","title":"Jeux de donn\u00e9es","text":"Pour cette formation, nous utilisons des donn\u00e9es libres de droit :
Il peut est charg\u00e9 en base avec cette commande : pg_restore -d \"NOM_BASE\" data_formation.dump
Ce jeu de donn\u00e9es a pour sources :
Extraction de donn\u00e9es d'OpenStreetMap dans un format SIG, sous licence ODBL ( site https://github.com/igeofr/osm2igeo ). On utilisera par exemple les donn\u00e9es de l'ancienne r\u00e9gion Haute-Normandie: https://www.data.data-wax.com/OSM2IGEO/FRANCE/202103_OSM2IGEO_23_HAUTE_NORMANDIE_SHP_L93_2154.zip
Donn\u00e9es cadastrales (site https://cadastre.data.gouv.fr ), sous licence Par exemple pour la Seine-Maritime: https://cadastre.data.gouv.fr/data/etalab-cadastre/2019-01-01/shp/departements/76/
PLU (site https://www.geoportail-z_formation.gouv.fr/map/ ). Par exemple les donn\u00e9es de la ville du Havre: https://www.geoportail-z_formation.gouv.fr/map/#tile=1&lon=0.13496041707835396&lat=49.49246433172931&zoom=12&mlon=0.117760&mlat=49.502918 Cliquer sur la commune, et utiliser le lien de t\u00e9l\u00e9chargement, actuellement:
Ces donn\u00e9es peuvent aussi \u00eatre import\u00e9es dans la base de formation via les outils de QGIS.
"},{"location":"links_and_data/#concepts-de-base-de-donnees","title":"Concepts de base de donn\u00e9es:","text":"Un rappel sur les concepts de table, champs, relations.
Lire la formation QGIS \u00e9galement
Continuer vers Gestion des donn\u00e9es PostgreSQL dans QGIS
"},{"location":"merge_geometries/","title":"Fusionner des g\u00e9om\u00e9tries","text":"On souhaite cr\u00e9er une seule g\u00e9om\u00e9trie qui est issue de la fusion de toutes les g\u00e9om\u00e9tries regroup\u00e9es par un crit\u00e8re (nature, code, etc.)
Par exemple un polygone fusionnant les zonages qui partagent le m\u00eame type
SELECT count(id_zone_urba) AS nb_objets, typezone,\nST_Union(geom) AS geom\nFROM z_formation.zone_urba\nGROUP BY typezone\n
On souhaite parfois fusionner toutes les g\u00e9om\u00e9tries qui sont jointives. Par exemple, on veut fusionner toutes les parcelles jointives pour cr\u00e9er des blocs.
DROP TABLE IF EXISTS z_formation.bloc_parcelle_havre;\nCREATE TABLE z_formation.bloc_parcelle_havre AS\nSELECT\nrow_number() OVER() AS id,\nstring_agg(id::text, ', ') AS ids, t.geom::geometry(polygon, 2154) AS geom\nFROM (\n SELECT\n (St_Dump(ST_Union(a.geom))).geom AS geom\n FROM z_formation.parcelle_havre AS a\n WHERE ST_IsValid(a.geom)\n) t\nJOIN z_formation.parcelle_havre AS p\n ON ST_Intersects(p.geom, t.geom)\nGROUP BY t.geom\n;\nALTER TABLE z_formation.bloc_parcelle_havre ADD PRIMARY KEY (id);\nCREATE INDEX ON z_formation.bloc_parcelle_havre USING GIST (geom);\n
Continuer vers Les triggers
"},{"location":"perform_calculation/","title":"Faire des calculs","text":""},{"location":"perform_calculation/#calcul-sur-des-attributs","title":"Calcul sur des attributs","text":"Le SQL permet de r\u00e9aliser des calculs ou des modifications \u00e0 partir de champs. On peut donc faire des calculs sur des nombres, ou des modifications (remplacement de texte, mise en majuscule, etc.)
Faire un calcul tr\u00e8s simple, avec des op\u00e9rateurs + - /
et *
, ainsi que des parenth\u00e8ses
-- On multiplie 10 par 2\nSELECT\n10 * 2 AS vingt,\n(2.5 -1) * 10 AS quinze\n
Il est aussi possible de faire des calculs \u00e0 partir d'un ou plusieurs champs.
Nous souhaitons par exemple cr\u00e9er un champ qui contiendra la population des communes. Dans la donn\u00e9e source, le champ popul
est de type cha\u00eene de caract\u00e8re, car il contient parfois la valeur 'NC'
lorsque la population n'est pas connue.
Nous ne pouvons pas faire de calculs \u00e0 partir d'un champ texte. On souhaite donc cr\u00e9er un nouveau champ population pour y stocker les valeurs enti\u00e8res.
-- Ajout d'un champ de type entier dans la table\nALTER TABLE z_formation.commune ADD COLUMN population integer;\n
Modifier le nouveau champ population pour y mettre la valeur enti\u00e8re lorsqu'elle est connue. La modification d'une table se fait avec la requ\u00eate UPDATE
, en passant les champs \u00e0 modifier et leur nouvelle valeur via SET
-- Mise \u00e0 jour d'un champ \u00e0 partir d'un calcul\nUPDATE z_formation.commune SET population =\nCASE\n WHEN popul != 'NC' THEN popul::integer\n ELSE NULL\nEND\n;\n
Dans cette requ\u00eate, le CASE WHEN condition THEN valeur ELSE autre_valeur END
permet de faire un test sur la valeur d'origine, et de proposer une valeur si la condition est remplie ( https://sql.sh/cours/case )
Une fois ce champ population
renseign\u00e9 correctement, dans un type entier, on peut r\u00e9aliser un calcul tr\u00e8s simple, par exemple doubler la population:
-- Calcul simple : on peut utiliser les op\u00e9rateurs math\u00e9matiques\nSELECT id_commune, code_insee, nom, geom,\npopulation,\npopulation * 2 AS double_population\nFROM z_formation.commune\nLIMIT 10\n
Il est possible de combiner plusieurs champs pour r\u00e9aliser un calcul. Nous verrons plus loin comment calculer la densit\u00e9 de population \u00e0 partir de la population et de la surface des communes.
"},{"location":"perform_calculation/#calculer-des-caracteristiques-spatiales","title":"Calculer des caract\u00e9ristiques spatiales","text":"Par exemple la longueur ou la surface
Calculer la longueur d'objets lin\u00e9aires
-- Calcul des longueurs de route\nSELECT id_route, id, nature,\nST_Length(geom) AS longueur_m\nFROM z_formation.route\nLIMIT 100\n
Calculer la surface de polygones, et utiliser ce r\u00e9sultat dans un calcul. Par exemple ici la densit\u00e9 de population:
-- Calculer des donn\u00e9es \u00e0 partir de champs et de fonctions spatiales\nSELECT id_commune, code_insee, nom, geom,\npopulation,\nST_Area(geom) AS surface,\npopulation / ( ST_Area(geom) / 1000000 ) AS densite_hab_km\nFROM z_formation.commune\nLIMIT 10\n
"},{"location":"perform_calculation/#creer-des-geometries-a-partir-de-geometries","title":"Cr\u00e9er des g\u00e9om\u00e9tries \u00e0 partir de g\u00e9om\u00e9tries","text":"On peut modifier les g\u00e9om\u00e9tries avec des fonctions spatiales, ce qui revient \u00e0 effectuer un calcul sur les g\u00e9om\u00e9tries. Deux exemples classiques : centroides et tampons
Calculer le centro\u00efde de polygones
-- Centroides des communes\nSELECT id_commune, code_insee, nom,\nST_Centroid(geom) AS geom\nFROM z_formation.commune\n
Le centro\u00efde peut ne pas \u00eatre \u00e0 l'int\u00e9rieur du polygone, par exemple sur la commune de Arni\u00e8res-sur-Iton. Forcer le centro\u00efde \u00e0 l'int\u00e9rieur du polygone. Attention, ce calcul est plus long. Si vous souhaitez mieux comprendre l'algorithme derri\u00e8re cette fonction
-- Centro\u00efdes \u00e0 l'int\u00e9rieur des communes\n-- Attention, c'est plus long \u00e0 calculer\nSELECT id_commune, code_insee, nom,\nST_PointOnSurface(geom) AS geom\nFROM z_formation.commune\n
Calculer le tampon autour d'objets
-- Tampons de 1km autour des communes\nSELECT id_commune, nom, population,\nST_Buffer(geom, 1000) AS geom\nFROM z_formation.commune\nLIMIT 10\n
Continuer vers Filtrer des donn\u00e9es: WHERE
"},{"location":"postgresql_in_qgis/","title":"Gestion des donn\u00e9es PostgreSQL dans QGIS","text":""},{"location":"postgresql_in_qgis/#introduction","title":"Introduction","text":"Lorsqu'on travaille avec des donn\u00e9es PostgreSQL, QGIS n'acc\u00e8de pas \u00e0 la donn\u00e9e en lisant un ou plusieurs fichiers, mais fait des requ\u00eates \u00e0 la base, \u00e0 chaque fois qu'il en a besoin: d\u00e9placement de carte, zoom, ouverture de la table attributaire, s\u00e9lection par expression, etc.
La base de donn\u00e9es fournit donc un lieu de stockage des donn\u00e9es centralis\u00e9. On peut g\u00e9rer les droits d'acc\u00e8s ou d'\u00e9criture sur les sch\u00e9mas et les tables.
"},{"location":"postgresql_in_qgis/#creer-une-connexion-qgis-a-la-base-de-donnees","title":"Cr\u00e9er une connexion QGIS \u00e0 la base de donn\u00e9es","text":"Dans QGIS, il faut cr\u00e9er une nouvelle connexion \u00e0 PostgreSQL, via l'outil \"\u00c9l\u00e9phant\" : menu Couches / Ajouter une couche / Ajouter une couche PostgreSQL . Configurer les options suivantes:
Attention Pour plus de s\u00e9curit\u00e9, privil\u00e9gier l'usage d'un service PostgreSQL: https://docs.qgis.org/latest/fr/docs/user_manual/managing_data_source/opening_data.html#pg-service-file
Il est aussi int\u00e9ressant pour les performances d'acc\u00e8s aux donn\u00e9es PostgreSQL de modifier une option dans les options de QGIS, onglet Rendu: il faut cocher la case R\u00e9aliser la simplification par le fournisseur de donn\u00e9es lorsque c'est possible. Cela permet de t\u00e9l\u00e9charger des versions all\u00e9g\u00e9es des donn\u00e9es aux petites \u00e9chelles. Documentation
NB Pour les couches PostGIS qui auraient d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9es avant d'avoir activ\u00e9 cette option, vous pouvez manuellement changer dans vos projets via l'onglet Rendu de la bo\u00eete de dialogue des propri\u00e9t\u00e9s de chaque couche PostGIS.
"},{"location":"postgresql_in_qgis/#ouvrir-une-couche-postgresql-dans-qgis","title":"Ouvrir une couche PostgreSQL dans QGIS","text":"Trois solutions sont possibles:
On travaille via QGIS, avec le gestionnaire de bases de donn\u00e9es : menu Base de donn\u00e9es > Gestionnaire BD (sinon via l'ic\u00f4ne de la barre d\u2019outil base de donn\u00e9es).
Dans l'arbre qui se pr\u00e9sente \u00e0 gauche du gestionnaire de bdd, on peut choisir sa connexion, puis double-cliquer, ce qui montre l'ensemble des sch\u00e9mas, et l'ouverture d'un sch\u00e9ma montre la liste des tables et vues. Les menus du gestionnaire permettent de cr\u00e9er ou d'\u00e9diter des objets (sch\u00e9mas, tables).
Une fen\u00eatre SQL permet de lancer manuellement des requ\u00eates SQL. Nous allons principalement utiliser cet outil : menu Base de donn\u00e9es / Fen\u00eatre SQL (on peut aussi le lancer via F2). :
"},{"location":"postgresql_in_qgis/#creation-de-tables","title":"Cr\u00e9ation de tables","text":"Depuis QGIS: dans le gestionnaire de base de donn\u00e9es, menu ** Table / Cr\u00e9er une table**:
NB: on a cr\u00e9\u00e9 une table dans cet exemple z_formation.borne_incendie
avec les champs id_borne (text), code (text), debit (real) et geom (g\u00e9om\u00e9trie de type Point, code SRID 2154)
Cr\u00e9er une table en SQL
-- cr\u00e9ation d'un sch\u00e9ma\nCREATE SCHEMA IF NOT EXISTS z_formation;\n\n-- cr\u00e9ation de la table\nCREATE TABLE z_formation.borne_incendie (\n -- un serial est un entier auto-incr\u00e9ment\u00e9\n id_borne serial NOT NULL PRIMARY KEY,\n code text NOT NULL,\n debit real,\n geom geometry(Point, 2154)\n);\n-- Cr\u00e9ation de l'index spatial\nDROP INDEX IF EXISTS borne_incendie_geom_idx;\nCREATE INDEX ON z_formation.borne_incendie USING GIST (geom);\n
"},{"location":"postgresql_in_qgis/#ajouter-des-donnees-dans-une-table","title":"Ajouter des donn\u00e9es dans une table","text":"On peut bien s\u00fbr charger la table dans QGIS, puis utiliser les outils d'\u00e9dition classique pour cr\u00e9er des nouveaux objets.
En SQL, il est aussi possible d'ins\u00e9rer des donn\u00e9es ( https://sql.sh/cours/insert-into ). Par exemple pour les bornes \u00e0 incendie:
INSERT INTO z_formation.borne_incendie (code, debit, geom)\n VALUES\n ('ABC', 1.5, ST_SetSRID(ST_MakePoint(490846.0,6936902.7), 2154)),\n ('XYZ', 4.1, ST_SetSRID(ST_MakePoint(491284.9,6936551.6), 2154)),\n ('FGH', 2.9, ST_SetSRID(ST_MakePoint(490839.8,6937794.8), 2154)),\n ('IOP', 3.6, ST_SetSRID(ST_MakePoint(491203.3,6937488.1), 2154))\n;\n
NB: Nous verrons plus loin l'utlisation de fonctions de cr\u00e9ation de g\u00e9om\u00e9trie, comme ST_MakePoint
"},{"location":"postgresql_in_qgis/#creation-dun-schema-z_formation-dans-la-base","title":"Cr\u00e9ation d\u2019un sch\u00e9ma z_formation dans la base","text":"CREATE SCHEMA IF NOT EXISTS z_formation;\n
-- On donne ici tous les droits \u00e0 \"utilisateur\"\nGRANT ALL PRIVILEGES ON SCHEMA z_formation TO \"utilisateur\";\n
-- Suppression du sch\u00e9ma si il est vide\nDROP SCHEMA monschema;\n\n-- suppression du sch\u00e9ma et de toutes les tables de ce sch\u00e9ma (via CASCADE) !!! ATTENTION !!!\nDROP SCHEMA monschema CASCADE;\n
ALTER SCHEMA monschema RENAME TO unschema;\n
"},{"location":"postgresql_in_qgis/#verifier-et-creer-les-indexes-spatiaux","title":"V\u00e9rifier et cr\u00e9er les indexes spatiaux","text":"On peut v\u00e9rifier si chaque table contient un index spatial via le gestionnaire de base de donn\u00e9es de QGIS, en cliquant sur la table dans l'arbre, puis en regardant les informations de l'onglet Info. On peut alors cr\u00e9er l'index spatial via le lien bleu Aucun index spatial d\u00e9fini (en cr\u00e9er un).
Sinon, il est possible de le faire en SQL via la requ\u00eate suivante:
CREATE INDEX ON nom_du_schema.nom_de_la_table USING GIST (geom);\n
Si on souhaite automatiser la cr\u00e9ation des indexes pour toutes les tables qui n'en ont pas, on peut utiliser une fonction, d\u00e9crite dans la partie Fonctions utiles
Continuer vers l'Import des donn\u00e9es dans PostgreSQL
"},{"location":"save_queries/","title":"Enregistrer une requ\u00eate","text":""},{"location":"save_queries/#les-vues","title":"Les vues","text":"Une vue est l'enregistrement d'une requ\u00eate, appel\u00e9e d\u00e9finition de la vue, qui est stock\u00e9 dans la base, et peut \u00eatre utilis\u00e9e comme une table.
Cr\u00e9er une vue via CREATE VIEW
-- On supprime d'abord la vue si elle existe\nDROP VIEW IF EXISTS z_formation.v_voies;\n-- On cr\u00e9e la vue en r\u00e9cup\u00e9rant les routes de plus de 5 km\nCREATE VIEW z_formation.v_voies AS\nSELECT id_route, id AS code, ST_Length(geom) AS longueur, geom\nFROM z_formation.route\nWHERE ST_Length(geom) > 5000\n
Utiliser cette vue dans une autre requ\u00eate
-- Ou filtrer les donn\u00e9es\nSELECT * FROM z_formation.v_voies\nWHERE longueur > 10000\n
"},{"location":"save_queries/#enregistrer-une-requete-comme-une-table","title":"Enregistrer une requ\u00eate comme une table","text":"C'est la m\u00eame chose que pour enregistrer une vue, sauf qu'on cr\u00e9e une table: les donn\u00e9es sont donc stock\u00e9es en base, et n'\u00e9voluent plus en fonction des donn\u00e9es source. Cela permet d'acc\u00e9der rapidement aux donn\u00e9es, car la requ\u00eate sous-jacente n'est plus ex\u00e9cut\u00e9e une fois la table cr\u00e9\u00e9e.
"},{"location":"save_queries/#exemple-1-creer-la-table-des-voies-rassemblant-les-routes-et-les-chemins","title":"Exemple 1 - cr\u00e9er la table des voies rassemblant les routes et les chemins","text":"DROP TABLE IF EXISTS z_formation.t_voies;\nCREATE TABLE z_formation.t_voies AS\nSELECT\n-- on r\u00e9cup\u00e8re tous les champs\nsource.*,\n-- on calcule la longueur apr\u00e8s rassemblement des donn\u00e9es\nST_Length(geom) AS longueur\nFROM (\n (SELECT id, geom\n FROM z_formation.chemin\n LIMIT 100)\n UNION ALL\n (SELECT id, geom\n FROM z_formation.route\n LIMIT 100)\n) AS source\nORDER BY longueur\n;\n
Comme c'est une table, il est int\u00e9ressant d'ajouter un index spatial.
CREATE INDEX ON z_formation.t_voies USING GIST (geom);\n
On peut aussi ajouter une cl\u00e9 primaire
ALTER TABLE z_formation.t_voies ADD COLUMN gid serial;\nALTER TABLE z_formation.t_voies ADD PRIMARY KEY (gid);\n
Attention Les donn\u00e9es de la table n'\u00e9voluent plus en fonction des donn\u00e9es des tables source. Il faut donc supprimer la table puis la recr\u00e9er si besoin. Pour r\u00e9pondre \u00e0 ce besoin, il existe les vues mat\u00e9rialis\u00e9es.
"},{"location":"save_queries/#exemple-2-creer-une-table-de-nomenclature-a-partir-des-valeurs-distinctes-dun-champ","title":"Exemple 2 - cr\u00e9er une table de nomenclature \u00e0 partir des valeurs distinctes d'un champ.","text":"On cr\u00e9e la table si besoin. On ajoutera ensuite les donn\u00e9es via INSERT
-- Suppression de la table\nDROP TABLE IF EXISTS z_formation.nomenclature;\n-- Cr\u00e9ation de la table\nCREATE TABLE z_formation.nomenclature (\n id serial primary key,\n code text,\n libelle text,\n ordre smallint\n);\n
On ajoute ensuite les donn\u00e9es. La clause WITH
permet de r\u00e9aliser une sous-requ\u00eate, et de l'utiliser ensuite comme une table. La clause INSERT INTO
permet d'ajouter les donn\u00e9es. On ne lui passe pas le champ id, car c'est un serial, c'est-\u00e0-dire un entier auto-incr\u00e9ment\u00e9.
-- Ajout des donn\u00e9es \u00e0 partir d'une table via commande INSERT\nINSERT INTO z_formation.nomenclature\n(code, libelle, ordre)\n-- Clause WITH pour r\u00e9cup\u00e9rer les valeurs distinctes comme une table virtuelle\nWITH source AS (\n SELECT DISTINCT\n nature AS libelle\n FROM z_formation.lieu_dit_habite\n WHERE nature IS NOT NULL\n ORDER BY nature\n)\n-- S\u00e9lection des donn\u00e9es dans cette table virtuelle \"source\"\nSELECT\n-- on cr\u00e9e un code \u00e0 partir de l'ordre d'arriv\u00e9e.\n-- row_number() OVER() permet de r\u00e9cup\u00e9rer l'identifiant de la ligne dans l'ordre d'arriv\u00e9e\n-- (un_champ)::text permet de convertir un champ ou un calcul en texte\n-- lpad permet de compl\u00e9ter le chiffre avec des z\u00e9ro. 1 devient 01\nlpad( (row_number() OVER())::text, 2, '0' ) AS code,\nlibelle,\nrow_number() OVER() AS ordre\nFROM source\n;\n
Le r\u00e9sultat est le suivant:
code libelle ordre 01 Ch\u00e2teau 1 02 Lieu-dit habit\u00e9 2 03 Moulin 3 04 Quartier 4 05 Refuge 5 06 Ruines 6"},{"location":"save_queries/#exemple-3-creer-une-table-avec-lextraction-des-parcelles-sur-une-commune","title":"Exemple 3 - cr\u00e9er une table avec l'extraction des parcelles sur une commune","text":"On utilise le champ commune
pour filtrer. On n'oublie pas de cr\u00e9er l'index spatial, qui sera utilis\u00e9 pour am\u00e9liorer les performances lors des jointures spatiales.
-- supprimer la table si elle existe d\u00e9j\u00e0\nDROP TABLE IF EXISTS z_formation.parcelle_havre ;\n\n-- Cr\u00e9er la table via filtre sur le champ commune\nCREATE TABLE z_formation.parcelle_havre AS\nSELECT p.*\nFROM z_formation.parcelle AS p\nWHERE p.commune = '76351';\n\n-- Ajouter la cl\u00e9 primaire\nALTER TABLE z_formation.parcelle_havre ADD PRIMARY KEY (id_parcelle);\n\n-- Ajouter l'index spatial\nCREATE INDEX ON z_formation.parcelle_havre USING GIST (geom);\n
"},{"location":"save_queries/#enregistrer-une-requete-comme-une-vue-materialisee","title":"Enregistrer une requ\u00eate comme une vue mat\u00e9rialis\u00e9e","text":"-- On supprime d'abord la vue mat\u00e9rialis\u00e9e si elle existe\nDROP MATERIALIZED VIEW IF EXISTS z_formation.vm_voies;\n-- On cr\u00e9e la vue en r\u00e9cup\u00e9rant les routes de plus de 5 km\nCREATE MATERIALIZED VIEW z_formation.vm_voies AS\nSELECT id_route, id AS code, ST_Length(geom) AS longueur, geom\nFROM z_formation.route\nWHERE ST_Length(geom) > 6000\n\n-- Ajout des indexes sur le champ id_route et de g\u00e9om\u00e9trie\nCREATE INDEX ON z_formation.vm_voies (id_route);\nCREATE INDEX ON z_formation.vm_voies USING GIST (geom);\n\n-- On rafra\u00eechit la vue mat\u00e9rialis\u00e9e quand on en a besoin\n-- par exemple quand les donn\u00e9es source ont \u00e9t\u00e9 modifi\u00e9es\nREFRESH MATERIALIZED VIEW z_formation.vm_voies;\n
Continuer vers R\u00e9aliser des jointures attributaires et spatiales; JOIN
"},{"location":"sql_select/","title":"S\u00e9lectionner","text":"Nous allons pr\u00e9senter des requ\u00eates SQL de plus en plus complexes pour acc\u00e9der aux donn\u00e9es, et exploiter les capacit\u00e9s de PostgreSQL/PostGIS. Une requ\u00eate est construite avec des instructions standardis\u00e9es, appel\u00e9es clauses
-- Ordre des clauses SQL\nSELECT une_colonne, une_autre_colonne\nFROM nom_du_schema.nom_de_la_table\n(LEFT) JOIN autre_schema.autre_table\n ON critere_de_jointure\nWHERE condition\nGROUP BY champs_de_regroupement\nORDER BY champs_d_ordre\nLIMIT 10\n
R\u00e9cup\u00e9rer tous les objets d'une table, et les valeurs pour toutes les colonnes -- S\u00e9lectionner l'ensemble des donn\u00e9es d'une couche: l'\u00e9toile veut dire \"tous les champs de la table\"\nSELECT *\nFROM z_formation.borne_incendie\n;\n
Les 10 premiers objets
-- S\u00e9lectionner les 10 premi\u00e8res communes par ordre alphab\u00e9tique\nSELECT *\nFROM z_formation.commune\nORDER BY nom\nLIMIT 10\n
Les 10 premiers objets par ordre alphab\u00e9tique
-- S\u00e9lectionner les 10 premi\u00e8res communes par ordre alphab\u00e9tique descendant\nSELECT *\nFROM z_formation.commune\nORDER BY nom DESC\nLIMIT 10\n
Les 10 premiers objets avec un ordre sur plusieurs champs
-- On peut utiliser plusieurs champs pour l'ordre\nSELECT *\nFROM z_formation.commune\nORDER BY depart, nom\nLIMIT 10\n
S\u00e9lectionner seulement certains champs
-- S\u00e9lectionner seulement certains champs, et avec un ordre\nSELECT id_commune, code_insee, nom\nFROM z_formation.commune\nORDER BY nom\n
Donner un alias (un autre nom) aux champs
-- Donner des alias aux noms des colonnes\nSELECT id_commune AS identifiant,\ncode_insee AS \"code_commune\",\nnom\nFROM z_formation.commune\nORDER BY nom\n
On peut donc facilement, \u00e0 partir de la clause SELECT
, choisir quels champs on souhaite r\u00e9cup\u00e9rer, dans l'ordre voulu, et renommer le champ en sortie.
Si on veut charger le r\u00e9sultat de la requ\u00eate dans QGIS, il suffit de cocher la case Charger en tant que nouvelle couche puis de choisir le champ d'identifiant unique, et si et seulement si c'est une couche spatiale, choisir le champ de g\u00e9om\u00e9trie .
Attention, si la table est non spatiale, il faut bien penser \u00e0 d\u00e9cocher Colonne de g\u00e9om\u00e9trie !
Par exemple, pour afficher les communes avec leur information sommaire:
-- Ajouter la g\u00e9om\u00e9trie pour visualiser les donn\u00e9es dans QGIS\nSELECT id_commune AS identifiant,\ncode_insee AS \"code_commune\",\nnom, geom\nFROM z_formation.commune\nORDER BY nom\n
On choisira ici le champ identifiant comme identifiant unique, et le champ geom comme g\u00e9om\u00e9trie
Continuer vers R\u00e9aliser des calculs et cr\u00e9er des g\u00e9om\u00e9tries: FONCTIONS
"},{"location":"triggers/","title":"Les triggers","text":"Les triggers, aussi appel\u00e9s en fran\u00e7ais d\u00e9clencheurs, permettent de lancer des actions avant ou apr\u00e8s ajout, modification ou suppression de donn\u00e9es sur des tables (ou des vues).
Les triggers peuvent par exemple \u00eatre utilis\u00e9s
Des fonctions trigger sont associ\u00e9es aux triggers. Elles peuvent \u00eatre \u00e9crites en PL/pgSQL ou d'autres languages (p. ex. PL/Python). Une fonction trigger doit renvoyer soit NULL soit une valeur record ayant exactement la structure de la table pour laquelle le trigger a \u00e9t\u00e9 lanc\u00e9. Lire les derniers paragraphes ici pour en savoir plus.
"},{"location":"triggers/#calcul-automatique-de-certains-champs","title":"Calcul automatique de certains champs","text":"On cr\u00e9e une table borne_incendie
pour pouvoir tester cette fonctionnalit\u00e9:
CREATE TABLE z_formation.borne_incendie (\n id_borne serial primary key,\n code text NOT NULL,\n debit integer,\n geom geometry(point, 2154)\n);\nCREATE INDEX ON z_formation.borne_incendie USING GIST (geom);\n
On y ajoute des champs \u00e0 renseigner de mani\u00e8re automatique
-- TRIGGERS\n-- Modification de certains champs apr\u00e8s ajout ou modification\n-- Cr\u00e9er les champs dans la table\nALTER TABLE z_formation.borne_incendie ADD COLUMN modif_date date;\nALTER TABLE z_formation.borne_incendie ADD COLUMN modif_user text;\nALTER TABLE z_formation.borne_incendie ADD COLUMN longitude real;\nALTER TABLE z_formation.borne_incendie ADD COLUMN latitude real;\nALTER TABLE z_formation.borne_incendie ADD COLUMN donnee_validee boolean;\nALTER TABLE z_formation.borne_incendie ADD COLUMN last_action text;\n
On cr\u00e9e la fonction trigger qui ajoutera les m\u00e9tadonn\u00e9es dans la table
-- Cr\u00e9er la fonction qui sera lanc\u00e9e sur modif ou ajout de donn\u00e9es\nCREATE OR REPLACE FUNCTION z_formation.ajout_metadonnees_modification()\nRETURNS TRIGGER\nAS $limite$\nDECLARE newjsonb jsonb;\nBEGIN\n\n -- on transforme l'enregistrement NEW (la ligne modifi\u00e9e ou ajout\u00e9e) en JSON\n -- pour conna\u00eetre la liste des champs\n newjsonb = to_jsonb(NEW);\n\n -- on peut ainsi tester si chaque champ existe dans la table\n -- avant de modifier sa valeur\n -- Par exemple, on teste si le champ modif_date est bien dans l'enregistrement courant\n IF newjsonb ? 'modif_date' THEN\n NEW.modif_date = now();\n RAISE NOTICE 'Date modifi\u00e9e %', NEW.modif_date;\n END IF;\n\n IF newjsonb ? 'modif_user' THEN\n NEW.modif_user = CURRENT_USER;\n END IF;\n\n -- longitude et latitude\n IF newjsonb ? 'longitude' AND newjsonb ? 'latitude'\n THEN\n -- Soit on fait un UPDATE et les g\u00e9om\u00e9tries sont diff\u00e9rentes\n -- Soit on fait un INSERT\n -- Sinon pas besoin de calculer les coordonn\u00e9es\n IF\n (TG_OP = 'UPDATE' AND NOT ST_Equals(OLD.geom, NEW.geom))\n OR (TG_OP = 'INSERT')\n THEN\n NEW.longitude = ST_X(ST_Centroid(NEW.geom));\n NEW.latitude = ST_Y(ST_Centroid(NEW.geom));\n END IF;\n END IF;\n\n -- Si je trouve un champ donnee_validee, je le mets \u00e0 False pour revue par l'administrateur\n -- Je peux faire une symbologie dans QGIS qui montre les donn\u00e9es modifi\u00e9es depuis derni\u00e8re validation\n IF newjsonb ? 'donnee_validee' THEN\n NEW.donnee_validee = False;\n END IF;\n\n -- Si je trouve un champ last_action, je peux y mettre UPDATE ou INSERT\n -- Pour savoir quelle est la derni\u00e8re op\u00e9ration utilis\u00e9e\n IF newjsonb ? 'last_action' THEN\n NEW.last_action = TG_OP;\n END IF;\n\n RETURN NEW;\nEND;\n$limite$\nLANGUAGE plpgsql\n;\n
On cr\u00e9e enfin le d\u00e9clencheur pour la ou les tables souhait\u00e9es, ce qui active le lancement de la fonction trigger pr\u00e9c\u00e9dente sur certaines actions:
-- Dire \u00e0 PostgreSQL d'\u00e9couter les modifications et ajouts sur la table\nCREATE TRIGGER trg_ajout_metadonnees_modification\nBEFORE INSERT OR UPDATE ON z_formation.borne_incendie\nFOR EACH ROW EXECUTE PROCEDURE z_formation.ajout_metadonnees_modification();\n
"},{"location":"triggers/#controles-de-conformite","title":"Contr\u00f4les de conformit\u00e9","text":"Il est aussi possible d'utiliser les triggers pour lancer des contr\u00f4les sur les valeurs de certains champs. Par exemple, on peut ajouter un contr\u00f4le sur la g\u00e9om\u00e9trie lors de l'ajout ou de la modification de donn\u00e9es: on v\u00e9rifie si la g\u00e9om\u00e9trie est bien en intersection avec les objets de la table des communes
-- Contr\u00f4le de la g\u00e9om\u00e9trie\n-- qui doit \u00eatre dans la zone d'int\u00e9r\u00eat\n-- On cr\u00e9e une fonction g\u00e9n\u00e9rique qui pourra s'appliquer pour toutes les couches\nCREATE OR REPLACE FUNCTION z_formation.validation_geometrie_dans_zone_interet()\nRETURNS TRIGGER AS $limite$\nBEGIN\n -- On v\u00e9rifie l'intersection avec les communes, on renvoie une erreur si souci\n IF NOT ST_Intersects(\n NEW.geom,\n st_collectionextract((SELECT ST_Collect(geom) FROM z_formation.commune), 3)::geometry(multipolygon, 2154)\n ) THEN\n -- On renvoie une erreur\n RAISE EXCEPTION 'La g\u00e9om\u00e9trie doit se trouver dans les communes';\n END IF;\n\n RETURN NEW;\nEND;\n$limite$\nLANGUAGE plpgsql;\n\n-- On l'applique sur la couches de test\nDROP TRIGGER IF EXISTS trg_validation_geometrie_dans_zone_interet ON z_formation.borne_incendie;\nCREATE TRIGGER trg_validation_geometrie_dans_zone_interet\nBEFORE INSERT OR UPDATE ON z_formation.borne_incendie\nFOR EACH ROW EXECUTE PROCEDURE z_formation.validation_geometrie_dans_zone_interet();\n
Si on essaye de cr\u00e9er un point dans la table z_formation.borne_incendie
en dehors des communes, la base renverra une erreur.
On cr\u00e9e d'abord une table qui permettra de stocker les actions
CREATE TABLE IF NOT EXISTS z_formation.log (\n id serial primary key,\n log_date timestamp,\n log_user text,\n log_action text,\n log_data jsonb\n);\n
On peut maintenant cr\u00e9er un trigger qui stocke dans cette table les actions effectu\u00e9es. Dans cet exemple, toutes les donn\u00e9es sont stock\u00e9es, mais on pourrait bien s\u00fbr choisir de simplifier cela.
CREATE OR REPLACE FUNCTION z_formation.log_actions()\nRETURNS TRIGGER AS $limite$\nDECLARE\n row_data jsonb;\nBEGIN\n -- We keep data\n IF TG_OP = 'INSERT' THEN\n -- for insert, we take the new data\n row_data = to_jsonb(NEW);\n ELSE\n -- for UPDATE and DELETE, we keep data before changes\n row_data = to_jsonb(OLD);\n END IF;\n\n -- We insert a new log item\n INSERT INTO z_formation.log (\n log_date,\n log_user,\n log_action,\n log_data\n )\n VALUES (\n now(),\n CURRENT_USER,\n TG_OP,\n row_data\n );\n IF TG_OP != 'DELETE' THEN\n RETURN NEW;\n ELSE\n RETURN OLD;\n END IF;\nEND;\n$limite$\nLANGUAGE plpgsql;\n\n-- On l'applique sur la couches de test\n-- On \u00e9coute apr\u00e8s l'action, d'o\u00f9 l'utilisation de `AFTER`\n-- On \u00e9coute pour INSERT, UPDATE ou DELETE\nDROP TRIGGER IF EXISTS trg_log_actions ON z_formation.borne_incendie;\nCREATE TRIGGER trg_log_actions\nAFTER INSERT OR UPDATE OR DELETE ON z_formation.borne_incendie\nFOR EACH ROW EXECUTE PROCEDURE z_formation.log_actions();\n
NB:
Continuer vers Correction des g\u00e9om\u00e9tries invalides
"},{"location":"triggers/#quiz","title":"Quiz","text":"Cr\u00e9er une table avec un champ id de type 'serial' et une g\u00e9om\u00e9trie de type polygone en 2154. Puis cr\u00e9er un trigger s'assurant que les g\u00e9om\u00e9tries aient au minimum **4** points dessin\u00e9s. -- Table: z_formation.polygone_mini_quatre_points\n -- DROP TABLE IF EXISTS z_formation.polygone_mini_quatre_points;\n CREATE TABLE IF NOT EXISTS z_formation.polygone_mini_quatre_points\n (\n id serial NOT NULL PRIMARY KEY,\n geom geometry(Polygon,2154)\n )\n\n -- FUNCTION: z_formation.contrainte_mini_quatre_points()\n -- DROP FUNCTION IF EXISTS z_formation.contrainte_mini_quatre_points();\n CREATE OR REPLACE FUNCTION z_formation.contrainte_mini_quatre_points()\n RETURNS trigger AS $limite$\n BEGIN\n -- On v\u00e9rifie que le polygone a au moins 4 points dessin\u00e9s\n -- => soit 5 points en comptant le dernier point qui ferme le polygone !\n IF ST_NPoints(NEW.geom) < 5\n THEN\n -- On renvoie une erreur\n RAISE EXCEPTION 'Le polygone doit avoir au moins 4 points dessin\u00e9s';\n END IF;\n\n RETURN NEW;\n END;\n $limite$\n LANGUAGE plpgsql;\n\n -- Trigger: trg_contrainte_mini_quatre_points\n -- DROP TRIGGER IF EXISTS trg_contrainte_mini_quatre_points ON z_formation.polygone_mini_quatre_points;\n CREATE OR REPLACE TRIGGER trg_contrainte_mini_quatre_points\n BEFORE INSERT OR UPDATE \n ON z_formation.polygone_mini_quatre_points\n FOR EACH ROW\n EXECUTE FUNCTION z_formation.contrainte_mini_quatre_points();\n
"},{"location":"tutoriel/","title":"Tutoriel","text":"Afin de vous entra\u00eener il existe diff\u00e9rentes tutoriels en ligne vous permettant de vous exercer.
La clause UNION
peut \u00eatre utilis\u00e9e pour regrouper les donn\u00e9es de sources diff\u00e9rentes dans une m\u00eame table. Le UNION ALL
fait la m\u00eame choses, mais sans r\u00e9aliser de d\u00e9doublonnement, ce qui est plus rapide.
Rassembler les routes et les chemins ensemble, en ajoutant un champ \"nature\" pour les diff\u00e9rencier
-- Rassembler des donn\u00e9es de tables diff\u00e9rentes\n-- On utilise une UNION ALL\n\n (SELECT 'chemin' AS nature,\n geom,\n ROUND(ST_LENGTH(geom))::integer AS longueur\n FROM z_formation.chemin\n LIMIT 100)\n-- UNION ALL est plac\u00e9 entre 2 SELECT\nUNION ALL \n (SELECT 'route' AS nature,\n geom,\n ROUND(ST_LENGTH(geom))::integer AS longueur\n FROM z_formation.route\n LIMIT 100)\n-- Le ORDER BY doit \u00eatre r\u00e9alis\u00e9 \u00e0 la fin, et non sur chaque SELECT\nORDER BY longueur\n
Si on doit r\u00e9aliser le m\u00eame calcul sur chaque sous-ensemble (chaque SELECT), on peut le faire en 2 \u00e9tapes via une sous-requ\u00eate (ou une clause WITH)
SELECT\n-- on r\u00e9cup\u00e8re tous les champs\nsource.*,\n-- on calcule la longueur apr\u00e8s rassemblement des donn\u00e9es\nst_length(geom) AS longueur\nFROM (\n (SELECT id, geom\n FROM z_formation.chemin\n LIMIT 100)\n UNION ALL\n (SELECT id, geom\n FROM z_formation.route\n LIMIT 100)\n) AS source\nORDER BY longueur DESC\n;\n
Continuer vers Enregistrer les requ\u00eates: VIEW
"},{"location":"utils/","title":"Fonctions utiles","text":"Nous regroupons ici quelques fonctions r\u00e9alis\u00e9es au cours de formations ou d'accompagnements d'utilisateurs de PostgreSQL.
"},{"location":"utils/#ajout-de-lauto-incrementation-sur-un-champ-entier","title":"Ajout de l'auto-incr\u00e9mentation sur un champ entier","text":"Lorsqu'on importe une couche dans une table via les outils de QGIS, le champ d'identifiant choisi n'a pas le support de l'auto-incr\u00e9mentation, ce qui peut poser des probl\u00e8mes de l'ajout de nouvelles donn\u00e9es.
Depuis PostgreSQL 10, on peut maintenant utiliser des identit\u00e9s au lieu des serial pour avoir un champ auto-compl\u00e9t\u00e9. Voir par exemple l'article https://www.loxodata.com/post/identity/
Pour ajouter le support de l'auto-incr\u00e9mentation sur un champ entier \u00e0 une table existante, on peut utiliser les commandes suivantes :
-- Activer la g\u00e9n\u00e9ration automatique\nALTER TABLE \"monschema\".\"test\" ALTER \"id\" ADD GENERATED BY DEFAULT AS IDENTITY;\n\n-- Mettre la valeur de la s\u00e9quence (implicite et cach\u00e9e) \u00e0 la valeur max du champ d'identifiant\nSELECT setval(pg_get_serial_sequence('\"monschema\".\"test\"', 'id'), (SELECT max(\"id\") FROM \"monschema\".\"test\"));\n
Pour transformer les s\u00e9quences cr\u00e9\u00e9es pr\u00e9c\u00e9demment via des serial
en identit\u00e9 avec identity
, on peut lancer :
-- Enlever la valeur par d\u00e9faut sur le champ d'identifiant\nALTER TABLE \"monschema\".\"test\" ALTER COLUMN id DROP DEFAULT;\n\n-- Supprimer la s\u00e9quence\nDROP SEQUENCE IF EXISTS \"monschema\".\"test_id_seq\";\n\n-- Activer la g\u00e9n\u00e9ration automatique\nALTER TABLE \"monschema\".\"test\" ALTER \"id\" ADD GENERATED BY DEFAULT AS IDENTITY;\n\n-- Mettre la valeur de la s\u00e9quence (implicite et cach\u00e9e) \u00e0 la valeur max du champ d'identifiant\nSELECT setval(pg_get_serial_sequence('\"monschema\".\"test\"', 'id'), (SELECT max(\"id\") FROM \"monschema\".\"test\"));\n
"},{"location":"utils/#creation-automatique-dindexes-spatiaux","title":"Cr\u00e9ation automatique d'indexes spatiaux","text":"Pour des donn\u00e9es spatiales volumineuses, les performances d'affichage sont bien meilleures \u00e0 grande \u00e9chelle si on a ajout\u00e9 un index spatial. L'index est aussi beaucoup utilis\u00e9 pour am\u00e9liorer les performances d'analyses spatiales.
On peut cr\u00e9er l'index spatial table par table, ou bien automatiser cette cr\u00e9ation, c'est-\u00e0-dire cr\u00e9er les indexes spatiaux pour toutes les tables qui n'en ont pas.
Pour cela, nous avons con\u00e7u une fonction, t\u00e9l\u00e9chargeable ici: https://gist.github.com/mdouchin/cfa0e37058bcf102ed490bc59d762042
On doit copier/coller le script SQL de cette page GIST
dans la fen\u00eatre SQL du Gestionnaire de bases de donn\u00e9es de QGIS, puis lancer la requ\u00eate avec Ex\u00e9cuter. On peut ensuite vider le contenu de la fen\u00eatre, puis appeler la fonction create_missing_spatial_indexes
via le code SQL suivant :
-- On lance avec le param\u00e8tre \u00e0 True si on veut juste voir les tables qui n'ont pas d'index spatial\n-- On lance avec False si on veut cr\u00e9er les indexes automatiquement\n\n-- V\u00e9rification\nSELECT * FROM create_missing_spatial_indexes( True );\n\n-- Cr\u00e9ation\nSELECT * FROM create_missing_spatial_indexes( False );\n
"},{"location":"utils/#trouver-toutes-les-tables-sans-cle-primaire","title":"Trouver toutes les tables sans cl\u00e9 primaire","text":"Il est tr\u00e8s important de d\u00e9clarer une cl\u00e9 primaire pour vos tables stock\u00e9es dans PostgreSQL. Cela fournit un moyen aux logiciels comme QGIS d'identifier de mani\u00e8re performante les lignes dans une table. Sans cl\u00e9 primaire, les performances d'acc\u00e8s aux donn\u00e9es peuvent \u00eatre d\u00e9grad\u00e9es.
Vous pouvez trouver l'ensemble des tables de votre base de donn\u00e9es sans cl\u00e9 primaire en construisant cette vue PostgreSQL tables_without_primary_key
:
DROP VIEW IF EXISTS tables_without_primary_key;\nCREATE VIEW tables_without_primary_key AS\nSELECT t.table_schema, t.table_name\nFROM information_schema.tables AS t\nLEFT JOIN information_schema.table_constraints AS c\n ON t.table_schema = c.table_schema\n AND t.table_name = c.table_name\n AND c.constraint_type = 'PRIMARY KEY'\nWHERE True\nAND t.table_type = 'BASE TABLE'\nAND t.table_schema not in ('pg_catalog', 'information_schema')\nAND c.constraint_name IS NULL\nORDER BY table_schema, table_name\n;\n
SELECT *\nFROM tables_without_primary_key;\n
Ce qui peut donner par exemple:
table_schema table_name agriculture parcelles agriculture puits cadastre sections environnement znieff environnement parcs_naturelscadastre
, vous pouvez ensuite lancer la requ\u00eate :SELECT *\nFROM tables_without_primary_key\nWHERE table_schema IN ('cadastre');\n
Ce qui peut alors donner:
table_schema table_name cadastre sections"},{"location":"utils/#ajouter-automatiquement-plusieurs-champs-a-plusieurs-tables","title":"Ajouter automatiquement plusieurs champs \u00e0 plusieurs tables","text":"Il est parfois n\u00e9cessaire d'ajouter des champs \u00e0 une ou plusieurs tables, par exemple pour y stocker ensuite des m\u00e9tadonn\u00e9es (date de modification, date d'ajout, utilisateur, lien, etc).
Nous proposons pour cela la fonction ajout_champs_dynamiques
qui permet de fournir un nom de sch\u00e9ma, un nom de table, et une cha\u00eene de caract\u00e8re contenant la liste s\u00e9par\u00e9e par virgule des champs et de leur type.
La fonction est accessible ici: https://gist.github.com/mdouchin/50234f1f33801aed6f4f2cbab9f4887c
commune
du sch\u00e9ma test
: on ajoute les champs date_creation
, date_modification
et utilisateur
SELECT\najout_champs_dynamiques('test', 'commune', 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text')\n;\n
test
. On utilise dans cette exemple la vue geometry_columns
qui liste les tables spatiales, car on souhaite aussi ne faire cet ajout que pour les donn\u00e9es de type POINT-- Lancer la cr\u00e9ation de champs sur toutes les tables\n-- du sch\u00e9ma test\n-- contenant des g\u00e9om\u00e9tries de type Point\nSELECT f_table_schema, f_table_name,\najout_champs_dynamiques(\n -- sch\u00e9ma\n f_table_schema,\n -- table\n f_table_name,\n -- liste des champs, au format nom_du_champ TYPE\n 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text'\n)\nFROM geometry_columns\nWHERE True\nAND \"type\" LIKE '%POINT'\nAND f_table_schema IN ('test')\nORDER BY f_table_schema, f_table_name\n;\n
"},{"location":"utils/#verifier-la-taille-des-bases-tables-et-schemas","title":"V\u00e9rifier la taille des bases, tables et sch\u00e9mas","text":""},{"location":"utils/#connaitre-la-taille-des-bases-de-donnees","title":"Conna\u00eetre la taille des bases de donn\u00e9es","text":"On peut lancer la requ\u00eate suivante, qui renvoie les bases de donn\u00e9es ordonn\u00e9es par taille descendante.
SELECT\npg_database.datname AS db_name,\npg_database_size(pg_database.datname) AS db_size,\npg_size_pretty(pg_database_size(pg_database.datname)) AS db_pretty_size\nFROM pg_database\nWHERE datname NOT IN ('postgres', 'template0', 'template1')\nORDER BY db_size DESC;\n
"},{"location":"utils/#calculer-la-taille-des-tables","title":"Calculer la taille des tables","text":"On cr\u00e9e une fonction get_table_info
qui utilise les tables syst\u00e8me pour lister les tables, r\u00e9cup\u00e9rer leur sch\u00e9ma et les informations de taille.
DROP FUNCTION IF EXISTS get_table_info();\nCREATE OR REPLACE FUNCTION get_table_info()\nRETURNS TABLE (\n oid oid,\n schema_name text,\n table_name text,\n row_count integer,\n total_size bigint,\n pretty_total_size text\n)\nAS $$\nBEGIN\n RETURN QUERY\n SELECT\n b.oid, b.schema_name::text, b.table_name::text,\n b.row_count::integer,\n b.total_size::bigint,\n pg_size_pretty(b.total_size) AS pretty_total_size\n FROM (\n SELECT *,\n a.total_size - index_bytes - COALESCE(toast_bytes,0) AS table_bytes\n FROM (\n SELECT\n c.oid,\n nspname AS schema_name,\n relname AS TABLE_NAME,\n c.reltuples AS row_count,\n pg_total_relation_size(c.oid) AS total_size,\n pg_indexes_size(c.oid) AS index_bytes,\n pg_total_relation_size(reltoastrelid) AS toast_bytes\n FROM pg_class c\n LEFT JOIN pg_namespace n\n ON n.oid = c.relnamespace\n WHERE relkind = 'r'\n AND nspname NOT IN ('pg_catalog', 'information_schema')\n ) AS a\n ) AS b\n ;\nEND; $$\nLANGUAGE 'plpgsql';\n
On peut l'utiliser simplement de la mani\u00e8re suivante
-- Liste les tables\nSELECT * FROM get_table_info() ORDER BY schema_name, table_name DESC;\n\n-- Lister les tables dans l'ordre inverse de taille\nSELECT * FROM get_table_info() ORDER BY total_size DESC;\n
"},{"location":"utils/#calculer-la-taille-des-schemas","title":"Calculer la taille des sch\u00e9mas","text":"On cr\u00e9e une simple fonction qui renvoie la somme des tailles des tables d'un sch\u00e9ma
-- Fonction pour calculer la taille d'un sch\u00e9ma\nCREATE OR REPLACE FUNCTION pg_schema_size(schema_name text)\nRETURNS BIGINT AS\n$$\n SELECT\n SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))::BIGINT\n FROM pg_tables\n WHERE schemaname = schema_name\n$$\nLANGUAGE SQL;\n
On peut alors l'utiliser pour conna\u00eetre la taille d'un sch\u00e9ma
-- utilisation pour un sch\u00e9ma\nSELECT pg_size_pretty(pg_schema_size('public')) AS ;\n
Ou lister l'ensemble des sch\u00e9mas
-- lister les sch\u00e9mas et r\u00e9cup\u00e9rer leur taille\nSELECT schema_name, pg_size_pretty(pg_schema_size(schema_name))\nFROM information_schema.schemata\nWHERE schema_name NOT IN ('pg_catalog', 'information_schema')\nORDER BY pg_schema_size(schema_name) DESC;\n
"},{"location":"utils/#lister-les-triggers-appliques-sur-les-tables","title":"Lister les triggers appliqu\u00e9s sur les tables","text":"On peut utiliser la requ\u00eate suivante pour lister l'ensemble des triggers activ\u00e9s sur les tables
SELECT\n event_object_schema AS table_schema,\n event_object_table AS table_name,\n trigger_schema,\n trigger_name,\n string_agg(event_manipulation, ',') AS event,\n action_timing AS activation,\n action_condition AS condition, \n CASE WHEN tgenabled = 'O' THEN True ELSE False END AS trigger_active,\n action_statement AS definition\nFROM information_schema.triggers AS t\nINNER JOIN pg_trigger AS p\n ON p.tgrelid = concat('\"', event_object_schema, '\".\"', event_object_table, '\"')::regclass \n AND trigger_name = tgname\nWHERE True\nGROUP BY 1,2,3,4,6,7,8,9\nORDER BY table_schema, table_name\n;\n
Cette requ\u00eate renvoie un tableau de la forme :
table_schema table_name trigger_schema trigger_name event activation condition trigger_active definition gestion acteur gestion tr_date_maj UPDATE BEFORE f EXECUTE FUNCTION occtax.maj_date() occtax organisme occtax tr_date_maj UPDATE BEFORE t EXECUTE FUNCTION occtax.maj_date() taxon iso_metadata_reference taxon update_imr_timestamp UPDATE BEFORE t EXECUTE FUNCTION taxon.update_imr_timestamp_column()"},{"location":"utils/#lister-les-fonctions-installees-par-les-extensions","title":"Lister les fonctions install\u00e9es par les extensions","text":"Il est parfois utile de lister les fonctions des extensions, par exemple pour :
La requ\u00eate suivante permet d'afficher les informations essentielles des fonctions cr\u00e9\u00e9es par les extensions install\u00e9es dans la base :
SELECT DISTINCT\n ne.nspname AS extension_schema,\n e.extname AS extension_name,\n np.nspname AS function_schema,\n p.proname AS function_name,\n pg_get_function_identity_arguments(p.oid) AS function_params,\n proowner::regrole AS function_owner\nFROM\n pg_catalog.pg_extension AS e\n INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)\n INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)\n INNER JOIN pg_catalog.pg_namespace AS ne ON (ne.oid = e.extnamespace)\n INNER JOIN pg_catalog.pg_namespace AS np ON (np.oid = p.pronamespace)\nWHERE\n TRUE\n -- only extensions\n AND d.deptype = 'e'\n -- not in pg_catalog\n AND ne.nspname NOT IN ('pg_catalog')\n -- optionnally filter some extensions\n -- AND e.extname IN ('postgis', 'postgis_raster')\n -- optionnally filter by some owner\n AND proowner::regrole::text IN ('postgres')\n ORDER BY\n extension_name,\n function_name;\n;\n
qui renvoie une r\u00e9sultat comme ceci (cet exemple est un extrait de quelques lignes) :
extension_schema extension_name function_schema function_name function_params function_owner public fuzzystrmatch public levenshtein_less_equal text, text, integer johndoe public fuzzystrmatch public metaphone text, integer johndoe public fuzzystrmatch public soundex text johndoe public fuzzystrmatch public text_soundex text johndoe public hstore public akeys hstore johndoe public hstore public avals hstore johndoe public hstore public defined hstore, text johndoe public postgis public st_buffer text, double precision, integer johndoe public postgis public st_buffer geom geometry, radius double precision, options text johndoe public postgis public st_buildarea geometry johndoeOn peut bien s\u00fbr modifier la clause WHERE
pour filtrer plus ou moins les fonctions renvoy\u00e9es.
row_number() over()
non typ\u00e9 en integer
","text":"Si on utilise des vues dans QGIS qui cr\u00e9ent un identifiant unique via le num\u00e9ro de ligne, il est important :
integer
et pas entier long bigint
ORDER BY
pour essayer au maximum que QGIS r\u00e9cup\u00e8re les objets toujours dans le m\u00eame ordre.Quand une requ\u00eate d'une vue utilise row_number() OVER()
, depuis des versions r\u00e9centes de PostgreSQL, cela renvoie un entier long bigint
ce qui n'est pas conseill\u00e9.
On peut trouver ces vues ou vues mat\u00e9rialis\u00e9es via cette requ\u00eate :
-- vues\nSELECT\n concat('\"', schemaname, '\".\"', viewname, '\"') AS row_number_view\nFROM pg_views\nWHERE \"definition\" ~* '(.)+row_number\\(\\s*\\)\\s*over\\s*\\(\\s*\\) (.)+'\nORDER BY schemaname, viewname\n;\n\n-- vues mat\u00e9rialis\u00e9es\nSELECT\n concat('\"', schemaname, '\".\"', matviewname, '\"') AS row_number_view\nFROM pg_views\nWHERE \"definition\" ~* '(.)+row_number\\(\\s*\\)\\s*over\\s*\\(\\s*\\) (.)+'\nORDER BY schemaname, matviewname\n;\n
"},{"location":"utils/#lister-les-tables-qui-ont-une-cle-primaire-non-entiere","title":"Lister les tables qui ont une cl\u00e9 primaire non enti\u00e8re","text":"Pour \u00e9viter des soucis de performances sur les gros jeux de donn\u00e9es, il faut \u00e9viter d'avoir des tables avec des cl\u00e9s primaires sur des champs qui ne sont pas de type entier integer
.
En effet, dans QGIS, l'ouverture de ce type de table avec une cl\u00e9 primaire de type text
, ou m\u00eame bigint
, cela entra\u00eene la cr\u00e9ation et le stockage en m\u00e9moire d'une table de correspondance entre chaque objet de la couche et le num\u00e9ro d'arriv\u00e9e de la ligne. Sur les tables volumineuses, cela peut \u00eatre sensible.
Pour trouver toutes les tables, on peut faire cette requ\u00eate :
SELECT\n nspname AS table_schema, relname AS table_name,\n a.attname AS column_name,\n format_type(a.atttypid, a.atttypmod) AS column_type\nFROM pg_index AS i\nJOIN pg_class AS c\n ON i.indrelid = c.oid\nJOIN pg_attribute AS a\n ON a.attrelid = c.oid\n AND a.attnum = any(i.indkey)\nJOIN pg_namespace AS n\n ON n.oid = c.relnamespace\nWHERE indisprimary AND nspname NOT LIKE 'pg_%' AND nspname NOT LIKE 'lizmap_%'\nAND format_type(a.atttypid, a.atttypmod) != 'integer';\n
Ce qui donne par exemple :
table_schema table_name column_name column_type un_schema une_table_a id bigint un_schema une_table_b id bigint un_autre_schema autre_table_c id character varying un_autre_schema autre_table_d id character varying"},{"location":"utils/#trouver-les-tables-spatiales-avec-une-geometrie-non-typee","title":"Trouver les tables spatiales avec une g\u00e9om\u00e9trie non typ\u00e9e","text":"Il est important lorsqu'on cr\u00e9e des champs de type g\u00e9om\u00e9trie geometry
de pr\u00e9ciser le type des objets (point, ligne, polygone, etc.) et la projection.
On doit donc cr\u00e9er les champs comme ceci :
CREATE TABLE test (\n id serial primary key,\n geom geometry(Point, 2154)\n);\n
et non comme ceci :
CREATE TABLE test (\n id serial primary key,\n geom geometry\n);\n
C'est donc important lorsqu'on cr\u00e9e des tables \u00e0 partir de requ\u00eates SQL de toujours bien typer les g\u00e9om\u00e9tries. Par exemple :
CREATE TABLE test AS\nSELECT id,\nST_Centroid(geom)::geometry(Point, 2154) AS geom\n-- ne pas faire :\n-- ST_Centroid(geom) AS geom\nFROM autre_table\n
On peut trouver toutes les tables qui auraient \u00e9t\u00e9 cr\u00e9\u00e9es avec des champs de g\u00e9om\u00e9trie non typ\u00e9s via la requ\u00eate suivante :
SELECT *\nFROM geometry_columns\nWHERE srid = 0 OR lower(type) = 'geometry'\n;\n
Il faut corriger ces vues ou tables.
"},{"location":"utils/#trouver-les-objets-avec-des-geometries-trop-complexes","title":"Trouver les objets avec des g\u00e9om\u00e9tries trop complexes","text":"SELECT count(*)\nFROM ma_table\nWHERE ST_NPoints(geom) > 10000\n;\n
Les trop gros polygones (zones inondables, zonages issus de regroupement de nombreux objets, etc.) peuvent poser de r\u00e9els soucis de performance, notamment sur les op\u00e9rations d'intersection avec les objets d'autres couches via ST_Intersects
.
On peut corriger cela via la fonction ST_Subdivide
. Voir Documentation de ST_Subdivide
Nous souhaitons comparer deux tables de la base, par exemple une table de communes en 2021 communes_2021
et une table de communes en 2022 communes_2022
.
On peut utiliser une fonction qui utilise les possibilit\u00e9s du format hstore pour comparer les donn\u00e9es entre elles.
-- On ajoute le support du format hstore\nCREATE EXTENSION IF NOT EXISTS hstore;\n\n-- On cr\u00e9e la fonction de comparaison\nDROP FUNCTION compare_tables(text,text,text,text,text,text[]);\nCREATE OR REPLACE FUNCTION compare_tables(\n p_schema_name_a text,\n p_table_name_a text,\n p_schema_name_b text,\n p_table_name_b text,\n p_common_identifier_field text,\n p_excluded_fields text[]\n\n) RETURNS TABLE(\n uid text,\n status text,\n table_a_values hstore,\n table_b_values hstore\n)\n LANGUAGE plpgsql\n AS $_$\nDECLARE\n sqltemplate text;\nBEGIN\n\n -- Compare data\n sqltemplate = '\n SELECT\n coalesce(ta.\"%1$s\", tb.\"%1$s\") AS \"%1$s\",\n CASE\n WHEN ta.\"%1$s\" IS NULL THEN ''not in table A''\n WHEN tb.\"%1$s\" IS NULL THEN ''not in table B''\n ELSE ''table A != table B''\n END AS status,\n CASE\n WHEN ta.\"%1$s\" IS NULL THEN NULL\n ELSE (hstore(ta.*) - ''%6$s''::text[]) - (hstore(tb) - ''%6$s''::text[])\n END AS values_in_table_a,\n CASE\n WHEN tb.\"%1$s\" IS NULL THEN NULL\n ELSE (hstore(tb.*) - ''%6$s''::text[]) - (hstore(ta) - ''%6$s''::text[])\n END AS values_in_table_b\n FROM \"%2$s\".\"%3$s\" AS ta\n FULL JOIN \"%4$s\".\"%5$s\" AS tb\n ON ta.\"%1$s\" = tb.\"%1$s\"\n WHERE\n (hstore(ta.*) - ''%6$s''::text[]) != (hstore(tb.*) - ''%6$s''::text[])\n OR (ta.\"%1$s\" IS NULL)\n OR (tb.\"%1$s\" IS NULL)\n ';\n\n RETURN QUERY\n EXECUTE format(sqltemplate,\n p_common_identifier_field,\n p_schema_name_a,\n p_table_name_a,\n p_schema_name_b,\n p_table_name_b,\n p_excluded_fields\n );\n\nEND;\n$_$;\n
Cette fonction attend en param\u00e8tres
referentiels
communes_2021
referentiels
communes_2022
code_commune
array['region', 'departement']
La requ\u00eate \u00e0 lancer est la suivantes
SELECT \"uid\", \"status\", \"table_a_values\", \"table_b_values\"\nFROM compare_tables(\n 'referentiels', 'commune_2021',\n 'referentiels', 'commune_2022',\n 'code_commune',\n array['region', 'departement']\n)\nORDER BY status, uid\n;\n
Exemple de donn\u00e9es renvoy\u00e9es:
uid status table_a_values table_b_values 12345 not in table A NULL \"annee_ref\"=>\"2022\", \"nom_commune\"=>\"Nouvelle commune\", \"population\"=>\"5723\" 97612 not in table B \"annee_ref\"=>\"2021\", \"nom_commune\"=>\"Ancienne commune\", \"population\"=>\"840\" NULL 97602 table A != table B \"annee_ref\"=>\"2021\", \"population\"=>\"1245\" \"annee_ref\"=>\"2022\", \"population\"=>\"1322\"Dans l'affichage ci-dessus, je n'ai pas affich\u00e9 le champ de g\u00e9om\u00e9trie, mais la fonction teste aussi les diff\u00e9rences de g\u00e9om\u00e9tries.
Attention, les performances de ce type de requ\u00eate ne sont pas forc\u00e9ment assur\u00e9es pour des volumes de donn\u00e9es importants.
"},{"location":"utils/#trouver-les-valeurs-distinctes-des-champs-dune-table","title":"Trouver les valeurs distinctes des champs d'une table","text":"Pour comprendre quelles donn\u00e9es sont pr\u00e9sentes dans une table PostgreSQL, vous pouvez exploiter la puissance des fonctions de manipulation du JSON
et r\u00e9cup\u00e9rer automatiquement toutes les valeurs distinctes d'une table.
Cela permet de lister les champs de cette table et de bien se repr\u00e9senter ce qu'ils contiennent.
SELECT\n -- nom du champ de la table\n key AS champ,\n\n -- On regroupe les valeurs distinctes du champ\n -- depuis le JSON calcul\u00e9 plus bas via to_jsonb\n -- On compte les valeurs distinctes\n count(DISTINCT value) AS nombre,\n\n -- On r\u00e9cup\u00e8re les valeurs uniques pour ce champ\n json_agg(DISTINCT value) AS valeurs\nFROM\n -- Table dans laquelle chercher les valeurs uniques\n velo.amenagement AS i,\n -- Transformation de chaque ligne de la table en JSON (paires cl\u00e9/valeurs)\n jsonb_each(\n -- on utilise le - 'id' - 'geom' pour ne pas r\u00e9cup\u00e9rer les valeurs de ces champs\n to_jsonb(i) - 'id' - 'geom'\n )\n-- On regroupe par cl\u00e9, c'est-\u00e0-dire par champ\nGROUP BY key;\n
ce qui donnera comme r\u00e9sultat
champ | nombre | valeurs\n------------+--------+--------------------------------------------------------------------------------------------------------\n commune | 8 | [\"AMBON\", \"ARZAL\", \"BILLIERS\", \"LA ROCHE-BERNARD\", \"LE GUERNO\", \"MUZILLAC\", \"NIVILLAC\", \"SAINT-DOLAY\"]\n gestionnai | 3 | [\"Commune\", \"D\u00e9partement\", \"EPCI\"]\n id_iti | 9 | [\"iti_02\", \"iti_03\", \"iti_06\", \"iti_07\", \"iti_08\", \"iti_09\", \"iti_13\", \"iti_15\", \"iti_18\"]\n insee | 9 | [\"56002\", \"56004\", \"56018\", \"56077\", \"56143\", \"56147\", \"56149\", \"56195\", \"56212\"]\n maitre_ouv | 3 | [\"Commune\", \"D\u00e9partement\", \"EPCI\"]\n rlv_chauss | 5 | [\"Double sens\", \"Interdit \u00e0 la circ.\", \"NC\", \"Rond-point\", \"Sens unique\"]\n rlv_md_dx_ | 5 | [\"Aucun am\u00e9nagement\", \"Bande\", \"Contresens cyclable\", \"Voie uniquement pi\u00e9tonne\", \"Voie verte\"]\n rlv_pente | 5 | [\"Forte (ponctuelle)\", \"Forte (tron\u00e7on)\", \"Moyenne\", \"NC\", \"Nulle ou faible\"]\n rlv_vitess | 7 | [\"< 20\", \"20\", \"30\", \"50\", \"70\", \"80 et plus\", \"NC\"]\n type_surfa | 3 | [\"Lisse\", \"Meuble\", \"Rugueux\"]\n vvv | 3 | [\"V3\", \"V42\", \"V45\"]\n
Points d'attention:
Continuer vers Gestion des droits
"},{"location":"validate_geometries/","title":"Correction des g\u00e9om\u00e9tries","text":"Avec PostgreSQL on peut tester la validit\u00e9 des g\u00e9om\u00e9tries d'une table, comprendre la raison et localiser les soucis de validit\u00e9:
SELECT\nid_parcelle,\n-- v\u00e9rifier si la g\u00e9om est valide\nST_IsValid(geom) AS validite_geom,\n-- connaitre la raison d'invalidit\u00e9\nst_isvalidreason(geom) AS validite_raison,\n-- sortir un point qui localise le souci de validit\u00e9\nST_SetSRID(location(st_isvaliddetail(geom)), 2154) AS geom\nFROM z_formation.parcelle_havre\nWHERE ST_IsValid(geom) IS FALSE\n
qui renvoie 2 erreurs de polygones crois\u00e9s.
id_parcelle validite_geom validite_raison point_invalide 707847 False Self-intersection[492016.260004897 6938870.66384629] 010100000041B93E0AC1071E4122757CAA3D785A41 742330 False Self-intersection[489317.48266784 6939616.89391708] 0101000000677A40EE95DD1D41FBEF3539F8785A41et qu'on peut ouvrir comme une nouvelle couche, avec le champ g\u00e9om\u00e9trie point_invalide, ce qui permet de visualiser dans QGIS les positions des erreurs.
PostGIS fournir l'outil ST_MakeValid pour corriger automatiquement les g\u00e9om\u00e9tries invalides. On peut l'utiliser pour les lignes et polygones.
Attention, pour les polygones, cela peut conduire \u00e0 des g\u00e9om\u00e9tries de type diff\u00e9rent (par exemple une polygone \u00e0 2 noeuds devient une ligne). On utilise donc aussi la fonction ST_CollectionExtract pour ne r\u00e9cup\u00e9rer que les polygones.
-- Corriger les g\u00e9om\u00e9tries\nUPDATE z_formation.parcelle_havre\nSET geom = ST_Multi(ST_CollectionExtract(ST_MakeValid(geom), 3))\nWHERE NOT ST_isvalid(geom)\n\n-- Tester\nSELECT count(*)\nFROM z_formation.parcelle_havre\nWHERE NOT ST_isvalid(geom)\n
Il faut aussi supprimer l'ensemble des lignes dans la table qui ne correspondent pas au type de la couche import\u00e9e. Par exemple, pour les polygones, supprimer les objets dont le nombre de n\u0153uds est inf\u00e9rieur \u00e0 3.
SELECT *\nFROM z_formation.parcelle_havre\nWHERE ST_NPoints(geom) < 3\n
DELETE\nFROM z_formation.parcelle_havre\nWHERE ST_NPoints(geom) < 3\n
Continuer vers V\u00e9rifier la topologie
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Formation PostGIS","text":""},{"location":"#pre-requis","title":"Pr\u00e9-requis","text":"Cette formation concerne des utilisateurs de QGIS, g\u00e9omaticiens, qui souhaitent comprendre l'apport de l'utilisation de PostgreSQL comme outil de centralisation de la donn\u00e9es spatiale (et non spatiale):
Avant de v\u00e9rifier la topologie, il faut au pr\u00e9alable avoir des g\u00e9om\u00e9tries valides (cf. chapitre pr\u00e9c\u00e9dent).
Certaines micro-erreurs de topologie peuvent peuvent \u00eatre corrig\u00e9es en r\u00e9alisant une simplification des donn\u00e9es \u00e0 l'aide d'une grille, par exemple pour corriger des soucis d'arrondis. Pour cela, PostGIS a une fonction ST_SnapToGrid.
On peut utiliser conjointement ST_Simplify et ST_SnapToGrid pour effectuer une premi\u00e8re correction sur les donn\u00e9es. Attention, ces fonctions modifient la donn\u00e9e. A vous de choisir la bonne tol\u00e9rance, par exemple 5 cm, qui d\u00e9pend de votre donn\u00e9e et de votre cas d'utilisation.
Tester la simplification en lan\u00e7ant la requ\u00eate suivante, et en chargeant le r\u00e9sultat comme une nouvelle couche dans QGIS
SELECT\n ST_Multi(\n ST_CollectionExtract(\n ST_MakeValid(\n ST_SnapToGrid(\n st_simplify(geom,0),\n 0.05 -- 5 cm\n )\n ),\n 3\n )\n )::geometry(multipolygon, 2154)\nFROM z_formation.parcelle_havre\n;\n
Une fois le r\u00e9sultat visuellement test\u00e9 dans QGIS, par comparaison avec la table source, on peut choisir de modifier la g\u00e9om\u00e9trie de la table avec la version simplifi\u00e9e des donn\u00e9es:
-- Parcelles\nUPDATE z_formation.parcelle_havre\nSET geom =\nST_Multi(\n ST_CollectionExtract(\n ST_MakeValid(\n ST_SnapToGrid(\n st_simplify(geom,0),\n 0.05 -- 5 cm\n )\n ),\n 3\n )\n)\n;\n;\n
Attention: Si vous avez d'autres tables avec des objets en relation spatiale avec cette table, il faut aussi effectuer le m\u00eame traitement pour que les g\u00e9om\u00e9tries de toutes les couches se calent sur la m\u00eame grille. Par exemple la table des zonages.
UPDATE z_formation.zone_urba\nSET geom =\nST_Multi(\n ST_CollectionExtract(\n ST_MakeValid(\n ST_SnapToGrid(\n st_simplify(geom,0),\n 0.05 -- 5 cm\n )\n ),\n 3\n )\n)\n;\n
"},{"location":"check_topology/#reperer-certaines-erreurs-de-topologies","title":"Rep\u00e9rer certaines erreurs de topologies","text":"PostGIS poss\u00e8de de nombreuses fonctions de relations spatiales qui permettent de trouver les objets qui se chevauchent, qui se touchent, etc. Ces fonctions peuvent \u00eatre utilis\u00e9es pour comparer les objets d'une m\u00eame table, ou de deux tables diff\u00e9rentes. Voir: https://postgis.net/docs/reference.html#Spatial_Relationships_Measurements
Par exemple, trouver les parcelles voisines qui se recouvrent: on utilise la fonction ST_Overlaps. On peut cr\u00e9er une couche listant les recouvrements:
DROP TABLE IF EXISTS z_formation.recouvrement_parcelle_voisines;\nCREATE TABLE z_formation.recouvrement_parcelle_voisines AS\nSELECT DISTINCT ON (geom)\nparcelle_a, parcelle_b, aire_a, aire_b, ST_Area(geom) AS aire, geom\nFROM (\n SELECT\n a.id_parcelle AS parcelle_a, ST_Area(a.geom) AS aire_a,\n b.id_parcelle AS parcelle_b, ST_Area(b.geom) AS aire_b,\n (ST_Multi(\n st_collectionextract(\n ST_MakeValid(ST_Intersection(a.geom, b.geom))\n , 3)\n ))::geometry(MultiPolygon,2154) AS geom\n FROM z_formation.parcelle_havre AS a\n JOIN z_formation.parcelle_havre AS b\n ON a.id_parcelle != b.id_parcelle\n --ON ST_Intersects(a.geom, b.geom)\n AND ST_Overlaps(a.geom, b.geom)\n) AS voisin\nORDER BY geom\n;\n\nCREATE INDEX ON z_formation.recouvrement_parcelle_voisines USING GIST (geom);\n
On peut alors ouvrir cette couche dans QGIS pour zoomer sur chaque objet de recouvrement.
R\u00e9cup\u00e9rer la liste des identifiants de ces parcelles:
SELECT string_agg( parcelle_a::text, ',') FROM z_formation.recouvrement_parcelle_voisines;\n
On peut utiliser le r\u00e9sultat de cette requ\u00eate pour s\u00e9lectionner les parcelles probl\u00e9matiques: on s\u00e9lectionne le r\u00e9sultat dans le tableau du gestionnaire de base de donn\u00e9es, et on copie (CTRL + C). On peut alors utiliser cette liste dans une s\u00e9lection par expression dans QGIS, avec par exemple l'expression
\"id_parcelle\" IN (\n729091,742330,742783,742513,742514,743114,742992,742578,742991,742544,743009,744282,744378,744378,744281,744199,743646,746445,743680,744280,\n743653,743812,743208,743812,743813,744199,694298,694163,721712,707463,744412,707907,707069,721715,721715,696325,696372,746305,722156,722555,\n722195,714500,715969,722146,722287,723526,720296,720296,722296,723576,723572,723572,723571,724056,723570,723568,740376,722186,724055,714706,\n723413,723988,721808,721808,723413,724064,723854,723854,724063,723518,720736,720653,741079,741227,740932,740932,740891,721259,741304,741304,\n741501,741226,741812)\n
Une fois les parcelles s\u00e9lectionn\u00e9es, on peut utiliser certains outils de QGIS pour faciliter la correction:
Dans PostGIS, on peut utiliser la fonction ST_Snap dans une requ\u00eate SQL pour d\u00e9placer les n\u0153uds d'une g\u00e9om\u00e9trie et les coller sur ceux d'une autre.
Par exemple, coller les g\u00e9om\u00e9tries choisies (via identifiants dans le WHERE) de la table de zonage sur les parcelles choisies (via identifiants dans le WHERE):
WITH a AS (\n SELECT DISTINCT z.id_zone_urba,\n st_force2d(\n ST_Multi(\n ST_Snap(\n ST_Simplify(z.geom, 1),\n ST_Collect(p.geom),\n 0.5\n )\n )\n ) AS geom\n FROM z_formation.parcelle_havre AS p\n INNER JOIN z_formation.zone_urba AS z\n ON st_dwithin(z.geom, p.geom, 0.5)\n WHERE TRUE\n AND z.id_zone_urba IN (113,29)\n AND p.id_parcelle IN (711337,711339,711240,711343)\n GROUP BY z.id_zone_urba\n)\nUPDATE z_formation.zone_urba pz\nSET geom = a.geom\nFROM a\nWHERE pz.id_zone_urba = a.id_zone_urba\n
Attention: Cette fonction ne sait coller qu'aux n\u0153uds de la table de r\u00e9f\u00e9rence, pas aux segments. Il serait n\u00e9anmoins possible de cr\u00e9er automatiquement les n\u0153uds situ\u00e9s sur la projection du n\u0153ud \u00e0 d\u00e9placer sur la g\u00e9om\u00e9trie de r\u00e9f\u00e9rence.
Dans la pratique, il est tr\u00e8s souvent fastidieux de corriger les erreurs de topologie d'une couche. Les outils automatiques ( V\u00e9rifier les g\u00e9om\u00e9tries de QGIS ou outil v.clean de Grass) ne permettent pas toujours de bien voir ce qui a \u00e9t\u00e9 modifi\u00e9.
Au contraire, une modification manuelle est plus pr\u00e9cise, mais prend beaucoup de temps.
Le Minist\u00e8re du D\u00e9veloppement Durable a mis en ligne un document int\u00e9ressant sur les outils disponibles dans QGIS, OpenJump et PostgreSQL pour valider et corriger les g\u00e9om\u00e9tries: http://www.geoinformations.developpement-durable.gouv.fr/verification-et-corrections-des-geometries-a3522.html
"},{"location":"fdw/","title":"Acc\u00e9der \u00e0 des donn\u00e9es externes : les Foreign Data Wrapper (FDW)","text":"L'utilisation d'un FDW permet de consulter des donn\u00e9es externes \u00e0 la base comme si elles \u00e9taient stock\u00e9es dans des tables. On peut lancer des requ\u00eates pour r\u00e9cup\u00e9rer seulement certains champs, filtrer les donn\u00e9es, etc.
Des tables \u00e9trang\u00e8res sont cr\u00e9\u00e9es, qui pointent vers les donn\u00e9es externes. A chaque requ\u00eate sur ces tables, PostgreSQL r\u00e9cup\u00e8re les donn\u00e9es depuis la connexion au serveur externe.
On passe classiquement par les \u00e9tapes suivantes:
postgres_fdw
(bases PostgreSQL externes), ogr_fdw
(donn\u00e9es vectorielles via ogr2ogr), etc.Avec ce Foreign Data Wrapper ogr_fdw, on peut appeler n'importe quelle source de donn\u00e9es externe compatible avec la librairie ogr2ogr et les exploiter comme des tables: fichiers GeoJSON ou Shapefile, GPX, CSV, mais aussi les protocoles comme le WFS.
Voir la documentation officielle de ogr_fdw.
"},{"location":"fdw/#installation","title":"Installation","text":"Pour l'installer sur une machine Linux, il suffit d'installer le paquet correspondant \u00e0 la version de PostgreSQL, par exemple postgresql-11-ogr-fdw
.
Sous Windows, il est disponible avec le paquet PostGIS via l'outil StackBuilder.
"},{"location":"fdw/#exemple-dutilisation-recuperer-des-couches-dun-serveur-wfs","title":"Exemple d'utilisation: r\u00e9cup\u00e9rer des couches d'un serveur WFS","text":"Nous allons utiliser le FDW pour r\u00e9cup\u00e9rer des donn\u00e9es mises \u00e0 disposition sur le serveur de l'INPN via le protocole WFS.
Vous pouvez d'abord tester dans QGIS quelles donn\u00e9es sont disponibles sur ce serveur en cr\u00e9ant une nouvelle connexion WFS avec l'URL http://ws.carmencarto.fr/WFS/119/fxx_inpn?
Via QGIS ou un autre client \u00e0 la base de donn\u00e9es, nous pouvons maintenant montrer comment r\u00e9cup\u00e9rer ces donn\u00e9es:
ogr_fdw
:-- Ajouter l'extension pour lire des fichiers SIG\n-- Cette commande doit \u00eatre lanc\u00e9e par un super utilisateur (ou un utilisateur ayant le droit de le faire)\nCREATE EXTENSION IF NOT EXISTS ogr_fdw;\n
-- Cr\u00e9er le serveur\nDROP SERVER IF EXISTS fdw_ogr_inpn_metropole;\nCREATE SERVER fdw_ogr_inpn_metropole FOREIGN DATA WRAPPER ogr_fdw\nOPTIONS (\n datasource 'WFS:http://ws.carmencarto.fr/WFS/119/fxx_inpn?',\n format 'WFS'\n);\n
-- Cr\u00e9er un sch\u00e9ma pour la dreal\nCREATE SCHEMA IF NOT EXISTS inpn_metropole;\n
IMPORT SCHEMA
:-- R\u00e9cup\u00e9rer l'ensemble des couches WFS comme des tables dans le sch\u00e9ma ref_dreal\nIMPORT FOREIGN SCHEMA ogr_all\nFROM SERVER fdw_ogr_inpn_metropole\nINTO inpn_metropole\nOPTIONS (\n -- mettre le nom des tables en minuscule et sans caract\u00e8res bizarres\n launder_table_names 'true',\n -- mettre le nom des champs en minuscule\n launder_column_names 'true'\n)\n;\n
SELECT foreign_table_schema, foreign_table_name\nFROM information_schema.foreign_tables\nWHERE foreign_table_schema = 'inpn_metropole'\nORDER BY foreign_table_schema, foreign_table_name;\n
ce qui montre:
foreign_table_schema foreign_table_name inpn_metropole arretes_de_protection_de_biotope inpn_metropole arretes_de_protection_de_geotope inpn_metropole bien_du_patrimoine_mondial_de_l_unesco inpn_metropole geoparcs inpn_metropole ospar inpn_metropole parc_naturel_marin inpn_metropole parcs_nationaux inpn_metropole parcs_naturels_regionaux inpn_metropole reserves_biologiques inpn_metropole reserves_de_la_biosphere inpn_metropole reserves_integrales_de_parcs_nationaux inpn_metropole reserves_nationales_de_chasse_et_faune_sauvage inpn_metropole reserves_naturelles_nationales inpn_metropole reserves_naturelles_regionales inpn_metropole rnc inpn_metropole sites_d_importance_communautaire inpn_metropole sites_d_importance_communautaire_joue__zsc_sic_ inpn_metropole sites_ramsar inpn_metropole terrains_des_conservatoires_des_espaces_naturels inpn_metropole terrains_du_conservatoire_du_littoral inpn_metropole zico inpn_metropole znieff1 inpn_metropole znieff1_mer inpn_metropole znieff2 inpn_metropole znieff2_mer inpn_metropole zones_de_protection_speciale-- Tester\nSELECT *\nFROM inpn_metropole.zico\nLIMIT 1;\n
Attention, lorsqu'on acc\u00e8de depuis PostgreSQL \u00e0 un serveur WFS, on est tributaire
Nous d\u00e9conseillons fortement dans ce cas de charger le serveur externe en r\u00e9alisant des requ\u00eates complexes (ou trop fr\u00e9quentes) sur ces tables \u00e9trang\u00e8res, surtout lorsque les donn\u00e9es \u00e9voluent peu.
Au contraire, nous conseillons de cr\u00e9er des vues mat\u00e9rialis\u00e9es \u00e0 partir des tables \u00e9trang\u00e8res pour \u00e9viter des requ\u00eates lourdes en stockant les donn\u00e9es dans la base:
-- Pour \u00e9viter de requ\u00eater \u00e0 chaque fois le WFS, on peut cr\u00e9er des vues mat\u00e9rialis\u00e9es\n\n-- suppression de la vue si elle existe d\u00e9j\u00e0\nDROP MATERIALIZED VIEW IF EXISTS inpn_metropole.vm_zico;\n\n-- cr\u00e9ation de la vue: on doit parfois forcer le type de g\u00e9om\u00e9trie attendue\nCREATE MATERIALIZED VIEW inpn_metropole.vm_zico AS\nSELECT *, \n(ST_multi(msgeometry))::geometry(multipolygon, 2154) AS geom\nFROM inpn_metropole.zico\n;\n\n-- Ajout d'un index spatial sur la g\u00e9om\u00e9trie\nCREATE INDEX ON inpn_metropole.vm_zico USING GIST (geom);\n
Une fois la vue cr\u00e9\u00e9e, vous pouvez faire vos requ\u00eates sur cette vue, avec des performances bien meilleures et un all\u00e8gement de la charge sur le serveur externe.
Pour rafra\u00eechir les donn\u00e9es \u00e0 partir du serveur WFS, il suffit de rafra\u00eechir la ou les vues mat\u00e9rialis\u00e9es:
-- Rafra\u00eechir la vue, par exemple \u00e0 lancer une fois par mois\nREFRESH MATERIALIZED VIEW inpn_metropole.vm_zico;\n
"},{"location":"fdw/#le-fdw-postgres_fdw-pour-acceder-aux-tables-dune-autre-base-de-donnees-postgresql","title":"Le FDW postgres_fdw pour acc\u00e9der aux tables d'une autre base de donn\u00e9es PostgreSQL","text":"-- Cr\u00e9ation du serveur externe\nDROP SERVER IF EXISTS foreign_server_test CASCADE;\nCREATE SERVER IF NOT EXISTS foreign_server_test\nFOREIGN DATA WRAPPER postgres_fdw\nOPTIONS (host 'mon_serveur_postgresql_externe.com', port '5432', dbname 'external_database')\n;\n\n-- on d\u00e9clare se connecter en tant qu'utilisateur `mon_utilisateur_externe` lorsqu'on r\u00e9cup\u00e8re des donn\u00e9es\n-- depuis une connexion avec l'utilisateur interne `mon_utilisateur`\nCREATE USER MAPPING FOR \"mon_utilisateur\"\nSERVER foreign_server_test\nOPTIONS (user 'mon_utilisateur_externe', password '***********');\n\n-- on stocke les tables \u00e9trang\u00e8res dans un sch\u00e9ma sp\u00e9cifique pour isoler des autres sch\u00e9mas en dur\nDROP SCHEMA IF EXISTS fdw_test_schema CASCADE;\nCREATE SCHEMA IF NOT EXISTS fdw_test_schema;\n\n-- importer automatiquement les tables d'un sch\u00e9ma de la base distante\nIMPORT FOREIGN SCHEMA \"un_schema\"\nLIMIT TO (\"une_table\", \"une_autre_table\")\nFROM SERVER foreign_server_test\nINTO fdw_test_schema;\n\n-- Tester\nSELECT * FROM fdw_test_schema.une_table LIMIT 1;\n
Continuer vers Tutoriels en ligne
"},{"location":"filter_data/","title":"Filtrer les donn\u00e9es : la clause WHERE","text":"R\u00e9cup\u00e9rer les donn\u00e9es \u00e0 partir de la valeur exacte d'un champ. Ici le nom de la commune
-- R\u00e9cup\u00e9rer seulement la commune du Havre\nSELECT id_commune, code_insee, nom,\npopulation\nFROM z_formation.commune\nWHERE nom = 'Le Havre'\n
On peut chercher les lignes dont le champ correspondant \u00e0 plusieurs valeurs
-- R\u00e9cup\u00e9rer la commune du Havre et de Rouen\nSELECT id_commune, code_insee, nom,\npopulation\nFROM z_formation.commune\nWHERE nom IN ('Le Havre', 'Rouen')\n
On peut aussi filtrer sur des champs de type entier ou nombres r\u00e9els, et faire des conditions comme des in\u00e9galit\u00e9s.
-- Filtrer les donn\u00e9es, par exemple par d\u00e9partement et population\nSELECT *\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\nAND population > 1000\n;\n
On peut chercher des lignes dont un champ commence et/ou se termine par un texte
-- Filtrer les donn\u00e9es, par exemple par d\u00e9partement et d\u00e9but et/ou fin de nom\nSELECT *\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\n-- commence par C\nAND nom LIKE 'C%'\n-- se termine par ville\nAND nom ILIKE '%ville'\n;\n
On peut utiliser les calculs sur les g\u00e9om\u00e9tries pour filtrer les donn\u00e9es. Par exemple filtrer par longueur de lignes
-- Les routes qui font plus que 10km\n-- on peut utiliser des fonctions dans la clause WHERE\nSELECT id_route, id, geom\nFROM z_formation.route\nWHERE True\nAND ST_Length(geom) > 10000\n
Continuer vers Regrouper des donn\u00e9es: GROUP BY
"},{"location":"filter_data/#quiz","title":"Quiz","text":"\u00c9crire une requ\u00eate retournant toutes les communes de Seine-Maritime qui contiennent la cha\u00eene de caract\u00e8res 'saint'-- Toutes les communes de Seine-Maritime qui contiennent le mot saint\nSELECT *\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\nAND nom ILIKE '%saint%';\n
\u00c9crire une requ\u00eate retournant les nom et centro\u00efde des communes de Seine-Maritime avec une population inf\u00e9rieure ou \u00e9gale \u00e0 50 -- Nom et centro\u00efde des communes de Seine-Maritime avec une population <= 50\nSELECT nom, ST_Centroid(geom) as geom\nFROM z_formation.commune\nWHERE True\nAND depart = 'SEINE-MARITIME'\nAND population <= 50\n
"},{"location":"grant/","title":"Gestion des droits","text":"Dans PostgreSQL, on peut cr\u00e9er des r\u00f4les (des utilisateurs) et g\u00e9rer les droits sur les diff\u00e9rents objets : base, sch\u00e9mas, tables, fonctions, etc.
La documentation officielle de PostgreSQL est compl\u00e8te, et propose plusieurs exemples.
Nous montrons ci-dessous quelques utilisations possibles. Attention, pour pouvoir r\u00e9aliser certaines op\u00e9rations, vous devez :
Cr\u00e9ation d'un sch\u00e9ma de test et d'un r\u00f4le de connexion, en tant qu'utilisateur avec des droits forts sur la base de donn\u00e9es (cr\u00e9ation de sch\u00e9mas, de tables, etc.).
-- cr\u00e9ation d'un sch\u00e9ma de test\nCREATE SCHEMA IF NOT EXISTS nouveau_schema;\n\n-- cr\u00e9ation de tables pour tester\nCREATE TABLE IF NOT EXISTS nouveau_schema.observation (id serial primary key, nom text, geom geometry(point, 2154));\nCREATE TABLE IF NOT EXISTS nouveau_schema.nomenclature (id serial primary key, code text, libelle text);\n
Cr\u00e9ation d'un r\u00f4le de connexion (en tant que super-utilisateur, ou en tant qu'utilisateur ayant le droit de cr\u00e9er des r\u00f4les)
-- cr\u00e9ation d'un r\u00f4le nomm\u00e9 invite\nCREATE ROLE invite WITH PASSWORD 'mot_de_passe_a_changer' LOGIN;\n
On donne le droit de connexion sur la base (nomm\u00e9e ici qgis)
-- on donne le droit de connexion sur la base\nGRANT CONNECT ON DATABASE qgis TO invite;\n
Exemple de requ\u00eates pratiques pour donner ou retirer des droits (en tant qu'utilisateur propri\u00e9taire de la base et des objets)
-- on donne le droit \u00e0 invite d'utiliser les sch\u00e9ma public et nouveau_schema\n-- Utile pour pouvoir lister les tables\n-- Si un r\u00f4le n'a pas le droit USAGE sur un sch\u00e9ma,\n-- il ne peut pas lire les donn\u00e9es des tables\n-- m\u00eame si des droits SELECT on \u00e9t\u00e9 donn\u00e9es sur ces tables\nGRANT USAGE ON SCHEMA public, nouveau_schema TO \"invite\", \"autre_role\";\n\n-- on permet \u00e0 invite de lire les donn\u00e9es (SELECT)\n-- de toutes les tables du sch\u00e9ma nouveau_schema\nGRANT SELECT ON ALL TABLES IN SCHEMA nouveau_schema TO \"invite\", \"autre_role\";\n\n-- On permet l'ajout et la modification de donn\u00e9es sur la table observation seulement\nGRANT INSERT OR UPDATE ON TABLE nouveau_schema.observation TO \"invite\";\n\n-- On peut aussi enlever des droits avec REVOKE.\n-- Cela enl\u00e8ve seulement les droits donn\u00e9s pr\u00e9c\u00e9demment avec GRANT\n-- Ex: On pourrait donner tous les droits sur une table\n-- puis retirer la possibilit\u00e9 de faire des suppressions\nGRANT ALL ON TABLE nouveau_schema.observation TO \"autre_role\";\n-- on retire les droits DELETE et TRUNCATE\nREVOKE DELETE, TRUNCATE ON TABLE nouveau_schema.observation FROM \"autre_role\";\n\n-- On peut aussi par exemple retirer tous les privil\u00e8ges sur les tables du sch\u00e9ma public\nREVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"invite\";\n
"},{"location":"grant/#droits-par-defaut-sur-les-nouveaux-objets-crees-par-un-utilisateur","title":"Droits par d\u00e9faut sur les nouveaux objets cr\u00e9\u00e9s par un utilisateur.","text":"Lorsqu'un utilisateur cr\u00e9e un sch\u00e9ma, une table ou une vue, aucun droit n'est donn\u00e9 sur cet objet aux autres utilisateurs. Par d\u00e9faut les autres utilisateurs ne peuvent donc pas par exemple lire les donn\u00e9es de ce nouvel objet.
PostgreSQL fournit un moyen de d\u00e9finir en quelque sorte: Donner ce(s) droit(s) sur tous ces objets cr\u00e9\u00e9s par cet utilisateur \u00e0 ces autres utilisateurs
Documentation officielle : https://docs.postgresql.fr/current/sql-alterdefaultprivileges.html
-- Donner le droit SELECT pour toutes les nouvelles tables cr\u00e9\u00e9es \u00e0 l'avenir\n-- dans le sch\u00e9ma nouveau_schema\nALTER DEFAULT PRIVILEGES IN SCHEMA \"nouveau_schema\" GRANT SELECT ON TABLES TO \"invite\", \"autre_role\";\n
"},{"location":"grant/#lister-tous-les-droits-donnes-sur-tous-les-objets-de-la-base","title":"Lister tous les droits donn\u00e9s sur tous les objets de la base","text":"Une requ\u00eate SQL peut \u00eatre utilis\u00e9e pour lister tous les droits accord\u00e9s sur plusieurs types d'objets : sch\u00e9ma, tables, fonctions, types, aggr\u00e9gats, etc.
Un exemple de r\u00e9sultat :
object_schema object_type object_name object_owner grantor grantee privileges is_grantable urbanisme schema urbanisme role_sig role_sig role_urba CREATE, USAGE f urbanisme table zone_urba role_sig role_sig role_urba INSERT, SELECT, UPDATE f cadastre schema cadastre role_sig role_sig role_lecteur USAGE f cadastre table commune role_sig role_sig role_lecteur SELECT f cadastre table parcelle role_sig role_sig role_lecteur SELECT fSi un objet n'est pas retourn\u00e9 par cette requ\u00eate, c'est qu'aucun droit sp\u00e9cifique ne lui a \u00e9t\u00e9 accord\u00e9.
Requ\u00eate SQL permettant de r\u00e9cup\u00e9rer les droits accord\u00e9s sur tous les objets de la base, ainsi que les propri\u00e9taires et les r\u00f4les qui ont accord\u00e9 ces privil\u00e8ges-- Adapted from https://dba.stackexchange.com/a/285632\nWITH rol AS (\n SELECT oid,\n rolname::text AS role_name\n FROM pg_roles\n UNION\n SELECT 0::oid AS oid,\n 'public'::text\n),\nschemas AS ( -- Schemas\n SELECT oid AS schema_oid,\n n.nspname::text AS schema_name,\n n.nspowner AS owner_oid,\n 'schema'::text AS object_type,\n coalesce ( n.nspacl, acldefault ( 'n'::\"char\", n.nspowner ) ) AS acl\n FROM pg_catalog.pg_namespace n\n WHERE n.nspname !~ '^pg_'\n AND n.nspname <> 'information_schema'\n),\nclasses AS ( -- Tables, views, etc.\n SELECT schemas.schema_oid,\n schemas.schema_name AS object_schema,\n c.oid,\n c.relname::text AS object_name,\n c.relowner AS owner_oid,\n CASE\n WHEN c.relkind = 'r' THEN 'table'\n WHEN c.relkind = 'v' THEN 'view'\n WHEN c.relkind = 'm' THEN 'materialized view'\n WHEN c.relkind = 'c' THEN 'type'\n WHEN c.relkind = 'i' THEN 'index'\n WHEN c.relkind = 'S' THEN 'sequence'\n WHEN c.relkind = 's' THEN 'special'\n WHEN c.relkind = 't' THEN 'TOAST table'\n WHEN c.relkind = 'f' THEN 'foreign table'\n WHEN c.relkind = 'p' THEN 'partitioned table'\n WHEN c.relkind = 'I' THEN 'partitioned index'\n ELSE c.relkind::text\n END AS object_type,\n CASE\n WHEN c.relkind = 'S' THEN coalesce ( c.relacl, acldefault ( 's'::\"char\", c.relowner ) )\n ELSE coalesce ( c.relacl, acldefault ( 'r'::\"char\", c.relowner ) )\n END AS acl\n FROM pg_class c\n JOIN schemas\n ON ( schemas.schema_oid = c.relnamespace )\n WHERE c.relkind IN ( 'r', 'v', 'm', 'S', 'f', 'p' )\n),\ncols AS ( -- Columns\n SELECT c.object_schema,\n null::integer AS oid,\n c.object_name || '.' || a.attname::text AS object_name,\n 'column' AS object_type,\n c.owner_oid,\n coalesce ( a.attacl, acldefault ( 'c'::\"char\", c.owner_oid ) ) AS acl\n FROM pg_attribute a\n JOIN classes c\n ON ( a.attrelid = c.oid )\n WHERE a.attnum > 0\n AND NOT a.attisdropped\n),\nprocs AS ( -- Procedures and functions\n SELECT schemas.schema_oid,\n schemas.schema_name AS object_schema,\n p.oid,\n p.proname::text AS object_name,\n p.proowner AS owner_oid,\n CASE p.prokind\n WHEN 'a' THEN 'aggregate'\n WHEN 'w' THEN 'window'\n WHEN 'p' THEN 'procedure'\n ELSE 'function'\n END AS object_type,\n pg_catalog.pg_get_function_arguments ( p.oid ) AS calling_arguments,\n coalesce ( p.proacl, acldefault ( 'f'::\"char\", p.proowner ) ) AS acl\n FROM pg_proc p\n JOIN schemas\n ON ( schemas.schema_oid = p.pronamespace )\n),\nudts AS ( -- User defined types\n SELECT schemas.schema_oid,\n schemas.schema_name AS object_schema,\n t.oid,\n t.typname::text AS object_name,\n t.typowner AS owner_oid,\n CASE t.typtype\n WHEN 'b' THEN 'base type'\n WHEN 'c' THEN 'composite type'\n WHEN 'd' THEN 'domain'\n WHEN 'e' THEN 'enum type'\n WHEN 't' THEN 'pseudo-type'\n WHEN 'r' THEN 'range type'\n WHEN 'm' THEN 'multirange'\n ELSE t.typtype::text\n END AS object_type,\n coalesce ( t.typacl, acldefault ( 'T'::\"char\", t.typowner ) ) AS acl\n FROM pg_type t\n JOIN schemas\n ON ( schemas.schema_oid = t.typnamespace )\n WHERE ( t.typrelid = 0\n OR ( SELECT c.relkind = 'c'\n FROM pg_catalog.pg_class c\n WHERE c.oid = t.typrelid ) )\n AND NOT EXISTS (\n SELECT 1\n FROM pg_catalog.pg_type el\n WHERE el.oid = t.typelem\n AND el.typarray = t.oid )\n),\nfdws AS ( -- Foreign data wrappers\n SELECT null::oid AS schema_oid,\n null::text AS object_schema,\n p.oid,\n p.fdwname::text AS object_name,\n p.fdwowner AS owner_oid,\n 'foreign data wrapper' AS object_type,\n coalesce ( p.fdwacl, acldefault ( 'F'::\"char\", p.fdwowner ) ) AS acl\n FROM pg_foreign_data_wrapper p\n),\nfsrvs AS ( -- Foreign servers\n SELECT null::oid AS schema_oid,\n null::text AS object_schema,\n p.oid,\n p.srvname::text AS object_name,\n p.srvowner AS owner_oid,\n 'foreign server' AS object_type,\n coalesce ( p.srvacl, acldefault ( 'S'::\"char\", p.srvowner ) ) AS acl\n FROM pg_foreign_server p\n),\nall_objects AS (\n SELECT schema_name AS object_schema,\n object_type,\n schema_name AS object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM schemas\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM classes\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM cols\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n calling_arguments,\n owner_oid,\n acl\n FROM procs\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM udts\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM fdws\n UNION\n SELECT object_schema,\n object_type,\n object_name,\n null::text AS calling_arguments,\n owner_oid,\n acl\n FROM fsrvs\n),\nacl_base AS (\n SELECT object_schema,\n object_type,\n object_name,\n calling_arguments,\n owner_oid,\n ( aclexplode ( acl ) ).grantor AS grantor_oid,\n ( aclexplode ( acl ) ).grantee AS grantee_oid,\n ( aclexplode ( acl ) ).privilege_type AS privilege_type,\n ( aclexplode ( acl ) ).is_grantable AS is_grantable\n FROM all_objects\n),\nungrouped AS (\n SELECT acl_base.object_schema,\n acl_base.object_type,\n acl_base.object_name,\n --acl_base.calling_arguments,\n owner.role_name AS object_owner,\n grantor.role_name AS grantor,\n grantee.role_name AS grantee,\n acl_base.privilege_type,\n acl_base.is_grantable\n FROM acl_base\n JOIN rol owner\n ON ( owner.oid = acl_base.owner_oid )\n JOIN rol grantor\n ON ( grantor.oid = acl_base.grantor_oid )\n JOIN rol grantee\n ON ( grantee.oid = acl_base.grantee_oid )\n WHERE acl_base.grantor_oid <> acl_base.grantee_oid\n)\nSELECT\n object_schema, object_type, object_name, object_owner,\n grantor, grantee,\n -- The same function name can be used many times\n -- Since we do not include the calling_arguments field, we should add a DISTINCT below\n string_agg(DISTINCT privilege_type, ' - ' ORDER BY privilege_type) AS privileges,\n is_grantable\nFROM ungrouped\nWHERE True\n-- Simplify objects returned\n-- You can comment the following line to get these types too\nAND object_type NOT IN ('function', 'window', 'aggregate', 'base type', 'composite type')\n-- You can also filter for specific schemas or object names by uncommenting and adapting the following lines\n-- AND object_schema IN ('cadastre', 'environment')\n-- AND object_type = 'table'\n-- AND object_name ILIKE '%parcelle%'\nGROUP BY object_schema, object_type, object_name, object_owner, grantor, grantee, is_grantable\nORDER BY object_schema, object_type, grantor, grantee, object_name\n;\n
Continuer vers Acc\u00e9der \u00e0 des donn\u00e9es externes: Foreign Data Wrapper
"},{"location":"group_data/","title":"Grouper des donn\u00e9es et calculer des statistiques","text":"Les fonctions d'agr\u00e9gat dans PostgreSQL
"},{"location":"group_data/#valeurs-distinctes-dun-champ","title":"Valeurs distinctes d'un champ","text":"On souhaite r\u00e9cup\u00e9rer toutes les valeurs possibles d'un champ
-- V\u00e9rifier les valeurs distinctes d'un champ: table commune\nSELECT DISTINCT depart\nFROM z_formation.commune\nORDER BY depart\n\n-- idem sur la table lieu_dit_habite\nSELECT DISTINCT nature\nFROM z_formation.lieu_dit_habite\nORDER BY nature\n
"},{"location":"group_data/#regrouper-des-donnees-en-specifiant-les-champs-de-regroupement","title":"Regrouper des donn\u00e9es en sp\u00e9cifiant les champs de regroupement","text":"Certains calculs n\u00e9cessitent le regroupement de lignes, comme les moyennes, les sommes ou les totaux. Pour cela, il faut r\u00e9aliser un regroupement via la clause GROUP BY
Compter les communes par d\u00e9partement et calculer la population totale
-- Regrouper des donn\u00e9es\n-- Compter le nombre de communes par d\u00e9partement\nSELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nWHERE True\nGROUP BY depart\nORDER BY nb_commune DESC\n
Calculer des statistiques sur l'aire des communes pour chaque d\u00e9partement
SELECT depart,\ncount(id_commune) AS nb,\nmin(ST_Area(geom)/10000)::int AS min_aire_ha,\nmax(ST_Area(geom)/10000)::int AS max_aire_ha,\navg(ST_Area(geom)/10000)::int AS moy_aire_ha,\nsum(ST_Area(geom)/10000)::int AS total_aire_ha\nFROM z_formation.commune\nGROUP BY depart\n
Compter le nombre de routes par nature
-- Compter le nombre de routes par nature\nSELECT count(id_route) AS nb_route, nature\nFROM z_formation.route\nWHERE True\nGROUP BY nature\nORDER BY nb_route DESC\n
Compter le nombre de routes par nature et par sens
SELECT count(id_route) AS nb_route, nature, sens\nFROM z_formation.route\nWHERE True\nGROUP BY nature, sens\nORDER BY nature, sens DESC\n
Les caculs sur des ensembles group\u00e9s peuvent aussi \u00eatre r\u00e9alis\u00e9 sur les g\u00e9om\u00e9tries.. Les plus utilis\u00e9s sont
ST_Collect
qui regroupe les g\u00e9om\u00e9tries dans une multi-g\u00e9om\u00e9trie,ST_Union
qui fusionne les g\u00e9om\u00e9tries.Par exemple, on peut souhaiter trouver l'enveloppe convexe autour de points (\u00e9lastique tendu autour d'un groupe de points). Ici, nous regroupons les lieux-dits par nature (ce qui n'a pas beaucoup de sens, mais c'est pour l'exemple). Dans ce cas, il faut faire une sous-requ\u00eate pour filtrer seulement les r\u00e9sultats de type polygone (car s'il y a seulement 1 ou 2 objets par nature, alors on ne peut cr\u00e9er de polygone)
SELECT *\nFROM (\n SELECT\n nature,\n -- ST_Convexhull renvoie l'enveloppe convexe\n ST_Convexhull(ST_Collect(geom)) AS geom\n FROM z_formation.lieu_dit_habite\n GROUP BY nature\n) AS source\n-- GeometryType renvoie le type de g\u00e9om\u00e9trie\nWHERE Geometrytype(geom) = 'POLYGON'\n
Attention, on doit donner un alias \u00e0 la sous-requ\u00eate (ici source
)
Un autre exemple sur les bornes. Ici, on groupe les bornes par identifiant pair ou impair, et on calcule l'enveloppe convexe
SELECT count(id_borne), ((id_borne % 2) = 0) AS pair,\n(st_convexhull(ST_Collect(geom))) AS geom\nFROM z_formation.borne_incendie\nGROUP BY pair\n
On peut r\u00e9aliser l'\u00e9quivalent d'un DISSOLVE
de QGIS en regroupant les g\u00e9om\u00e9tries via ST_Union
. Par exemple fusionner l'ensemble des communes pour construire les g\u00e9om\u00e9tries des d\u00e9partements:
SELECT\ndepart,\ncount(id_commune) AS nb_com,\n-- ST_Union cr\u00e9e une seule g\u00e9om\u00e9trie en fusionnant les g\u00e9om\u00e9tries.\nST_Union(geom) AS geom\n\nFROM z_formation.commune\n\nGROUP BY depart\n
Attention, cette requ\u00eate est lourde, et devra \u00eatre enregistr\u00e9e comme une table.
"},{"location":"group_data/#filtrer-sur-les-regroupements","title":"Filtrer sur les regroupements","text":"Si on souhaite compter les communes par d\u00e9partement, calculer la population totale et aussi filter celles qui ont plus de 500 000 habitants, il peut para\u00eetre logique d'\u00e9crire cette requ\u00eate :
SELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nGROUP BY depart\nWHERE sum(population) > 500000\nORDER BY nb_commune DESC\n
ou bien encore :
SELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nGROUP BY depart\nWHERE total_population > 500000\nORDER BY nb_commune DESC\n
Ces deux requ\u00eates renvoient une erreur. La bonne requ\u00eate est :
SELECT depart,\ncount(code_insee) AS nb_commune,\nsum(population) AS total_population\nFROM z_formation.commune\nGROUP BY depart\nHAVING sum(population) > 500000\nORDER BY nb_commune DESC\n
Il faut savoir que la clause WHERE
est ex\u00e9cut\u00e9e avant la clause GROUP BY
, il n'est donc pas possible de filtrer sur des regroupements avec celle-ci. C'est le r\u00f4le de la clause HAVING
.
Aussi la clause SELECT
est ex\u00e9cut\u00e9e apr\u00e8s les clauses WHERE
et HAVING
, il n'est donc pas possible d'utiliser des alias d\u00e9clar\u00e9s avec celle-ci.
Un sch\u00e9ma illustrant ceci est disponible sur le site postgresqltutorial.com.
Continuer vers Rassembler des donn\u00e9es: UNION ALL
"},{"location":"group_data/#quiz","title":"Quiz","text":"\u00c9crire une requ\u00eate retournant, pour le/les d\u00e9partement(s) dont la population moyenne des villes est sup\u00e9rieure ou \u00e9gale \u00e0 1500 habitants, le nom du/des d\u00e9partement(s) ainsi que cette moyenne.SELECT depart,\navg(population) AS moyenne_population\nFROM z_formation.commune\nGROUP BY depart\nHAVING avg(population) >= 1500\n
\u00c9crire une requ\u00eate retournant pour les d\u00e9partements 'SEINE-MARITIME' et 'EURE', leur nom, le nombre de communes ainsi que la surface et la surface de l'enveloppe convexe en m\u00e8tre carr\u00e9 sous forme d'entier. SELECT depart,\ncount(id_commune) AS nb_commune,\nST_Area(ST_Collect(geom))::int8 AS surface,\nST_Area(ST_Convexhull(ST_Collect(geom)))::int8 AS surface_enveloppe_convexe\nFROM z_formation.commune\nWHERE depart IN ('SEINE-MARITIME', 'EURE')\nGROUP BY depart\n
"},{"location":"import_data/","title":"Importer des donn\u00e9es","text":"Pour la formation, on doit importer des donn\u00e9es pour pouvoir travailler.
"},{"location":"import_data/#import-dune-couche-depuis-qgis","title":"Import d'une couche depuis QGIS","text":"On doit charger au pr\u00e9alable la couche source dans QGIS (SHP, TAB, etc.), puis on doit v\u00e9rifier :
EPSG:2154
UTF-8
, ISO-8859-15
, etc. Il faut ouvrir la table attributaire, et v\u00e9rifier si les accents sont bien affich\u00e9s. Sinon choisir le bon encodage dans l'onglet G\u00e9n\u00e9ral des propri\u00e9t\u00e9s de la couchePour importer, il existe plusieurs mani\u00e8res dans QGIS. La plus performante pour des gros volumes de donn\u00e9es est l'utilisation de l'algorithme de la bo\u00eete \u00e0 outils
du menu Traitement
appel\u00e9 Exporter vers PostgreSQL (Connexions disponibles
.
Pour trouver cet algorithme, chercher PosgreSQL
dans le champ du haut, et lancer l'algorithme Exporter vers PostgreSQL (connexions disponibles) de GDAL. Il faut choisir les options suivantes :
z_formation
commune
id
dans le champ Clef primaire si aucun champ entier auto-incr\u00e9ment\u00e9 existe, ou choisir le champ appropri\u00e9Apr\u00e8s l'import, on peut charger la table comme une couche via l'explorateur de QGIS :
Rafra\u00eechir
Il est possible d'utiliser l'outil Importer un vecteur vers une base de donn\u00e9es PostGIS (connexions disponibles) par lot. Pour cela, une fois la bo\u00eete de dialogue de cet algorithme ouverte, cliquer sur le bouton Ex\u00e9cuter comme processus de lot. Cela affiche un tableau, ou chaque ligne repr\u00e9sente les variables d'entr\u00e9e d'un algorithme.
Vous pouvez cr\u00e9er manuellement chaque ligne, ou choisir directement les couches depuis votre projet QGIS. Voir la documentation QGIS pour plus de d\u00e9tail: https://docs.qgis.org/latest/fr/docs/user_manual/processing/batch.html
Continuer vers S\u00e9lectionner des donn\u00e9es : SELECT
"},{"location":"join_data/","title":"Les jointures","text":"Les jointures permettent de r\u00e9cup\u00e9rer des donn\u00e9es en relation les unes par rapport aux autres.
"},{"location":"join_data/#les-jointures-attributaires","title":"Les jointures attributaires","text":"La condition de jointure est faite sur des champs non g\u00e9om\u00e9triques. Par exemple une \u00e9galit\u00e9 (code, identifiant).
"},{"location":"join_data/#exemple-1-parcelles-et-communes","title":"Exemple 1: parcelles et communes","text":"R\u00e9cup\u00e9ration des informations de la commune pour un ensemble de parcelles
-- Jointure attributaire: r\u00e9cup\u00e9ration du nom de la commune pour un ensemble de parcelles\nSELECT c.nom, p.*\nFROM z_formation.parcelle as p\nJOIN z_formation.commune as c\nON p.commune = c.code_insee\nLIMIT 100\n-- IMPORTANT: ne pas oublier le ON cad le crit\u00e8re de jointure,\n-- sous peine de \"produit cart\u00e9sien\" (calcul co\u00fbteux de tous les possibles)\n;\n
Il est souvent int\u00e9ressant, pour des donn\u00e9es volumineuses, de cr\u00e9er un index sur le champ de jointure (par exemple ici sur les champs commune
et code_insee
.
-- cr\u00e9ation\nCREATE TABLE z_formation.observation (\n id serial NOT NULL PRIMARY KEY,\n date date DEFAULT (now())::date NOT NULL,\n description text,\n geom public.geometry(Point,2154),\n code_insee character varying(5)\n);\nCREATE INDEX sidx_observation_geom ON z_formation.observation USING gist (geom);\n\n-- on y met des donn\u00e9es\nINSERT INTO z_formation.observation VALUES (1, '2020-07-08', 'un', '01010000206A080000D636D95AFB832141279BD2C8FEA65A41', '76618');\nINSERT INTO z_formation.observation VALUES (2, '2020-07-08', 'deux', '01010000206A08000010248E173E37224156920AEA21525A41', '27213');\nINSERT INTO z_formation.observation VALUES (3, '2020-07-08', 'trois', '01010000206A08000018BF3048EA112341183933F6CC885A41', NULL);\n
On fait une jointure attributaire entre les points des observations et les communes
SELECT\n -- tous les champs de la table observation\n o.*,\n -- le nom de la commune\n c.nom,\n -- l'aire enti\u00e8re en hectares\n ST_area(c.geom)::integer/10000 AS surface_commune\nFROM z_formation.observation AS o\nJOIN z_formation.commune AS c ON o.code_insee = c.code_insee\nWHERE True\n
R\u00e9sultat:
id date description geom code_insee nom surface_commune 2 2020-07-08 deux .... 27213 Vexin-sur-Epte 11434 1 2020-07-08 un .... 76618 Petit-Caux 9243On ne r\u00e9cup\u00e8re ici que 2 lignes alors qu'il y a bien 3 observations dans la table.
Pour r\u00e9cup\u00e9rer les 3 lignes, on doit faire une jointure LEFT
. On peut utiliser un CASE WHEN
pour tester si la commune est trouv\u00e9e sous chaque point
SELECT\n o.*, c.nom, ST_area(c.geom)::integer/10000 AS surface_commune,\n CASE\n WHEN c.code_insee IS NULL THEN 'pas de commune'\n ELSE 'ok'\n END AS test_commune\nFROM z_formation.observation AS o\nLEFT JOIN z_formation.commune AS c ON o.code_insee = c.code_insee\nWHERE True\n
R\u00e9sultat
id date description geom code_insee nom surface_commune test_commune 2 2020-07-08 deux .... 27213 Vexin-sur-Epte 11434 ok 1 2020-07-08 un .... 76618 Petit-Caux 9243 ok 3 2020-07-08 trois .... Null Null Null pas de commune"},{"location":"join_data/#les-jointures-spatiales","title":"Les jointures spatiales","text":"Le crit\u00e8re de jointure peut \u00eatre une condition spatiale. On r\u00e9alise souvent une jointure par intersection ou par proximit\u00e9.
"},{"location":"join_data/#joindre-des-points-avec-des-polygones","title":"Joindre des points avec des polygones","text":"Un exemple classique de r\u00e9cup\u00e9ration des donn\u00e9es de la table commune (nom, etc.) depuis une table de points.
-- Pour chaque lieu-dit, on veut le nom de la commune\nSELECT\nl.id_lieu_dit_habite, l.nom,\nc.nom AS nom_commune, c.code_insee,\nl.geom\nFROM \"z_formation\".lieu_dit_habite AS l\nJOIN \"z_formation\".commune AS c\n ON st_intersects(c.geom, l.geom)\nORDER BY l.nom\n
id_lieu_dit_habite nom nom_commune code_insee geom 58 Abbaye du Valasse Gruchet-le-Valasse 76329 .... 1024 Ablemont Bacqueville-en-Caux 76051 .... 1043 Agranville Douvrend 76220 .... 1377 All des Artisans Mesnils-sur-Iton 27198 .... 1801 All\u00e9e des Maronniers Heudebouville 27332 .... 1293 Alliquerville Trouville 76715 .... 507 Alventot Sainte-H\u00e9l\u00e8ne-Bondeville 76587 .... 555 Alvinbuc Veauville-l\u00e8s-Baons 76729 .... 69 Ancien h\u00f4tel de ville Rouen 76540 .... On peut facilement inverser la table principale pour afficher les lignes ordonn\u00e9es par commune.
SELECT\nc.nom, c.code_insee,\nl.id_lieu_dit_habite, l.nom\nFROM \"z_formation\".commune AS c\nJOIN \"z_formation\".lieu_dit_habite AS l\n ON st_intersects(c.geom, l.geom)\nORDER BY c.nom\n
nom code_insee id_lieu_dit_habite nom Aclou 27001 107 Manoir de la Haule Acquigny 27003 106 Manoir de Becdal Ailly 27005 596 Quaizes Ailly 27005 595 Ingremare Ailly 27005 594 Gruchet Alizay 27008 667 Le Solitaire Ambenay 27009 204 Les Siaules Ambenay 27009 201 Les Renardieres Ambenay 27009 202 Le Culoron On a plusieurs lignes par commune, autant que de lieux-dits pour cette commune. Par contre, comme ce n'est pas une jointure LEFT
, on ne trouve que des r\u00e9sultats pour les communes qui ont des lieux-dits.
On pourrait aussi faire des statistiques, en regroupant par les champs de la table principale, ici les communes.
SELECT\nc.nom, c.code_insee,\ncount(l.id_lieu_dit_habite) AS nb_lieu_dit,\nc.geom\nFROM \"z_formation\".commune AS c\nJOIN \"z_formation\".lieu_dit_habite AS l\n ON st_intersects(c.geom, l.geom)\nGROUP BY c.nom, c.code_insee, c.geom\nORDER BY nb_lieu_dit DESC\nLIMIT 10\n
nom code_insee nb_lieu_dit geom Heudebouville 27332 61 .... Mesnils-sur-Iton 27198 52 .... Rouen 76540 20 .... Saint-Sa\u00ebns 76648 19 .... Les Grandes-Ventes 76321 19 .... Mesnil-en-Ouche 27049 18 .... Quincampoix 76517 18 ...."},{"location":"join_data/#joindre-des-lignes-avec-des-polygones","title":"Joindre des lignes avec des polygones","text":"R\u00e9cup\u00e9rer le code commune de chaque chemin, par intersection entre le chemin et la commune.
"},{"location":"join_data/#jointure-spatiale-simple-entre-les-geometries-brutes","title":"Jointure spatiale simple entre les g\u00e9om\u00e9tries brutes","text":"-- Ici, on peut r\u00e9cup\u00e9rer plusieurs fois le m\u00eame chemin\n-- s'il passe par plusieurs communes\nSELECT\nv.*,\nc.nom, c.code_insee\nFROM \"z_formation\".chemin AS v\nJOIN \"z_formation\".commune AS c\n ON ST_Intersects(v.geom, c.geom)\nORDER BY id_chemin, nom\n
Cela peut renvoyer plusieurs lignes par chemin, car chaque chemin peut passer par plusieurs communes.
"},{"location":"join_data/#jointure-spatiale-entre-le-centroide-des-chemins-et-la-geometrie-des-communes","title":"Jointure spatiale entre le centro\u00efde des chemins et la g\u00e9om\u00e9trie des communes","text":"On peut utiliser le centro\u00efde de chaque chemin pour avoir un seul objet par chemin comme r\u00e9sultat.
-- cr\u00e9ation de l'index\nCREATE INDEX ON z_formation.chemin USING gist (ST_Centroid(geom));\n-- Jointure spatiale\n-- On ne veut qu'une seule ligne par chemin\n-- Donc on fait l'intersection entre le centro\u00efde des chemins (pour avoir un point) et les communes\nSELECT\nv.*,\nc.nom, c.code_insee\nFROM \"z_formation\".chemin AS v\nJOIN \"z_formation\".commune AS c\n ON ST_Intersects(ST_Centroid(v.geom), c.geom)\n
NB: Attention, dans ce cas, l'index spatial sur la g\u00e9om\u00e9trie des chemins n'est pas utilis\u00e9. C'est pour cela que nous avons cr\u00e9\u00e9 un index spatial sur ST_Centroid(geom)
pour la table des chemins.
A l'inverse, on peut vouloir faire des statistiques pour chaque commune via jointure spatiale. Par exemple le nombre de chemins et le total des longueurs par commune.
-- A l'inverse, on veut r\u00e9cup\u00e9rer des statistiques par commune\n -- On veut une ligne par commune, avec des donn\u00e9es sur les voies\nSELECT\nc.id_commune, c.nom, c.code_insee,\ncount(v.id_chemin) AS nb_chemin,\nsum(st_length(v.geom)) AS somme_longueur_chemins_entiers\nFROM z_formation.commune AS c\nJOIN z_formation.chemin AS v\n ON st_intersects(c.geom, st_centroid(v.geom))\nGROUP BY c.id_commune, c.nom, c.code_insee\n;\n
"},{"location":"join_data/#utilisation-dune-jointure-left-pour-garder-les-communes-sans-chemins","title":"Utilisation d'une jointure LEFT pour garder les communes sans chemins","text":"La requ\u00eate pr\u00e9c\u00e9dente ne renvoie pas de lignes pour les communes qui n'ont pas de chemin dont le centro\u00efde est dans une commune. C'est une jointure de type INNER JOIN
Si on veut quand m\u00eame r\u00e9cup\u00e9rer ces communes, on fait une jointure LEFT JOIN
: pour les lignes sans chemins, les champs li\u00e9s \u00e0 la table des chemins seront mis \u00e0 NULL
.
SELECT\nc.id_commune, c.nom, c.code_insee,\ncount(v.id_chemin) AS nb_chemin,\nsum(st_length(v.geom)) AS somme_longueur_chemins_entiers\nFROM z_formation.commune AS c\nLEFT JOIN z_formation.chemin AS v\n ON st_intersects(c.geom, st_centroid(v.geom))\nGROUP BY c.id_commune, c.nom, c.code_insee\n;\n
C'est beaucoup plus long, car la requ\u00eate n'utilise pas d'abord l'intersection, donc l'index spatial des communes, mais fait un parcours de toutes les lignes des communes, puis un calcul d'intersection. Pour acc\u00e9l\u00e9rer la requ\u00eate, on doit cr\u00e9er l'index sur les centro\u00efdes des chemins
CREATE INDEX ON z_formation.chemin USING GIST(ST_Centroid(geom))\n
puis la relancer. Dans cet exemple, on passe de 100 secondes \u00e0 1 seconde, gr\u00e2ce \u00e0 ce nouvel index spatial.
"},{"location":"join_data/#affiner-le-resultat-en-decoupant-les-chemins","title":"Affiner le r\u00e9sultat en d\u00e9coupant les chemins","text":"Dans la requ\u00eate pr\u00e9c\u00e9dente, on calculait la longueur totale de chaque chemin, pas le morceau exacte qui est sur chaque commune. Pour cela, on va utiliser la fonction ST_Intersection
. La requ\u00eate va \u00eatre plus co\u00fbteuse, car il faut r\u00e9aliser le d\u00e9coupage des lignes des chemins par les polygones des communes.
On va d\u00e9couper exactement les chemins par commune et r\u00e9cup\u00e9rer les informations
CREATE TABLE z_formation.decoupe_chemin_par_commune AS\n-- D\u00e9couper les chemins par commune\nSELECT\n-- id unique\n-- infos du chemin\nl.id AS id_chemin,\n-- infos de la commune\nc.nom, c.code_insee,\nST_Multi(st_collectionextract(ST_Intersection(c.geom, l.geom), 2))::geometry(multilinestring, 2154) AS geom\nFROM \"z_formation\".commune AS c\nJOIN \"z_formation\".chemin AS l\n ON st_intersects(c.geom, l.geom)\n;\nCREATE INDEX ON z_formation.decoupe_chemin_par_commune USING GIST (geom);\n
NB: Attention \u00e0 ne pas confondre ST_Intersects
qui renvoie vrai ou faux, et ST_Intersection
qui renvoie la g\u00e9om\u00e9trie issue du d\u00e9coupage d'une g\u00e9om\u00e9trie par une autre.
On peut bien s\u00fbr r\u00e9aliser des jointures spatiales entre 2 couches de polygones, et d\u00e9couper les polygones par intersection. Attention, les performances sont forc\u00e9ment moins bonnes qu'avec des points.
Trouver l'ensemble des zonages PLU pour les parcelles du Havre.
On va r\u00e9cup\u00e9rer plusieurs r\u00e9sultats pour chaque parcelle si plusieurs zonages chevauchent une parcelle.
-- Jointure spatiale\nSELECT\np.id_parcelle,\nz.libelle, z.libelong, z.typezone\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.zone_urba AS z\n ON st_intersects(z.geom, p.geom)\nWHERE True\n
Compter pour chaque parcelle le nombre de zonages en intersection: on veut une seule ligne par parcelle.
SELECT\np.id_parcelle,\ncount(z.libelle) AS nombre_zonage\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.zone_urba AS z\n ON st_intersects(z.geom, p.geom)\nWHERE True\nGROUP BY p.id_parcelle\nORDER BY nombre_zonage DESC\n
D\u00e9couper les parcelles par les zonages, et pouvoir calculer les surfaces des zonages, et le pourcentage par rapport \u00e0 la surface de chaque parcelle. On essaye le SQL suivant:
SELECT\np.id_parcelle,\nz.libelle, z.libelong, z.typezone,\n-- d\u00e9couper les g\u00e9om\u00e9tries\nst_intersection(z.geom, p.geom) AS geom\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.zone_urba AS z\n ON st_intersects(z.geom, p.geom)\nWHERE True\nORDER BY p.id_parcelle\n
Il renvoie l'erreur
ERREUR: Error performing intersection: TopologyException: Input geom 1 is invalid: Self-intersection at or near point 492016.26000489673 6938870.663846286 at 492016.26000489673 6938870.663846286\n
On a ici des soucis de validit\u00e9 de g\u00e9om\u00e9trie. Il nous faut donc corriger les g\u00e9om\u00e9tries avant de poursuivre. Voir chapitre sur la validation des g\u00e9om\u00e9tries.
Une fois les g\u00e9om\u00e9tries valid\u00e9es, la requ\u00eate fonctionne. On l'utilise dans une sous-requ\u00eate pour cr\u00e9er une table et calculer les surfaces
-- suppression de la table\nDROP TABLE IF EXISTS z_formation.decoupe_zonage_parcelle;\n-- cr\u00e9ation de la table avec calcul de pourcentage de surface\nCREATE TABLE z_formation.decoupe_zonage_parcelle AS\nSELECT row_number() OVER() AS id,\nsource.*,\nST_Area(geom) AS aire,\n100 * ST_Area(geom) / aire_parcelle AS pourcentage\nFROM (\nSELECT\n p.id_parcelle, p.id AS idpar, ST_Area(p.geom) AS aire_parcelle,\n z.id_zone_urba, z.libelle, z.libelong, z.typezone,\n -- d\u00e9couper les g\u00e9om\u00e9tries\n (ST_Multi(st_intersection(z.geom, p.geom)))::geometry(MultiPolygon,2154) AS geom\n FROM z_formation.parcelle_havre AS p\n JOIN z_formation.zone_urba AS z ON st_intersects(z.geom, p.geom)\n WHERE True\n) AS source;\n\n-- Ajout de la cl\u00e9 primaire\nALTER TABLE z_formation.decoupe_zonage_parcelle ADD PRIMARY KEY (id);\n\n-- Ajout de l'index spatial\nCREATE INDEX ON z_formation.decoupe_zonage_parcelle USING GIST (geom);\n
"},{"location":"join_data/#faire-un-rapport-des-surfaces-intersectees-de-zonages-sur-une-table-principale","title":"Faire un rapport des surfaces intersect\u00e9es de zonages sur une table principale","text":"Par exemple, pour chacune des communes, on souhaite calculer la somme des surfaces intersect\u00e9e par chaque type de zone (parcs, znieff, etc.).
Afin d'avoir \u00e0 disposition des donn\u00e9es de test pour cet exemple de rapport, nous allons cr\u00e9er 2 tables z_formation.parc_national
et z_formation.znieff
, et y ins\u00e9rer des fausses donn\u00e9es:
-- Table des parcs nationaux\nCREATE TABLE IF NOT EXISTS z_formation.parc_national (\n id serial primary key,\n nom text,\n geom geometry(multipolygon, 2154)\n);\nCREATE INDEX ON z_formation.parc_national USING GIST (geom);\n\n-- Table des znieff\nCREATE TABLE IF NOT EXISTS z_formation.znieff(\n id serial primary key,\n nom_znieff text,\n geom geometry(multipolygon, 2154)\n);\nCREATE INDEX ON z_formation.znieff USING GIST (geom);\n
On ins\u00e8re des polygones dans ces deux tables:
-- donn\u00e9es de test\n-- parcs\nINSERT INTO z_formation.parc_national VALUES (1, 'un', '01060000206A0800000100000001030000000100000008000000C3F7DE73553D20411B3DC1FB0C625A410531F757E93D2041BAECB21FA85E5A41F35B09978081204195F05B9787595A41D61E4865A1A7204147BC8A3AC0605A41ED76A806317F2041A79F7E4876605A41B80752433C832041037846623A655A41E10ED595BA6120413CC1D1C18C685A41C3F7DE73553D20411B3DC1FB0C625A41');\nINSERT INTO z_formation.parc_national VALUES (2, 'deux', '01060000206A080000010000000103000000010000000900000024D68B4AE0412141AAAAAA3C685B5A4130642ACBD01421413A85AE4B72585A41CA08F0240E382141746C4BD107535A41FA30F7A78A4A2141524A29E544555A414796BF5CE63621414DD2E222A4565A416B92160F9B5D2141302807F981575A4130DC700B2E782141DC0ED50B6B5C5A4106FBB8C8294F214150AC17BF015E5A4124D68B4AE0412141AAAAAA3C685B5A41');\nINSERT INTO z_formation.parc_national VALUES (3, 'trois', '01060000206A0800000100000001030000000100000006000000918DCFE7E0861F4137AB79AF14515A411AE56040588A1F41642A43EEC74F5A41DF2EBB3CEBA41F418C31C66ADA4F5A4168864C9562A81F416E87EA40B8505A415CBC8A74C3A31F410FA4F63202515A41918DCFE7E0861F4137AB79AF14515A41');\nINSERT INTO z_formation.parc_national VALUES (4, 'quatre', '01060000206A080000010000000103000000010000000500000004474FE81DBA2041269A684EFD625A41AB17C51223C9204120B507BEAD605A4116329539BBF22041A3273886D5615A416F611F0FB6E32041FA1A9F0F4A645A4104474FE81DBA2041269A684EFD625A41');\nINSERT INTO z_formation.parc_national VALUES (5, 'cinq', '01060000206A0800000100000001030000000100000005000000F2E3C256231E2041E0ACE631AE535A41F7C823E772202041E89C73B6EF505A41B048BCC266362041DAC785A15E515A419E999911782F204180C9F223F8535A41F2E3C256231E2041E0ACE631AE535A41');\nSELECT pg_catalog.setval('z_formation.parc_national_id_seq', 5, true);\n\n-- znieff\nINSERT INTO z_formation.znieff VALUES (1, 'uno', '01060000206A08000001000000010300000001000000050000004039188C39D12041770A5DF74A4A5A413A54B7FBE9CE20410C5DA7C8F5455A41811042C0A4EA204130ECE38267475A416F611F0FB6E320417125FC66FB475A414039188C39D12041770A5DF74A4A5A41');\nINSERT INTO z_formation.znieff VALUES (2, 'dos', '01060000206A080000010000000103000000010000000500000076BEC6DF62492141513FFDF0525A5A417CA32770B24B21411EDBD22150595A419437ABB1F05421410F06E50CBF595A419437ABB1F0542141B022F1FE085A5A4176BEC6DF62492141513FFDF0525A5A41');\nINSERT INTO z_formation.znieff VALUES (3, 'tres', '01060000206A0800000100000001030000000100000005000000A6E6CD62DF5B2141B607528F585C5A41ACCB2EF32E5E2141C5DC3FA4E95B5A414CB7438DE46A2141C5DC3FA4E95B5A41B895F013CE62214189888850A55D5A41A6E6CD62DF5B2141B607528F585C5A41');\nINSERT INTO z_formation.znieff VALUES (4, 'quatro', '01060000206A0800000100000001030000000100000005000000CE857DF445102041985D7665365D5A41DA4F3F15E5142041339521C7305B5A41C2F7DE73553D2041927815D5E65A5A410393E50712252041B607528F585C5A41CE857DF445102041985D7665365D5A41');\nINSERT INTO z_formation.znieff VALUES (5, 'cinco', '01060000206A080000010000000103000000010000000500000045A632DC2B702041FD25CB033C5F5A41CEFDC334A373204115EB459D0E5C5A41F25B099780812041397A8257805D5A415755558D1A7720419E42D7F5855F5A4145A632DC2B702041FD25CB033C5F5A41');\nSELECT pg_catalog.setval('z_formation.znieff_id_seq', 5, true);\n
Pour chaque commune, on souhaite calculer la somme des surfaces intersect\u00e9es par chaque type de zone. On doit donc utiliser toutes les tables de zonage (ici seulement 2 tables, mais c'est possible d'en ajouter)
R\u00e9sultat attendu:
id_commune code_insee nom surface_commune_ha somme_surface_parcs somme_surface_znieff 1139 27042 Barville 275.138028733401 87.2237204013011 None 410 27057 Bernienville 779.74546553394 None 5.26504189468878 1193 27061 Berthouville 757.19696570046 19.9975421896336 None 495 27074 Boisney 576.995877227961 0.107059260396721 None 432 27077 Boissey-le-Ch\u00e2tel 438.373848703835 434.510197417769 83.9289621127432SELECT\n c.id_commune, c.code_insee, c.nom,\n ST_Area(c.geom) / 10000 AS surface_commune_ha,\n (SELECT sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) FROM z_formation.parc_national AS p WHERE ST_Intersects(p.geom, c.geom) ) AS surface_parc_national,\n (SELECT sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) FROM z_formation.znieff AS p WHERE ST_Intersects(p.geom, c.geom) ) AS surface_znieff\nFROM z_formation.commune AS c\nORDER BY c.nom\n
LEFT
SELECT\n -- champs choisis dans la table commune\n c.id_commune, c.code_insee, c.nom,\n -- surface en ha\n ST_Area(c.geom) / 10000 AS surface_commune_ha,\n -- somme des d\u00e9coupages des parcs par commune\n sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) AS somme_surface_parcs,\n -- somme des d\u00e9coupages des znieff par commune\n sum(ST_Area(ST_Intersection(c.geom, z.geom)) / 10000 ) AS somme_surface_znieff\n\nFROM z_formation.commune AS c\n-- jointure spatiale sur les parcs\nLEFT JOIN z_formation.parc_national AS p\n ON ST_Intersects(c.geom, p.geom)\n-- jointure spatiale sur les znieff\nLEFT JOIN z_formation.znieff AS z\n ON ST_Intersects(c.geom, z.geom)\n\n-- clause WHERE optionelle\n-- WHERE p.id IS NOT NULL OR z.id IS NOT NULL\n\n-- on regroupe sur les champs des communes\nGROUP BY c.id_commune, c.code_insee, c.nom, c.geom\n\n-- on ordonne par nom\nORDER BY c.nom\n
Avantages:
WHERE
des conditions sur les champs des tables jointes. Par exemple ne r\u00e9cup\u00e9rer que les lignes qui sont concern\u00e9es par un parc ou une znieff, via WHERE p.id IS NOT NULL OR z.id IS NOT NULL
(comment\u00e9 ci-dessus pour le d\u00e9sactiver)ATTENTION:
ATTENTION:
geom
de toutes les tablesPour chaque objets d'une table, on souhaite r\u00e9cup\u00e9rer des informations sur les objets proches d'une autre table. Au lieu d'utiliser un tampon puis une intersection, on utilise la fonction ST_DWithin
On prend comme exemple la table des bornes \u00e0 incendie cr\u00e9\u00e9e pr\u00e9c\u00e9demment (remplie avec quelques donn\u00e9es de test).
Trouver toutes les parcelles \u00e0 moins de 200m d'une borne \u00e0 incendie
SELECT\np.id_parcelle, p.geom,\nb.id_borne, b.code,\nST_Distance(b.geom, p.geom) AS distance\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.borne_incendie AS b\n ON ST_DWithin(p.geom, b.geom, 200)\nORDER BY id_parcelle, id_borne\n
Attention, elle peut renvoyer plusieurs fois la m\u00eame parcelle si 2 bornes sont assez proches. Pour ne r\u00e9cup\u00e9rer que la borne la plus proche, on peut faire la requ\u00eate suivante. La clause DISTINCT ON
permet de dire quel champ doit \u00eatre unique (ici id_parcelle).
On ordonne ensuite par ce champ et par la distance pour prendre seulement la ligne correspondant \u00e0 la parcelle la plus proche
SELECT DISTINCT ON (p.id_parcelle)\np.id_parcelle, p.geom,\nb.id_borne, b.code,\nST_Distance(b.geom, p.geom) AS distance\nFROM z_formation.parcelle_havre AS p\nJOIN z_formation.borne_incendie AS b\n ON ST_DWithin(p.geom, b.geom, 200)\nORDER BY id_parcelle, distance\n
Pour information, on peut v\u00e9rifier en cr\u00e9ant les tampons
-- Tampons non dissous\nSELECT id_borne, ST_Buffer(geom, 200) AS geom\nFROM z_formation.borne_incendie\n\n-- Tampons dissous\nSELECT ST_Union(ST_Buffer(geom, 200)) AS geom\nFROM z_formation.borne_incendie\n
Un article int\u00e9ressant de Paul Ramsey sur le calcul de distance via l'op\u00e9rateur <->
pour trouver le plus proche voisin d'un objet.
Continuer vers Fusionner des g\u00e9om\u00e9tries
"},{"location":"links_and_data/","title":"Liens utiles","text":""},{"location":"links_and_data/#documentation","title":"Documentation","text":"Documentation de PostgreSQL : https://docs.postgresql.fr/current/
Documentation des fonctions PostGIS:
Nous pr\u00e9supposons qu'une base de donn\u00e9es est accessible pour la formation, via un r\u00f4le PostgreSQL avec des droits \u00e9lev\u00e9s (notamment pour cr\u00e9er des sch\u00e9mas et des tables). L'extension PostGIS doit aussi \u00eatre activ\u00e9e sur cette base de donn\u00e9es.
"},{"location":"links_and_data/#jeux-de-donnees","title":"Jeux de donn\u00e9es","text":"Pour cette formation, nous utilisons des donn\u00e9es libres de droit :
Il peut est charg\u00e9 en base avec cette commande :
pg_restore -h URL_SERVEUR -p 5432 -U NOM_UTILISATEUR -d NOM_BASE --no-owner --no-acl data_formation.dump\n
Ce jeu de donn\u00e9es a pour sources :
Extraction de donn\u00e9es d'OpenStreetMap dans un format SIG, sous licence \"ODBL\" (site https://github.com/igeofr/osm2igeo ). On utilisera par exemple les donn\u00e9es de l'ancienne r\u00e9gion Haute-Normandie.
Donn\u00e9es cadastrales (site https://cadastre.data.gouv.fr ), sous licence \"Licence Ouverte 2.0\" Par exemple pour la Seine-Maritime : https://cadastre.data.gouv.fr/data/etalab-cadastre/2024-10-01/shp/departements/76/
PLU (site https://www.geoportail-urbanisme.gouv.fr/map/ ). Par exemple les donn\u00e9es de la ville du Havre. Cliquer sur la commune, et utiliser le lien de t\u00e9l\u00e9chargement.
Ces donn\u00e9es peuvent aussi \u00eatre import\u00e9es dans la base de formation via les outils de QGIS.
"},{"location":"links_and_data/#concepts-de-base-de-donnees","title":"Concepts de base de donn\u00e9es","text":"Un rappel sur les concepts de table, champs, relations.
Lire la formation QGIS \u00e9galement
Continuer vers Gestion des donn\u00e9es PostgreSQL dans QGIS
"},{"location":"merge_geometries/","title":"Fusionner des g\u00e9om\u00e9tries","text":"On souhaite cr\u00e9er une seule g\u00e9om\u00e9trie qui est issue de la fusion de toutes les g\u00e9om\u00e9tries regroup\u00e9es par un crit\u00e8re (nature, code, etc.)
Par exemple un polygone fusionnant les zonages qui partagent le m\u00eame type
SELECT count(id_zone_urba) AS nb_objets, typezone,\nST_Union(geom) AS geom\nFROM z_formation.zone_urba\nGROUP BY typezone\n
On souhaite parfois fusionner toutes les g\u00e9om\u00e9tries qui sont jointives. Par exemple, on veut fusionner toutes les parcelles jointives pour cr\u00e9er des blocs.
DROP TABLE IF EXISTS z_formation.bloc_parcelle_havre;\nCREATE TABLE z_formation.bloc_parcelle_havre AS\nSELECT\nrow_number() OVER() AS id,\nstring_agg(id::text, ', ') AS ids, t.geom::geometry(polygon, 2154) AS geom\nFROM (\n SELECT\n (St_Dump(ST_Union(a.geom))).geom AS geom\n FROM z_formation.parcelle_havre AS a\n WHERE ST_IsValid(a.geom)\n) t\nJOIN z_formation.parcelle_havre AS p\n ON ST_Intersects(p.geom, t.geom)\nGROUP BY t.geom\n;\nALTER TABLE z_formation.bloc_parcelle_havre ADD PRIMARY KEY (id);\nCREATE INDEX ON z_formation.bloc_parcelle_havre USING GIST (geom);\n
Continuer vers Les triggers
"},{"location":"perform_calculation/","title":"Faire des calculs","text":""},{"location":"perform_calculation/#calcul-sur-des-attributs","title":"Calcul sur des attributs","text":"Le SQL permet de r\u00e9aliser des calculs ou des modifications \u00e0 partir de champs. On peut donc faire des calculs sur des nombres, ou des modifications (remplacement de texte, mise en majuscule, etc.)
Faire un calcul tr\u00e8s simple, avec des op\u00e9rateurs + - /
et *
, ainsi que des parenth\u00e8ses
-- On multiplie 10 par 2\nSELECT\n10 * 2 AS vingt,\n(2.5 -1) * 10 AS quinze\n
Il est aussi possible de faire des calculs \u00e0 partir d'un ou plusieurs champs.
Nous souhaitons par exemple cr\u00e9er un champ qui contiendra la population des communes. Dans la donn\u00e9e source, le champ popul
est de type cha\u00eene de caract\u00e8re, car il contient parfois la valeur 'NC'
lorsque la population n'est pas connue.
Nous ne pouvons pas faire de calculs \u00e0 partir d'un champ texte. On souhaite donc cr\u00e9er un nouveau champ population pour y stocker les valeurs enti\u00e8res.
-- Ajout d'un champ de type entier dans la table\nALTER TABLE z_formation.commune ADD COLUMN population integer;\n
Modifier le nouveau champ population pour y mettre la valeur enti\u00e8re lorsqu'elle est connue. La modification d'une table se fait avec la requ\u00eate UPDATE
, en passant les champs \u00e0 modifier et leur nouvelle valeur via SET
-- Mise \u00e0 jour d'un champ \u00e0 partir d'un calcul\nUPDATE z_formation.commune SET population =\nCASE\n WHEN popul != 'NC' THEN popul::integer\n ELSE NULL\nEND\n;\n
Dans cette requ\u00eate, le CASE WHEN condition THEN valeur ELSE autre_valeur END
permet de faire un test sur la valeur d'origine, et de proposer une valeur si la condition est remplie ( https://sql.sh/cours/case )
Une fois ce champ population
renseign\u00e9 correctement, dans un type entier, on peut r\u00e9aliser un calcul tr\u00e8s simple, par exemple doubler la population:
-- Calcul simple : on peut utiliser les op\u00e9rateurs math\u00e9matiques\nSELECT id_commune, code_insee, nom, geom,\npopulation,\npopulation * 2 AS double_population\nFROM z_formation.commune\nLIMIT 10\n
Il est possible de combiner plusieurs champs pour r\u00e9aliser un calcul. Nous verrons plus loin comment calculer la densit\u00e9 de population \u00e0 partir de la population et de la surface des communes.
"},{"location":"perform_calculation/#calculer-des-caracteristiques-spatiales","title":"Calculer des caract\u00e9ristiques spatiales","text":"Par exemple la longueur ou la surface
Calculer la longueur d'objets lin\u00e9aires
-- Calcul des longueurs de route\nSELECT id_route, id, nature,\nST_Length(geom) AS longueur_m\nFROM z_formation.route\nLIMIT 100\n
Calculer la surface de polygones, et utiliser ce r\u00e9sultat dans un calcul. Par exemple ici la densit\u00e9 de population:
-- Calculer des donn\u00e9es \u00e0 partir de champs et de fonctions spatiales\nSELECT id_commune, code_insee, nom, geom,\npopulation,\nST_Area(geom) AS surface,\npopulation / ( ST_Area(geom) / 1000000 ) AS densite_hab_km\nFROM z_formation.commune\nLIMIT 10\n
"},{"location":"perform_calculation/#creer-des-geometries-a-partir-de-geometries","title":"Cr\u00e9er des g\u00e9om\u00e9tries \u00e0 partir de g\u00e9om\u00e9tries","text":"On peut modifier les g\u00e9om\u00e9tries avec des fonctions spatiales, ce qui revient \u00e0 effectuer un calcul sur les g\u00e9om\u00e9tries. Deux exemples classiques : centroides et tampons
Calculer le centro\u00efde de polygones
-- Centroides des communes\nSELECT id_commune, code_insee, nom,\nST_Centroid(geom) AS geom\nFROM z_formation.commune\n
Le centro\u00efde peut ne pas \u00eatre \u00e0 l'int\u00e9rieur du polygone, par exemple sur la commune de Arni\u00e8res-sur-Iton. Forcer le centro\u00efde \u00e0 l'int\u00e9rieur du polygone. Attention, ce calcul est plus long. Si vous souhaitez mieux comprendre l'algorithme derri\u00e8re cette fonction
-- Centro\u00efdes \u00e0 l'int\u00e9rieur des communes\n-- Attention, c'est plus long \u00e0 calculer\nSELECT id_commune, code_insee, nom,\nST_PointOnSurface(geom) AS geom\nFROM z_formation.commune\n
Calculer le tampon autour d'objets
-- Tampons de 1km autour des communes\nSELECT id_commune, nom, population,\nST_Buffer(geom, 1000) AS geom\nFROM z_formation.commune\nLIMIT 10\n
Continuer vers Filtrer des donn\u00e9es: WHERE
"},{"location":"postgresql_in_qgis/","title":"Gestion des donn\u00e9es PostgreSQL dans QGIS","text":""},{"location":"postgresql_in_qgis/#introduction","title":"Introduction","text":"Lorsqu'on travaille avec des donn\u00e9es PostgreSQL, QGIS n'acc\u00e8de pas \u00e0 la donn\u00e9e en lisant un ou plusieurs fichiers, mais fait des requ\u00eates \u00e0 la base, \u00e0 chaque fois qu'il en a besoin: d\u00e9placement de carte, zoom, ouverture de la table attributaire, s\u00e9lection par expression, etc.
La base de donn\u00e9es fournit donc un lieu de stockage des donn\u00e9es centralis\u00e9. On peut g\u00e9rer les droits d'acc\u00e8s ou d'\u00e9criture sur les sch\u00e9mas et les tables.
"},{"location":"postgresql_in_qgis/#creer-une-connexion-qgis-a-la-base-de-donnees","title":"Cr\u00e9er une connexion QGIS \u00e0 la base de donn\u00e9es","text":"Dans QGIS, il faut cr\u00e9er une nouvelle connexion \u00e0 PostgreSQL, via l'outil \"\u00c9l\u00e9phant\" : menu Couches / Ajouter une couche / Ajouter une couche PostgreSQL. Configurer les options suivantes :
Attention Pour plus de s\u00e9curit\u00e9, privil\u00e9gier l'usage d'un service PostgreSQL: https://docs.qgis.org/latest/fr/docs/user_manual/managing_data_source/opening_data.html#pg-service-file (plugin QGIS int\u00e9ressant : PG Service Parser)
Il est aussi int\u00e9ressant pour les performances d'acc\u00e8s aux donn\u00e9es PostgreSQL de modifier une option dans les options de QGIS, onglet Rendu : il faut cocher la case R\u00e9aliser la simplification par le fournisseur de donn\u00e9es lorsque c'est possible. Cela permet de t\u00e9l\u00e9charger des versions all\u00e9g\u00e9es des donn\u00e9es aux petites \u00e9chelles. Documentation QGIS
NB Pour les couches PostGIS qui auraient d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9es avant d'avoir activ\u00e9 cette option, vous pouvez manuellement changer dans vos projets via l'onglet Rendu de la bo\u00eete de dialogue des propri\u00e9t\u00e9s de chaque couche PostGIS.
"},{"location":"postgresql_in_qgis/#ouvrir-une-couche-postgresql-dans-qgis","title":"Ouvrir une couche PostgreSQL dans QGIS","text":"Trois solutions sont possibles :
sch\u00e9mas
, puis les tables
ou vues
exploitables. Une ic\u00f4ne devant chaque table/vue indique si une table est g\u00e9om\u00e9trique ou non ainsi que le type de g\u00e9om\u00e9trie, point, ligne ou polygone. On peut utiliser le menu Clic-Droit
sur les objets de l'arbre.On peut travailler avec le gestionnaire de bases de donn\u00e9es de QGIS : menu Base de donn\u00e9es > Gestionnaire BD (sinon via l'ic\u00f4ne de la barre d\u2019outil base de donn\u00e9es) ou avec l'explorateur (recommand\u00e9).
Dans l'arbre qui se pr\u00e9sente, on peut choisir sa connexion, puis double-cliquer, ce qui montre l'ensemble des sch\u00e9mas, et l'ouverture d'un sch\u00e9ma montre la liste des tables et vues. Les menus permettent de cr\u00e9er ou d'\u00e9diter des objets (sch\u00e9mas, tables).
Une fen\u00eatre SQL permet de lancer manuellement des requ\u00eates SQL. Nous allons principalement utiliser cet outil : menu Base de donn\u00e9es / Fen\u00eatre SQL (on peut aussi le lancer via F2).
NB: C'est possible aussi d'utiliser le fen\u00eatre SQL de l'explorateur via clic-droit Ex\u00e9cuter le SQL ...
, mais elle ne permet pas encore de ne lancer que le texte surlign\u00e9, ce qui est pourtant tr\u00e8s pratique pendant une formation.
Les sch\u00e9mas dans une base PostgreSQL sont utiles pour regrouper les tables.
On recommande de ne pas cr\u00e9er de tables dans le sch\u00e9ma public
, mais d'utiliser des sch\u00e9mas (par th\u00e9matique, pour la gestion des droits, etc.).
Pour la formation, nous allons cr\u00e9er un sch\u00e9ma z_formation
:
Cr\u00e9er un sch\u00e9ma
.Ensuite, on peut cr\u00e9er une table dans ce sch\u00e9ma : dans l'explorateur, faire un clic-droit sur le sch\u00e9ma z_formation
, puis Nouvelle table...
:
NB: on a cr\u00e9\u00e9 une table dans cet exemple z_formation.borne_incendie
avec les champs code (text), debit (real) et geom (g\u00e9om\u00e9trie de type Point, code SRID 2154)
id
de type entier auto-incr\u00e9ment\u00e9 a \u00e9t\u00e9 cr\u00e9\u00e9 automatiquement par QGIS en tant que cl\u00e9 primaire de la table.On peut aussi utiliser du SQL pour cr\u00e9er des objets dans la base :
-- cr\u00e9ation d'un sch\u00e9ma\nCREATE SCHEMA IF NOT EXISTS z_formation;\n\n-- cr\u00e9ation de la table\nCREATE TABLE IF NOT EXISTS z_formation.borne_incendie (\n -- un serial est un entier auto-incr\u00e9ment\u00e9\n id_borne serial NOT NULL PRIMARY KEY,\n code text NOT NULL,\n debit real,\n geom geometry(Point, 2154)\n);\n-- Cr\u00e9ation de l'index spatial\nDROP INDEX IF EXISTS borne_incendie_geom_idx;\nCREATE INDEX ON z_formation.borne_incendie USING GIST (geom);\n
"},{"location":"postgresql_in_qgis/#ajouter-des-donnees-dans-une-table","title":"Ajouter des donn\u00e9es dans une table","text":"On peut bien s\u00fbr charger la table dans QGIS, puis utiliser les outils d'\u00e9dition classique pour cr\u00e9er des nouveaux objets ou les modifier.
En SQL, il est aussi possible d'ins\u00e9rer des donn\u00e9es ( https://sql.sh/cours/insert-into ). Par exemple pour les bornes \u00e0 incendie :
INSERT INTO z_formation.borne_incendie (code, debit, geom)\n VALUES\n ('ABC', 1.5, ST_SetSRID(ST_MakePoint(490846.0,6936902.7), 2154)),\n ('XYZ', 4.1, ST_SetSRID(ST_MakePoint(491284.9,6936551.6), 2154)),\n ('FGH', 2.9, ST_SetSRID(ST_MakePoint(490839.8,6937794.8), 2154)),\n ('IOP', 3.6, ST_SetSRID(ST_MakePoint(491203.3,6937488.1), 2154))\n;\n
NB: Nous verrons plus loin l'utilisation de fonctions de cr\u00e9ation de g\u00e9om\u00e9trie, comme ST_MakePoint
"},{"location":"postgresql_in_qgis/#verifier-et-creer-les-indexes-spatiaux","title":"V\u00e9rifier et cr\u00e9er les indexes spatiaux","text":"On peut v\u00e9rifier si chaque table contient un index spatial via le gestionnaire de base de donn\u00e9es de QGIS, en cliquant sur la table dans l'arbre, puis en regardant les informations de l'onglet Info. On peut alors cr\u00e9er l'index spatial via le lien bleu Aucun index spatial d\u00e9fini (en cr\u00e9er un).
Sinon, il est possible de le faire en SQL via la requ\u00eate suivante :
CREATE INDEX ON nom_du_schema.nom_de_la_table USING GIST (geom);\n
Si on souhaite automatiser la cr\u00e9ation des indexes pour toutes les tables qui n'en ont pas, on peut utiliser une fonction, d\u00e9crite dans la partie Fonctions utiles
Continuer vers l'Import des donn\u00e9es dans PostgreSQL
"},{"location":"save_queries/","title":"Enregistrer une requ\u00eate","text":""},{"location":"save_queries/#les-vues","title":"Les vues","text":"Une vue est l'enregistrement d'une requ\u00eate, appel\u00e9e d\u00e9finition de la vue, qui est stock\u00e9 dans la base, et peut \u00eatre utilis\u00e9e comme une table.
Cr\u00e9er une vue via CREATE VIEW
-- On supprime d'abord la vue si elle existe\nDROP VIEW IF EXISTS z_formation.v_voies;\n-- On cr\u00e9e la vue en r\u00e9cup\u00e9rant les routes de plus de 5 km\nCREATE VIEW z_formation.v_voies AS\nSELECT id_route, id AS code, ST_Length(geom) AS longueur, geom\nFROM z_formation.route\nWHERE ST_Length(geom) > 5000\n
Utiliser cette vue dans une autre requ\u00eate
-- Ou filtrer les donn\u00e9es\nSELECT * FROM z_formation.v_voies\nWHERE longueur > 10000\n
"},{"location":"save_queries/#enregistrer-une-requete-comme-une-table","title":"Enregistrer une requ\u00eate comme une table","text":"C'est la m\u00eame chose que pour enregistrer une vue, sauf qu'on cr\u00e9e une table: les donn\u00e9es sont donc stock\u00e9es en base, et n'\u00e9voluent plus en fonction des donn\u00e9es source. Cela permet d'acc\u00e9der rapidement aux donn\u00e9es, car la requ\u00eate sous-jacente n'est plus ex\u00e9cut\u00e9e une fois la table cr\u00e9\u00e9e.
"},{"location":"save_queries/#exemple-1-creer-la-table-des-voies-rassemblant-les-routes-et-les-chemins","title":"Exemple 1 - cr\u00e9er la table des voies rassemblant les routes et les chemins","text":"DROP TABLE IF EXISTS z_formation.t_voies;\nCREATE TABLE z_formation.t_voies AS\nSELECT\n-- on r\u00e9cup\u00e8re tous les champs\nsource.*,\n-- on calcule la longueur apr\u00e8s rassemblement des donn\u00e9es\nST_Length(geom) AS longueur\nFROM (\n (SELECT id, geom\n FROM z_formation.chemin\n LIMIT 100)\n UNION ALL\n (SELECT id, geom\n FROM z_formation.route\n LIMIT 100)\n) AS source\nORDER BY longueur\n;\n
Comme c'est une table, il est int\u00e9ressant d'ajouter un index spatial.
CREATE INDEX ON z_formation.t_voies USING GIST (geom);\n
On peut aussi ajouter une cl\u00e9 primaire
ALTER TABLE z_formation.t_voies ADD COLUMN gid serial;\nALTER TABLE z_formation.t_voies ADD PRIMARY KEY (gid);\n
Attention Les donn\u00e9es de la table n'\u00e9voluent plus en fonction des donn\u00e9es des tables source. Il faut donc supprimer la table puis la recr\u00e9er si besoin. Pour r\u00e9pondre \u00e0 ce besoin, il existe les vues mat\u00e9rialis\u00e9es.
"},{"location":"save_queries/#exemple-2-creer-une-table-de-nomenclature-a-partir-des-valeurs-distinctes-dun-champ","title":"Exemple 2 - cr\u00e9er une table de nomenclature \u00e0 partir des valeurs distinctes d'un champ.","text":"On cr\u00e9e la table si besoin. On ajoutera ensuite les donn\u00e9es via INSERT
-- Suppression de la table\nDROP TABLE IF EXISTS z_formation.nomenclature;\n-- Cr\u00e9ation de la table\nCREATE TABLE z_formation.nomenclature (\n id serial primary key,\n code text,\n libelle text,\n ordre smallint\n);\n
On ajoute ensuite les donn\u00e9es. La clause WITH
permet de r\u00e9aliser une sous-requ\u00eate, et de l'utiliser ensuite comme une table. La clause INSERT INTO
permet d'ajouter les donn\u00e9es. On ne lui passe pas le champ id, car c'est un serial, c'est-\u00e0-dire un entier auto-incr\u00e9ment\u00e9.
-- Ajout des donn\u00e9es \u00e0 partir d'une table via commande INSERT\nINSERT INTO z_formation.nomenclature\n(code, libelle, ordre)\n-- Clause WITH pour r\u00e9cup\u00e9rer les valeurs distinctes comme une table virtuelle\nWITH source AS (\n SELECT DISTINCT\n nature AS libelle\n FROM z_formation.lieu_dit_habite\n WHERE nature IS NOT NULL\n ORDER BY nature\n)\n-- S\u00e9lection des donn\u00e9es dans cette table virtuelle \"source\"\nSELECT\n-- on cr\u00e9e un code \u00e0 partir de l'ordre d'arriv\u00e9e.\n-- row_number() OVER() permet de r\u00e9cup\u00e9rer l'identifiant de la ligne dans l'ordre d'arriv\u00e9e\n-- (un_champ)::text permet de convertir un champ ou un calcul en texte\n-- lpad permet de compl\u00e9ter le chiffre avec des z\u00e9ro. 1 devient 01\nlpad( (row_number() OVER())::text, 2, '0' ) AS code,\nlibelle,\nrow_number() OVER() AS ordre\nFROM source\n;\n
Le r\u00e9sultat est le suivant:
code libelle ordre 01 Ch\u00e2teau 1 02 Lieu-dit habit\u00e9 2 03 Moulin 3 04 Quartier 4 05 Refuge 5 06 Ruines 6"},{"location":"save_queries/#exemple-3-creer-une-table-avec-lextraction-des-parcelles-sur-une-commune","title":"Exemple 3 - cr\u00e9er une table avec l'extraction des parcelles sur une commune","text":"On utilise le champ commune
pour filtrer. On n'oublie pas de cr\u00e9er l'index spatial, qui sera utilis\u00e9 pour am\u00e9liorer les performances lors des jointures spatiales.
-- supprimer la table si elle existe d\u00e9j\u00e0\nDROP TABLE IF EXISTS z_formation.parcelle_havre ;\n\n-- Cr\u00e9er la table via filtre sur le champ commune\nCREATE TABLE z_formation.parcelle_havre AS\nSELECT p.*\nFROM z_formation.parcelle AS p\nWHERE p.commune = '76351';\n\n-- Ajouter la cl\u00e9 primaire\nALTER TABLE z_formation.parcelle_havre ADD PRIMARY KEY (id_parcelle);\n\n-- Ajouter l'index spatial\nCREATE INDEX ON z_formation.parcelle_havre USING GIST (geom);\n
"},{"location":"save_queries/#enregistrer-une-requete-comme-une-vue-materialisee","title":"Enregistrer une requ\u00eate comme une vue mat\u00e9rialis\u00e9e","text":"-- On supprime d'abord la vue mat\u00e9rialis\u00e9e si elle existe\nDROP MATERIALIZED VIEW IF EXISTS z_formation.vm_voies;\n-- On cr\u00e9e la vue en r\u00e9cup\u00e9rant les routes de plus de 5 km\nCREATE MATERIALIZED VIEW z_formation.vm_voies AS\nSELECT id_route, id AS code, ST_Length(geom) AS longueur, geom\nFROM z_formation.route\nWHERE ST_Length(geom) > 6000\n\n-- Ajout des indexes sur le champ id_route et de g\u00e9om\u00e9trie\nCREATE INDEX ON z_formation.vm_voies (id_route);\nCREATE INDEX ON z_formation.vm_voies USING GIST (geom);\n\n-- On rafra\u00eechit la vue mat\u00e9rialis\u00e9e quand on en a besoin\n-- par exemple quand les donn\u00e9es source ont \u00e9t\u00e9 modifi\u00e9es\nREFRESH MATERIALIZED VIEW z_formation.vm_voies;\n
Continuer vers R\u00e9aliser des jointures attributaires et spatiales; JOIN
"},{"location":"sql_select/","title":"S\u00e9lectionner","text":"Nous allons pr\u00e9senter des requ\u00eates SQL de plus en plus complexes pour acc\u00e9der aux donn\u00e9es, et exploiter les capacit\u00e9s de PostgreSQL/PostGIS. Une requ\u00eate est construite avec des instructions standardis\u00e9es, appel\u00e9es clauses
-- Ordre des clauses SQL\nSELECT une_colonne, une_autre_colonne\nFROM nom_du_schema.nom_de_la_table\n(LEFT) JOIN autre_schema.autre_table\n ON critere_de_jointure\nWHERE condition\nGROUP BY champs_de_regroupement\nORDER BY champs_d_ordre\nLIMIT 10\n
R\u00e9cup\u00e9rer tous les objets d'une table, et les valeurs pour toutes les colonnes -- S\u00e9lectionner l'ensemble des donn\u00e9es d'une couche: l'\u00e9toile veut dire \"tous les champs de la table\"\nSELECT *\nFROM z_formation.borne_incendie\n;\n
Les 10 premiers objets
-- S\u00e9lectionner les 10 premi\u00e8res communes par ordre alphab\u00e9tique\nSELECT *\nFROM z_formation.commune\nORDER BY nom\nLIMIT 10\n
Les 10 premiers objets par ordre alphab\u00e9tique
-- S\u00e9lectionner les 10 premi\u00e8res communes par ordre alphab\u00e9tique descendant\nSELECT *\nFROM z_formation.commune\nORDER BY nom DESC\nLIMIT 10\n
Les 10 premiers objets avec un ordre sur plusieurs champs
-- On peut utiliser plusieurs champs pour l'ordre\nSELECT *\nFROM z_formation.commune\nORDER BY depart, nom\nLIMIT 10\n
S\u00e9lectionner seulement certains champs
-- S\u00e9lectionner seulement certains champs, et avec un ordre\nSELECT id_commune, code_insee, nom\nFROM z_formation.commune\nORDER BY nom\n
Donner un alias (un autre nom) aux champs
-- Donner des alias aux noms des colonnes\nSELECT id_commune AS identifiant,\ncode_insee AS \"code_commune\",\nnom\nFROM z_formation.commune\nORDER BY nom\n
On peut donc facilement, \u00e0 partir de la clause SELECT
, choisir quels champs on souhaite r\u00e9cup\u00e9rer, dans l'ordre voulu, et renommer le champ en sortie.
Si on veut charger le r\u00e9sultat de la requ\u00eate dans QGIS, il suffit de cocher la case Charger en tant que nouvelle couche puis de choisir le champ d'identifiant unique, et si et seulement si c'est une couche spatiale, choisir le champ de g\u00e9om\u00e9trie .
Attention, si la table est non spatiale, il faut bien penser \u00e0 d\u00e9cocher Colonne de g\u00e9om\u00e9trie !
Par exemple, pour afficher les communes avec leur information sommaire:
-- Ajouter la g\u00e9om\u00e9trie pour visualiser les donn\u00e9es dans QGIS\nSELECT id_commune AS identifiant,\ncode_insee AS \"code_commune\",\nnom, geom\nFROM z_formation.commune\nORDER BY nom\n
On choisira ici le champ identifiant comme identifiant unique, et le champ geom comme g\u00e9om\u00e9trie
Continuer vers R\u00e9aliser des calculs et cr\u00e9er des g\u00e9om\u00e9tries: FONCTIONS
"},{"location":"triggers/","title":"Les triggers","text":"Les triggers, aussi appel\u00e9s en fran\u00e7ais d\u00e9clencheurs, permettent de lancer des actions avant ou apr\u00e8s ajout, modification ou suppression de donn\u00e9es sur des tables (ou des vues).
Les triggers peuvent par exemple \u00eatre utilis\u00e9s
Des fonctions trigger sont associ\u00e9es aux triggers. Elles peuvent \u00eatre \u00e9crites en PL/pgSQL ou d'autres languages (p. ex. PL/Python). Une fonction trigger doit renvoyer soit NULL soit une valeur record ayant exactement la structure de la table pour laquelle le trigger a \u00e9t\u00e9 lanc\u00e9. Lire les derniers paragraphes ici pour en savoir plus.
"},{"location":"triggers/#calcul-automatique-de-certains-champs","title":"Calcul automatique de certains champs","text":"On cr\u00e9e une table borne_incendie
pour pouvoir tester cette fonctionnalit\u00e9:
CREATE TABLE z_formation.borne_incendie (\n id_borne serial primary key,\n code text NOT NULL,\n debit integer,\n geom geometry(point, 2154)\n);\nCREATE INDEX ON z_formation.borne_incendie USING GIST (geom);\n
On y ajoute des champs \u00e0 renseigner de mani\u00e8re automatique
-- TRIGGERS\n-- Modification de certains champs apr\u00e8s ajout ou modification\n-- Cr\u00e9er les champs dans la table\nALTER TABLE z_formation.borne_incendie ADD COLUMN modif_date date;\nALTER TABLE z_formation.borne_incendie ADD COLUMN modif_user text;\nALTER TABLE z_formation.borne_incendie ADD COLUMN longitude real;\nALTER TABLE z_formation.borne_incendie ADD COLUMN latitude real;\nALTER TABLE z_formation.borne_incendie ADD COLUMN donnee_validee boolean;\nALTER TABLE z_formation.borne_incendie ADD COLUMN last_action text;\n
On cr\u00e9e la fonction trigger qui ajoutera les m\u00e9tadonn\u00e9es dans la table
-- Cr\u00e9er la fonction qui sera lanc\u00e9e sur modif ou ajout de donn\u00e9es\nCREATE OR REPLACE FUNCTION z_formation.ajout_metadonnees_modification()\nRETURNS TRIGGER\nAS $limite$\nDECLARE newjsonb jsonb;\nBEGIN\n\n -- on transforme l'enregistrement NEW (la ligne modifi\u00e9e ou ajout\u00e9e) en JSON\n -- pour conna\u00eetre la liste des champs\n newjsonb = to_jsonb(NEW);\n\n -- on peut ainsi tester si chaque champ existe dans la table\n -- avant de modifier sa valeur\n -- Par exemple, on teste si le champ modif_date est bien dans l'enregistrement courant\n IF newjsonb ? 'modif_date' THEN\n NEW.modif_date = now();\n RAISE NOTICE 'Date modifi\u00e9e %', NEW.modif_date;\n END IF;\n\n IF newjsonb ? 'modif_user' THEN\n NEW.modif_user = CURRENT_USER;\n END IF;\n\n -- longitude et latitude\n IF newjsonb ? 'longitude' AND newjsonb ? 'latitude'\n THEN\n -- Soit on fait un UPDATE et les g\u00e9om\u00e9tries sont diff\u00e9rentes\n -- Soit on fait un INSERT\n -- Sinon pas besoin de calculer les coordonn\u00e9es\n IF\n (TG_OP = 'UPDATE' AND NOT ST_Equals(OLD.geom, NEW.geom))\n OR (TG_OP = 'INSERT')\n THEN\n NEW.longitude = ST_X(ST_Centroid(NEW.geom));\n NEW.latitude = ST_Y(ST_Centroid(NEW.geom));\n END IF;\n END IF;\n\n -- Si je trouve un champ donnee_validee, je le mets \u00e0 False pour revue par l'administrateur\n -- Je peux faire une symbologie dans QGIS qui montre les donn\u00e9es modifi\u00e9es depuis derni\u00e8re validation\n IF newjsonb ? 'donnee_validee' THEN\n NEW.donnee_validee = False;\n END IF;\n\n -- Si je trouve un champ last_action, je peux y mettre UPDATE ou INSERT\n -- Pour savoir quelle est la derni\u00e8re op\u00e9ration utilis\u00e9e\n IF newjsonb ? 'last_action' THEN\n NEW.last_action = TG_OP;\n END IF;\n\n RETURN NEW;\nEND;\n$limite$\nLANGUAGE plpgsql\n;\n
On cr\u00e9e enfin le d\u00e9clencheur pour la ou les tables souhait\u00e9es, ce qui active le lancement de la fonction trigger pr\u00e9c\u00e9dente sur certaines actions:
-- Dire \u00e0 PostgreSQL d'\u00e9couter les modifications et ajouts sur la table\nCREATE TRIGGER trg_ajout_metadonnees_modification\nBEFORE INSERT OR UPDATE ON z_formation.borne_incendie\nFOR EACH ROW EXECUTE PROCEDURE z_formation.ajout_metadonnees_modification();\n
"},{"location":"triggers/#controles-de-conformite","title":"Contr\u00f4les de conformit\u00e9","text":"Il est aussi possible d'utiliser les triggers pour lancer des contr\u00f4les sur les valeurs de certains champs. Par exemple, on peut ajouter un contr\u00f4le sur la g\u00e9om\u00e9trie lors de l'ajout ou de la modification de donn\u00e9es: on v\u00e9rifie si la g\u00e9om\u00e9trie est bien en intersection avec les objets de la table des communes
-- Contr\u00f4le de la g\u00e9om\u00e9trie\n-- qui doit \u00eatre dans la zone d'int\u00e9r\u00eat\n-- On cr\u00e9e une fonction g\u00e9n\u00e9rique qui pourra s'appliquer pour toutes les couches\nCREATE OR REPLACE FUNCTION z_formation.validation_geometrie_dans_zone_interet()\nRETURNS TRIGGER AS $limite$\nBEGIN\n -- On v\u00e9rifie l'intersection avec les communes, on renvoie une erreur si souci\n IF NOT ST_Intersects(\n NEW.geom,\n st_collectionextract((SELECT ST_Collect(geom) FROM z_formation.commune), 3)::geometry(multipolygon, 2154)\n ) THEN\n -- On renvoie une erreur\n RAISE EXCEPTION 'La g\u00e9om\u00e9trie doit se trouver dans les communes';\n END IF;\n\n RETURN NEW;\nEND;\n$limite$\nLANGUAGE plpgsql;\n\n-- On l'applique sur la couches de test\nDROP TRIGGER IF EXISTS trg_validation_geometrie_dans_zone_interet ON z_formation.borne_incendie;\nCREATE TRIGGER trg_validation_geometrie_dans_zone_interet\nBEFORE INSERT OR UPDATE ON z_formation.borne_incendie\nFOR EACH ROW EXECUTE PROCEDURE z_formation.validation_geometrie_dans_zone_interet();\n
Si on essaye de cr\u00e9er un point dans la table z_formation.borne_incendie
en dehors des communes, la base renverra une erreur.
On cr\u00e9e d'abord une table qui permettra de stocker les actions
CREATE TABLE IF NOT EXISTS z_formation.log (\n id serial primary key,\n log_date timestamp,\n log_user text,\n log_action text,\n log_data jsonb\n);\n
On peut maintenant cr\u00e9er un trigger qui stocke dans cette table les actions effectu\u00e9es. Dans cet exemple, toutes les donn\u00e9es sont stock\u00e9es, mais on pourrait bien s\u00fbr choisir de simplifier cela.
CREATE OR REPLACE FUNCTION z_formation.log_actions()\nRETURNS TRIGGER AS $limite$\nDECLARE\n row_data jsonb;\nBEGIN\n -- We keep data\n IF TG_OP = 'INSERT' THEN\n -- for insert, we take the new data\n row_data = to_jsonb(NEW);\n ELSE\n -- for UPDATE and DELETE, we keep data before changes\n row_data = to_jsonb(OLD);\n END IF;\n\n -- We insert a new log item\n INSERT INTO z_formation.log (\n log_date,\n log_user,\n log_action,\n log_data\n )\n VALUES (\n now(),\n CURRENT_USER,\n TG_OP,\n row_data\n );\n IF TG_OP != 'DELETE' THEN\n RETURN NEW;\n ELSE\n RETURN OLD;\n END IF;\nEND;\n$limite$\nLANGUAGE plpgsql;\n\n-- On l'applique sur la couches de test\n-- On \u00e9coute apr\u00e8s l'action, d'o\u00f9 l'utilisation de `AFTER`\n-- On \u00e9coute pour INSERT, UPDATE ou DELETE\nDROP TRIGGER IF EXISTS trg_log_actions ON z_formation.borne_incendie;\nCREATE TRIGGER trg_log_actions\nAFTER INSERT OR UPDATE OR DELETE ON z_formation.borne_incendie\nFOR EACH ROW EXECUTE PROCEDURE z_formation.log_actions();\n
NB:
Continuer vers Correction des g\u00e9om\u00e9tries invalides
"},{"location":"triggers/#quiz","title":"Quiz","text":"Cr\u00e9er une table avec un champ id de type 'serial' et une g\u00e9om\u00e9trie de type polygone en 2154. Puis cr\u00e9er un trigger s'assurant que les g\u00e9om\u00e9tries aient au minimum **4** points dessin\u00e9s. -- Table: z_formation.polygone_mini_quatre_points\n -- DROP TABLE IF EXISTS z_formation.polygone_mini_quatre_points;\n CREATE TABLE IF NOT EXISTS z_formation.polygone_mini_quatre_points\n (\n id serial NOT NULL PRIMARY KEY,\n geom geometry(Polygon,2154)\n )\n\n -- FUNCTION: z_formation.contrainte_mini_quatre_points()\n -- DROP FUNCTION IF EXISTS z_formation.contrainte_mini_quatre_points();\n CREATE OR REPLACE FUNCTION z_formation.contrainte_mini_quatre_points()\n RETURNS trigger AS $limite$\n BEGIN\n -- On v\u00e9rifie que le polygone a au moins 4 points dessin\u00e9s\n -- => soit 5 points en comptant le dernier point qui ferme le polygone !\n IF ST_NPoints(NEW.geom) < 5\n THEN\n -- On renvoie une erreur\n RAISE EXCEPTION 'Le polygone doit avoir au moins 4 points dessin\u00e9s';\n END IF;\n\n RETURN NEW;\n END;\n $limite$\n LANGUAGE plpgsql;\n\n -- Trigger: trg_contrainte_mini_quatre_points\n -- DROP TRIGGER IF EXISTS trg_contrainte_mini_quatre_points ON z_formation.polygone_mini_quatre_points;\n CREATE OR REPLACE TRIGGER trg_contrainte_mini_quatre_points\n BEFORE INSERT OR UPDATE \n ON z_formation.polygone_mini_quatre_points\n FOR EACH ROW\n EXECUTE FUNCTION z_formation.contrainte_mini_quatre_points();\n
"},{"location":"tutoriel/","title":"Tutoriel","text":"Afin de vous entra\u00eener il existe diff\u00e9rentes tutoriels en ligne vous permettant de vous exercer.
La clause UNION
peut \u00eatre utilis\u00e9e pour regrouper les donn\u00e9es de sources diff\u00e9rentes dans une m\u00eame table. Le UNION ALL
fait la m\u00eame choses, mais sans r\u00e9aliser de d\u00e9doublonnement, ce qui est plus rapide.
Rassembler les routes et les chemins ensemble, en ajoutant un champ \"nature\" pour les diff\u00e9rencier
-- Rassembler des donn\u00e9es de tables diff\u00e9rentes\n-- On utilise une UNION ALL\n\n (SELECT 'chemin' AS nature,\n geom,\n ROUND(ST_LENGTH(geom))::integer AS longueur\n FROM z_formation.chemin\n LIMIT 100)\n-- UNION ALL est plac\u00e9 entre 2 SELECT\nUNION ALL \n (SELECT 'route' AS nature,\n geom,\n ROUND(ST_LENGTH(geom))::integer AS longueur\n FROM z_formation.route\n LIMIT 100)\n-- Le ORDER BY doit \u00eatre r\u00e9alis\u00e9 \u00e0 la fin, et non sur chaque SELECT\nORDER BY longueur\n
Si on doit r\u00e9aliser le m\u00eame calcul sur chaque sous-ensemble (chaque SELECT), on peut le faire en 2 \u00e9tapes via une sous-requ\u00eate (ou une clause WITH)
SELECT\n-- on r\u00e9cup\u00e8re tous les champs\nsource.*,\n-- on calcule la longueur apr\u00e8s rassemblement des donn\u00e9es\nst_length(geom) AS longueur\nFROM (\n (SELECT id, geom\n FROM z_formation.chemin\n LIMIT 100)\n UNION ALL\n (SELECT id, geom\n FROM z_formation.route\n LIMIT 100)\n) AS source\nORDER BY longueur DESC\n;\n
Continuer vers Enregistrer les requ\u00eates: VIEW
"},{"location":"utils/","title":"Fonctions utiles","text":"Nous regroupons ici quelques fonctions r\u00e9alis\u00e9es au cours de formations ou d'accompagnements d'utilisateurs de PostgreSQL.
"},{"location":"utils/#ajout-de-lauto-incrementation-sur-un-champ-entier","title":"Ajout de l'auto-incr\u00e9mentation sur un champ entier","text":"Lorsqu'on importe une couche dans une table via les outils de QGIS, le champ d'identifiant choisi n'a pas le support de l'auto-incr\u00e9mentation, ce qui peut poser des probl\u00e8mes de l'ajout de nouvelles donn\u00e9es.
Depuis PostgreSQL 10, on peut maintenant utiliser des identit\u00e9s au lieu des serial pour avoir un champ auto-compl\u00e9t\u00e9. Voir par exemple l'article https://www.loxodata.com/post/identity/
Pour ajouter le support de l'auto-incr\u00e9mentation sur un champ entier \u00e0 une table existante, on peut utiliser les commandes suivantes :
-- Activer la g\u00e9n\u00e9ration automatique\nALTER TABLE \"monschema\".\"test\" ALTER \"id\" ADD GENERATED BY DEFAULT AS IDENTITY;\n\n-- Mettre la valeur de la s\u00e9quence (implicite et cach\u00e9e) \u00e0 la valeur max du champ d'identifiant\nSELECT setval(pg_get_serial_sequence('\"monschema\".\"test\"', 'id'), (SELECT max(\"id\") FROM \"monschema\".\"test\"));\n
Pour transformer les s\u00e9quences cr\u00e9\u00e9es pr\u00e9c\u00e9demment via des serial
en identit\u00e9 avec identity
, on peut lancer :
-- Enlever la valeur par d\u00e9faut sur le champ d'identifiant\nALTER TABLE \"monschema\".\"test\" ALTER COLUMN id DROP DEFAULT;\n\n-- Supprimer la s\u00e9quence\nDROP SEQUENCE IF EXISTS \"monschema\".\"test_id_seq\";\n\n-- Activer la g\u00e9n\u00e9ration automatique\nALTER TABLE \"monschema\".\"test\" ALTER \"id\" ADD GENERATED BY DEFAULT AS IDENTITY;\n\n-- Mettre la valeur de la s\u00e9quence (implicite et cach\u00e9e) \u00e0 la valeur max du champ d'identifiant\nSELECT setval(pg_get_serial_sequence('\"monschema\".\"test\"', 'id'), (SELECT max(\"id\") FROM \"monschema\".\"test\"));\n
"},{"location":"utils/#creation-automatique-dindexes-spatiaux","title":"Cr\u00e9ation automatique d'indexes spatiaux","text":"Pour des donn\u00e9es spatiales volumineuses, les performances d'affichage sont bien meilleures \u00e0 grande \u00e9chelle si on a ajout\u00e9 un index spatial. L'index est aussi beaucoup utilis\u00e9 pour am\u00e9liorer les performances d'analyses spatiales.
On peut cr\u00e9er l'index spatial table par table, ou bien automatiser cette cr\u00e9ation, c'est-\u00e0-dire cr\u00e9er les indexes spatiaux pour toutes les tables qui n'en ont pas.
Pour cela, nous avons con\u00e7u une fonction, t\u00e9l\u00e9chargeable ici: https://gist.github.com/mdouchin/cfa0e37058bcf102ed490bc59d762042
On doit copier/coller le script SQL de cette page GIST
dans la fen\u00eatre SQL du Gestionnaire de bases de donn\u00e9es de QGIS, puis lancer la requ\u00eate avec Ex\u00e9cuter. On peut ensuite vider le contenu de la fen\u00eatre, puis appeler la fonction create_missing_spatial_indexes
via le code SQL suivant :
-- On lance avec le param\u00e8tre \u00e0 True si on veut juste voir les tables qui n'ont pas d'index spatial\n-- On lance avec False si on veut cr\u00e9er les indexes automatiquement\n\n-- V\u00e9rification\nSELECT * FROM create_missing_spatial_indexes( True );\n\n-- Cr\u00e9ation\nSELECT * FROM create_missing_spatial_indexes( False );\n
"},{"location":"utils/#trouver-toutes-les-tables-sans-cle-primaire","title":"Trouver toutes les tables sans cl\u00e9 primaire","text":"Il est tr\u00e8s important de d\u00e9clarer une cl\u00e9 primaire pour vos tables stock\u00e9es dans PostgreSQL. Cela fournit un moyen aux logiciels comme QGIS d'identifier de mani\u00e8re performante les lignes dans une table. Sans cl\u00e9 primaire, les performances d'acc\u00e8s aux donn\u00e9es peuvent \u00eatre d\u00e9grad\u00e9es.
Vous pouvez trouver l'ensemble des tables de votre base de donn\u00e9es sans cl\u00e9 primaire en construisant cette vue PostgreSQL tables_without_primary_key
:
DROP VIEW IF EXISTS tables_without_primary_key;\nCREATE VIEW tables_without_primary_key AS\nSELECT t.table_schema, t.table_name\nFROM information_schema.tables AS t\nLEFT JOIN information_schema.table_constraints AS c\n ON t.table_schema = c.table_schema\n AND t.table_name = c.table_name\n AND c.constraint_type = 'PRIMARY KEY'\nWHERE True\nAND t.table_type = 'BASE TABLE'\nAND t.table_schema not in ('pg_catalog', 'information_schema')\nAND c.constraint_name IS NULL\nORDER BY table_schema, table_name\n;\n
SELECT *\nFROM tables_without_primary_key;\n
Ce qui peut donner par exemple:
table_schema table_name agriculture parcelles agriculture puits cadastre sections environnement znieff environnement parcs_naturelscadastre
, vous pouvez ensuite lancer la requ\u00eate :SELECT *\nFROM tables_without_primary_key\nWHERE table_schema IN ('cadastre');\n
Ce qui peut alors donner:
table_schema table_name cadastre sections"},{"location":"utils/#ajouter-automatiquement-plusieurs-champs-a-plusieurs-tables","title":"Ajouter automatiquement plusieurs champs \u00e0 plusieurs tables","text":"Il est parfois n\u00e9cessaire d'ajouter des champs \u00e0 une ou plusieurs tables, par exemple pour y stocker ensuite des m\u00e9tadonn\u00e9es (date de modification, date d'ajout, utilisateur, lien, etc).
Nous proposons pour cela la fonction ajout_champs_dynamiques
qui permet de fournir un nom de sch\u00e9ma, un nom de table, et une cha\u00eene de caract\u00e8re contenant la liste s\u00e9par\u00e9e par virgule des champs et de leur type.
La fonction est accessible ici: https://gist.github.com/mdouchin/50234f1f33801aed6f4f2cbab9f4887c
commune
du sch\u00e9ma test
: on ajoute les champs date_creation
, date_modification
et utilisateur
SELECT\najout_champs_dynamiques('test', 'commune', 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text')\n;\n
test
. On utilise dans cette exemple la vue geometry_columns
qui liste les tables spatiales, car on souhaite aussi ne faire cet ajout que pour les donn\u00e9es de type POINT-- Lancer la cr\u00e9ation de champs sur toutes les tables\n-- du sch\u00e9ma test\n-- contenant des g\u00e9om\u00e9tries de type Point\nSELECT f_table_schema, f_table_name,\najout_champs_dynamiques(\n -- sch\u00e9ma\n f_table_schema,\n -- table\n f_table_name,\n -- liste des champs, au format nom_du_champ TYPE\n 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text'\n)\nFROM geometry_columns\nWHERE True\nAND \"type\" LIKE '%POINT'\nAND f_table_schema IN ('test')\nORDER BY f_table_schema, f_table_name\n;\n
"},{"location":"utils/#verifier-la-taille-des-bases-tables-et-schemas","title":"V\u00e9rifier la taille des bases, tables et sch\u00e9mas","text":""},{"location":"utils/#connaitre-la-taille-des-bases-de-donnees","title":"Conna\u00eetre la taille des bases de donn\u00e9es","text":"On peut lancer la requ\u00eate suivante, qui renvoie les bases de donn\u00e9es ordonn\u00e9es par taille descendante.
SELECT\npg_database.datname AS db_name,\npg_database_size(pg_database.datname) AS db_size,\npg_size_pretty(pg_database_size(pg_database.datname)) AS db_pretty_size\nFROM pg_database\nWHERE datname NOT IN ('postgres', 'template0', 'template1')\nORDER BY db_size DESC;\n
"},{"location":"utils/#calculer-la-taille-des-tables","title":"Calculer la taille des tables","text":"On cr\u00e9e une fonction get_table_info
qui utilise les tables syst\u00e8me pour lister les tables, r\u00e9cup\u00e9rer leur sch\u00e9ma et les informations de taille.
DROP FUNCTION IF EXISTS get_table_info();\nCREATE OR REPLACE FUNCTION get_table_info()\nRETURNS TABLE (\n oid oid,\n schema_name text,\n table_name text,\n row_count integer,\n total_size bigint,\n pretty_total_size text\n)\nAS $$\nBEGIN\n RETURN QUERY\n SELECT\n b.oid, b.schema_name::text, b.table_name::text,\n b.row_count::integer,\n b.total_size::bigint,\n pg_size_pretty(b.total_size) AS pretty_total_size\n FROM (\n SELECT *,\n a.total_size - index_bytes - COALESCE(toast_bytes,0) AS table_bytes\n FROM (\n SELECT\n c.oid,\n nspname AS schema_name,\n relname AS TABLE_NAME,\n c.reltuples AS row_count,\n pg_total_relation_size(c.oid) AS total_size,\n pg_indexes_size(c.oid) AS index_bytes,\n pg_total_relation_size(reltoastrelid) AS toast_bytes\n FROM pg_class c\n LEFT JOIN pg_namespace n\n ON n.oid = c.relnamespace\n WHERE relkind = 'r'\n AND nspname NOT IN ('pg_catalog', 'information_schema')\n ) AS a\n ) AS b\n ;\nEND; $$\nLANGUAGE 'plpgsql';\n
On peut l'utiliser simplement de la mani\u00e8re suivante
-- Liste les tables\nSELECT * FROM get_table_info() ORDER BY schema_name, table_name DESC;\n\n-- Lister les tables dans l'ordre inverse de taille\nSELECT * FROM get_table_info() ORDER BY total_size DESC;\n
"},{"location":"utils/#calculer-la-taille-des-schemas","title":"Calculer la taille des sch\u00e9mas","text":"On cr\u00e9e une simple fonction qui renvoie la somme des tailles des tables d'un sch\u00e9ma
-- Fonction pour calculer la taille d'un sch\u00e9ma\nCREATE OR REPLACE FUNCTION pg_schema_size(schema_name text)\nRETURNS BIGINT AS\n$$\n SELECT\n SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))::BIGINT\n FROM pg_tables\n WHERE schemaname = schema_name\n$$\nLANGUAGE SQL;\n
On peut alors l'utiliser pour conna\u00eetre la taille d'un sch\u00e9ma
-- utilisation pour un sch\u00e9ma\nSELECT pg_size_pretty(pg_schema_size('public')) AS ;\n
Ou lister l'ensemble des sch\u00e9mas
-- lister les sch\u00e9mas et r\u00e9cup\u00e9rer leur taille\nSELECT schema_name, pg_size_pretty(pg_schema_size(schema_name))\nFROM information_schema.schemata\nWHERE schema_name NOT IN ('pg_catalog', 'information_schema')\nORDER BY pg_schema_size(schema_name) DESC;\n
"},{"location":"utils/#lister-les-triggers-appliques-sur-les-tables","title":"Lister les triggers appliqu\u00e9s sur les tables","text":"On peut utiliser la requ\u00eate suivante pour lister l'ensemble des triggers activ\u00e9s sur les tables
SELECT\n event_object_schema AS table_schema,\n event_object_table AS table_name,\n trigger_schema,\n trigger_name,\n string_agg(event_manipulation, ',') AS event,\n action_timing AS activation,\n action_condition AS condition, \n CASE WHEN tgenabled = 'O' THEN True ELSE False END AS trigger_active,\n action_statement AS definition\nFROM information_schema.triggers AS t\nINNER JOIN pg_trigger AS p\n ON p.tgrelid = concat('\"', event_object_schema, '\".\"', event_object_table, '\"')::regclass \n AND trigger_name = tgname\nWHERE True\nGROUP BY 1,2,3,4,6,7,8,9\nORDER BY table_schema, table_name\n;\n
Cette requ\u00eate renvoie un tableau de la forme :
table_schema table_name trigger_schema trigger_name event activation condition trigger_active definition gestion acteur gestion tr_date_maj UPDATE BEFORE f EXECUTE FUNCTION occtax.maj_date() occtax organisme occtax tr_date_maj UPDATE BEFORE t EXECUTE FUNCTION occtax.maj_date() taxon iso_metadata_reference taxon update_imr_timestamp UPDATE BEFORE t EXECUTE FUNCTION taxon.update_imr_timestamp_column()"},{"location":"utils/#lister-les-fonctions-installees-par-les-extensions","title":"Lister les fonctions install\u00e9es par les extensions","text":"Il est parfois utile de lister les fonctions des extensions, par exemple pour :
La requ\u00eate suivante permet d'afficher les informations essentielles des fonctions cr\u00e9\u00e9es par les extensions install\u00e9es dans la base :
SELECT DISTINCT\n ne.nspname AS extension_schema,\n e.extname AS extension_name,\n np.nspname AS function_schema,\n p.proname AS function_name,\n pg_get_function_identity_arguments(p.oid) AS function_params,\n proowner::regrole AS function_owner\nFROM\n pg_catalog.pg_extension AS e\n INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)\n INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)\n INNER JOIN pg_catalog.pg_namespace AS ne ON (ne.oid = e.extnamespace)\n INNER JOIN pg_catalog.pg_namespace AS np ON (np.oid = p.pronamespace)\nWHERE\n TRUE\n -- only extensions\n AND d.deptype = 'e'\n -- not in pg_catalog\n AND ne.nspname NOT IN ('pg_catalog')\n -- optionnally filter some extensions\n -- AND e.extname IN ('postgis', 'postgis_raster')\n -- optionnally filter by some owner\n AND proowner::regrole::text IN ('postgres')\n ORDER BY\n extension_name,\n function_name;\n;\n
qui renvoie une r\u00e9sultat comme ceci (cet exemple est un extrait de quelques lignes) :
extension_schema extension_name function_schema function_name function_params function_owner public fuzzystrmatch public levenshtein_less_equal text, text, integer johndoe public fuzzystrmatch public metaphone text, integer johndoe public fuzzystrmatch public soundex text johndoe public fuzzystrmatch public text_soundex text johndoe public hstore public akeys hstore johndoe public hstore public avals hstore johndoe public hstore public defined hstore, text johndoe public postgis public st_buffer text, double precision, integer johndoe public postgis public st_buffer geom geometry, radius double precision, options text johndoe public postgis public st_buildarea geometry johndoeOn peut bien s\u00fbr modifier la clause WHERE
pour filtrer plus ou moins les fonctions renvoy\u00e9es.
row_number() over()
non typ\u00e9 en integer
","text":"Si on utilise des vues dans QGIS qui cr\u00e9ent un identifiant unique via le num\u00e9ro de ligne, il est important :
integer
et pas entier long bigint
ORDER BY
pour essayer au maximum que QGIS r\u00e9cup\u00e8re les objets toujours dans le m\u00eame ordre.Quand une requ\u00eate d'une vue utilise row_number() OVER()
, depuis des versions r\u00e9centes de PostgreSQL, cela renvoie un entier long bigint
ce qui n'est pas conseill\u00e9.
On peut trouver ces vues ou vues mat\u00e9rialis\u00e9es via cette requ\u00eate :
-- vues\nSELECT\n concat('\"', schemaname, '\".\"', viewname, '\"') AS row_number_view\nFROM pg_views\nWHERE \"definition\" ~* '(.)+row_number\\(\\s*\\)\\s*over\\s*\\(\\s*\\) (.)+'\nORDER BY schemaname, viewname\n;\n\n-- vues mat\u00e9rialis\u00e9es\nSELECT\n concat('\"', schemaname, '\".\"', matviewname, '\"') AS row_number_view\nFROM pg_views\nWHERE \"definition\" ~* '(.)+row_number\\(\\s*\\)\\s*over\\s*\\(\\s*\\) (.)+'\nORDER BY schemaname, matviewname\n;\n
"},{"location":"utils/#lister-les-tables-qui-ont-une-cle-primaire-non-entiere","title":"Lister les tables qui ont une cl\u00e9 primaire non enti\u00e8re","text":"Pour \u00e9viter des soucis de performances sur les gros jeux de donn\u00e9es, il faut \u00e9viter d'avoir des tables avec des cl\u00e9s primaires sur des champs qui ne sont pas de type entier integer
.
En effet, dans QGIS, l'ouverture de ce type de table avec une cl\u00e9 primaire de type text
, ou m\u00eame bigint
, cela entra\u00eene la cr\u00e9ation et le stockage en m\u00e9moire d'une table de correspondance entre chaque objet de la couche et le num\u00e9ro d'arriv\u00e9e de la ligne. Sur les tables volumineuses, cela peut \u00eatre sensible.
Pour trouver toutes les tables, on peut faire cette requ\u00eate :
SELECT\n nspname AS table_schema, relname AS table_name,\n a.attname AS column_name,\n format_type(a.atttypid, a.atttypmod) AS column_type\nFROM pg_index AS i\nJOIN pg_class AS c\n ON i.indrelid = c.oid\nJOIN pg_attribute AS a\n ON a.attrelid = c.oid\n AND a.attnum = any(i.indkey)\nJOIN pg_namespace AS n\n ON n.oid = c.relnamespace\nWHERE indisprimary AND nspname NOT LIKE 'pg_%' AND nspname NOT LIKE 'lizmap_%'\nAND format_type(a.atttypid, a.atttypmod) != 'integer';\n
Ce qui donne par exemple :
table_schema table_name column_name column_type un_schema une_table_a id bigint un_schema une_table_b id bigint un_autre_schema autre_table_c id character varying un_autre_schema autre_table_d id character varying"},{"location":"utils/#trouver-les-tables-spatiales-avec-une-geometrie-non-typee","title":"Trouver les tables spatiales avec une g\u00e9om\u00e9trie non typ\u00e9e","text":"Il est important lorsqu'on cr\u00e9e des champs de type g\u00e9om\u00e9trie geometry
de pr\u00e9ciser le type des objets (point, ligne, polygone, etc.) et la projection.
On doit donc cr\u00e9er les champs comme ceci :
CREATE TABLE test (\n id serial primary key,\n geom geometry(Point, 2154)\n);\n
et non comme ceci :
CREATE TABLE test (\n id serial primary key,\n geom geometry\n);\n
C'est donc important lorsqu'on cr\u00e9e des tables \u00e0 partir de requ\u00eates SQL de toujours bien typer les g\u00e9om\u00e9tries. Par exemple :
CREATE TABLE test AS\nSELECT id,\nST_Centroid(geom)::geometry(Point, 2154) AS geom\n-- ne pas faire :\n-- ST_Centroid(geom) AS geom\nFROM autre_table\n
On peut trouver toutes les tables qui auraient \u00e9t\u00e9 cr\u00e9\u00e9es avec des champs de g\u00e9om\u00e9trie non typ\u00e9s via la requ\u00eate suivante :
SELECT *\nFROM geometry_columns\nWHERE srid = 0 OR lower(type) = 'geometry'\n;\n
Il faut corriger ces vues ou tables.
"},{"location":"utils/#trouver-les-objets-avec-des-geometries-trop-complexes","title":"Trouver les objets avec des g\u00e9om\u00e9tries trop complexes","text":"SELECT count(*)\nFROM ma_table\nWHERE ST_NPoints(geom) > 10000\n;\n
Les trop gros polygones (zones inondables, zonages issus de regroupement de nombreux objets, etc.) peuvent poser de r\u00e9els soucis de performance, notamment sur les op\u00e9rations d'intersection avec les objets d'autres couches via ST_Intersects
.
On peut corriger cela via la fonction ST_Subdivide
. Voir Documentation de ST_Subdivide
Nous souhaitons comparer deux tables de la base, par exemple une table de communes en 2021 communes_2021
et une table de communes en 2022 communes_2022
.
On peut utiliser une fonction qui utilise les possibilit\u00e9s du format hstore pour comparer les donn\u00e9es entre elles.
-- On ajoute le support du format hstore\nCREATE EXTENSION IF NOT EXISTS hstore;\n\n-- On cr\u00e9e la fonction de comparaison\nDROP FUNCTION compare_tables(text,text,text,text,text,text[]);\nCREATE OR REPLACE FUNCTION compare_tables(\n p_schema_name_a text,\n p_table_name_a text,\n p_schema_name_b text,\n p_table_name_b text,\n p_common_identifier_field text,\n p_excluded_fields text[]\n\n) RETURNS TABLE(\n uid text,\n status text,\n table_a_values hstore,\n table_b_values hstore\n)\n LANGUAGE plpgsql\n AS $_$\nDECLARE\n sqltemplate text;\nBEGIN\n\n -- Compare data\n sqltemplate = '\n SELECT\n coalesce(ta.\"%1$s\", tb.\"%1$s\") AS \"%1$s\",\n CASE\n WHEN ta.\"%1$s\" IS NULL THEN ''not in table A''\n WHEN tb.\"%1$s\" IS NULL THEN ''not in table B''\n ELSE ''table A != table B''\n END AS status,\n CASE\n WHEN ta.\"%1$s\" IS NULL THEN NULL\n ELSE (hstore(ta.*) - ''%6$s''::text[]) - (hstore(tb) - ''%6$s''::text[])\n END AS values_in_table_a,\n CASE\n WHEN tb.\"%1$s\" IS NULL THEN NULL\n ELSE (hstore(tb.*) - ''%6$s''::text[]) - (hstore(ta) - ''%6$s''::text[])\n END AS values_in_table_b\n FROM \"%2$s\".\"%3$s\" AS ta\n FULL JOIN \"%4$s\".\"%5$s\" AS tb\n ON ta.\"%1$s\" = tb.\"%1$s\"\n WHERE\n (hstore(ta.*) - ''%6$s''::text[]) != (hstore(tb.*) - ''%6$s''::text[])\n OR (ta.\"%1$s\" IS NULL)\n OR (tb.\"%1$s\" IS NULL)\n ';\n\n RETURN QUERY\n EXECUTE format(sqltemplate,\n p_common_identifier_field,\n p_schema_name_a,\n p_table_name_a,\n p_schema_name_b,\n p_table_name_b,\n p_excluded_fields\n );\n\nEND;\n$_$;\n
Cette fonction attend en param\u00e8tres
referentiels
communes_2021
referentiels
communes_2022
code_commune
array['region', 'departement']
La requ\u00eate \u00e0 lancer est la suivantes
SELECT \"uid\", \"status\", \"table_a_values\", \"table_b_values\"\nFROM compare_tables(\n 'referentiels', 'commune_2021',\n 'referentiels', 'commune_2022',\n 'code_commune',\n array['region', 'departement']\n)\nORDER BY status, uid\n;\n
Exemple de donn\u00e9es renvoy\u00e9es:
uid status table_a_values table_b_values 12345 not in table A NULL \"annee_ref\"=>\"2022\", \"nom_commune\"=>\"Nouvelle commune\", \"population\"=>\"5723\" 97612 not in table B \"annee_ref\"=>\"2021\", \"nom_commune\"=>\"Ancienne commune\", \"population\"=>\"840\" NULL 97602 table A != table B \"annee_ref\"=>\"2021\", \"population\"=>\"1245\" \"annee_ref\"=>\"2022\", \"population\"=>\"1322\"Dans l'affichage ci-dessus, je n'ai pas affich\u00e9 le champ de g\u00e9om\u00e9trie, mais la fonction teste aussi les diff\u00e9rences de g\u00e9om\u00e9tries.
Attention, les performances de ce type de requ\u00eate ne sont pas forc\u00e9ment assur\u00e9es pour des volumes de donn\u00e9es importants.
"},{"location":"utils/#trouver-les-valeurs-distinctes-des-champs-dune-table","title":"Trouver les valeurs distinctes des champs d'une table","text":"Pour comprendre quelles donn\u00e9es sont pr\u00e9sentes dans une table PostgreSQL, vous pouvez exploiter la puissance des fonctions de manipulation du JSON
et r\u00e9cup\u00e9rer automatiquement toutes les valeurs distinctes d'une table.
Cela permet de lister les champs de cette table et de bien se repr\u00e9senter ce qu'ils contiennent.
SELECT\n -- nom du champ de la table\n key AS champ,\n\n -- On regroupe les valeurs distinctes du champ\n -- depuis le JSON calcul\u00e9 plus bas via to_jsonb\n -- On compte les valeurs distinctes\n count(DISTINCT value) AS nombre,\n\n -- On r\u00e9cup\u00e8re les valeurs uniques pour ce champ\n json_agg(DISTINCT value) AS valeurs\nFROM\n -- Table dans laquelle chercher les valeurs uniques\n velo.amenagement AS i,\n -- Transformation de chaque ligne de la table en JSON (paires cl\u00e9/valeurs)\n jsonb_each(\n -- on utilise le - 'id' - 'geom' pour ne pas r\u00e9cup\u00e9rer les valeurs de ces champs\n to_jsonb(i) - 'id' - 'geom'\n )\n-- On regroupe par cl\u00e9, c'est-\u00e0-dire par champ\nGROUP BY key;\n
ce qui donnera comme r\u00e9sultat
champ | nombre | valeurs\n------------+--------+--------------------------------------------------------------------------------------------------------\n commune | 8 | [\"AMBON\", \"ARZAL\", \"BILLIERS\", \"LA ROCHE-BERNARD\", \"LE GUERNO\", \"MUZILLAC\", \"NIVILLAC\", \"SAINT-DOLAY\"]\n gestionnai | 3 | [\"Commune\", \"D\u00e9partement\", \"EPCI\"]\n id_iti | 9 | [\"iti_02\", \"iti_03\", \"iti_06\", \"iti_07\", \"iti_08\", \"iti_09\", \"iti_13\", \"iti_15\", \"iti_18\"]\n insee | 9 | [\"56002\", \"56004\", \"56018\", \"56077\", \"56143\", \"56147\", \"56149\", \"56195\", \"56212\"]\n maitre_ouv | 3 | [\"Commune\", \"D\u00e9partement\", \"EPCI\"]\n rlv_chauss | 5 | [\"Double sens\", \"Interdit \u00e0 la circ.\", \"NC\", \"Rond-point\", \"Sens unique\"]\n rlv_md_dx_ | 5 | [\"Aucun am\u00e9nagement\", \"Bande\", \"Contresens cyclable\", \"Voie uniquement pi\u00e9tonne\", \"Voie verte\"]\n rlv_pente | 5 | [\"Forte (ponctuelle)\", \"Forte (tron\u00e7on)\", \"Moyenne\", \"NC\", \"Nulle ou faible\"]\n rlv_vitess | 7 | [\"< 20\", \"20\", \"30\", \"50\", \"70\", \"80 et plus\", \"NC\"]\n type_surfa | 3 | [\"Lisse\", \"Meuble\", \"Rugueux\"]\n vvv | 3 | [\"V3\", \"V42\", \"V45\"]\n
Points d'attention:
Continuer vers Gestion des droits
"},{"location":"validate_geometries/","title":"Correction des g\u00e9om\u00e9tries","text":"Avec PostgreSQL on peut tester la validit\u00e9 des g\u00e9om\u00e9tries d'une table, comprendre la raison et localiser les soucis de validit\u00e9:
SELECT\nid_parcelle,\n-- v\u00e9rifier si la g\u00e9om est valide\nST_IsValid(geom) AS validite_geom,\n-- connaitre la raison d'invalidit\u00e9\nst_isvalidreason(geom) AS validite_raison,\n-- sortir un point qui localise le souci de validit\u00e9\nST_SetSRID(location(st_isvaliddetail(geom)), 2154) AS geom\nFROM z_formation.parcelle_havre\nWHERE ST_IsValid(geom) IS FALSE\n
qui renvoie 2 erreurs de polygones crois\u00e9s.
id_parcelle validite_geom validite_raison point_invalide 707847 False Self-intersection[492016.260004897 6938870.66384629] 010100000041B93E0AC1071E4122757CAA3D785A41 742330 False Self-intersection[489317.48266784 6939616.89391708] 0101000000677A40EE95DD1D41FBEF3539F8785A41et qu'on peut ouvrir comme une nouvelle couche, avec le champ g\u00e9om\u00e9trie point_invalide, ce qui permet de visualiser dans QGIS les positions des erreurs.
PostGIS fournir l'outil ST_MakeValid pour corriger automatiquement les g\u00e9om\u00e9tries invalides. On peut l'utiliser pour les lignes et polygones.
Attention, pour les polygones, cela peut conduire \u00e0 des g\u00e9om\u00e9tries de type diff\u00e9rent (par exemple une polygone \u00e0 2 noeuds devient une ligne). On utilise donc aussi la fonction ST_CollectionExtract pour ne r\u00e9cup\u00e9rer que les polygones.
-- Corriger les g\u00e9om\u00e9tries\nUPDATE z_formation.parcelle_havre\nSET geom = ST_Multi(ST_CollectionExtract(ST_MakeValid(geom), 3))\nWHERE NOT ST_isvalid(geom)\n\n-- Tester\nSELECT count(*)\nFROM z_formation.parcelle_havre\nWHERE NOT ST_isvalid(geom)\n
Il faut aussi supprimer l'ensemble des lignes dans la table qui ne correspondent pas au type de la couche import\u00e9e. Par exemple, pour les polygones, supprimer les objets dont le nombre de n\u0153uds est inf\u00e9rieur \u00e0 3.
SELECT *\nFROM z_formation.parcelle_havre\nWHERE ST_NPoints(geom) < 3\n
DELETE\nFROM z_formation.parcelle_havre\nWHERE ST_NPoints(geom) < 3\n
Continuer vers V\u00e9rifier la topologie
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index ab0a2f3..0b419a8 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,78 +2,78 @@