From 1ce595885fbae78cb052adeb6e77d32c8614bdbc Mon Sep 17 00:00:00 2001 From: 3Liz Bot Date: Mon, 18 Dec 2023 16:44:29 +0000 Subject: [PATCH] Deployed 1cefef2 with MkDocs version: 1.5.3 --- search/search_index.json | 2 +- sitemap.xml | 38 +++++++++++++++++++------------------- sitemap.xml.gz | Bin 366 -> 365 bytes triggers/index.html | 5 ++++- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/search/search_index.json b/search/search_index.json index 7764495..f539845 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"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):

"},{"location":"#sommaire","title":"Sommaire","text":""},{"location":"check_topology/","title":"V\u00e9rifier la topologie","text":""},{"location":"check_topology/#deplacer-les-noeuds-sur-une-grille","title":"D\u00e9placer les noeuds sur une grille","text":"

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:

"},{"location":"check_topology/#accrocher-les-geometries-sur-dautres-geometries","title":"Accrocher les g\u00e9om\u00e9tries sur d'autres g\u00e9om\u00e9tries","text":"

Dans PostGIS, on peut utiliser la fonction ST_Snap dans une requ\u00eate SQL pour d\u00e9placer les noeuds 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 noeuds de la table de r\u00e9f\u00e9rence, pas aux segments. Il serait n\u00e9anmoins possible de cr\u00e9er automatiquement les noeuds situ\u00e9s sur la projection du noeud \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:

"},{"location":"fdw/#le-fdw-ogr_fdw-pour-lire-des-donnees-vectorielles","title":"Le FDW ogr_fdw pour lire des donn\u00e9es vectorielles","text":"

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\u00e9cuperer ces donn\u00e9es:

-- 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
-- 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 bizares\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\nCREATE USER MAPPING FOR \"\"\nSERVER foreign_server_test\nOPTIONS (user 'mon_utilisateur', 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 chaine 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 Seinte-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 roles (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\nGRANT USAGE ON SCHEMA public, nouveau_schema TO \"invite\";\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\";\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-- Ex: on enl\u00e8ve la possibilit\u00e9 de faire des suppresions\nREVOKE DELETE ON TABLE nouveau_schema.observation FROM \"invite\";\n\n-- On enl\u00e8ve tous les privil\u00e8ges sur les tables du sch\u00e9ma public\nREVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"invite\";\n\n-- On donne les droits de s\u00e9lection sur les tables du sch\u00e9ma public\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO \"invite\";\n

Droits par d\u00e9faut sur les nouveaux objets cr\u00e9\u00e9s

-- TODO\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

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 autoincr\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 reimporter 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'autoincr\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.

"},{"location":"join_data/#exemple-2-observations-et-communes","title":"Exemple 2: observations et communes","text":"
-- 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 9243

On 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.

"},{"location":"join_data/#joindre-des-polygones-avec-des-polygones","title":"Joindre des polygones avec des polygones","text":"

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 renvoit 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.9289621127432
SELECT\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
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:

ATTENTION:

ATTENTION:

"},{"location":"join_data/#distances-et-tampons-entre-couches","title":"Distances et tampons entre couches","text":"

Pour 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:

"},{"location":"links_and_data/#base-de-donnees","title":"Base de donn\u00e9es","text":"

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 :

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.

"},{"location":"links_and_data/#quelques-extensions-qgis","title":"Quelques extensions QGIS","text":"

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

-- Centroides \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 \"El\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:

"},{"location":"postgresql_in_qgis/#le-gestionnaire-de-base-de-donnees","title":"Le Gestionnaire de base de donn\u00e9es","text":"

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.

"},{"location":"sql_select/#visualiser-une-requete-dans-qgis","title":"Visualiser une requ\u00eate dans QGIS","text":"

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

"},{"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 modifie ou ajou\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 renvoit 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 renvoit 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.

"},{"location":"triggers/#ecrire-les-actions-produites-sur-une-table","title":"\u00c9crire les actions produites sur une table","text":"

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":"tutoriel/","title":"Tutoriel","text":"

Afin de vous entrainer il existe diff\u00e9rentes tutoriels en ligne vous permettant de vous exercer.

"},{"location":"union/","title":"Rassembler des donn\u00e9es de plusieurs tables","text":"

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.

Par exemple, pour une s\u00e9quence monschema.ma_sequence, si la requ\u00eate suivante \u00e9choue, c'est que la s\u00e9quence n'est en effet pas correctement configur\u00e9e :

SELECT currval('\"monschema\".\"test_id_seq\"');\n

Pour ajouter le support de l'auto-incr\u00e9mentation sur un champ entier \u00e0 une table existante, on peut utiliser les commandes suivantes:

-- Cr\u00e9ation de la s\u00e9quence\nCREATE SEQUENCE monschema.test_id_seq;\n\n-- Modification du champ pour ajouter la valeur par d\u00e9faut\nALTER TABLE monschema.test ALTER COLUMN id SET DEFAULT nextval('\"monschema\".\"test_id_seq\"');\n\n-- Modification de la valeur actuelle de la s\u00e9quence au maximum du champ id\nSELECT setval('\"monschema\".\"test_id_seq\"', (SELECT max(id) FROM monschema.test));\n\n-- D\u00e9clarer \u00e0 PostgreSQL que la s\u00e9quence et le champ sont li\u00e9s\nALTER SEQUENCE monschema.test_id_seq OWNED BY monschema.test.id;\n

Dans l'exemple ci-dessus, le sch\u00e9ma est pr\u00e9cis\u00e9.

"},{"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_naturels
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

SELECT\najout_champs_dynamiques('test', 'commune', 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text')\n;\n
-- 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 renvoit 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 renvoit 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/#tester-les-differences-entre-2-tables-de-meme-structure","title":"Tester les diff\u00e9rences entre 2 tables de m\u00eame structure","text":"

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

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/#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    action_statement AS definition\nFROM information_schema.triggers\nGROUP BY 1,2,3,4,6,7,8\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 definition gestion acteur gestion tr_date_maj UPDATE BEFORE EXECUTE FUNCTION occtax.maj_date() occtax organisme occtax tr_date_maj UPDATE BEFORE EXECUTE FUNCTION occtax.maj_date() taxon iso_metadata_reference taxon update_imr_timestamp UPDATE BEFORE 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 johndoe

On peut bien s\u00fbr modifier la clause WHERE pour filtrer plus ou moins les fonctions renvoy\u00e9es.

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 renvoit 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] 0101000000677A40EE95DD1D41FBEF3539F8785A41

et 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 noeuds 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):

"},{"location":"#sommaire","title":"Sommaire","text":""},{"location":"check_topology/","title":"V\u00e9rifier la topologie","text":""},{"location":"check_topology/#deplacer-les-noeuds-sur-une-grille","title":"D\u00e9placer les noeuds sur une grille","text":"

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:

"},{"location":"check_topology/#accrocher-les-geometries-sur-dautres-geometries","title":"Accrocher les g\u00e9om\u00e9tries sur d'autres g\u00e9om\u00e9tries","text":"

Dans PostGIS, on peut utiliser la fonction ST_Snap dans une requ\u00eate SQL pour d\u00e9placer les noeuds 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 noeuds de la table de r\u00e9f\u00e9rence, pas aux segments. Il serait n\u00e9anmoins possible de cr\u00e9er automatiquement les noeuds situ\u00e9s sur la projection du noeud \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:

"},{"location":"fdw/#le-fdw-ogr_fdw-pour-lire-des-donnees-vectorielles","title":"Le FDW ogr_fdw pour lire des donn\u00e9es vectorielles","text":"

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\u00e9cuperer ces donn\u00e9es:

-- 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
-- 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 bizares\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\nCREATE USER MAPPING FOR \"\"\nSERVER foreign_server_test\nOPTIONS (user 'mon_utilisateur', 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 chaine 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 Seinte-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 roles (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\nGRANT USAGE ON SCHEMA public, nouveau_schema TO \"invite\";\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\";\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-- Ex: on enl\u00e8ve la possibilit\u00e9 de faire des suppresions\nREVOKE DELETE ON TABLE nouveau_schema.observation FROM \"invite\";\n\n-- On enl\u00e8ve tous les privil\u00e8ges sur les tables du sch\u00e9ma public\nREVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"invite\";\n\n-- On donne les droits de s\u00e9lection sur les tables du sch\u00e9ma public\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO \"invite\";\n

Droits par d\u00e9faut sur les nouveaux objets cr\u00e9\u00e9s

-- TODO\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

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 autoincr\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 reimporter 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'autoincr\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.

"},{"location":"join_data/#exemple-2-observations-et-communes","title":"Exemple 2: observations et communes","text":"
-- 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 9243

On 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.

"},{"location":"join_data/#joindre-des-polygones-avec-des-polygones","title":"Joindre des polygones avec des polygones","text":"

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 renvoit 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.9289621127432
SELECT\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
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:

ATTENTION:

ATTENTION:

"},{"location":"join_data/#distances-et-tampons-entre-couches","title":"Distances et tampons entre couches","text":"

Pour 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:

"},{"location":"links_and_data/#base-de-donnees","title":"Base de donn\u00e9es","text":"

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 :

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.

"},{"location":"links_and_data/#quelques-extensions-qgis","title":"Quelques extensions QGIS","text":"

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

-- Centroides \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 \"El\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:

"},{"location":"postgresql_in_qgis/#le-gestionnaire-de-base-de-donnees","title":"Le Gestionnaire de base de donn\u00e9es","text":"

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.

"},{"location":"sql_select/#visualiser-une-requete-dans-qgis","title":"Visualiser une requ\u00eate dans QGIS","text":"

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 modifie ou ajou\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 renvoit 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 renvoit 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.

"},{"location":"triggers/#ecrire-les-actions-produites-sur-une-table","title":"\u00c9crire les actions produites sur une table","text":"

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":"tutoriel/","title":"Tutoriel","text":"

Afin de vous entrainer il existe diff\u00e9rentes tutoriels en ligne vous permettant de vous exercer.

"},{"location":"union/","title":"Rassembler des donn\u00e9es de plusieurs tables","text":"

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.

Par exemple, pour une s\u00e9quence monschema.ma_sequence, si la requ\u00eate suivante \u00e9choue, c'est que la s\u00e9quence n'est en effet pas correctement configur\u00e9e :

SELECT currval('\"monschema\".\"test_id_seq\"');\n

Pour ajouter le support de l'auto-incr\u00e9mentation sur un champ entier \u00e0 une table existante, on peut utiliser les commandes suivantes:

-- Cr\u00e9ation de la s\u00e9quence\nCREATE SEQUENCE monschema.test_id_seq;\n\n-- Modification du champ pour ajouter la valeur par d\u00e9faut\nALTER TABLE monschema.test ALTER COLUMN id SET DEFAULT nextval('\"monschema\".\"test_id_seq\"');\n\n-- Modification de la valeur actuelle de la s\u00e9quence au maximum du champ id\nSELECT setval('\"monschema\".\"test_id_seq\"', (SELECT max(id) FROM monschema.test));\n\n-- D\u00e9clarer \u00e0 PostgreSQL que la s\u00e9quence et le champ sont li\u00e9s\nALTER SEQUENCE monschema.test_id_seq OWNED BY monschema.test.id;\n

Dans l'exemple ci-dessus, le sch\u00e9ma est pr\u00e9cis\u00e9.

"},{"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_naturels
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

SELECT\najout_champs_dynamiques('test', 'commune', 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text')\n;\n
-- 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 renvoit 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 renvoit 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/#tester-les-differences-entre-2-tables-de-meme-structure","title":"Tester les diff\u00e9rences entre 2 tables de m\u00eame structure","text":"

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

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/#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    action_statement AS definition\nFROM information_schema.triggers\nGROUP BY 1,2,3,4,6,7,8\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 definition gestion acteur gestion tr_date_maj UPDATE BEFORE EXECUTE FUNCTION occtax.maj_date() occtax organisme occtax tr_date_maj UPDATE BEFORE EXECUTE FUNCTION occtax.maj_date() taxon iso_metadata_reference taxon update_imr_timestamp UPDATE BEFORE 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 johndoe

On peut bien s\u00fbr modifier la clause WHERE pour filtrer plus ou moins les fonctions renvoy\u00e9es.

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 renvoit 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] 0101000000677A40EE95DD1D41FBEF3539F8785A41

et 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 noeuds 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 0a5926c..55bb6be 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,97 +2,97 @@ https://docs.3liz.org/formation-postgis/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/check_topology/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/fdw/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/filter_data/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/grant/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/group_data/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/import_data/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/join_data/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/links_and_data/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/merge_geometries/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/perform_calculation/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/postgresql_in_qgis/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/save_queries/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/sql_select/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/triggers/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/tutoriel/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/union/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/utils/ - 2023-12-12 + 2023-12-18 daily https://docs.3liz.org/formation-postgis/validate_geometries/ - 2023-12-12 + 2023-12-18 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 941441a1070a254097ab69185a1ffa665e7bed49..2c82aab25de1a5b9b994f39ef1056f02ea61f432 100644 GIT binary patch literal 365 zcmV-z0h0b7iwFqqc7SC9|8r?{Wo=<_E_iKh0M(bxZo?o9$M1cLDEEYZT)HVuZ+n96 z93kRTRtG28Y1TdcNLx+4PnrrBz!>Yt=O3#5SBKdd0z2}%d{r*<47{P%dArL$-rtIC zzONV67?44-=u(Q>TXoa@+GeqSxj?)Tj`oABva2( zIEHD-aJfPEeT{LT&}oS@MNcf&v8vSU{n%Nvbkx@C<$6=B*2QWYUGBCAH*?h9!Xdy= zx7y*QQV*6JmHNf{3t$|zIWd-zLJ_L9ozRpbjKBNI(vH2(z&<8qJ8;wgjN(JOB^a#H~Cep}VZl z&&AArp2+3ZbpZC~|2|zj&2z8@HKR>Ju+Di(UG<1n9scSn5 zk_hZ4IiCnBI)CO``z8_I7J`M^0<|E)K>~8nL71Gi(P##o?Les triggerspour contrôler certaines données avant enregistrement
  • pour lancer des requêtes après certaines actions (historiques de modifications)
  • +

    Des fonctions trigger sont associées aux triggers. Elles peuvent être écrites 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 été lancé. +Lire les derniers paragraphes ici pour en savoir plus.

    Calcul automatique de certains champs#

    On crée une table borne_incendie pour pouvoir tester cette fonctionnalité:

    1
    @@ -1099,7 +1102,7 @@ 

    Écrire les actions produite - January 24, 2023 + December 18, 2023