├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── check_topology.md
├── fdw.md
├── filter_data.md
├── grant.md
├── group_data.md
├── import_data.md
├── index.md
├── join_data.md
├── links_and_data.md
├── logo.svg
├── media
│ ├── qgis_connexion_PostgreSQL.png
│ ├── qgis_creer_schema_explorateur.png
│ ├── qgis_creer_table_explorateur.png
│ ├── qgis_rendu_simplification_fournisseur.png
│ ├── qgis_traitement_exporter_dialogue_algorithme.png
│ └── qgis_traitement_exporter_postgresql_ogr.png
├── merge_geometries.md
├── perform_calculation.md
├── postgresql_in_qgis.md
├── save_queries.md
├── sql_select.md
├── triggers.md
├── tutoriel.md
├── union.md
├── utils.md
└── validate_geometries.md
├── mkdocs.yml
└── requirements.txt
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: 📖 Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 |
13 | - name: Get source code
14 | uses: actions/checkout@v3
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Set up Python 3.10
19 | uses: actions/setup-python@v4.5.0
20 | with:
21 | python-version: '3.10'
22 |
23 | - name: Set up NodeJS (for search index prebuilding)
24 | uses: actions/setup-node@v3.6.0
25 | with:
26 | node-version: '12'
27 |
28 | - name: Cache project dependencies (pip)
29 | uses: actions/cache@v3.2.6
30 | with:
31 | path: ~/.cache/pip
32 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
33 | restore-keys: |
34 | ${{ runner.os }}-pip-
35 | ${{ runner.os }}-
36 |
37 | - name: Install dependencies
38 | run: |
39 | python -m pip install --upgrade pip setuptools wheel
40 | python -m pip install -r requirements.txt
41 |
42 | - name: Deploy to Github Pages
43 | run: |
44 | git config --global user.name "${{ secrets.BOT_NAME }}"
45 | git config --global user.email "${{ secrets.BOT_MAIL }}"
46 | mkdocs gh-deploy --clean --force --verbose
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .venv/
3 | build/
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Formation PostGIS
2 |
3 | Visible sur https://docs.3liz.org/formation-postgis/
4 |
5 | 
6 |
--------------------------------------------------------------------------------
/docs/check_topology.md:
--------------------------------------------------------------------------------
1 | # Vérifier la topologie
2 |
3 | ## Déplacer les nœuds sur une grille
4 |
5 | Avant de vérifier la topologie, il faut au préalable avoir des géométries valides (cf. chapitre précédent).
6 |
7 | Certaines micro-erreurs de topologie peuvent peuvent être corrigées en réalisant une simplification des données à l'aide d'une grille, par exemple pour corriger des soucis d'arrondis. Pour cela, PostGIS a une fonction **ST_SnapToGrid**.
8 |
9 | On peut utiliser conjointement **ST_Simplify** et **ST_SnapToGrid** pour effectuer une première correction sur les données. Attention, ces fonctions modifient la donnée. A vous de choisir la bonne tolérance, par exemple 5 cm, qui dépend de votre donnée et de votre cas d'utilisation.
10 |
11 | Tester la simplification en lançant la requête suivante, et en chargeant le résultat comme une nouvelle couche dans QGIS
12 |
13 | ```sql
14 | SELECT
15 | ST_Multi(
16 | ST_CollectionExtract(
17 | ST_MakeValid(
18 | ST_SnapToGrid(
19 | st_simplify(geom,0),
20 | 0.05 -- 5 cm
21 | )
22 | ),
23 | 3
24 | )
25 | )::geometry(multipolygon, 2154)
26 | FROM z_formation.parcelle_havre
27 | ;
28 | ```
29 |
30 | Une fois le résultat visuellement testé dans QGIS, par comparaison avec la table source, on peut choisir de *modifier la géométrie de la table* avec la version simplifiée des données:
31 |
32 | ```sql
33 | -- Parcelles
34 | UPDATE z_formation.parcelle_havre
35 | SET geom =
36 | ST_Multi(
37 | ST_CollectionExtract(
38 | ST_MakeValid(
39 | ST_SnapToGrid(
40 | st_simplify(geom,0),
41 | 0.05 -- 5 cm
42 | )
43 | ),
44 | 3
45 | )
46 | )
47 | ;
48 | ;
49 | ```
50 |
51 | **Attention:** Si vous avez d'autres tables avec des objets en relation spatiale avec cette table, il faut aussi effectuer le même traitement pour que les géométries de toutes les couches se calent sur la même grille. Par exemple la table des zonages.
52 |
53 | ```sql
54 | UPDATE z_formation.zone_urba
55 | SET geom =
56 | ST_Multi(
57 | ST_CollectionExtract(
58 | ST_MakeValid(
59 | ST_SnapToGrid(
60 | st_simplify(geom,0),
61 | 0.05 -- 5 cm
62 | )
63 | ),
64 | 3
65 | )
66 | )
67 | ;
68 | ```
69 |
70 |
71 | ## Repérer certaines erreurs de topologies
72 |
73 | PostGIS possède de nombreuses fonctions de **relations spatiales** qui permettent de trouver les objets qui se chevauchent, qui se touchent, etc. Ces fonctions peuvent être utilisées pour comparer les objets d'une même table, ou de deux tables différentes. Voir: https://postgis.net/docs/reference.html#Spatial_Relationships_Measurements
74 |
75 | Par exemple, trouver les parcelles voisines qui se recouvrent: on utilise la fonction **ST_Overlaps**. On peut créer une couche listant les recouvrements:
76 |
77 |
78 | ```sql
79 | DROP TABLE IF EXISTS z_formation.recouvrement_parcelle_voisines;
80 | CREATE TABLE z_formation.recouvrement_parcelle_voisines AS
81 | SELECT DISTINCT ON (geom)
82 | parcelle_a, parcelle_b, aire_a, aire_b, ST_Area(geom) AS aire, geom
83 | FROM (
84 | SELECT
85 | a.id_parcelle AS parcelle_a, ST_Area(a.geom) AS aire_a,
86 | b.id_parcelle AS parcelle_b, ST_Area(b.geom) AS aire_b,
87 | (ST_Multi(
88 | st_collectionextract(
89 | ST_MakeValid(ST_Intersection(a.geom, b.geom))
90 | , 3)
91 | ))::geometry(MultiPolygon,2154) AS geom
92 | FROM z_formation.parcelle_havre AS a
93 | JOIN z_formation.parcelle_havre AS b
94 | ON a.id_parcelle != b.id_parcelle
95 | --ON ST_Intersects(a.geom, b.geom)
96 | AND ST_Overlaps(a.geom, b.geom)
97 | ) AS voisin
98 | ORDER BY geom
99 | ;
100 |
101 | CREATE INDEX ON z_formation.recouvrement_parcelle_voisines USING GIST (geom);
102 |
103 | ```
104 |
105 | On peut alors ouvrir cette couche dans QGIS pour zoomer sur chaque objet de recouvrement.
106 |
107 | Récupérer la liste des identifiants de ces parcelles:
108 |
109 | ```sql
110 | SELECT string_agg( parcelle_a::text, ',') FROM z_formation.recouvrement_parcelle_voisines;
111 | ```
112 |
113 | On peut utiliser le résultat de cette requête pour sélectionner les parcelles problématiques: on sélectionne le résultat dans le tableau du gestionnaire de base de données, et on copie (CTRL + C). On peut alors utiliser cette liste dans une **sélection par expression** dans QGIS, avec par exemple l'expression
114 |
115 | ```sql
116 | "id_parcelle" IN (
117 | 729091,742330,742783,742513,742514,743114,742992,742578,742991,742544,743009,744282,744378,744378,744281,744199,743646,746445,743680,744280,
118 | 743653,743812,743208,743812,743813,744199,694298,694163,721712,707463,744412,707907,707069,721715,721715,696325,696372,746305,722156,722555,
119 | 722195,714500,715969,722146,722287,723526,720296,720296,722296,723576,723572,723572,723571,724056,723570,723568,740376,722186,724055,714706,
120 | 723413,723988,721808,721808,723413,724064,723854,723854,724063,723518,720736,720653,741079,741227,740932,740932,740891,721259,741304,741304,
121 | 741501,741226,741812)
122 | ```
123 |
124 | Une fois les parcelles sélectionnées, on peut utiliser certains outils de QGIS pour faciliter la correction:
125 |
126 | * plugin **Vérifier les géométries** en cochant la case **Uniquement les entités sélectionnées**
127 | * plugin **Accrochage de géométrie**
128 | * plugin **Go 2 next feature** pour facilement zoomer d'objets en objets
129 |
130 |
131 | ## Accrocher les géométries sur d'autres géométries
132 |
133 | Dans PostGIS, on peut utiliser la fonction **ST_Snap** dans une requête SQL pour déplacer les nœuds d'une géométrie et les coller sur ceux d'une autre.
134 |
135 | Par exemple, coller les géométries choisies (via identifiants dans le WHERE) de la table de zonage sur les parcelles choisies (via identifiants dans le WHERE):
136 |
137 | ```sql
138 | WITH a AS (
139 | SELECT DISTINCT z.id_zone_urba,
140 | st_force2d(
141 | ST_Multi(
142 | ST_Snap(
143 | ST_Simplify(z.geom, 1),
144 | ST_Collect(p.geom),
145 | 0.5
146 | )
147 | )
148 | ) AS geom
149 | FROM z_formation.parcelle_havre AS p
150 | INNER JOIN z_formation.zone_urba AS z
151 | ON st_dwithin(z.geom, p.geom, 0.5)
152 | WHERE TRUE
153 | AND z.id_zone_urba IN (113,29)
154 | AND p.id_parcelle IN (711337,711339,711240,711343)
155 | GROUP BY z.id_zone_urba
156 | )
157 | UPDATE z_formation.zone_urba pz
158 | SET geom = a.geom
159 | FROM a
160 | WHERE pz.id_zone_urba = a.id_zone_urba
161 | ```
162 |
163 | **Attention:** Cette fonction ne sait coller qu'**aux nœuds** de la table de référence, pas aux segments. Il serait néanmoins possible de créer automatiquement les nœuds situés sur la projection du nœud à déplacer sur la géométrie de référence.
164 |
165 | Dans la pratique, il est très souvent fastidieux de corriger les erreurs de topologie d'une couche. Les outils automatiques ( Vérifier les géométries de QGIS ou outil v.clean de Grass) ne permettent pas toujours de bien voir ce qui a été modifié.
166 |
167 | Au contraire, une modification manuelle est plus précise, mais prend beaucoup de temps.
168 |
169 | Le Ministère du Développement Durable a mis en ligne un document intéressant sur les outils disponibles dans QGIS, OpenJump et PostgreSQL pour valider et corriger les géométries: http://www.geoinformations.developpement-durable.gouv.fr/verification-et-corrections-des-geometries-a3522.html
170 |
--------------------------------------------------------------------------------
/docs/fdw.md:
--------------------------------------------------------------------------------
1 | # Accéder à des données externes : les Foreign Data Wrapper (FDW)
2 |
3 | L'utilisation d'un FDW permet de **consulter des données externes** à la base comme si elles étaient stockées dans des tables. On peut lancer des requêtes pour récupérer seulement certains champs, filtrer les données, etc.
4 |
5 | Des **tables étrangères** sont créées, qui pointent vers les données externes. A chaque requête sur ces tables, PostgreSQL récupère les données depuis la connexion au serveur externe.
6 |
7 | On passe classiquement par les étapes suivantes:
8 |
9 | * Ajout de l'**extension** correspondant au format souhaité: `postgres_fdw` (bases PostgreSQL externes), `ogr_fdw` (données vectorielles via ogr2ogr), etc.
10 | * Création d'un **serveur** qui permet de configurer les informations de connexion au serveur externe
11 | * Création optionnelle d'un **schéma** pour y stocker les tables de ce serveur
12 | * Création manuelle ou automatique de **tables étrangères** qui pointent vers les données externes
13 | * **Requêtes** sur ces tables étrangères
14 |
15 |
16 | ## Le FDW ogr_fdw pour lire des données vectorielles
17 |
18 | Avec ce Foreign Data Wrapper **ogr_fdw**, on peut appeler n'importe quelle source de données 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.
19 |
20 | Voir la [documentation officielle de ogr_fdw](https://github.com/pramsey/pgsql-ogr-fdw).
21 |
22 | ### Installation
23 |
24 | Pour l'installer sur une machine **Linux**, il suffit d'installer le paquet correspondant à la version de PostgreSQL, par exemple `postgresql-11-ogr-fdw`.
25 |
26 | Sous **Windows**, il est disponible avec le paquet PostGIS via l'outil [StackBuilder](https://www.postgresql.org/download/windows/).
27 |
28 | ### Exemple d'utilisation: récupérer des couches d'un serveur WFS
29 |
30 | Nous allons utiliser le FDW pour récupérer des données mises à disposition sur le serveur de l'INPN via le protocole WFS.
31 |
32 | Vous pouvez d'abord tester dans QGIS quelles données sont disponibles sur ce serveur en créant une nouvelle connexion WFS avec l'URL `http://ws.carmencarto.fr/WFS/119/fxx_inpn?`
33 |
34 | Via QGIS ou un autre client à la base de données, nous pouvons maintenant montrer comment récupérer ces données:
35 |
36 | * Ajouter l'**extension** `ogr_fdw`:
37 |
38 | ```sql
39 | -- Ajouter l'extension pour lire des fichiers SIG
40 | -- Cette commande doit être lancée par un super utilisateur (ou un utilisateur ayant le droit de le faire)
41 | CREATE EXTENSION IF NOT EXISTS ogr_fdw;
42 | ```
43 |
44 | * Créer le **serveur** de données:
45 |
46 | ```sql
47 | -- Créer le serveur
48 | DROP SERVER IF EXISTS fdw_ogr_inpn_metropole;
49 | CREATE SERVER fdw_ogr_inpn_metropole FOREIGN DATA WRAPPER ogr_fdw
50 | OPTIONS (
51 | datasource 'WFS:http://ws.carmencarto.fr/WFS/119/fxx_inpn?',
52 | format 'WFS'
53 | );
54 | ```
55 |
56 | * Créer un **schéma** pour y stocker les tables étrangères:
57 |
58 | ```sql
59 | -- Créer un schéma pour la dreal
60 | CREATE SCHEMA IF NOT EXISTS inpn_metropole;
61 | ```
62 |
63 | * Créer automatiquement les **tables étrangères** qui "pointent" vers les couches du WFS, via la commande `IMPORT SCHEMA`:
64 |
65 | ```sql
66 | -- Récupérer l'ensemble des couches WFS comme des tables dans le schéma ref_dreal
67 | IMPORT FOREIGN SCHEMA ogr_all
68 | FROM SERVER fdw_ogr_inpn_metropole
69 | INTO inpn_metropole
70 | OPTIONS (
71 | -- mettre le nom des tables en minuscule et sans caractères bizarres
72 | launder_table_names 'true',
73 | -- mettre le nom des champs en minuscule
74 | launder_column_names 'true'
75 | )
76 | ;
77 | ```
78 |
79 | * Lister les tables récupérées
80 |
81 | ```sql
82 | SELECT foreign_table_schema, foreign_table_name
83 | FROM information_schema.foreign_tables
84 | WHERE foreign_table_schema = 'inpn_metropole'
85 | ORDER BY foreign_table_schema, foreign_table_name;
86 | ```
87 |
88 | ce qui montre:
89 |
90 | | foreign_table_schema | foreign_table_name |
91 | |----------------------|--------------------------------------------------|
92 | | inpn_metropole | arretes_de_protection_de_biotope |
93 | | inpn_metropole | arretes_de_protection_de_geotope |
94 | | inpn_metropole | bien_du_patrimoine_mondial_de_l_unesco |
95 | | inpn_metropole | geoparcs |
96 | | inpn_metropole | ospar |
97 | | inpn_metropole | parc_naturel_marin |
98 | | inpn_metropole | parcs_nationaux |
99 | | inpn_metropole | parcs_naturels_regionaux |
100 | | inpn_metropole | reserves_biologiques |
101 | | inpn_metropole | reserves_de_la_biosphere |
102 | | inpn_metropole | reserves_integrales_de_parcs_nationaux |
103 | | inpn_metropole | reserves_nationales_de_chasse_et_faune_sauvage |
104 | | inpn_metropole | reserves_naturelles_nationales |
105 | | inpn_metropole | reserves_naturelles_regionales |
106 | | inpn_metropole | rnc |
107 | | inpn_metropole | sites_d_importance_communautaire |
108 | | inpn_metropole | sites_d_importance_communautaire_joue__zsc_sic_ |
109 | | inpn_metropole | sites_ramsar |
110 | | inpn_metropole | terrains_des_conservatoires_des_espaces_naturels |
111 | | inpn_metropole | terrains_du_conservatoire_du_littoral |
112 | | inpn_metropole | zico |
113 | | inpn_metropole | znieff1 |
114 | | inpn_metropole | znieff1_mer |
115 | | inpn_metropole | znieff2 |
116 | | inpn_metropole | znieff2_mer |
117 | | inpn_metropole | zones_de_protection_speciale |
118 |
119 |
120 | * **Lire les données** des couches WFS via une **simple requête** sur les tables étrangères:
121 |
122 | ```sql
123 | -- Tester
124 | SELECT *
125 | FROM inpn_metropole.zico
126 | LIMIT 1;
127 | ```
128 |
129 | **Attention**, lorsqu'on accède depuis PostgreSQL à un serveur WFS, on est tributaire
130 |
131 | * des performances de ce serveur,
132 | * et du temps de transfert des données vers la base.
133 |
134 | Nous **déconseillons fortement** dans ce cas de charger le serveur externe en réalisant des requêtes complexes (ou trop fréquentes) sur ces tables étrangères, surtout lorsque les données évoluent peu.
135 |
136 | Au contraire, nous conseillons de créer des **vues matérialisées** à partir des tables étrangères pour éviter des requêtes lourdes en stockant les données dans la base:
137 |
138 | ```sql
139 | -- Pour éviter de requêter à chaque fois le WFS, on peut créer des vues matérialisées
140 |
141 | -- suppression de la vue si elle existe déjà
142 | DROP MATERIALIZED VIEW IF EXISTS inpn_metropole.vm_zico;
143 |
144 | -- création de la vue: on doit parfois forcer le type de géométrie attendue
145 | CREATE MATERIALIZED VIEW inpn_metropole.vm_zico AS
146 | SELECT *,
147 | (ST_multi(msgeometry))::geometry(multipolygon, 2154) AS geom
148 | FROM inpn_metropole.zico
149 | ;
150 |
151 | -- Ajout d'un index spatial sur la géométrie
152 | CREATE INDEX ON inpn_metropole.vm_zico USING GIST (geom);
153 | ```
154 |
155 | Une fois la vue créée, vous pouvez faire vos requêtes sur cette vue, avec des performances bien meilleures et un allègement de la charge sur le serveur externe.
156 |
157 | Pour **rafraîchir** les données à partir du serveur WFS, il suffit de rafraîchir la ou les vues matérialisées:
158 |
159 | ```sql
160 | -- Rafraîchir la vue, par exemple à lancer une fois par mois
161 | REFRESH MATERIALIZED VIEW inpn_metropole.vm_zico;
162 | ```
163 |
164 | ## Le FDW postgres_fdw pour accéder aux tables d'une autre base de données PostgreSQL
165 |
166 | ```sql
167 | -- Création du serveur externe
168 | DROP SERVER IF EXISTS foreign_server_test CASCADE;
169 | CREATE SERVER IF NOT EXISTS foreign_server_test
170 | FOREIGN DATA WRAPPER postgres_fdw
171 | OPTIONS (host 'mon_serveur_postgresql_externe.com', port '5432', dbname 'external_database')
172 | ;
173 |
174 | -- on déclare se connecter en tant qu'utilisateur `mon_utilisateur_externe` lorsqu'on récupère des données
175 | -- depuis une connexion avec l'utilisateur interne `mon_utilisateur`
176 | CREATE USER MAPPING FOR "mon_utilisateur"
177 | SERVER foreign_server_test
178 | OPTIONS (user 'mon_utilisateur_externe', password '***********');
179 |
180 | -- on stocke les tables étrangères dans un schéma spécifique pour isoler des autres schémas en dur
181 | DROP SCHEMA IF EXISTS fdw_test_schema CASCADE;
182 | CREATE SCHEMA IF NOT EXISTS fdw_test_schema;
183 |
184 | -- importer automatiquement les tables d'un schéma de la base distante
185 | IMPORT FOREIGN SCHEMA "un_schema"
186 | LIMIT TO ("une_table", "une_autre_table")
187 | FROM SERVER foreign_server_test
188 | INTO fdw_test_schema;
189 |
190 | -- Tester
191 | SELECT * FROM fdw_test_schema.une_table LIMIT 1;
192 | ```
193 |
194 |
195 | Continuer vers [Tutoriels en ligne](./tutoriel.md)
196 |
--------------------------------------------------------------------------------
/docs/filter_data.md:
--------------------------------------------------------------------------------
1 | # Filtrer les données : la clause WHERE
2 |
3 | Récupérer les données à partir de la **valeur exacte d'un champ**. Ici le nom de la commune
4 |
5 | ```sql
6 | -- Récupérer seulement la commune du Havre
7 | SELECT id_commune, code_insee, nom,
8 | population
9 | FROM z_formation.commune
10 | WHERE nom = 'Le Havre'
11 | ```
12 |
13 | On peut chercher les lignes dont le champ correspondant à **plusieurs valeurs**
14 |
15 | ```sql
16 | -- Récupérer la commune du Havre et de Rouen
17 | SELECT id_commune, code_insee, nom,
18 | population
19 | FROM z_formation.commune
20 | WHERE nom IN ('Le Havre', 'Rouen')
21 | ```
22 |
23 | On peut aussi filtrer sur des champs de type **entier ou nombres réels**, et faire des conditions comme des inégalités.
24 |
25 | ```sql
26 | -- Filtrer les données, par exemple par département et population
27 | SELECT *
28 | FROM z_formation.commune
29 | WHERE True
30 | AND depart = 'SEINE-MARITIME'
31 | AND population > 1000
32 | ;
33 | ```
34 |
35 | On peut chercher des lignes dont un champ **commence et/ou se termine** par un texte
36 |
37 | ```sql
38 | -- Filtrer les données, par exemple par département et début et/ou fin de nom
39 | SELECT *
40 | FROM z_formation.commune
41 | WHERE True
42 | AND depart = 'SEINE-MARITIME'
43 | -- commence par C
44 | AND nom LIKE 'C%'
45 | -- se termine par ville
46 | AND nom ILIKE '%ville'
47 | ;
48 | ```
49 |
50 | On peut utiliser les **calculs sur les géométries** pour filtrer les données. Par exemple filtrer par longueur de lignes
51 |
52 | ```sql
53 | -- Les routes qui font plus que 10km
54 | -- on peut utiliser des fonctions dans la clause WHERE
55 | SELECT id_route, id, geom
56 | FROM z_formation.route
57 | WHERE True
58 | AND ST_Length(geom) > 10000
59 | ```
60 |
61 | Continuer vers [Regrouper des données: GROUP BY](./group_data.md)
62 |
63 | ## Quiz
64 |
65 | Écrire une requête retournant toutes les communes de Seine-Maritime qui contiennent la chaîne de caractères 'saint'
66 |
67 | ```sql
68 | -- Toutes les communes de Seine-Maritime qui contiennent le mot saint
69 | SELECT *
70 | FROM z_formation.commune
71 | WHERE True
72 | AND depart = 'SEINE-MARITIME'
73 | AND nom ILIKE '%saint%';
74 | ```
75 |
76 |
77 |
78 | Écrire une requête retournant les nom et centroïde des communes de Seine-Maritime avec une population inférieure ou égale à 50
79 |
80 | ```sql
81 | -- Nom et centroïde des communes de Seine-Maritime avec une population <= 50
82 | SELECT nom, ST_Centroid(geom) as geom
83 | FROM z_formation.commune
84 | WHERE True
85 | AND depart = 'SEINE-MARITIME'
86 | AND population <= 50
87 | ```
88 |
89 |
--------------------------------------------------------------------------------
/docs/grant.md:
--------------------------------------------------------------------------------
1 | # Gestion des droits
2 |
3 | Dans PostgreSQL, on peut créer des rôles (des utilisateurs) et gérer les droits sur les différents objets :
4 | base, schémas, tables, fonctions, etc.
5 |
6 | La [documentation officielle de PostgreSQL](https://www.postgresql.org/docs/current/sql-grant.html) est complète, et propose plusieurs exemples.
7 |
8 | Nous montrons ci-dessous quelques utilisations possibles.
9 | Attention, pour pouvoir réaliser certaines opérations, vous devez :
10 |
11 | * soit être **super-utilisateur** (créer un rôle de connexion)
12 | * soit être **propriétaire** des objets pour lesquels modifier les droits (schémas, tables)
13 |
14 | ## Donner ou retirer des droits sur des objets existants
15 |
16 | Création d'un schéma de test et d'un rôle de connexion, en tant qu'utilisateur avec des droits forts sur la base de données (création de schémas, de tables, etc.).
17 |
18 | ```sql
19 | -- création d'un schéma de test
20 | CREATE SCHEMA IF NOT EXISTS nouveau_schema;
21 |
22 | -- création de tables pour tester
23 | CREATE TABLE IF NOT EXISTS nouveau_schema.observation (id serial primary key, nom text, geom geometry(point, 2154));
24 | CREATE TABLE IF NOT EXISTS nouveau_schema.nomenclature (id serial primary key, code text, libelle text);
25 | ```
26 |
27 | Création d'un rôle de connexion (en tant que super-utilisateur, ou en tant qu'utilisateur ayant le droit de créer des rôles)
28 |
29 | ```sql
30 | -- création d'un rôle nommé invite
31 | CREATE ROLE invite WITH PASSWORD 'mot_de_passe_a_changer' LOGIN;
32 | ```
33 |
34 | On donne le droit de connexion sur la base (nommée ici qgis)
35 |
36 | ```sql
37 | -- on donne le droit de connexion sur la base
38 | GRANT CONNECT ON DATABASE qgis TO invite;
39 | ```
40 |
41 | Exemple de requêtes pratiques pour donner ou retirer des droits (en tant qu'utilisateur propriétaire de la base et des objets)
42 |
43 | ```sql
44 | -- on donne le droit à invite d'utiliser les schéma public et nouveau_schema
45 | -- Utile pour pouvoir lister les tables
46 | -- Si un rôle n'a pas le droit USAGE sur un schéma,
47 | -- il ne peut pas lire les données des tables
48 | -- même si des droits SELECT on été données sur ces tables
49 | GRANT USAGE ON SCHEMA public, nouveau_schema TO "invite", "autre_role";
50 |
51 | -- on permet à invite de lire les données (SELECT)
52 | -- de toutes les tables du schéma nouveau_schema
53 | GRANT SELECT ON ALL TABLES IN SCHEMA nouveau_schema TO "invite", "autre_role";
54 |
55 | -- On permet l'ajout et la modification de données sur la table observation seulement
56 | GRANT INSERT OR UPDATE ON TABLE nouveau_schema.observation TO "invite";
57 |
58 | -- On peut aussi enlever des droits avec REVOKE.
59 | -- Cela enlève seulement les droits donnés précédemment avec GRANT
60 | -- Ex: On pourrait donner tous les droits sur une table
61 | -- puis retirer la possibilité de faire des suppressions
62 | GRANT ALL ON TABLE nouveau_schema.observation TO "autre_role";
63 | -- on retire les droits DELETE et TRUNCATE
64 | REVOKE DELETE, TRUNCATE ON TABLE nouveau_schema.observation FROM "autre_role";
65 |
66 | -- On peut aussi par exemple retirer tous les privilèges sur les tables du schéma public
67 | REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "invite";
68 |
69 | ```
70 |
71 | ## Droits par défaut sur les nouveaux objets créés par un utilisateur.
72 |
73 | Lorsqu'un utilisateur crée un schéma, une table ou une vue, aucun droit n'est donné
74 | sur cet objet aux autres utilisateurs. Par défaut les autres utilisateurs ne peuvent
75 | donc pas par exemple lire les données de ce nouvel objet.
76 |
77 | PostgreSQL fournit un moyen de définir en quelque sorte:
78 | *Donner ce(s) droit(s) sur tous ces objets créés par cet utilisateur à ces autres utilisateurs*
79 |
80 | Documentation officielle : https://docs.postgresql.fr/current/sql-alterdefaultprivileges.html
81 |
82 | ```sql
83 | -- Donner le droit SELECT pour toutes les nouvelles tables créées à l'avenir
84 | -- dans le schéma nouveau_schema
85 | ALTER DEFAULT PRIVILEGES IN SCHEMA "nouveau_schema" GRANT SELECT ON TABLES TO "invite", "autre_role";
86 | ```
87 |
88 | ## Lister tous les droits donnés sur tous les objets de la base
89 |
90 | Une requête SQL peut être utilisée pour lister tous les droits accordés
91 | sur plusieurs types d'objets : schéma, tables, fonctions, types, aggrégats, etc.
92 |
93 | Un exemple de **résultat** :
94 |
95 | object_schema | object_type | object_name | object_owner | grantor | grantee | privileges | is_grantable |
96 | ---------------|-------------|---------------|---------------|----------|--------------|-------------------------|---------------|
97 | urbanisme | schema | urbanisme | role_sig | role_sig | role_urba | CREATE, USAGE | f |
98 | urbanisme | table | zone_urba | role_sig | role_sig | role_urba | INSERT, SELECT, UPDATE | f |
99 | cadastre | schema | cadastre | role_sig | role_sig | role_lecteur | USAGE | f |
100 | cadastre | table | commune | role_sig | role_sig | role_lecteur | SELECT | f |
101 | cadastre | table | parcelle | role_sig | role_sig | role_lecteur | SELECT | f |
102 |
103 | > Si un objet n'est pas retourné par cette requête,
104 | c'est qu'aucun droit spécifique ne lui a été accordé.
105 |
106 |
107 |
108 | Requête SQL permettant de récupérer les droits accordés
109 | sur tous les objets de la base, ainsi que les propriétaires
110 | et les rôles qui ont accordé ces privilèges
111 |
112 |
113 | ```sql
114 | -- Adapted from https://dba.stackexchange.com/a/285632
115 | WITH rol AS (
116 | SELECT oid,
117 | rolname::text AS role_name
118 | FROM pg_roles
119 | UNION
120 | SELECT 0::oid AS oid,
121 | 'public'::text
122 | ),
123 | schemas AS ( -- Schemas
124 | SELECT oid AS schema_oid,
125 | n.nspname::text AS schema_name,
126 | n.nspowner AS owner_oid,
127 | 'schema'::text AS object_type,
128 | coalesce ( n.nspacl, acldefault ( 'n'::"char", n.nspowner ) ) AS acl
129 | FROM pg_catalog.pg_namespace n
130 | WHERE n.nspname !~ '^pg_'
131 | AND n.nspname <> 'information_schema'
132 | ),
133 | classes AS ( -- Tables, views, etc.
134 | SELECT schemas.schema_oid,
135 | schemas.schema_name AS object_schema,
136 | c.oid,
137 | c.relname::text AS object_name,
138 | c.relowner AS owner_oid,
139 | CASE
140 | WHEN c.relkind = 'r' THEN 'table'
141 | WHEN c.relkind = 'v' THEN 'view'
142 | WHEN c.relkind = 'm' THEN 'materialized view'
143 | WHEN c.relkind = 'c' THEN 'type'
144 | WHEN c.relkind = 'i' THEN 'index'
145 | WHEN c.relkind = 'S' THEN 'sequence'
146 | WHEN c.relkind = 's' THEN 'special'
147 | WHEN c.relkind = 't' THEN 'TOAST table'
148 | WHEN c.relkind = 'f' THEN 'foreign table'
149 | WHEN c.relkind = 'p' THEN 'partitioned table'
150 | WHEN c.relkind = 'I' THEN 'partitioned index'
151 | ELSE c.relkind::text
152 | END AS object_type,
153 | CASE
154 | WHEN c.relkind = 'S' THEN coalesce ( c.relacl, acldefault ( 's'::"char", c.relowner ) )
155 | ELSE coalesce ( c.relacl, acldefault ( 'r'::"char", c.relowner ) )
156 | END AS acl
157 | FROM pg_class c
158 | JOIN schemas
159 | ON ( schemas.schema_oid = c.relnamespace )
160 | WHERE c.relkind IN ( 'r', 'v', 'm', 'S', 'f', 'p' )
161 | ),
162 | cols AS ( -- Columns
163 | SELECT c.object_schema,
164 | null::integer AS oid,
165 | c.object_name || '.' || a.attname::text AS object_name,
166 | 'column' AS object_type,
167 | c.owner_oid,
168 | coalesce ( a.attacl, acldefault ( 'c'::"char", c.owner_oid ) ) AS acl
169 | FROM pg_attribute a
170 | JOIN classes c
171 | ON ( a.attrelid = c.oid )
172 | WHERE a.attnum > 0
173 | AND NOT a.attisdropped
174 | ),
175 | procs AS ( -- Procedures and functions
176 | SELECT schemas.schema_oid,
177 | schemas.schema_name AS object_schema,
178 | p.oid,
179 | p.proname::text AS object_name,
180 | p.proowner AS owner_oid,
181 | CASE p.prokind
182 | WHEN 'a' THEN 'aggregate'
183 | WHEN 'w' THEN 'window'
184 | WHEN 'p' THEN 'procedure'
185 | ELSE 'function'
186 | END AS object_type,
187 | pg_catalog.pg_get_function_arguments ( p.oid ) AS calling_arguments,
188 | coalesce ( p.proacl, acldefault ( 'f'::"char", p.proowner ) ) AS acl
189 | FROM pg_proc p
190 | JOIN schemas
191 | ON ( schemas.schema_oid = p.pronamespace )
192 | ),
193 | udts AS ( -- User defined types
194 | SELECT schemas.schema_oid,
195 | schemas.schema_name AS object_schema,
196 | t.oid,
197 | t.typname::text AS object_name,
198 | t.typowner AS owner_oid,
199 | CASE t.typtype
200 | WHEN 'b' THEN 'base type'
201 | WHEN 'c' THEN 'composite type'
202 | WHEN 'd' THEN 'domain'
203 | WHEN 'e' THEN 'enum type'
204 | WHEN 't' THEN 'pseudo-type'
205 | WHEN 'r' THEN 'range type'
206 | WHEN 'm' THEN 'multirange'
207 | ELSE t.typtype::text
208 | END AS object_type,
209 | coalesce ( t.typacl, acldefault ( 'T'::"char", t.typowner ) ) AS acl
210 | FROM pg_type t
211 | JOIN schemas
212 | ON ( schemas.schema_oid = t.typnamespace )
213 | WHERE ( t.typrelid = 0
214 | OR ( SELECT c.relkind = 'c'
215 | FROM pg_catalog.pg_class c
216 | WHERE c.oid = t.typrelid ) )
217 | AND NOT EXISTS (
218 | SELECT 1
219 | FROM pg_catalog.pg_type el
220 | WHERE el.oid = t.typelem
221 | AND el.typarray = t.oid )
222 | ),
223 | fdws AS ( -- Foreign data wrappers
224 | SELECT null::oid AS schema_oid,
225 | null::text AS object_schema,
226 | p.oid,
227 | p.fdwname::text AS object_name,
228 | p.fdwowner AS owner_oid,
229 | 'foreign data wrapper' AS object_type,
230 | coalesce ( p.fdwacl, acldefault ( 'F'::"char", p.fdwowner ) ) AS acl
231 | FROM pg_foreign_data_wrapper p
232 | ),
233 | fsrvs AS ( -- Foreign servers
234 | SELECT null::oid AS schema_oid,
235 | null::text AS object_schema,
236 | p.oid,
237 | p.srvname::text AS object_name,
238 | p.srvowner AS owner_oid,
239 | 'foreign server' AS object_type,
240 | coalesce ( p.srvacl, acldefault ( 'S'::"char", p.srvowner ) ) AS acl
241 | FROM pg_foreign_server p
242 | ),
243 | all_objects AS (
244 | SELECT schema_name AS object_schema,
245 | object_type,
246 | schema_name AS object_name,
247 | null::text AS calling_arguments,
248 | owner_oid,
249 | acl
250 | FROM schemas
251 | UNION
252 | SELECT object_schema,
253 | object_type,
254 | object_name,
255 | null::text AS calling_arguments,
256 | owner_oid,
257 | acl
258 | FROM classes
259 | UNION
260 | SELECT object_schema,
261 | object_type,
262 | object_name,
263 | null::text AS calling_arguments,
264 | owner_oid,
265 | acl
266 | FROM cols
267 | UNION
268 | SELECT object_schema,
269 | object_type,
270 | object_name,
271 | calling_arguments,
272 | owner_oid,
273 | acl
274 | FROM procs
275 | UNION
276 | SELECT object_schema,
277 | object_type,
278 | object_name,
279 | null::text AS calling_arguments,
280 | owner_oid,
281 | acl
282 | FROM udts
283 | UNION
284 | SELECT object_schema,
285 | object_type,
286 | object_name,
287 | null::text AS calling_arguments,
288 | owner_oid,
289 | acl
290 | FROM fdws
291 | UNION
292 | SELECT object_schema,
293 | object_type,
294 | object_name,
295 | null::text AS calling_arguments,
296 | owner_oid,
297 | acl
298 | FROM fsrvs
299 | ),
300 | acl_base AS (
301 | SELECT object_schema,
302 | object_type,
303 | object_name,
304 | calling_arguments,
305 | owner_oid,
306 | ( aclexplode ( acl ) ).grantor AS grantor_oid,
307 | ( aclexplode ( acl ) ).grantee AS grantee_oid,
308 | ( aclexplode ( acl ) ).privilege_type AS privilege_type,
309 | ( aclexplode ( acl ) ).is_grantable AS is_grantable
310 | FROM all_objects
311 | ),
312 | ungrouped AS (
313 | SELECT acl_base.object_schema,
314 | acl_base.object_type,
315 | acl_base.object_name,
316 | --acl_base.calling_arguments,
317 | owner.role_name AS object_owner,
318 | grantor.role_name AS grantor,
319 | grantee.role_name AS grantee,
320 | acl_base.privilege_type,
321 | acl_base.is_grantable
322 | FROM acl_base
323 | JOIN rol owner
324 | ON ( owner.oid = acl_base.owner_oid )
325 | JOIN rol grantor
326 | ON ( grantor.oid = acl_base.grantor_oid )
327 | JOIN rol grantee
328 | ON ( grantee.oid = acl_base.grantee_oid )
329 | WHERE acl_base.grantor_oid <> acl_base.grantee_oid
330 | )
331 | SELECT
332 | object_schema, object_type, object_name, object_owner,
333 | grantor, grantee,
334 | -- The same function name can be used many times
335 | -- Since we do not include the calling_arguments field, we should add a DISTINCT below
336 | string_agg(DISTINCT privilege_type, ' - ' ORDER BY privilege_type) AS privileges,
337 | is_grantable
338 | FROM ungrouped
339 | WHERE True
340 | -- Simplify objects returned
341 | -- You can comment the following line to get these types too
342 | AND object_type NOT IN ('function', 'window', 'aggregate', 'base type', 'composite type')
343 | -- You can also filter for specific schemas or object names by uncommenting and adapting the following lines
344 | -- AND object_schema IN ('cadastre', 'environment')
345 | -- AND object_type = 'table'
346 | -- AND object_name ILIKE '%parcelle%'
347 | GROUP BY object_schema, object_type, object_name, object_owner, grantor, grantee, is_grantable
348 | ORDER BY object_schema, object_type, grantor, grantee, object_name
349 | ;
350 | ```
351 |
352 |
353 |
354 |
355 | Continuer vers [Accéder à des données externes: Foreign Data Wrapper](./fdw.md)
356 |
--------------------------------------------------------------------------------
/docs/group_data.md:
--------------------------------------------------------------------------------
1 | # Grouper des données et calculer des statistiques
2 |
3 | [Les fonctions d'agrégat dans PostgreSQL](https://docs.postgresql.fr/14/functions-aggregate.html)
4 |
5 | ## Valeurs distinctes d'un champ
6 |
7 | On souhaite récupérer **toutes les valeurs possibles** d'un champ
8 |
9 | ```sql
10 | -- Vérifier les valeurs distinctes d'un champ: table commune
11 | SELECT DISTINCT depart
12 | FROM z_formation.commune
13 | ORDER BY depart
14 |
15 | -- idem sur la table lieu_dit_habite
16 | SELECT DISTINCT nature
17 | FROM z_formation.lieu_dit_habite
18 | ORDER BY nature
19 | ```
20 |
21 |
22 | ## Regrouper des données en spécifiant les champs de regroupement
23 |
24 | Certains calculs nécessitent le regroupement de lignes, comme les moyennes, les sommes ou les totaux. Pour cela, il faut réaliser un **regroupement** via la clause `GROUP BY`
25 |
26 | **Compter** les communes par département et calculer la **population totale**
27 |
28 | ```sql
29 | -- Regrouper des données
30 | -- Compter le nombre de communes par département
31 | SELECT depart,
32 | count(code_insee) AS nb_commune,
33 | sum(population) AS total_population
34 | FROM z_formation.commune
35 | WHERE True
36 | GROUP BY depart
37 | ORDER BY nb_commune DESC
38 | ```
39 |
40 | Calculer des **statistiques sur l'aire** des communes pour chaque département
41 |
42 |
43 | ```sql
44 | SELECT depart,
45 | count(id_commune) AS nb,
46 | min(ST_Area(geom)/10000)::int AS min_aire_ha,
47 | max(ST_Area(geom)/10000)::int AS max_aire_ha,
48 | avg(ST_Area(geom)/10000)::int AS moy_aire_ha,
49 | sum(ST_Area(geom)/10000)::int AS total_aire_ha
50 | FROM z_formation.commune
51 | GROUP BY depart
52 | ```
53 |
54 | **Compter** le nombre de routes par nature
55 |
56 | ```sql
57 | -- Compter le nombre de routes par nature
58 | SELECT count(id_route) AS nb_route, nature
59 | FROM z_formation.route
60 | WHERE True
61 | GROUP BY nature
62 | ORDER BY nb_route DESC
63 | ```
64 |
65 | Compter le nombre de routes par nature et par sens
66 |
67 | ```sql
68 | SELECT count(id_route) AS nb_route, nature, sens
69 | FROM z_formation.route
70 | WHERE True
71 | GROUP BY nature, sens
72 | ORDER BY nature, sens DESC
73 | ```
74 |
75 | Les caculs sur des ensembles groupés peuvent aussi être réalisé **sur les géométries.**. Les plus utilisés sont
76 |
77 | * `ST_Collect` qui regroupe les géométries dans une multi-géométrie,
78 | * `ST_Union` qui fusionne les géométries.
79 |
80 | Par exemple, on peut souhaiter trouver l'**enveloppe convexe** autour de points (élastique 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ête pour filtrer seulement les résultats de type polygone (car s'il y a seulement 1 ou 2 objets par nature, alors on ne peut créer de polygone)
81 |
82 |
83 | ```sql
84 | SELECT *
85 | FROM (
86 | SELECT
87 | nature,
88 | -- ST_Convexhull renvoie l'enveloppe convexe
89 | ST_Convexhull(ST_Collect(geom)) AS geom
90 | FROM z_formation.lieu_dit_habite
91 | GROUP BY nature
92 | ) AS source
93 | -- GeometryType renvoie le type de géométrie
94 | WHERE Geometrytype(geom) = 'POLYGON'
95 | ```
96 |
97 | Attention, on doit donner un alias à la sous-requête (ici `source`)
98 |
99 |
100 | Un autre exemple sur les bornes. Ici, on groupe les bornes par identifiant pair ou impair, et on calcule l'enveloppe convexe
101 |
102 | ```sql
103 | SELECT count(id_borne), ((id_borne % 2) = 0) AS pair,
104 | (st_convexhull(ST_Collect(geom))) AS geom
105 | FROM z_formation.borne_incendie
106 | GROUP BY pair
107 | ```
108 |
109 |
110 | On peut réaliser l'équivalent d'un `DISSOLVE` de QGIS en regroupant les géométries via `ST_Union`. Par exemple fusionner l'ensemble des communes pour construire les géométries des départements:
111 |
112 | ```sql
113 | SELECT
114 | depart,
115 | count(id_commune) AS nb_com,
116 | -- ST_Union crée une seule géométrie en fusionnant les géométries.
117 | ST_Union(geom) AS geom
118 |
119 | FROM z_formation.commune
120 |
121 | GROUP BY depart
122 | ```
123 |
124 | Attention, cette requête est lourde, et devra être enregistrée comme une table.
125 |
126 | ## Filtrer sur les regroupements
127 |
128 | Si on souhaite **compter** les communes par département, calculer la **population totale** et aussi **filter celles qui ont plus de 500 000 habitants**, il peut paraître logique d'écrire cette requête :
129 |
130 | ```sql
131 | SELECT depart,
132 | count(code_insee) AS nb_commune,
133 | sum(population) AS total_population
134 | FROM z_formation.commune
135 | GROUP BY depart
136 | WHERE sum(population) > 500000
137 | ORDER BY nb_commune DESC
138 | ```
139 |
140 | ou bien encore :
141 |
142 | ```sql
143 | SELECT depart,
144 | count(code_insee) AS nb_commune,
145 | sum(population) AS total_population
146 | FROM z_formation.commune
147 | GROUP BY depart
148 | WHERE total_population > 500000
149 | ORDER BY nb_commune DESC
150 | ```
151 |
152 | Ces deux requêtes renvoient une erreur. La bonne requête est :
153 |
154 | ```sql
155 | SELECT depart,
156 | count(code_insee) AS nb_commune,
157 | sum(population) AS total_population
158 | FROM z_formation.commune
159 | GROUP BY depart
160 | HAVING sum(population) > 500000
161 | ORDER BY nb_commune DESC
162 | ```
163 |
164 | Il faut savoir que la clause `WHERE` est exécutée avant la clause `GROUP BY`, il n'est donc pas possible de filtrer sur des regroupements avec celle-ci. C'est le rôle de la clause `HAVING`.
165 |
166 | Aussi la clause `SELECT` est exécutée après les clauses `WHERE` et `HAVING`, il n'est donc pas possible d'utiliser des alias déclarés avec celle-ci.
167 |
168 | Un schéma illustrant ceci est disponible sur le site [postgresqltutorial.com](https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-having/).
169 |
170 | Continuer vers [Rassembler des données: UNION ALL](./union.md)
171 |
172 | ## Quiz
173 |
174 |
175 | Écrire une requête retournant, pour le/les département(s) dont la population moyenne des villes est supérieure ou égale à 1500 habitants, le nom du/des département(s) ainsi que cette moyenne.
176 |
177 | ```sql
178 | SELECT depart,
179 | avg(population) AS moyenne_population
180 | FROM z_formation.commune
181 | GROUP BY depart
182 | HAVING avg(population) >= 1500
183 | ```
184 |
185 |
186 |
187 | Écrire une requête retournant pour les départements 'SEINE-MARITIME' et 'EURE', leur nom, le nombre de communes ainsi que la surface et la surface de l'enveloppe convexe en mètre carré sous forme d'entier.
188 |
189 | ```sql
190 | SELECT depart,
191 | count(id_commune) AS nb_commune,
192 | ST_Area(ST_Collect(geom))::int8 AS surface,
193 | ST_Area(ST_Convexhull(ST_Collect(geom)))::int8 AS surface_enveloppe_convexe
194 | FROM z_formation.commune
195 | WHERE depart IN ('SEINE-MARITIME', 'EURE')
196 | GROUP BY depart
197 | ```
198 |
199 |
--------------------------------------------------------------------------------
/docs/import_data.md:
--------------------------------------------------------------------------------
1 | # Importer des données
2 |
3 | Pour la formation, on doit **importer des données** pour pouvoir travailler.
4 |
5 | ## Import d'une couche depuis QGIS
6 |
7 | On doit **charger au préalable la couche source** dans QGIS (SHP, TAB, etc.), puis on doit **vérifier** :
8 |
9 | * la **projection**, idéalement `EPSG:2154`
10 | * l'**encodage** : `UTF-8`, `ISO-8859-15`, etc. Il faut ouvrir la **table attributaire**, et vérifier si les accents sont bien affichés. Sinon choisir le bon encodage dans l'onglet **Général** des **propriétés de la couche**
11 | * les **champs**: noms, type, contenu
12 |
13 | Pour importer, il existe plusieurs manières dans QGIS. La plus **performante** pour des gros volumes de données est l'utilisation de l'algorithme de la `boîte à outils` du menu `Traitement` appelé `Exporter vers PostgreSQL (Connexions disponibles`.
14 |
15 | 
16 |
17 | Pour trouver cet algorithme, chercher `PosgreSQL` dans le champ du haut, et lancer l'algorithme **Exporter vers PostgreSQL (connexions disponibles)** de **GDAL**. Il faut choisir les options suivantes :
18 |
19 | * choisir la bonne **connexion**, la couche en entrée, etc.
20 | * choisir le **schéma**, par exemple `z_formation`
21 | * choisir le **nom de la table**, par exemple `commune`
22 | * laisser `id` dans le champ **Clef primaire** si aucun champ entier auto-incrémenté existe, ou choisir le champ approprié
23 | * décocher **Convertir en morceaux multiples** pour les couches de points (et aussi pour les lignes et polygones si on est sûr)
24 | * laisser le reste par défaut.
25 |
26 | 
27 |
28 | Après l'import, on peut charger la table comme une couche via **l'explorateur de QGIS** :
29 |
30 | * **rafraîchir** le contenu du schéma via clic-droit et `Rafraîchir`
31 | * **double-cliquer** sur la table
32 |
33 | ## Importer plusieurs couches en batch
34 |
35 | Il est possible d'utiliser l'outil **Importer un vecteur vers une base de données PostGIS (connexions disponibles)** par lot. Pour cela, une fois la boîte de dialogue de cet algorithme ouverte, cliquer sur le bouton **Exécuter comme processus de lot**. Cela affiche un tableau, ou chaque ligne représente les variables d'entrée d'un algorithme.
36 |
37 | Vous pouvez créer manuellement chaque ligne, ou choisir directement les couches depuis votre projet QGIS. Voir la documentation QGIS pour plus de détail:
38 | https://docs.qgis.org/latest/fr/docs/user_manual/processing/batch.html
39 |
40 |
41 | Continuer vers [Sélectionner des données : SELECT](./sql_select.md)
42 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | Title: PostGIS
3 | Favicon: logo.svg
4 | ...
5 |
6 | # Formation PostGIS
7 |
8 | ## Pré-requis
9 |
10 | Cette formation concerne des utilisateurs de QGIS, géomaticiens, qui souhaitent comprendre l'apport de l'utilisation de PostgreSQL comme outil de centralisation de la données spatiale (et non spatiale):
11 |
12 | * un lieu unique de stockage
13 | * une gestion des droits d'accès (lecture, écriture)
14 | * la reproduction de quasiment tous les besoins en traitements SIG : intersections, tampons, extraction, correction, etc.
15 | * une grande souplesse de manipulation des données
16 | * des performances élevés sur certains traitements spatiaux (et non spatiaux)
17 | * le stockage de fonctions et de triggers pour assurer la cohérence des données, stocker des outils directement dans la base
18 |
19 |
20 | ## Sommaire
21 |
22 | * [Liens utiles et jeu de données](./links_and_data.md)
23 | * [Gestion des données PostgreSQL dans QGIS](./postgresql_in_qgis.md)
24 | * [Import des données dans PostgreSQL](./import_data.md)
25 | * [Sélectionner des données: SELECT](./sql_select.md)
26 | * [Réaliser des calculs et créer des géométries: FONCTIONS](./perform_calculation.md)
27 | * [Filtrer des données: WHERE](./filter_data.md)
28 | * [Regrouper des données: GROUP BY](./group_data.md)
29 | * [Rassembler des données: UNION ALL](./union.md)
30 | * [Enregistrer les requêtes: VIEW](./save_queries.md)
31 | * [Réaliser des jointures attributaires et spatiales; JOIN](./join_data.md)
32 | * [Fusionner des géométries](./merge_geometries.md)
33 | * [Les triggers](./triggers.md)
34 | * [Correction des géométries invalides](./validate_geometries.md)
35 | * [Vérifier la topologie](./check_topology.md)
36 | * [Fonctions utiles](./utils.md)
37 | * [Gestion des droits](./grant.md)
38 | * [Accéder à des données externes: Foreign Data Wrapper](./fdw.md)
39 | * [Tutoriels en ligne](./tutoriel.md)
40 |
--------------------------------------------------------------------------------
/docs/join_data.md:
--------------------------------------------------------------------------------
1 | # Les jointures
2 |
3 | Les jointures permettent de récupérer des données en relation les unes par rapport aux autres.
4 |
5 | ## Les jointures attributaires
6 |
7 | La condition de jointure est faite sur des champs non géométriques. Par exemple une égalité (code, identifiant).
8 |
9 | ### Exemple 1: parcelles et communes
10 |
11 | Récupération des informations de la commune pour un ensemble de parcelles
12 |
13 | ```sql
14 | -- Jointure attributaire: récupération du nom de la commune pour un ensemble de parcelles
15 | SELECT c.nom, p.*
16 | FROM z_formation.parcelle as p
17 | JOIN z_formation.commune as c
18 | ON p.commune = c.code_insee
19 | LIMIT 100
20 | -- IMPORTANT: ne pas oublier le ON cad le critère de jointure,
21 | -- sous peine de "produit cartésien" (calcul coûteux de tous les possibles)
22 | ;
23 | ```
24 |
25 | Il est souvent intéressant, pour des données volumineuses, de **créer un index sur le champ de jointure** (par exemple ici sur les champs `commune` et `code_insee`.
26 |
27 |
28 | ### Exemple 2: observations et communes
29 |
30 | * On crée une table de points qui contiendra des observations
31 |
32 | ```sql
33 | -- création
34 | CREATE TABLE z_formation.observation (
35 | id serial NOT NULL PRIMARY KEY,
36 | date date DEFAULT (now())::date NOT NULL,
37 | description text,
38 | geom public.geometry(Point,2154),
39 | code_insee character varying(5)
40 | );
41 | CREATE INDEX sidx_observation_geom ON z_formation.observation USING gist (geom);
42 |
43 | -- on y met des données
44 | INSERT INTO z_formation.observation VALUES (1, '2020-07-08', 'un', '01010000206A080000D636D95AFB832141279BD2C8FEA65A41', '76618');
45 | INSERT INTO z_formation.observation VALUES (2, '2020-07-08', 'deux', '01010000206A08000010248E173E37224156920AEA21525A41', '27213');
46 | INSERT INTO z_formation.observation VALUES (3, '2020-07-08', 'trois', '01010000206A08000018BF3048EA112341183933F6CC885A41', NULL);
47 |
48 | ```
49 |
50 | On fait une jointure attributaire entre les points des observations et les communes
51 |
52 | ```sql
53 | SELECT
54 | -- tous les champs de la table observation
55 | o.*,
56 | -- le nom de la commune
57 | c.nom,
58 | -- l'aire entière en hectares
59 | ST_area(c.geom)::integer/10000 AS surface_commune
60 | FROM z_formation.observation AS o
61 | JOIN z_formation.commune AS c ON o.code_insee = c.code_insee
62 | WHERE True
63 | ```
64 |
65 | Résultat:
66 |
67 | | id | date | description | geom | code_insee | nom | surface_commune |
68 | |----|------------|-------------|------|------------|----------------|-----------------|
69 | | 2 | 2020-07-08 | deux | .... | 27213 | Vexin-sur-Epte | 11434 |
70 | | 1 | 2020-07-08 | un | .... | 76618 | Petit-Caux | 9243 |
71 |
72 | On ne récupère ici que 2 lignes alors qu'il y a bien 3 observations dans la table.
73 |
74 | Pour récupérer les 3 lignes, on doit faire une jointure `LEFT`. On peut utiliser un `CASE WHEN` pour tester si la commune est trouvée sous chaque point
75 |
76 | ```sql
77 | SELECT
78 | o.*, c.nom, ST_area(c.geom)::integer/10000 AS surface_commune,
79 | CASE
80 | WHEN c.code_insee IS NULL THEN 'pas de commune'
81 | ELSE 'ok'
82 | END AS test_commune
83 | FROM z_formation.observation AS o
84 | LEFT JOIN z_formation.commune AS c ON o.code_insee = c.code_insee
85 | WHERE True
86 | ```
87 |
88 | Résultat
89 |
90 | | id | date | description | geom | code_insee | nom | surface_commune | test_commune |
91 | |----|------------|-------------|------|------------|----------------|-----------------|----------------|
92 | | 2 | 2020-07-08 | deux | .... | 27213 | Vexin-sur-Epte | 11434 | ok |
93 | | 1 | 2020-07-08 | un | .... | 76618 | Petit-Caux | 9243 | ok |
94 | | 3 | 2020-07-08 | trois | .... | Null | Null | Null | pas de commune |
95 |
96 | ## Les jointures spatiales
97 |
98 | Le critère de jointure peut être une **condition spatiale**. On réalise souvent une jointure par **intersection** ou par **proximité**.
99 |
100 | ### Joindre des points avec des polygones
101 |
102 | Un exemple classique de récupération des données de la table commune (nom, etc.) depuis une table de points.
103 |
104 | ```sql
105 | -- Pour chaque lieu-dit, on veut le nom de la commune
106 | SELECT
107 | l.id_lieu_dit_habite, l.nom,
108 | c.nom AS nom_commune, c.code_insee,
109 | l.geom
110 | FROM "z_formation".lieu_dit_habite AS l
111 | JOIN "z_formation".commune AS c
112 | ON st_intersects(c.geom, l.geom)
113 | ORDER BY l.nom
114 | ```
115 |
116 | | id_lieu_dit_habite | nom | nom_commune | code_insee | geom |
117 | |--------------------|-----------------------|--------------------------|------------|------|
118 | | 58 | Abbaye du Valasse | Gruchet-le-Valasse | 76329 | .... |
119 | | 1024 | Ablemont | Bacqueville-en-Caux | 76051 | .... |
120 | | 1043 | Agranville | Douvrend | 76220 | .... |
121 | | 1377 | All des Artisans | Mesnils-sur-Iton | 27198 | .... |
122 | | 1801 | Allée des Maronniers | Heudebouville | 27332 | .... |
123 | | 1293 | Alliquerville | Trouville | 76715 | .... |
124 | | 507 | Alventot | Sainte-Hélène-Bondeville | 76587 | .... |
125 | | 555 | Alvinbuc | Veauville-lès-Baons | 76729 | .... |
126 | | 69 | Ancien hôtel de ville | Rouen | 76540 | .... |
127 |
128 |
129 | On peut facilement inverser la table principale pour afficher les lignes ordonnées par commune.
130 |
131 | ```sql
132 | SELECT
133 | c.nom, c.code_insee,
134 | l.id_lieu_dit_habite, l.nom
135 | FROM "z_formation".commune AS c
136 | JOIN "z_formation".lieu_dit_habite AS l
137 | ON st_intersects(c.geom, l.geom)
138 | ORDER BY c.nom
139 | ```
140 |
141 | | nom | code_insee | id_lieu_dit_habite | nom |
142 | |----------|------------|--------------------|--------------------|
143 | | Aclou | 27001 | 107 | Manoir de la Haule |
144 | | Acquigny | 27003 | 106 | Manoir de Becdal |
145 | | Ailly | 27005 | 596 | Quaizes |
146 | | Ailly | 27005 | 595 | Ingremare |
147 | | Ailly | 27005 | 594 | Gruchet |
148 | | Alizay | 27008 | 667 | Le Solitaire |
149 | | Ambenay | 27009 | 204 | Les Siaules |
150 | | Ambenay | 27009 | 201 | Les Renardieres |
151 | | Ambenay | 27009 | 202 | Le Culoron |
152 |
153 |
154 | 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ésultats pour les communes qui ont des lieux-dits.
155 |
156 | On pourrait aussi faire des statistiques, en regroupant par les champs de la table principale, ici les communes.
157 |
158 | ```sql
159 | SELECT
160 | c.nom, c.code_insee,
161 | count(l.id_lieu_dit_habite) AS nb_lieu_dit,
162 | c.geom
163 | FROM "z_formation".commune AS c
164 | JOIN "z_formation".lieu_dit_habite AS l
165 | ON st_intersects(c.geom, l.geom)
166 | GROUP BY c.nom, c.code_insee, c.geom
167 | ORDER BY nb_lieu_dit DESC
168 | LIMIT 10
169 | ```
170 |
171 | | nom | code_insee | nb_lieu_dit | geom |
172 | |--------------------|------------|-------------|------|
173 | | Heudebouville | 27332 | 61 | .... |
174 | | Mesnils-sur-Iton | 27198 | 52 | .... |
175 | | Rouen | 76540 | 20 | .... |
176 | | Saint-Saëns | 76648 | 19 | .... |
177 | | Les Grandes-Ventes | 76321 | 19 | .... |
178 | | Mesnil-en-Ouche | 27049 | 18 | .... |
179 | | Quincampoix | 76517 | 18 | .... |
180 |
181 |
182 |
183 | ### Joindre des lignes avec des polygones
184 |
185 | Récupérer le code commune de chaque chemin, par **intersection entre le chemin et la commune**.
186 |
187 | #### Jointure spatiale simple entre les géométries brutes
188 |
189 | ```sql
190 | -- Ici, on peut récupérer plusieurs fois le même chemin
191 | -- s'il passe par plusieurs communes
192 | SELECT
193 | v.*,
194 | c.nom, c.code_insee
195 | FROM "z_formation".chemin AS v
196 | JOIN "z_formation".commune AS c
197 | ON ST_Intersects(v.geom, c.geom)
198 | ORDER BY id_chemin, nom
199 | ```
200 |
201 | Cela peut renvoyer plusieurs lignes par chemin, car chaque chemin peut passer par plusieurs communes.
202 |
203 | #### Jointure spatiale entre le centroïde des chemins et la géométrie des communes
204 |
205 | On peut utiliser le **centroïde de chaque chemin** pour avoir un seul objet par chemin comme résultat.
206 |
207 | ```sql
208 | -- création de l'index
209 | CREATE INDEX ON z_formation.chemin USING gist (ST_Centroid(geom));
210 | -- Jointure spatiale
211 | -- On ne veut qu'une seule ligne par chemin
212 | -- Donc on fait l'intersection entre le centroïde des chemins (pour avoir un point) et les communes
213 | SELECT
214 | v.*,
215 | c.nom, c.code_insee
216 | FROM "z_formation".chemin AS v
217 | JOIN "z_formation".commune AS c
218 | ON ST_Intersects(ST_Centroid(v.geom), c.geom)
219 | ```
220 |
221 | **NB:** Attention, dans ce cas, l'index spatial sur la géométrie des chemins n'est pas utilisé. C'est pour cela que nous avons créé un index spatial sur `ST_Centroid(geom)` pour la table des chemins.
222 |
223 |
224 | 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.
225 |
226 | ```sql
227 | -- A l'inverse, on veut récupérer des statistiques par commune
228 | -- On veut une ligne par commune, avec des données sur les voies
229 | SELECT
230 | c.id_commune, c.nom, c.code_insee,
231 | count(v.id_chemin) AS nb_chemin,
232 | sum(st_length(v.geom)) AS somme_longueur_chemins_entiers
233 | FROM z_formation.commune AS c
234 | JOIN z_formation.chemin AS v
235 | ON st_intersects(c.geom, st_centroid(v.geom))
236 | GROUP BY c.id_commune, c.nom, c.code_insee
237 | ;
238 | ```
239 |
240 | #### Utilisation d'une jointure LEFT pour garder les communes sans chemins
241 |
242 | La requête précédente ne renvoie pas de lignes pour les communes qui n'ont pas de chemin dont le centroïde est dans une commune. C'est une jointure de type `INNER JOIN`
243 |
244 | Si on veut quand même récupérer ces communes, on fait une jointure `LEFT JOIN`: pour les lignes sans chemins, les champs liés à la table des chemins seront mis à `NULL`.
245 |
246 |
247 | ```sql
248 | SELECT
249 | c.id_commune, c.nom, c.code_insee,
250 | count(v.id_chemin) AS nb_chemin,
251 | sum(st_length(v.geom)) AS somme_longueur_chemins_entiers
252 | FROM z_formation.commune AS c
253 | LEFT JOIN z_formation.chemin AS v
254 | ON st_intersects(c.geom, st_centroid(v.geom))
255 | GROUP BY c.id_commune, c.nom, c.code_insee
256 | ;
257 | ```
258 |
259 | C'est **beaucoup plus long**, car la requête 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élérer la requête, on doit créer l'index sur les centroïdes des chemins
260 |
261 | ```sql
262 | CREATE INDEX ON z_formation.chemin USING GIST(ST_Centroid(geom))
263 | ```
264 |
265 | puis la relancer. Dans cet exemple, on passe de 100 secondes à 1 seconde, grâce à ce nouvel index spatial.
266 |
267 | #### Affiner le résultat en découpant les chemins
268 |
269 | Dans la requête précédente, 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ête va être plus coûteuse, car il faut réaliser le découpage des lignes des chemins par les polygones des communes.
270 |
271 | On va découper exactement les chemins par commune et récupérer les informations
272 |
273 | ```sql
274 | CREATE TABLE z_formation.decoupe_chemin_par_commune AS
275 | -- Découper les chemins par commune
276 | SELECT
277 | -- id unique
278 | -- infos du chemin
279 | l.id AS id_chemin,
280 | -- infos de la commune
281 | c.nom, c.code_insee,
282 | ST_Multi(st_collectionextract(ST_Intersection(c.geom, l.geom), 2))::geometry(multilinestring, 2154) AS geom
283 | FROM "z_formation".commune AS c
284 | JOIN "z_formation".chemin AS l
285 | ON st_intersects(c.geom, l.geom)
286 | ;
287 | CREATE INDEX ON z_formation.decoupe_chemin_par_commune USING GIST (geom);
288 | ```
289 |
290 |
291 | **NB**: Attention à ne pas confondre `ST_Intersects` qui renvoie vrai ou faux, et `ST_Intersection` qui renvoie la géométrie issue du découpage d'une géométrie par une autre.
292 |
293 | ### Joindre des polygones avec des polygones
294 |
295 | On peut bien sûr réaliser des **jointures spatiales** entre 2 couches de **polygones**, et découper les polygones par intersection. Attention, les performances sont forcément moins bonnes qu'avec des points.
296 |
297 | Trouver l'ensemble des zonages PLU pour les parcelles du Havre.
298 |
299 | On va récupérer **plusieurs résultats pour chaque parcelle** si plusieurs zonages chevauchent une parcelle.
300 |
301 | ```sql
302 | -- Jointure spatiale
303 | SELECT
304 | p.id_parcelle,
305 | z.libelle, z.libelong, z.typezone
306 | FROM z_formation.parcelle_havre AS p
307 | JOIN z_formation.zone_urba AS z
308 | ON st_intersects(z.geom, p.geom)
309 | WHERE True
310 | ```
311 |
312 |
313 |
314 | Compter pour chaque parcelle le nombre de zonages en intersection: on veut **une seule ligne par parcelle**.
315 |
316 | ```sql
317 | SELECT
318 | p.id_parcelle,
319 | count(z.libelle) AS nombre_zonage
320 | FROM z_formation.parcelle_havre AS p
321 | JOIN z_formation.zone_urba AS z
322 | ON st_intersects(z.geom, p.geom)
323 | WHERE True
324 | GROUP BY p.id_parcelle
325 | ORDER BY nombre_zonage DESC
326 | ```
327 |
328 | Découper les parcelles par les zonages, et pouvoir calculer les surfaces des zonages, et le pourcentage par rapport à la surface de chaque parcelle. On essaye le SQL suivant:
329 |
330 | ```sql
331 | SELECT
332 | p.id_parcelle,
333 | z.libelle, z.libelong, z.typezone,
334 | -- découper les géométries
335 | st_intersection(z.geom, p.geom) AS geom
336 | FROM z_formation.parcelle_havre AS p
337 | JOIN z_formation.zone_urba AS z
338 | ON st_intersects(z.geom, p.geom)
339 | WHERE True
340 | ORDER BY p.id_parcelle
341 | ```
342 |
343 | Il renvoie l'erreur
344 |
345 | ```
346 | 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
347 | ```
348 |
349 | On a ici des soucis de **validité de géométrie**. Il nous faut donc corriger les géométries avant de poursuivre. Voir chapitre sur la validation des géométries.
350 |
351 | Une fois les géométries validées, la requête fonctionne. On l'utilise dans une sous-requête pour créer une table et calculer les surfaces
352 |
353 | ```sql
354 | -- suppression de la table
355 | DROP TABLE IF EXISTS z_formation.decoupe_zonage_parcelle;
356 | -- création de la table avec calcul de pourcentage de surface
357 | CREATE TABLE z_formation.decoupe_zonage_parcelle AS
358 | SELECT row_number() OVER() AS id,
359 | source.*,
360 | ST_Area(geom) AS aire,
361 | 100 * ST_Area(geom) / aire_parcelle AS pourcentage
362 | FROM (
363 | SELECT
364 | p.id_parcelle, p.id AS idpar, ST_Area(p.geom) AS aire_parcelle,
365 | z.id_zone_urba, z.libelle, z.libelong, z.typezone,
366 | -- découper les géométries
367 | (ST_Multi(st_intersection(z.geom, p.geom)))::geometry(MultiPolygon,2154) AS geom
368 | FROM z_formation.parcelle_havre AS p
369 | JOIN z_formation.zone_urba AS z ON st_intersects(z.geom, p.geom)
370 | WHERE True
371 | ) AS source;
372 |
373 | -- Ajout de la clé primaire
374 | ALTER TABLE z_formation.decoupe_zonage_parcelle ADD PRIMARY KEY (id);
375 |
376 | -- Ajout de l'index spatial
377 | CREATE INDEX ON z_formation.decoupe_zonage_parcelle USING GIST (geom);
378 |
379 | ```
380 |
381 | ### Faire un rapport des surfaces intersectées de zonages sur une table principale
382 |
383 | Par exemple, pour chacune des communes, on souhaite calculer la somme des surfaces intersectée par chaque type de zone (parcs, znieff, etc.).
384 |
385 | Afin d'avoir à disposition des données de test pour cet exemple de rapport, nous allons créer 2 tables `z_formation.parc_national` et `z_formation.znieff`, et y insérer des fausses données:
386 |
387 |
388 | ```sql
389 | -- Table des parcs nationaux
390 | CREATE TABLE IF NOT EXISTS z_formation.parc_national (
391 | id serial primary key,
392 | nom text,
393 | geom geometry(multipolygon, 2154)
394 | );
395 | CREATE INDEX ON z_formation.parc_national USING GIST (geom);
396 |
397 | -- Table des znieff
398 | CREATE TABLE IF NOT EXISTS z_formation.znieff(
399 | id serial primary key,
400 | nom_znieff text,
401 | geom geometry(multipolygon, 2154)
402 | );
403 | CREATE INDEX ON z_formation.znieff USING GIST (geom);
404 | ```
405 |
406 | On insère des polygones dans ces deux tables:
407 |
408 | ```sql
409 | -- données de test
410 | -- parcs
411 | INSERT INTO z_formation.parc_national VALUES (1, 'un', '01060000206A0800000100000001030000000100000008000000C3F7DE73553D20411B3DC1FB0C625A410531F757E93D2041BAECB21FA85E5A41F35B09978081204195F05B9787595A41D61E4865A1A7204147BC8A3AC0605A41ED76A806317F2041A79F7E4876605A41B80752433C832041037846623A655A41E10ED595BA6120413CC1D1C18C685A41C3F7DE73553D20411B3DC1FB0C625A41');
412 | INSERT INTO z_formation.parc_national VALUES (2, 'deux', '01060000206A080000010000000103000000010000000900000024D68B4AE0412141AAAAAA3C685B5A4130642ACBD01421413A85AE4B72585A41CA08F0240E382141746C4BD107535A41FA30F7A78A4A2141524A29E544555A414796BF5CE63621414DD2E222A4565A416B92160F9B5D2141302807F981575A4130DC700B2E782141DC0ED50B6B5C5A4106FBB8C8294F214150AC17BF015E5A4124D68B4AE0412141AAAAAA3C685B5A41');
413 | INSERT INTO z_formation.parc_national VALUES (3, 'trois', '01060000206A0800000100000001030000000100000006000000918DCFE7E0861F4137AB79AF14515A411AE56040588A1F41642A43EEC74F5A41DF2EBB3CEBA41F418C31C66ADA4F5A4168864C9562A81F416E87EA40B8505A415CBC8A74C3A31F410FA4F63202515A41918DCFE7E0861F4137AB79AF14515A41');
414 | INSERT INTO z_formation.parc_national VALUES (4, 'quatre', '01060000206A080000010000000103000000010000000500000004474FE81DBA2041269A684EFD625A41AB17C51223C9204120B507BEAD605A4116329539BBF22041A3273886D5615A416F611F0FB6E32041FA1A9F0F4A645A4104474FE81DBA2041269A684EFD625A41');
415 | INSERT INTO z_formation.parc_national VALUES (5, 'cinq', '01060000206A0800000100000001030000000100000005000000F2E3C256231E2041E0ACE631AE535A41F7C823E772202041E89C73B6EF505A41B048BCC266362041DAC785A15E515A419E999911782F204180C9F223F8535A41F2E3C256231E2041E0ACE631AE535A41');
416 | SELECT pg_catalog.setval('z_formation.parc_national_id_seq', 5, true);
417 |
418 | -- znieff
419 | INSERT INTO z_formation.znieff VALUES (1, 'uno', '01060000206A08000001000000010300000001000000050000004039188C39D12041770A5DF74A4A5A413A54B7FBE9CE20410C5DA7C8F5455A41811042C0A4EA204130ECE38267475A416F611F0FB6E320417125FC66FB475A414039188C39D12041770A5DF74A4A5A41');
420 | INSERT INTO z_formation.znieff VALUES (2, 'dos', '01060000206A080000010000000103000000010000000500000076BEC6DF62492141513FFDF0525A5A417CA32770B24B21411EDBD22150595A419437ABB1F05421410F06E50CBF595A419437ABB1F0542141B022F1FE085A5A4176BEC6DF62492141513FFDF0525A5A41');
421 | INSERT INTO z_formation.znieff VALUES (3, 'tres', '01060000206A0800000100000001030000000100000005000000A6E6CD62DF5B2141B607528F585C5A41ACCB2EF32E5E2141C5DC3FA4E95B5A414CB7438DE46A2141C5DC3FA4E95B5A41B895F013CE62214189888850A55D5A41A6E6CD62DF5B2141B607528F585C5A41');
422 | INSERT INTO z_formation.znieff VALUES (4, 'quatro', '01060000206A0800000100000001030000000100000005000000CE857DF445102041985D7665365D5A41DA4F3F15E5142041339521C7305B5A41C2F7DE73553D2041927815D5E65A5A410393E50712252041B607528F585C5A41CE857DF445102041985D7665365D5A41');
423 | INSERT INTO z_formation.znieff VALUES (5, 'cinco', '01060000206A080000010000000103000000010000000500000045A632DC2B702041FD25CB033C5F5A41CEFDC334A373204115EB459D0E5C5A41F25B099780812041397A8257805D5A415755558D1A7720419E42D7F5855F5A4145A632DC2B702041FD25CB033C5F5A41');
424 | SELECT pg_catalog.setval('z_formation.znieff_id_seq', 5, true);
425 | ```
426 |
427 | Pour chaque commune, on souhaite calculer la somme des surfaces intersectées 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)
428 |
429 | Résultat attendu:
430 |
431 | | id_commune | code_insee | nom | surface_commune_ha | somme_surface_parcs | somme_surface_znieff |
432 | |------------|------------|-------------------|--------------------|---------------------|----------------------|
433 | | 1139 | 27042 | Barville | 275.138028733401 | 87.2237204013011 | None |
434 | | 410 | 27057 | Bernienville | 779.74546553394 | None | 5.26504189468878 |
435 | | 1193 | 27061 | Berthouville | 757.19696570046 | 19.9975421896336 | None |
436 | | 495 | 27074 | Boisney | 576.995877227961 | 0.107059260396721 | None |
437 | | 432 | 27077 | Boissey-le-Châtel | 438.373848703835 | 434.510197417769 | 83.9289621127432 |
438 |
439 |
440 | * Méthode avec des sous-requêtes
441 |
442 | ```sql
443 | SELECT
444 | c.id_commune, c.code_insee, c.nom,
445 | ST_Area(c.geom) / 10000 AS surface_commune_ha,
446 | (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,
447 | (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
448 | FROM z_formation.commune AS c
449 | ORDER BY c.nom
450 | ```
451 |
452 | * Méthode avec des **jointures** `LEFT`
453 |
454 | ```sql
455 | SELECT
456 | -- champs choisis dans la table commune
457 | c.id_commune, c.code_insee, c.nom,
458 | -- surface en ha
459 | ST_Area(c.geom) / 10000 AS surface_commune_ha,
460 | -- somme des découpages des parcs par commune
461 | sum(ST_Area(ST_Intersection(c.geom, p.geom)) / 10000 ) AS somme_surface_parcs,
462 | -- somme des découpages des znieff par commune
463 | sum(ST_Area(ST_Intersection(c.geom, z.geom)) / 10000 ) AS somme_surface_znieff
464 |
465 | FROM z_formation.commune AS c
466 | -- jointure spatiale sur les parcs
467 | LEFT JOIN z_formation.parc_national AS p
468 | ON ST_Intersects(c.geom, p.geom)
469 | -- jointure spatiale sur les znieff
470 | LEFT JOIN z_formation.znieff AS z
471 | ON ST_Intersects(c.geom, z.geom)
472 |
473 | -- clause WHERE optionelle
474 | -- WHERE p.id IS NOT NULL OR z.id IS NOT NULL
475 |
476 | -- on regroupe sur les champs des communes
477 | GROUP BY c.id_commune, c.code_insee, c.nom, c.geom
478 |
479 | -- on ordonne par nom
480 | ORDER BY c.nom
481 | ```
482 |
483 | **Avantages**:
484 |
485 | * on peut intégrer facilement dans la clause `WHERE` des conditions sur les champs des tables jointes. Par exemple ne récupérer que les lignes qui sont concernées par un parc ou une znieff, via `WHERE p.id IS NOT NULL OR z.id IS NOT NULL` (commenté ci-dessus pour le désactiver)
486 | * On peut sortir plusieurs agrégats pour les tables jointes. Par exemple un décompte des parcs, un décompte des znieff
487 |
488 | ATTENTION:
489 |
490 | * on peut avoir des doublons qui vont créer des erreurs. Voir cet exemple: http://sqlfiddle.com/#!17/73485c/2/0
491 | * cette méthode peut poser des soucis de performance
492 |
493 |
494 |
495 |
496 | **ATTENTION**:
497 |
498 | * il faut absolument avoir un index spatial sur le champ `geom` de toutes les tables
499 | * le calcul de découpage des polygones des communes par ceux des zonages peut être très long (et l'index spatial ne sert à rien ici)
500 |
501 |
502 | ### Distances et tampons entre couches
503 |
504 | Pour chaque objets d'une table, on souhaite récupérer 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`
505 |
506 | On prend comme exemple la table des bornes à incendie créée précédemment (remplie avec quelques données de test).
507 |
508 | Trouver toutes les parcelles **à moins de 200m** d'une borne à incendie
509 |
510 | ```sql
511 | SELECT
512 | p.id_parcelle, p.geom,
513 | b.id_borne, b.code,
514 | ST_Distance(b.geom, p.geom) AS distance
515 | FROM z_formation.parcelle_havre AS p
516 | JOIN z_formation.borne_incendie AS b
517 | ON ST_DWithin(p.geom, b.geom, 200)
518 | ORDER BY id_parcelle, id_borne
519 | ```
520 |
521 | Attention, elle peut renvoyer **plusieurs fois la même parcelle** si 2 bornes sont assez proches. Pour ne récupérer que la borne la plus proche, on peut faire la requête suivante. La clause `DISTINCT ON` permet de dire quel champ doit être **unique** (ici id_parcelle).
522 |
523 | On **ordonne** ensuite **par ce champ et par la distance** pour prendre seulement la ligne correspondant à la parcelle **la plus proche**
524 |
525 | ```sql
526 | SELECT DISTINCT ON (p.id_parcelle)
527 | p.id_parcelle, p.geom,
528 | b.id_borne, b.code,
529 | ST_Distance(b.geom, p.geom) AS distance
530 | FROM z_formation.parcelle_havre AS p
531 | JOIN z_formation.borne_incendie AS b
532 | ON ST_DWithin(p.geom, b.geom, 200)
533 | ORDER BY id_parcelle, distance
534 | ```
535 |
536 | Pour information, on peut vérifier en créant les tampons
537 |
538 | ```sql
539 | -- Tampons non dissous
540 | SELECT id_borne, ST_Buffer(geom, 200) AS geom
541 | FROM z_formation.borne_incendie
542 |
543 | -- Tampons dissous
544 | SELECT ST_Union(ST_Buffer(geom, 200)) AS geom
545 | FROM z_formation.borne_incendie
546 | ```
547 |
548 | Un [article intéressant de Paul Ramsey](http://blog.cleverelephant.ca/2021/12/knn-syntax.html) sur le calcul de distance via l'opérateur `<->` pour trouver le plus proche voisin d'un objet.
549 |
550 |
551 |
552 | Continuer vers [Fusionner des géométries](./merge_geometries.md)
553 |
--------------------------------------------------------------------------------
/docs/links_and_data.md:
--------------------------------------------------------------------------------
1 | # Liens utiles
2 |
3 | ## Documentation
4 |
5 | Documentation de PostgreSQL : https://docs.postgresql.fr/current/
6 |
7 | Documentation des fonctions PostGIS:
8 |
9 | * en anglais : https://postgis.net/docs/reference.html
10 | * en français https://postgis.net/docs/postgis-fr.html notamment la référence des fonctions spatiales : https://postgis.net/docs/postgis-fr.html#reference
11 |
12 | ## Base de données
13 |
14 | Nous présupposons qu'une **base de données** est accessible pour la formation, via un **rôle PostgreSQL** avec des droits élevés (notamment pour créer des schémas et des tables). L'extension **PostGIS** doit aussi être activée sur cette base de données.
15 |
16 | ## Jeux de données
17 |
18 | Pour cette formation, nous utilisons des données libres de droit :
19 |
20 | * Un dump est téléchargeable en cliquant sur ce [lien](https://github.com/3liz/formation-postgis/releases/download/1.0/data_formation.dump).
21 |
22 | Il peut est chargé en base avec cette commande :
23 | ```bash
24 | pg_restore -h URL_SERVEUR -p 5432 -U NOM_UTILISATEUR -d NOM_BASE --no-owner --no-acl data_formation.dump
25 | ```
26 |
27 | Ce jeu de données a pour sources :
28 |
29 | * Extraction de données d'**OpenStreetMap** dans un format SIG, sous licence "ODBL" (site https://github.com/igeofr/osm2igeo ). On utilisera par exemple les données de l'ancienne région Haute-Normandie.
30 |
31 | * Données cadastrales (site https://cadastre.data.gouv.fr ), sous licence "Licence Ouverte 2.0" Par exemple pour la Seine-Maritime :
32 | https://cadastre.data.gouv.fr/data/etalab-cadastre/2024-10-01/shp/departements/76/
33 |
34 | * PLU (site https://www.geoportail-urbanisme.gouv.fr/map/ ). Par exemple les données de la ville du Havre. Cliquer sur la commune, et utiliser le lien de téléchargement.
35 |
36 | Ces données peuvent aussi être importées dans la base de formation via les outils de QGIS.
37 |
38 | ## Concepts de base de données
39 |
40 | Un rappel sur les concepts de table, champs, relations.
41 |
42 | * Documentation de QGIS : https://docs.qgis.org/latest/fr/docs/training_manual/database_concepts/index.html
43 |
44 |
45 | ## Quelques extensions QGIS
46 |
47 | [Lire la formation QGIS également](https://3liz.github.io/formation-qgis/extensions.html)
48 |
49 | * **Autosaver** : sauvegarde automatique du projet QGIS toutes les N minutes
50 | * **Layer Board** : liste l'ensemble des couches du projet et permet de modifier des caractéristiques pour plusieurs couches à la fois
51 | * **Cadastre** : import et exploitation des données EDIGEO ET MAJIC dans PostgreSQL
52 |
53 | Continuer vers [Gestion des données PostgreSQL dans QGIS](./postgresql_in_qgis.md)
54 |
--------------------------------------------------------------------------------
/docs/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/docs/media/qgis_connexion_PostgreSQL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3liz/formation-postgis/585f720bed11820fc22cc555fe96614bc7bd149c/docs/media/qgis_connexion_PostgreSQL.png
--------------------------------------------------------------------------------
/docs/media/qgis_creer_schema_explorateur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3liz/formation-postgis/585f720bed11820fc22cc555fe96614bc7bd149c/docs/media/qgis_creer_schema_explorateur.png
--------------------------------------------------------------------------------
/docs/media/qgis_creer_table_explorateur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3liz/formation-postgis/585f720bed11820fc22cc555fe96614bc7bd149c/docs/media/qgis_creer_table_explorateur.png
--------------------------------------------------------------------------------
/docs/media/qgis_rendu_simplification_fournisseur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3liz/formation-postgis/585f720bed11820fc22cc555fe96614bc7bd149c/docs/media/qgis_rendu_simplification_fournisseur.png
--------------------------------------------------------------------------------
/docs/media/qgis_traitement_exporter_dialogue_algorithme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3liz/formation-postgis/585f720bed11820fc22cc555fe96614bc7bd149c/docs/media/qgis_traitement_exporter_dialogue_algorithme.png
--------------------------------------------------------------------------------
/docs/media/qgis_traitement_exporter_postgresql_ogr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3liz/formation-postgis/585f720bed11820fc22cc555fe96614bc7bd149c/docs/media/qgis_traitement_exporter_postgresql_ogr.png
--------------------------------------------------------------------------------
/docs/merge_geometries.md:
--------------------------------------------------------------------------------
1 | # Fusionner des géométries
2 |
3 | On souhaite créer une seule géométrie qui est issue de la **fusion de toutes les géométries** regroupées par un critère (nature, code, etc.)
4 |
5 | Par exemple un polygone fusionnant les zonages qui partagent le même type
6 |
7 | ```sql
8 | SELECT count(id_zone_urba) AS nb_objets, typezone,
9 | ST_Union(geom) AS geom
10 | FROM z_formation.zone_urba
11 | GROUP BY typezone
12 | ```
13 |
14 | On souhaite parfois **fusionner toutes les géométries qui sont jointives**.
15 | Par exemple, on veut fusionner **toutes les parcelles jointives** pour créer des blocs.
16 |
17 | ```sql
18 | DROP TABLE IF EXISTS z_formation.bloc_parcelle_havre;
19 | CREATE TABLE z_formation.bloc_parcelle_havre AS
20 | SELECT
21 | row_number() OVER() AS id,
22 | string_agg(id::text, ', ') AS ids, t.geom::geometry(polygon, 2154) AS geom
23 | FROM (
24 | SELECT
25 | (St_Dump(ST_Union(a.geom))).geom AS geom
26 | FROM z_formation.parcelle_havre AS a
27 | WHERE ST_IsValid(a.geom)
28 | ) t
29 | JOIN z_formation.parcelle_havre AS p
30 | ON ST_Intersects(p.geom, t.geom)
31 | GROUP BY t.geom
32 | ;
33 | ALTER TABLE z_formation.bloc_parcelle_havre ADD PRIMARY KEY (id);
34 | CREATE INDEX ON z_formation.bloc_parcelle_havre USING GIST (geom);
35 | ```
36 |
37 | Continuer vers [Les triggers](./triggers.md)
38 |
--------------------------------------------------------------------------------
/docs/perform_calculation.md:
--------------------------------------------------------------------------------
1 | # Faire des calculs
2 |
3 | ## Calcul sur des attributs
4 |
5 | Le SQL permet de réaliser des calculs ou des modifications à partir de champs. On peut donc faire des calculs sur des nombres, ou des modifications (remplacement de texte, mise en majuscule, etc.)
6 |
7 | Faire un calcul très simple, avec des opérateurs `+ - /` et `*`, ainsi que des parenthèses
8 |
9 | ```sql
10 | -- On multiplie 10 par 2
11 | SELECT
12 | 10 * 2 AS vingt,
13 | (2.5 -1) * 10 AS quinze
14 | ```
15 |
16 | Il est aussi possible de faire des calculs à partir d'un ou plusieurs champs.
17 |
18 | Nous souhaitons par exemple créer un champ qui contiendra la **population** des communes. Dans la donnée source, le champ `popul` est de type chaîne de caractère, car il contient parfois la valeur `'NC'` lorsque la population n'est pas connue.
19 |
20 | Nous ne pouvons pas faire de calculs à partir d'un champ texte. On souhaite donc **créer un nouveau champ** population pour y stocker les valeurs entières.
21 |
22 | ```sql
23 | -- Ajout d'un champ de type entier dans la table
24 | ALTER TABLE z_formation.commune ADD COLUMN population integer;
25 | ```
26 |
27 | **Modifier** le nouveau champ population pour y mettre la valeur entière lorsqu'elle est connue. La modification d'une table se fait avec la requête `UPDATE`, en passant les champs à modifier et leur nouvelle valeur via `SET`
28 |
29 | ```sql
30 | -- Mise à jour d'un champ à partir d'un calcul
31 | UPDATE z_formation.commune SET population =
32 | CASE
33 | WHEN popul != 'NC' THEN popul::integer
34 | ELSE NULL
35 | END
36 | ;
37 | ```
38 |
39 | Dans cette requête, 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 )
40 |
41 | Une fois ce champ `population` renseigné correctement, dans un type entier, on peut réaliser un calcul très simple, par exemple **doubler la population**:
42 |
43 | ```sql
44 | -- Calcul simple : on peut utiliser les opérateurs mathématiques
45 | SELECT id_commune, code_insee, nom, geom,
46 | population,
47 | population * 2 AS double_population
48 | FROM z_formation.commune
49 | LIMIT 10
50 | ```
51 |
52 | Il est possible de **combiner plusieurs champs** pour réaliser un calcul. Nous verrons plus loin comment calculer la **densité de population** à partir de la population et de la surface des communes.
53 |
54 | ## Calculer des caractéristiques spatiales
55 |
56 | Par exemple la **longueur** ou la **surface**
57 |
58 | Calculer la longueur d'objets linéaires
59 |
60 | ```sql
61 | -- Calcul des longueurs de route
62 | SELECT id_route, id, nature,
63 | ST_Length(geom) AS longueur_m
64 | FROM z_formation.route
65 | LIMIT 100
66 | ```
67 |
68 | Calculer la **surface** de polygones, et utiliser ce résultat dans un calcul. Par exemple ici la **densité de population**:
69 |
70 | ```sql
71 | -- Calculer des données à partir de champs et de fonctions spatiales
72 | SELECT id_commune, code_insee, nom, geom,
73 | population,
74 | ST_Area(geom) AS surface,
75 | population / ( ST_Area(geom) / 1000000 ) AS densite_hab_km
76 | FROM z_formation.commune
77 | LIMIT 10
78 | ```
79 |
80 | ## Créer des géométries à partir de géométries
81 |
82 | On peut modifier les géométries avec des fonctions spatiales, ce qui revient à effectuer un calcul sur les géométries. Deux exemples classiques : **centroides** et **tampons**
83 |
84 | Calculer le **centroïde** de polygones
85 |
86 | ```sql
87 | -- Centroides des communes
88 | SELECT id_commune, code_insee, nom,
89 | ST_Centroid(geom) AS geom
90 | FROM z_formation.commune
91 | ```
92 |
93 | Le centroïde peut ne pas être à l'intérieur du polygone, par exemple sur la commune de **Arnières-sur-Iton**.
94 | Forcer le **centroïde à l'intérieur du polygone**. Attention, ce calcul est plus long.
95 | [Si vous souhaitez mieux comprendre l'algorithme derrière cette fonction](https://gis.stackexchange.com/questions/76498/how-is-st-pointonsurface-calculated)
96 |
97 | ```sql
98 | -- Centroïdes à l'intérieur des communes
99 | -- Attention, c'est plus long à calculer
100 | SELECT id_commune, code_insee, nom,
101 | ST_PointOnSurface(geom) AS geom
102 | FROM z_formation.commune
103 | ```
104 |
105 | Calculer le **tampon** autour d'objets
106 |
107 | ```sql
108 | -- Tampons de 1km autour des communes
109 | SELECT id_commune, nom, population,
110 | ST_Buffer(geom, 1000) AS geom
111 | FROM z_formation.commune
112 | LIMIT 10
113 | ```
114 |
115 | Continuer vers [Filtrer des données: WHERE](./filter_data.md)
116 |
--------------------------------------------------------------------------------
/docs/postgresql_in_qgis.md:
--------------------------------------------------------------------------------
1 | # Gestion des données PostgreSQL dans QGIS
2 |
3 | ## Introduction
4 |
5 | Lorsqu'on travaille avec des données **PostgreSQL**, QGIS n'accède pas à la donnée en lisant un ou plusieurs fichiers, mais fait des **requêtes** à la base, à chaque fois qu'il en a besoin: déplacement de carte, zoom, ouverture de la table attributaire, sélection par expression, etc.
6 |
7 | * QGIS **se connecte** à la base de données, et récupère des données qui sont stockées dans des tables. Il doit donc **télécharger la donnée** à chaque action (pas de cache car la donnée peut changer entre temps).
8 | * une **table** équivaut à une **couche SIG**, définie par un nom, une **liste de champs typés**, et un ou plusieurs champs de **géométrie**.
9 | * une **géométrie** est caractérisée par un **type** (polygone, point, ligne, etc.), une **dimension** (2D ou 3D) et une **projection** (Ex: EPSG:2154) codifiée via un SRID (Ex: 2154)
10 | * certaines tables n'ont pas de géométrie: on les appelle alors **non spatiales**. QGIS sait les exploiter, ce qui permet de stocker des informations de contexte (nomenclature, événements).
11 |
12 | La base de données fournit donc un lieu de stockage des données centralisé. On peut gérer les droits d'accès ou d'écriture sur les schémas et les tables.
13 |
14 |
15 | ## Créer une connexion QGIS à la base de données
16 |
17 | Dans QGIS, il faut **créer une nouvelle connexion** à PostgreSQL, via l'outil "Éléphant" : menu **Couches / Ajouter une couche / Ajouter une couche PostgreSQL**. Configurer les options suivantes :
18 |
19 | * laisser le champ **Service** vide (sauf si vous savez utiliser les fichiers de service PostgreSQL, ce qui est recommandé)
20 | * cocher les cases **Enregistrer** à côté de l'utilisateur et du mot de passe, après avoir **Tester la connexion** (via le bouton dédié)
21 | * cocher les cases en bas **Lister les tables sans géométries** et **Utiliser la table de métadonnées estimées**
22 | * Valider
23 |
24 | 
25 |
26 | **Attention** Pour plus de sécurité, privilégier l'usage d'un service PostgreSQL:
27 | https://docs.qgis.org/latest/fr/docs/user_manual/managing_data_source/opening_data.html#pg-service-file (plugin QGIS intéressant : PG Service Parser)
28 |
29 | Il est aussi intéressant pour les **performances** d'accès aux données PostgreSQL de modifier une option dans les options de QGIS, onglet **Rendu** : il faut cocher la case **Réaliser la simplification par le fournisseur de données lorsque c'est possible**. Cela permet de télécharger des versions allégées des données aux petites échelles. [Documentation QGIS](https://docs.qgis.org/latest/fr/docs/user_manual/introduction/qgis_configuration.html#rendering-settings)
30 |
31 | 
32 |
33 | **NB** Pour les couches PostGIS qui auraient déjà été ajoutées **avant d'avoir activé cette option**, vous pouvez manuellement changer dans vos projets via l'onglet **Rendu** de la boîte de dialogue des propriétés de chaque couche PostGIS.
34 |
35 |
36 | ## Ouvrir une couche PostgreSQL dans QGIS
37 |
38 | Trois solutions sont possibles :
39 |
40 | * **utiliser l'explorateur** : Le panneau présente un arbre qui liste les `schémas`, puis les `tables` ou `vues` exploitables. Une icône devant chaque table/vue indique si une table est géométrique ou non ainsi que le type de géométrie, point, ligne ou polygone. On peut utiliser le menu `Clic-Droit` sur les objets de l'arbre.
41 | * utiliser le menu **Couche / Ajouter une couche**. La boite de dialogue propose de se connecter, puis liste les schémas et les tables (ancienne méthode pas recommandée)
42 | * utiliser le **Gestionnaire de base de données**, qui présente une fenêtre QGIS séparée dédiée aux manipulations sur les données.
43 |
44 | ## Création de schémas et de tables
45 |
46 | On peut travailler avec le gestionnaire de bases de données de QGIS : menu **Base de données > Gestionnaire BD** (sinon via l'icône de la barre d’outil base de données) ou avec l'**explorateur** (recommandé).
47 |
48 | Dans l'arbre qui se présente, on peut **choisir sa connexion**, puis double-cliquer, ce qui montre l'ensemble des **schémas**, et l'ouverture d'un schéma montre la liste des tables et vues. Les menus permettent de créer ou d'éditer des objets (schémas, tables).
49 |
50 | Une **fenêtre SQL** permet de lancer manuellement des requêtes SQL. Nous allons principalement utiliser cet outil : menu **Base de données / Fenêtre SQL** (on peut aussi le lancer via F2).
51 |
52 | NB: C'est possible aussi d'utiliser le **fenêtre SQL de l'explorateur** via clic-droit `Exécuter le SQL ...`, mais elle ne permet pas encore de ne lancer que le **texte surligné**, ce qui est pourtant très pratique pendant une formation.
53 |
54 | ### Création du schéma
55 |
56 | Les **schémas** dans une base PostgreSQL sont utiles pour regrouper les tables.
57 |
58 | On recommande de ne pas créer de tables dans le schéma `public`, mais d'utiliser des schémas (par thématique, pour la gestion des droits, etc.).
59 |
60 | Pour la formation, nous allons créer un schéma `z_formation` :
61 |
62 | * Dans l'explorateur, faire un clic-droit sur le nom de la connexion et `Créer un schéma`.
63 |
64 | 
65 |
66 | ### Création d'une table
67 |
68 | Ensuite, on peut créer une **table** dans ce schéma : dans l'explorateur, faire un clic-droit sur le schéma `z_formation`, puis `Nouvelle table...` :
69 |
70 | * choisir le **schéma** et le **nom** de la table, en minuscule, sans accents ni caractères complexes
71 | * Via le bouton **Ajouter un champ**, on crée autant de champs que nécessaire en choisissant le nom et le type. **Choisir des noms de champ simples sans majuscule, espace ni accents !**.
72 | * Choisir dans la liste déroulante le **champ de clé primaire** (ici id)
73 | * Cocher **Créer une colonne géométrique** et choisir le type et le SRID (par exemple 2154 pour le Lambert 93)
74 | * Cocher **Créer un index spatial**
75 |
76 |
77 | 
78 |
79 | **NB**: on a créé une table dans cet exemple `z_formation.borne_incendie` avec les champs **code** (text), **debit** (real) et **geom** (géométrie de type Point, code SRID 2154)
80 |
81 | * Un champ `id` de type **entier auto-incrémenté** a été créé automatiquement par QGIS en tant que **clé primaire** de la table.
82 | * Un **index spatial** a aussi été créé par QGIS sur le champ de géométrie.
83 |
84 | ### Utiliser du SQL au lieu des menus de QGIS
85 |
86 | On peut aussi utiliser du **SQL** pour créer des objets dans la base :
87 |
88 | ```sql
89 | -- création d'un schéma
90 | CREATE SCHEMA IF NOT EXISTS z_formation;
91 |
92 | -- création de la table
93 | CREATE TABLE IF NOT EXISTS z_formation.borne_incendie (
94 | -- un serial est un entier auto-incrémenté
95 | id_borne serial NOT NULL PRIMARY KEY,
96 | code text NOT NULL,
97 | debit real,
98 | geom geometry(Point, 2154)
99 | );
100 | -- Création de l'index spatial
101 | DROP INDEX IF EXISTS borne_incendie_geom_idx;
102 | CREATE INDEX ON z_formation.borne_incendie USING GIST (geom);
103 |
104 | ```
105 |
106 | ### Ajouter des données dans une table
107 |
108 | On peut bien sûr charger la table dans QGIS, puis utiliser les **outils d'édition** classique pour créer des nouveaux objets ou les modifier.
109 |
110 | En SQL, il est aussi possible d'insérer des données ( https://sql.sh/cours/insert-into ). Par exemple pour les bornes à incendie :
111 |
112 |
113 | ```sql
114 | INSERT INTO z_formation.borne_incendie (code, debit, geom)
115 | VALUES
116 | ('ABC', 1.5, ST_SetSRID(ST_MakePoint(490846.0,6936902.7), 2154)),
117 | ('XYZ', 4.1, ST_SetSRID(ST_MakePoint(491284.9,6936551.6), 2154)),
118 | ('FGH', 2.9, ST_SetSRID(ST_MakePoint(490839.8,6937794.8), 2154)),
119 | ('IOP', 3.6, ST_SetSRID(ST_MakePoint(491203.3,6937488.1), 2154))
120 | ;
121 | ```
122 |
123 | **NB**: Nous verrons plus loin l'utilisation de fonctions de création de géométrie, comme **ST_MakePoint**
124 |
125 |
126 | ## Vérifier et créer les indexes spatiaux
127 |
128 | On peut vérifier si chaque table contient un **index spatial** via le gestionnaire de base de données de QGIS, en cliquant sur la table dans l'arbre, puis en regardant les informations de l'onglet **Info**. On peut alors créer l'index spatial via le lien bleu **Aucun index spatial défini (en créer un)**.
129 |
130 | Sinon, il est possible de le faire en SQL via la requête suivante :
131 |
132 | ```sql
133 | CREATE INDEX ON nom_du_schema.nom_de_la_table USING GIST (geom);
134 | ```
135 |
136 | Si on souhaite automatiser la création des indexes pour toutes les tables qui n'en ont pas, on peut utiliser une fonction, décrite dans la partie [Fonctions utiles](./utils.md)
137 |
138 | Continuer vers l'[Import des données dans PostgreSQL](./import_data.md)
139 |
--------------------------------------------------------------------------------
/docs/save_queries.md:
--------------------------------------------------------------------------------
1 | # Enregistrer une requête
2 |
3 | ## Les vues
4 |
5 | Une vue est l'enregistrement d'une requête, appelée **définition de la vue**, qui est stocké dans la base, et peut être **utilisée comme une table**.
6 |
7 | Créer une vue via `CREATE VIEW`
8 |
9 | ```sql
10 | -- On supprime d'abord la vue si elle existe
11 | DROP VIEW IF EXISTS z_formation.v_voies;
12 | -- On crée la vue en récupérant les routes de plus de 5 km
13 | CREATE VIEW z_formation.v_voies AS
14 | SELECT id_route, id AS code, ST_Length(geom) AS longueur, geom
15 | FROM z_formation.route
16 | WHERE ST_Length(geom) > 5000
17 | ```
18 |
19 | Utiliser cette vue dans une autre requête
20 |
21 | * pour filtrer les données
22 |
23 | ```sql
24 | -- Ou filtrer les données
25 | SELECT * FROM z_formation.v_voies
26 | WHERE longueur > 10000
27 | ```
28 |
29 | ## Enregistrer une requête comme une table
30 |
31 | C'est la même chose que pour enregistrer une vue, sauf qu'on crée une table: les données sont donc stockées en base, et n'évoluent plus en fonction des données source. Cela permet d'accéder rapidement aux données, car la requête sous-jacente n'est plus exécutée une fois la table créée.
32 |
33 | ### Exemple 1 - créer la table des voies rassemblant les routes et les chemins
34 |
35 | ```sql
36 | DROP TABLE IF EXISTS z_formation.t_voies;
37 | CREATE TABLE z_formation.t_voies AS
38 | SELECT
39 | -- on récupère tous les champs
40 | source.*,
41 | -- on calcule la longueur après rassemblement des données
42 | ST_Length(geom) AS longueur
43 | FROM (
44 | (SELECT id, geom
45 | FROM z_formation.chemin
46 | LIMIT 100)
47 | UNION ALL
48 | (SELECT id, geom
49 | FROM z_formation.route
50 | LIMIT 100)
51 | ) AS source
52 | ORDER BY longueur
53 | ;
54 | ```
55 |
56 | Comme c'est une table, il est intéressant d'ajouter un index spatial.
57 |
58 | ```sql
59 | CREATE INDEX ON z_formation.t_voies USING GIST (geom);
60 | ```
61 |
62 | On peut aussi ajouter une clé primaire
63 |
64 | ```sql
65 | ALTER TABLE z_formation.t_voies ADD COLUMN gid serial;
66 | ALTER TABLE z_formation.t_voies ADD PRIMARY KEY (gid);
67 | ```
68 |
69 | **Attention** Les données de la table n'évoluent plus en fonction des données des tables source. Il faut donc supprimer la table puis la recréer si besoin. Pour répondre à ce besoin, il existe les **vues matérialisées**.
70 |
71 |
72 |
73 |
74 | ### Exemple 2 - créer une table de nomenclature à partir des valeurs distinctes d'un champ.
75 |
76 | On crée la table si besoin. On ajoutera ensuite les données via `INSERT`
77 |
78 | ```sql
79 | -- Suppression de la table
80 | DROP TABLE IF EXISTS z_formation.nomenclature;
81 | -- Création de la table
82 | CREATE TABLE z_formation.nomenclature (
83 | id serial primary key,
84 | code text,
85 | libelle text,
86 | ordre smallint
87 | );
88 |
89 | ```
90 |
91 | On ajoute ensuite les données. La clause `WITH` permet de réaliser une sous-requête, et de l'utiliser ensuite comme une table. La clause `INSERT INTO` permet d'ajouter les données. On ne lui passe pas le champ id, car c'est un **serial**, c'est-à-dire un entier **auto-incrémenté**.
92 |
93 | ```sql
94 | -- Ajout des données à partir d'une table via commande INSERT
95 | INSERT INTO z_formation.nomenclature
96 | (code, libelle, ordre)
97 | -- Clause WITH pour récupérer les valeurs distinctes comme une table virtuelle
98 | WITH source AS (
99 | SELECT DISTINCT
100 | nature AS libelle
101 | FROM z_formation.lieu_dit_habite
102 | WHERE nature IS NOT NULL
103 | ORDER BY nature
104 | )
105 | -- Sélection des données dans cette table virtuelle "source"
106 | SELECT
107 | -- on crée un code à partir de l'ordre d'arrivée.
108 | -- row_number() OVER() permet de récupérer l'identifiant de la ligne dans l'ordre d'arrivée
109 | -- (un_champ)::text permet de convertir un champ ou un calcul en texte
110 | -- lpad permet de compléter le chiffre avec des zéro. 1 devient 01
111 | lpad( (row_number() OVER())::text, 2, '0' ) AS code,
112 | libelle,
113 | row_number() OVER() AS ordre
114 | FROM source
115 | ;
116 | ```
117 |
118 | Le résultat est le suivant:
119 |
120 | | code | libelle | ordre |
121 | |------|-----------------|-------|
122 | | 01 | Château | 1 |
123 | | 02 | Lieu-dit habité | 2 |
124 | | 03 | Moulin | 3 |
125 | | 04 | Quartier | 4 |
126 | | 05 | Refuge | 5 |
127 | | 06 | Ruines | 6 |
128 |
129 |
130 | ### Exemple 3 - créer une table avec l'extraction des parcelles sur une commune
131 |
132 | On utilise le champ `commune` pour filtrer. On n'oublie pas de créer l'index spatial, qui sera utilisé pour améliorer les performances lors des jointures spatiales.
133 |
134 | ```sql
135 | -- supprimer la table si elle existe déjà
136 | DROP TABLE IF EXISTS z_formation.parcelle_havre ;
137 |
138 | -- Créer la table via filtre sur le champ commune
139 | CREATE TABLE z_formation.parcelle_havre AS
140 | SELECT p.*
141 | FROM z_formation.parcelle AS p
142 | WHERE p.commune = '76351';
143 |
144 | -- Ajouter la clé primaire
145 | ALTER TABLE z_formation.parcelle_havre ADD PRIMARY KEY (id_parcelle);
146 |
147 | -- Ajouter l'index spatial
148 | CREATE INDEX ON z_formation.parcelle_havre USING GIST (geom);
149 | ```
150 |
151 | ## Enregistrer une requête comme une vue matérialisée
152 |
153 |
154 | ```sql
155 | -- On supprime d'abord la vue matérialisée si elle existe
156 | DROP MATERIALIZED VIEW IF EXISTS z_formation.vm_voies;
157 | -- On crée la vue en récupérant les routes de plus de 5 km
158 | CREATE MATERIALIZED VIEW z_formation.vm_voies AS
159 | SELECT id_route, id AS code, ST_Length(geom) AS longueur, geom
160 | FROM z_formation.route
161 | WHERE ST_Length(geom) > 6000
162 |
163 | -- Ajout des indexes sur le champ id_route et de géométrie
164 | CREATE INDEX ON z_formation.vm_voies (id_route);
165 | CREATE INDEX ON z_formation.vm_voies USING GIST (geom);
166 |
167 | -- On rafraîchit la vue matérialisée quand on en a besoin
168 | -- par exemple quand les données source ont été modifiées
169 | REFRESH MATERIALIZED VIEW z_formation.vm_voies;
170 |
171 | ```
172 |
173 | Continuer vers [Réaliser des jointures attributaires et spatiales; JOIN](./join_data.md)
174 |
--------------------------------------------------------------------------------
/docs/sql_select.md:
--------------------------------------------------------------------------------
1 | # Sélectionner
2 |
3 | Nous allons présenter des **requêtes SQL** de plus en plus complexes pour accéder aux données, et exploiter les capacités de PostgreSQL/PostGIS. Une requête est construite avec des instructions standardisées, appelées **clauses**
4 |
5 | ```sql
6 | -- Ordre des clauses SQL
7 | SELECT une_colonne, une_autre_colonne
8 | FROM nom_du_schema.nom_de_la_table
9 | (LEFT) JOIN autre_schema.autre_table
10 | ON critere_de_jointure
11 | WHERE condition
12 | GROUP BY champs_de_regroupement
13 | ORDER BY champs_d_ordre
14 | LIMIT 10
15 |
16 | ```
17 | Récupérer tous les objets d'une table, et les valeurs pour toutes les colonnes
18 |
19 | ```sql
20 | -- Sélectionner l'ensemble des données d'une couche: l'étoile veut dire "tous les champs de la table"
21 | SELECT *
22 | FROM z_formation.borne_incendie
23 | ;
24 | ```
25 |
26 | Les 10 premiers objets
27 |
28 | ```sql
29 | -- Sélectionner les 10 premières communes par ordre alphabétique
30 | SELECT *
31 | FROM z_formation.commune
32 | ORDER BY nom
33 | LIMIT 10
34 | ```
35 |
36 | Les 10 premiers objets par ordre alphabétique
37 |
38 | ```sql
39 | -- Sélectionner les 10 premières communes par ordre alphabétique descendant
40 | SELECT *
41 | FROM z_formation.commune
42 | ORDER BY nom DESC
43 | LIMIT 10
44 | ```
45 |
46 | Les 10 premiers objets avec un ordre sur plusieurs champs
47 |
48 | ```sql
49 | -- On peut utiliser plusieurs champs pour l'ordre
50 | SELECT *
51 | FROM z_formation.commune
52 | ORDER BY depart, nom
53 | LIMIT 10
54 | ```
55 |
56 | Sélectionner seulement certains champs
57 |
58 | ```sql
59 | -- Sélectionner seulement certains champs, et avec un ordre
60 | SELECT id_commune, code_insee, nom
61 | FROM z_formation.commune
62 | ORDER BY nom
63 | ```
64 |
65 | Donner un alias (un autre nom) aux champs
66 |
67 | ```sql
68 | -- Donner des alias aux noms des colonnes
69 | SELECT id_commune AS identifiant,
70 | code_insee AS "code_commune",
71 | nom
72 | FROM z_formation.commune
73 | ORDER BY nom
74 | ```
75 |
76 | On peut donc facilement, à partir de la clause `SELECT`, choisir quels champs on souhaite récupérer, dans l'ordre voulu, et renommer le champ en sortie.
77 |
78 |
79 | ## Visualiser une requête dans QGIS
80 |
81 | Si on veut charger le résultat de la requête 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éométrie** .
82 |
83 | Attention, si la table est non spatiale, il faut bien penser à décocher **Colonne de géométrie** !
84 |
85 | Par exemple, pour afficher les communes avec leur information sommaire:
86 |
87 | ```sql
88 | -- Ajouter la géométrie pour visualiser les données dans QGIS
89 | SELECT id_commune AS identifiant,
90 | code_insee AS "code_commune",
91 | nom, geom
92 | FROM z_formation.commune
93 | ORDER BY nom
94 | ```
95 |
96 | On choisira ici le champ **identifiant** comme identifiant unique, et le champ **geom** comme géométrie
97 |
98 |
99 | Continuer vers [Réaliser des calculs et créer des géométries: FONCTIONS](./perform_calculation.md)
100 |
--------------------------------------------------------------------------------
/docs/triggers.md:
--------------------------------------------------------------------------------
1 | # Les triggers
2 |
3 | Les **triggers**, aussi appelés en français **déclencheurs**, permettent de lancer des actions avant ou après ajout, modification ou suppression de données sur des tables (ou des vues).
4 |
5 | Les triggers peuvent par exemple être utilisés
6 |
7 | * pour lancer le calcul de certains champs de manière automatique: date de dernière modification, utilisateur à l'origine d'un ajout
8 | * pour contrôler certaines données avant enregistrement
9 | * pour lancer des requêtes après certaines actions (historiques de modifications)
10 |
11 | Des **fonctions trigger** sont associées aux triggers. Elles peuvent être écrites en **PL/pgSQL** ou d'autres languages (p. ex. PL/Python).
12 | 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é.
13 | Lire les derniers paragraphes [ici pour en savoir plus](https://docs.postgresql.fr/16/plpgsql-trigger.html#PLPGSQL-DML-TRIGGER).
14 |
15 | ## Calcul automatique de certains champs
16 |
17 | On crée une table `borne_incendie` pour pouvoir tester cette fonctionnalité:
18 |
19 | ```sql
20 |
21 | CREATE TABLE z_formation.borne_incendie (
22 | id_borne serial primary key,
23 | code text NOT NULL,
24 | debit integer,
25 | geom geometry(point, 2154)
26 | );
27 | CREATE INDEX ON z_formation.borne_incendie USING GIST (geom);
28 | ```
29 |
30 | On y ajoute des champs à renseigner de manière automatique
31 |
32 | ```sql
33 | -- TRIGGERS
34 | -- Modification de certains champs après ajout ou modification
35 | -- Créer les champs dans la table
36 | ALTER TABLE z_formation.borne_incendie ADD COLUMN modif_date date;
37 | ALTER TABLE z_formation.borne_incendie ADD COLUMN modif_user text;
38 | ALTER TABLE z_formation.borne_incendie ADD COLUMN longitude real;
39 | ALTER TABLE z_formation.borne_incendie ADD COLUMN latitude real;
40 | ALTER TABLE z_formation.borne_incendie ADD COLUMN donnee_validee boolean;
41 | ALTER TABLE z_formation.borne_incendie ADD COLUMN last_action text;
42 |
43 | ```
44 |
45 | On crée la fonction trigger qui ajoutera les métadonnées dans la table
46 |
47 | ```sql
48 | -- Créer la fonction qui sera lancée sur modif ou ajout de données
49 | CREATE OR REPLACE FUNCTION z_formation.ajout_metadonnees_modification()
50 | RETURNS TRIGGER
51 | AS $limite$
52 | DECLARE newjsonb jsonb;
53 | BEGIN
54 |
55 | -- on transforme l'enregistrement NEW (la ligne modifiée ou ajoutée) en JSON
56 | -- pour connaître la liste des champs
57 | newjsonb = to_jsonb(NEW);
58 |
59 | -- on peut ainsi tester si chaque champ existe dans la table
60 | -- avant de modifier sa valeur
61 | -- Par exemple, on teste si le champ modif_date est bien dans l'enregistrement courant
62 | IF newjsonb ? 'modif_date' THEN
63 | NEW.modif_date = now();
64 | RAISE NOTICE 'Date modifiée %', NEW.modif_date;
65 | END IF;
66 |
67 | IF newjsonb ? 'modif_user' THEN
68 | NEW.modif_user = CURRENT_USER;
69 | END IF;
70 |
71 | -- longitude et latitude
72 | IF newjsonb ? 'longitude' AND newjsonb ? 'latitude'
73 | THEN
74 | -- Soit on fait un UPDATE et les géométries sont différentes
75 | -- Soit on fait un INSERT
76 | -- Sinon pas besoin de calculer les coordonnées
77 | IF
78 | (TG_OP = 'UPDATE' AND NOT ST_Equals(OLD.geom, NEW.geom))
79 | OR (TG_OP = 'INSERT')
80 | THEN
81 | NEW.longitude = ST_X(ST_Centroid(NEW.geom));
82 | NEW.latitude = ST_Y(ST_Centroid(NEW.geom));
83 | END IF;
84 | END IF;
85 |
86 | -- Si je trouve un champ donnee_validee, je le mets à False pour revue par l'administrateur
87 | -- Je peux faire une symbologie dans QGIS qui montre les données modifiées depuis dernière validation
88 | IF newjsonb ? 'donnee_validee' THEN
89 | NEW.donnee_validee = False;
90 | END IF;
91 |
92 | -- Si je trouve un champ last_action, je peux y mettre UPDATE ou INSERT
93 | -- Pour savoir quelle est la dernière opération utilisée
94 | IF newjsonb ? 'last_action' THEN
95 | NEW.last_action = TG_OP;
96 | END IF;
97 |
98 | RETURN NEW;
99 | END;
100 | $limite$
101 | LANGUAGE plpgsql
102 | ;
103 | ```
104 |
105 | On crée enfin le déclencheur pour la ou les tables souhaitées, ce qui active le lancement de la fonction trigger précédente sur certaines actions:
106 |
107 | ```sql
108 | -- Dire à PostgreSQL d'écouter les modifications et ajouts sur la table
109 | CREATE TRIGGER trg_ajout_metadonnees_modification
110 | BEFORE INSERT OR UPDATE ON z_formation.borne_incendie
111 | FOR EACH ROW EXECUTE PROCEDURE z_formation.ajout_metadonnees_modification();
112 | ```
113 |
114 | ## Contrôles de conformité
115 |
116 | Il est aussi possible d'utiliser les triggers pour lancer des contrôles sur les valeurs de certains champs. Par exemple, on peut ajouter un contrôle sur la géométrie lors de l'ajout ou de la modification de données: on vérifie si la géométrie est bien en intersection avec les objets de la table des communes
117 |
118 | ```sql
119 | -- Contrôle de la géométrie
120 | -- qui doit être dans la zone d'intérêt
121 | -- On crée une fonction générique qui pourra s'appliquer pour toutes les couches
122 | CREATE OR REPLACE FUNCTION z_formation.validation_geometrie_dans_zone_interet()
123 | RETURNS TRIGGER AS $limite$
124 | BEGIN
125 | -- On vérifie l'intersection avec les communes, on renvoie une erreur si souci
126 | IF NOT ST_Intersects(
127 | NEW.geom,
128 | st_collectionextract((SELECT ST_Collect(geom) FROM z_formation.commune), 3)::geometry(multipolygon, 2154)
129 | ) THEN
130 | -- On renvoie une erreur
131 | RAISE EXCEPTION 'La géométrie doit se trouver dans les communes';
132 | END IF;
133 |
134 | RETURN NEW;
135 | END;
136 | $limite$
137 | LANGUAGE plpgsql;
138 |
139 | -- On l'applique sur la couches de test
140 | DROP TRIGGER IF EXISTS trg_validation_geometrie_dans_zone_interet ON z_formation.borne_incendie;
141 | CREATE TRIGGER trg_validation_geometrie_dans_zone_interet
142 | BEFORE INSERT OR UPDATE ON z_formation.borne_incendie
143 | FOR EACH ROW EXECUTE PROCEDURE z_formation.validation_geometrie_dans_zone_interet();
144 | ```
145 |
146 | Si on essaye de créer un point dans la table `z_formation.borne_incendie` en dehors des communes, la base renverra une erreur.
147 |
148 |
149 | ## Écrire les actions produites sur une table
150 |
151 | On crée d'abord une table qui permettra de stocker les actions
152 |
153 | ```sql
154 |
155 | CREATE TABLE IF NOT EXISTS z_formation.log (
156 | id serial primary key,
157 | log_date timestamp,
158 | log_user text,
159 | log_action text,
160 | log_data jsonb
161 | );
162 | ```
163 |
164 | On peut maintenant créer un trigger qui stocke dans cette table les actions effectuées. Dans cet exemple, toutes les données sont stockées, mais on pourrait bien sûr choisir de simplifier cela.
165 |
166 | ```sql
167 | CREATE OR REPLACE FUNCTION z_formation.log_actions()
168 | RETURNS TRIGGER AS $limite$
169 | DECLARE
170 | row_data jsonb;
171 | BEGIN
172 | -- We keep data
173 | IF TG_OP = 'INSERT' THEN
174 | -- for insert, we take the new data
175 | row_data = to_jsonb(NEW);
176 | ELSE
177 | -- for UPDATE and DELETE, we keep data before changes
178 | row_data = to_jsonb(OLD);
179 | END IF;
180 |
181 | -- We insert a new log item
182 | INSERT INTO z_formation.log (
183 | log_date,
184 | log_user,
185 | log_action,
186 | log_data
187 | )
188 | VALUES (
189 | now(),
190 | CURRENT_USER,
191 | TG_OP,
192 | row_data
193 | );
194 | IF TG_OP != 'DELETE' THEN
195 | RETURN NEW;
196 | ELSE
197 | RETURN OLD;
198 | END IF;
199 | END;
200 | $limite$
201 | LANGUAGE plpgsql;
202 |
203 | -- On l'applique sur la couches de test
204 | -- On écoute après l'action, d'où l'utilisation de `AFTER`
205 | -- On écoute pour INSERT, UPDATE ou DELETE
206 | DROP TRIGGER IF EXISTS trg_log_actions ON z_formation.borne_incendie;
207 | CREATE TRIGGER trg_log_actions
208 | AFTER INSERT OR UPDATE OR DELETE ON z_formation.borne_incendie
209 | FOR EACH ROW EXECUTE PROCEDURE z_formation.log_actions();
210 |
211 | ```
212 |
213 | NB:
214 |
215 | * Attention, ce type de tables de log peut vite devenir très grosse !
216 | * pour un log d'audit plus évolué réalisé à partir de triggers, vous pouvez consulter [le dépôt audit_trigger](https://github.com/Oslandia/audit_trigger/blob/master/audit.sql)
217 |
218 |
219 | Continuer vers [Correction des géométries invalides](./validate_geometries.md)
220 |
221 | ## Quiz
222 |
223 |
224 | Créer une table avec un champ id de type 'serial' et une géométrie de type polygone en 2154.
225 | Puis créer un trigger s'assurant que les géométries aient au minimum **4** points dessinés.
226 |
227 |
228 | ```sql
229 | -- Table: z_formation.polygone_mini_quatre_points
230 | -- DROP TABLE IF EXISTS z_formation.polygone_mini_quatre_points;
231 | CREATE TABLE IF NOT EXISTS z_formation.polygone_mini_quatre_points
232 | (
233 | id serial NOT NULL PRIMARY KEY,
234 | geom geometry(Polygon,2154)
235 | )
236 |
237 | -- FUNCTION: z_formation.contrainte_mini_quatre_points()
238 | -- DROP FUNCTION IF EXISTS z_formation.contrainte_mini_quatre_points();
239 | CREATE OR REPLACE FUNCTION z_formation.contrainte_mini_quatre_points()
240 | RETURNS trigger AS $limite$
241 | BEGIN
242 | -- On vérifie que le polygone a au moins 4 points dessinés
243 | -- => soit 5 points en comptant le dernier point qui ferme le polygone !
244 | IF ST_NPoints(NEW.geom) < 5
245 | THEN
246 | -- On renvoie une erreur
247 | RAISE EXCEPTION 'Le polygone doit avoir au moins 4 points dessinés';
248 | END IF;
249 |
250 | RETURN NEW;
251 | END;
252 | $limite$
253 | LANGUAGE plpgsql;
254 |
255 | -- Trigger: trg_contrainte_mini_quatre_points
256 | -- DROP TRIGGER IF EXISTS trg_contrainte_mini_quatre_points ON z_formation.polygone_mini_quatre_points;
257 | CREATE OR REPLACE TRIGGER trg_contrainte_mini_quatre_points
258 | BEFORE INSERT OR UPDATE
259 | ON z_formation.polygone_mini_quatre_points
260 | FOR EACH ROW
261 | EXECUTE FUNCTION z_formation.contrainte_mini_quatre_points();
262 | ```
263 |
264 |
--------------------------------------------------------------------------------
/docs/tutoriel.md:
--------------------------------------------------------------------------------
1 | # Tutoriel
2 |
3 | Afin de vous entraîner il existe différentes tutoriels en ligne vous permettant de vous exercer.
4 |
5 | - https://sql.sh/exercices-sql
6 | - https://sqlzoo.net/wiki/SQL_Tutorial
7 | - https://fxjollois.github.io/cours-sql/
8 | - http://webtic.free.fr/sql/exint/q1.htm
9 | - https://www.hackerrank.com/domains/sql
10 |
--------------------------------------------------------------------------------
/docs/union.md:
--------------------------------------------------------------------------------
1 | # Rassembler des données de plusieurs tables
2 |
3 | La clause `UNION` peut être utilisée pour regrouper les données de sources différentes dans une même table. Le `UNION ALL` fait la même choses, mais sans réaliser de dédoublonnement, ce qui est plus rapide.
4 |
5 | **Rassembler les routes et les chemins** ensemble, en ajoutant un champ "nature" pour les différencier
6 |
7 | ```sql
8 | -- Rassembler des données de tables différentes
9 | -- On utilise une UNION ALL
10 |
11 | (SELECT 'chemin' AS nature,
12 | geom,
13 | ROUND(ST_LENGTH(geom))::integer AS longueur
14 | FROM z_formation.chemin
15 | LIMIT 100)
16 | -- UNION ALL est placé entre 2 SELECT
17 | UNION ALL
18 | (SELECT 'route' AS nature,
19 | geom,
20 | ROUND(ST_LENGTH(geom))::integer AS longueur
21 | FROM z_formation.route
22 | LIMIT 100)
23 | -- Le ORDER BY doit être réalisé à la fin, et non sur chaque SELECT
24 | ORDER BY longueur
25 | ```
26 |
27 | Si on doit réaliser le même calcul sur chaque sous-ensemble (chaque SELECT), on peut le faire en 2 étapes via une sous-requête (ou une clause WITH)
28 |
29 | ```sql
30 | SELECT
31 | -- on récupère tous les champs
32 | source.*,
33 | -- on calcule la longueur après rassemblement des données
34 | st_length(geom) AS longueur
35 | FROM (
36 | (SELECT id, geom
37 | FROM z_formation.chemin
38 | LIMIT 100)
39 | UNION ALL
40 | (SELECT id, geom
41 | FROM z_formation.route
42 | LIMIT 100)
43 | ) AS source
44 | ORDER BY longueur DESC
45 | ;
46 | ```
47 |
48 | Continuer vers [Enregistrer les requêtes: VIEW](./save_queries.md)
49 |
--------------------------------------------------------------------------------
/docs/utils.md:
--------------------------------------------------------------------------------
1 | # Fonctions utiles
2 |
3 | Nous regroupons ici quelques fonctions réalisées au cours de formations ou d'accompagnements d'utilisateurs de PostgreSQL.
4 |
5 | ## Ajout de l'auto-incrémentation sur un champ entier
6 |
7 | Lorsqu'on importe une couche dans une table via les outils de QGIS,
8 | le champ d'identifiant choisi n'a pas le support de l'auto-incrémentation,
9 | ce qui peut poser des problèmes de l'ajout de nouvelles données.
10 |
11 | Depuis PostgreSQL 10, on peut maintenant utiliser des **identités**
12 | au lieu des **serial** pour avoir un champ auto-complété.
13 | Voir par exemple l'article https://www.loxodata.com/post/identity/
14 |
15 | Pour ajouter le support de l'auto-incrémentation sur un champ entier
16 | à une table existante, on peut utiliser les commandes suivantes :
17 |
18 | ```sql
19 | -- Activer la génération automatique
20 | ALTER TABLE "monschema"."test" ALTER "id" ADD GENERATED BY DEFAULT AS IDENTITY;
21 |
22 | -- Mettre la valeur de la séquence (implicite et cachée) à la valeur max du champ d'identifiant
23 | SELECT setval(pg_get_serial_sequence('"monschema"."test"', 'id'), (SELECT max("id") FROM "monschema"."test"));
24 | ```
25 |
26 | Pour transformer les séquences créées précédemment via des `serial` en identité avec `identity`, on peut lancer :
27 |
28 | ```sql
29 | -- Enlever la valeur par défaut sur le champ d'identifiant
30 | ALTER TABLE "monschema"."test" ALTER COLUMN id DROP DEFAULT;
31 |
32 | -- Supprimer la séquence
33 | DROP SEQUENCE IF EXISTS "monschema"."test_id_seq";
34 |
35 | -- Activer la génération automatique
36 | ALTER TABLE "monschema"."test" ALTER "id" ADD GENERATED BY DEFAULT AS IDENTITY;
37 |
38 | -- Mettre la valeur de la séquence (implicite et cachée) à la valeur max du champ d'identifiant
39 | SELECT setval(pg_get_serial_sequence('"monschema"."test"', 'id'), (SELECT max("id") FROM "monschema"."test"));
40 | ```
41 |
42 |
43 | ## Création automatique d'indexes spatiaux
44 |
45 | Pour des données spatiales volumineuses, les performances d'affichage sont bien meilleures
46 | à grande échelle si on a ajouté un **index spatial**. L'index est aussi beaucoup utilisé
47 | pour améliorer les performances d'analyses spatiales.
48 |
49 | On peut créer l'index spatial table par table, ou bien automatiser cette création,
50 | c'est-à-dire créer les indexes spatiaux **pour toutes les tables qui n'en ont pas**.
51 |
52 | Pour cela, nous avons conçu une fonction, téléchargeable ici: https://gist.github.com/mdouchin/cfa0e37058bcf102ed490bc59d762042
53 |
54 | On doit copier/coller le script SQL de cette page `GIST` dans la **fenêtre SQL**
55 | du Gestionnaire de bases de données de QGIS, puis lancer la requête avec **Exécuter**.
56 | On peut ensuite vider le contenu de la fenêtre, puis appeler la fonction `create_missing_spatial_indexes` via le code SQL suivant :
57 |
58 | ```sql
59 | -- On lance avec le paramètre à True si on veut juste voir les tables qui n'ont pas d'index spatial
60 | -- On lance avec False si on veut créer les indexes automatiquement
61 |
62 | -- Vérification
63 | SELECT * FROM create_missing_spatial_indexes( True );
64 |
65 | -- Création
66 | SELECT * FROM create_missing_spatial_indexes( False );
67 | ```
68 |
69 | ## Trouver toutes les tables sans clé primaire
70 |
71 | Il est très important de déclarer une clé primaire pour vos tables stockées dans PostgreSQL. Cela fournit un moyen aux logiciels comme QGIS d'identifier de manière performante les lignes dans une table. Sans clé primaire, les performances d'accès aux données peuvent être dégradées.
72 |
73 | Vous pouvez trouver l'ensemble des tables de votre base de données sans clé primaire en construisant cette vue PostgreSQL `tables_without_primary_key`:
74 |
75 | ```sql
76 | DROP VIEW IF EXISTS tables_without_primary_key;
77 | CREATE VIEW tables_without_primary_key AS
78 | SELECT t.table_schema, t.table_name
79 | FROM information_schema.tables AS t
80 | LEFT JOIN information_schema.table_constraints AS c
81 | ON t.table_schema = c.table_schema
82 | AND t.table_name = c.table_name
83 | AND c.constraint_type = 'PRIMARY KEY'
84 | WHERE True
85 | AND t.table_type = 'BASE TABLE'
86 | AND t.table_schema not in ('pg_catalog', 'information_schema')
87 | AND c.constraint_name IS NULL
88 | ORDER BY table_schema, table_name
89 | ;
90 | ```
91 |
92 | * Pour lister les tables sans clé primaire, vous pouvez ensuite lancer la requête suivante:
93 |
94 | ```sql
95 | SELECT *
96 | FROM tables_without_primary_key;
97 | ```
98 |
99 | Ce qui peut donner par exemple:
100 |
101 | | table_schema | table_name |
102 | |---------------|----------------|
103 | | agriculture | parcelles |
104 | | agriculture | puits |
105 | | cadastre | sections |
106 | | environnement | znieff |
107 | | environnement | parcs_naturels |
108 |
109 |
110 | * Pour lister les tables sans clé primaire dans un schéma particulier, par exemple `cadastre`, vous pouvez ensuite lancer la requête :
111 |
112 | ```sql
113 | SELECT *
114 | FROM tables_without_primary_key
115 | WHERE table_schema IN ('cadastre');
116 | ```
117 |
118 | Ce qui peut alors donner:
119 |
120 | | table_schema | table_name |
121 | |---------------|----------------|
122 | | cadastre | sections |
123 |
124 |
125 |
126 | ## Ajouter automatiquement plusieurs champs à plusieurs tables
127 |
128 | Il est parfois nécessaire d'**ajouter des champs à une ou plusieurs tables**, par exemple pour y stocker ensuite des métadonnées (date de modification, date d'ajout, utilisateur, lien, etc).
129 |
130 | Nous proposons pour cela la fonction `ajout_champs_dynamiques` qui permet de fournir un nom de schéma, un nom de table, et une chaîne de caractère contenant la liste séparée par virgule des champs et de leur type.
131 |
132 | La fonction est accessible ici: https://gist.github.com/mdouchin/50234f1f33801aed6f4f2cbab9f4887c
133 |
134 | * Exemple d'utilisation **pour une table** `commune` du schéma `test`: on ajoute les champs `date_creation`, `date_modification` et `utilisateur`
135 |
136 | ```sql
137 | SELECT
138 | ajout_champs_dynamiques('test', 'commune', 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text')
139 | ;
140 | ```
141 |
142 | * Exemple d'utilisation pour **toutes les tables d'un schéma**, ici le schéma `test`. On utilise dans cette exemple la vue `geometry_columns` qui liste les tables spatiales, car on souhaite aussi ne faire cet ajout que pour les données de type **POINT**
143 |
144 | ```sql
145 | -- Lancer la création de champs sur toutes les tables
146 | -- du schéma test
147 | -- contenant des géométries de type Point
148 | SELECT f_table_schema, f_table_name,
149 | ajout_champs_dynamiques(
150 | -- schéma
151 | f_table_schema,
152 | -- table
153 | f_table_name,
154 | -- liste des champs, au format nom_du_champ TYPE
155 | 'date_creation timestamp DEFAULT now(), date_modification timestamp DEFAULT now(), utilisateur text'
156 | )
157 | FROM geometry_columns
158 | WHERE True
159 | AND "type" LIKE '%POINT'
160 | AND f_table_schema IN ('test')
161 | ORDER BY f_table_schema, f_table_name
162 | ;
163 | ```
164 |
165 | ## Vérifier la taille des bases, tables et schémas
166 |
167 | ### Connaître la taille des bases de données
168 |
169 | On peut lancer la requête suivante, qui renvoie les bases de données ordonnées par taille descendante.
170 |
171 | ```sql
172 | SELECT
173 | pg_database.datname AS db_name,
174 | pg_database_size(pg_database.datname) AS db_size,
175 | pg_size_pretty(pg_database_size(pg_database.datname)) AS db_pretty_size
176 | FROM pg_database
177 | WHERE datname NOT IN ('postgres', 'template0', 'template1')
178 | ORDER BY db_size DESC;
179 | ```
180 |
181 | ### Calculer la taille des tables
182 |
183 | On crée une fonction `get_table_info` qui utilise les tables système pour lister les tables, récupérer leur schéma et les informations de taille.
184 |
185 | ```sql
186 | DROP FUNCTION IF EXISTS get_table_info();
187 | CREATE OR REPLACE FUNCTION get_table_info()
188 | RETURNS TABLE (
189 | oid oid,
190 | schema_name text,
191 | table_name text,
192 | row_count integer,
193 | total_size bigint,
194 | pretty_total_size text
195 | )
196 | AS $$
197 | BEGIN
198 | RETURN QUERY
199 | SELECT
200 | b.oid, b.schema_name::text, b.table_name::text,
201 | b.row_count::integer,
202 | b.total_size::bigint,
203 | pg_size_pretty(b.total_size) AS pretty_total_size
204 | FROM (
205 | SELECT *,
206 | a.total_size - index_bytes - COALESCE(toast_bytes,0) AS table_bytes
207 | FROM (
208 | SELECT
209 | c.oid,
210 | nspname AS schema_name,
211 | relname AS TABLE_NAME,
212 | c.reltuples AS row_count,
213 | pg_total_relation_size(c.oid) AS total_size,
214 | pg_indexes_size(c.oid) AS index_bytes,
215 | pg_total_relation_size(reltoastrelid) AS toast_bytes
216 | FROM pg_class c
217 | LEFT JOIN pg_namespace n
218 | ON n.oid = c.relnamespace
219 | WHERE relkind = 'r'
220 | AND nspname NOT IN ('pg_catalog', 'information_schema')
221 | ) AS a
222 | ) AS b
223 | ;
224 | END; $$
225 | LANGUAGE 'plpgsql';
226 | ```
227 |
228 | On peut l'utiliser simplement de la manière suivante
229 |
230 | ```sql
231 | -- Liste les tables
232 | SELECT * FROM get_table_info() ORDER BY schema_name, table_name DESC;
233 |
234 | -- Lister les tables dans l'ordre inverse de taille
235 | SELECT * FROM get_table_info() ORDER BY total_size DESC;
236 |
237 | ```
238 |
239 | ### Calculer la taille des schémas
240 |
241 | On crée une simple fonction qui renvoie la somme des tailles des tables d'un schéma
242 |
243 | ```sql
244 | -- Fonction pour calculer la taille d'un schéma
245 | CREATE OR REPLACE FUNCTION pg_schema_size(schema_name text)
246 | RETURNS BIGINT AS
247 | $$
248 | SELECT
249 | SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))::BIGINT
250 | FROM pg_tables
251 | WHERE schemaname = schema_name
252 | $$
253 | LANGUAGE SQL;
254 | ```
255 |
256 | On peut alors l'utiliser pour connaître la taille d'un schéma
257 |
258 | ```sql
259 | -- utilisation pour un schéma
260 | SELECT pg_size_pretty(pg_schema_size('public')) AS ;
261 | ```
262 |
263 | Ou lister l'ensemble des schémas
264 |
265 | ```sql
266 | -- lister les schémas et récupérer leur taille
267 | SELECT schema_name, pg_size_pretty(pg_schema_size(schema_name))
268 | FROM information_schema.schemata
269 | WHERE schema_name NOT IN ('pg_catalog', 'information_schema')
270 | ORDER BY pg_schema_size(schema_name) DESC;
271 | ```
272 |
273 | ## Lister les triggers appliqués sur les tables
274 |
275 | On peut utiliser la requête suivante pour lister l'ensemble des triggers activés sur les tables
276 |
277 | ```sql
278 | SELECT
279 | event_object_schema AS table_schema,
280 | event_object_table AS table_name,
281 | trigger_schema,
282 | trigger_name,
283 | string_agg(event_manipulation, ',') AS event,
284 | action_timing AS activation,
285 | action_condition AS condition,
286 | CASE WHEN tgenabled = 'O' THEN True ELSE False END AS trigger_active,
287 | action_statement AS definition
288 | FROM information_schema.triggers AS t
289 | INNER JOIN pg_trigger AS p
290 | ON p.tgrelid = concat('"', event_object_schema, '"."', event_object_table, '"')::regclass
291 | AND trigger_name = tgname
292 | WHERE True
293 | GROUP BY 1,2,3,4,6,7,8,9
294 | ORDER BY table_schema, table_name
295 | ;
296 | ```
297 |
298 | Cette requête renvoie un tableau de la forme :
299 |
300 | | table_schema | table_name | trigger_schema | trigger_name | event | activation | condition | trigger_active | definition |
301 | |--------------|------------------------|----------------|----------------------|--------|------------|-----------|--------------- |------------------------------------------------------|
302 | | gestion | acteur | gestion | tr_date_maj | UPDATE | BEFORE | | f | EXECUTE FUNCTION occtax.maj_date() |
303 | | occtax | organisme | occtax | tr_date_maj | UPDATE | BEFORE | | t | EXECUTE FUNCTION occtax.maj_date() |
304 | | taxon | iso_metadata_reference | taxon | update_imr_timestamp | UPDATE | BEFORE | | t | EXECUTE FUNCTION taxon.update_imr_timestamp_column() |
305 |
306 |
307 | ## Lister les fonctions installées par les extensions
308 |
309 | Il est parfois utile de lister les **fonctions des extensions**, par exemple pour :
310 |
311 | * vérifier leur nom et leurs paramètres.
312 | * détecter celles qui n'ont pas le bon propriétaire
313 |
314 | La requête suivante permet d'afficher les informations essentielles des fonctions créées
315 | par les extensions installées dans la base :
316 |
317 | ```sql
318 | SELECT DISTINCT
319 | ne.nspname AS extension_schema,
320 | e.extname AS extension_name,
321 | np.nspname AS function_schema,
322 | p.proname AS function_name,
323 | pg_get_function_identity_arguments(p.oid) AS function_params,
324 | proowner::regrole AS function_owner
325 | FROM
326 | pg_catalog.pg_extension AS e
327 | INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
328 | INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
329 | INNER JOIN pg_catalog.pg_namespace AS ne ON (ne.oid = e.extnamespace)
330 | INNER JOIN pg_catalog.pg_namespace AS np ON (np.oid = p.pronamespace)
331 | WHERE
332 | TRUE
333 | -- only extensions
334 | AND d.deptype = 'e'
335 | -- not in pg_catalog
336 | AND ne.nspname NOT IN ('pg_catalog')
337 | -- optionnally filter some extensions
338 | -- AND e.extname IN ('postgis', 'postgis_raster')
339 | -- optionnally filter by some owner
340 | AND proowner::regrole::text IN ('postgres')
341 | ORDER BY
342 | extension_name,
343 | function_name;
344 | ;
345 | ```
346 |
347 | qui renvoie une résultat comme ceci (cet exemple est un extrait de quelques lignes) :
348 |
349 |
350 | | extension_schema | extension_name | function_schema | function_name | function_params | function_owner |
351 | |-------------------|----------------|-----------------|-----------------------------|------------------------------------------------------|-----------------|
352 | | public | fuzzystrmatch | public | levenshtein_less_equal | text, text, integer | johndoe |
353 | | public | fuzzystrmatch | public | metaphone | text, integer | johndoe |
354 | | public | fuzzystrmatch | public | soundex | text | johndoe |
355 | | public | fuzzystrmatch | public | text_soundex | text | johndoe |
356 | | public | hstore | public | akeys | hstore | johndoe |
357 | | public | hstore | public | avals | hstore | johndoe |
358 | | public | hstore | public | defined | hstore, text | johndoe |
359 | | public | postgis | public | st_buffer | text, double precision, integer | johndoe |
360 | | public | postgis | public | st_buffer | geom geometry, radius double precision, options text | johndoe |
361 | | public | postgis | public | st_buildarea | geometry | johndoe |
362 |
363 | On peut bien sûr modifier la clause `WHERE` pour filtrer plus ou moins les fonctions renvoyées.
364 |
365 |
366 | ## Lister les vues contenant `row_number() over()` non typé en `integer`
367 |
368 | Si on utilise des vues dans QGIS qui créent un identifiant unique via le numéro de ligne, il est important :
369 |
370 | * que le type de cet identifiant soit entier `integer` et pas entier long `bigint`
371 | * avoir une clause `ORDER BY` pour essayer au maximum que QGIS récupère les objets toujours dans le même ordre.
372 |
373 | Quand une requête d'une vue utilise `row_number() OVER()`, depuis des versions récentes de PostgreSQL, cela renvoie un entier long `bigint` ce qui n'est pas conseillé.
374 |
375 | On peut trouver ces vues ou vues matérialisées via cette requête :
376 |
377 | ```sql
378 | -- vues
379 | SELECT
380 | concat('"', schemaname, '"."', viewname, '"') AS row_number_view
381 | FROM pg_views
382 | WHERE "definition" ~* '(.)+row_number\(\s*\)\s*over\s*\(\s*\) (.)+'
383 | ORDER BY schemaname, viewname
384 | ;
385 |
386 | -- vues matérialisées
387 | SELECT
388 | concat('"', schemaname, '"."', matviewname, '"') AS row_number_view
389 | FROM pg_views
390 | WHERE "definition" ~* '(.)+row_number\(\s*\)\s*over\s*\(\s*\) (.)+'
391 | ORDER BY schemaname, matviewname
392 | ;
393 | ```
394 |
395 | ## Lister les tables qui ont une clé primaire non entière
396 |
397 | Pour éviter des soucis de performances sur les gros jeux de données, il faut éviter d'avoir des tables avec des clés primaires sur des champs qui ne sont pas de type entier `integer`.
398 |
399 | En effet, dans QGIS, l'ouverture de ce type de table avec une clé primaire de type `text`, ou même `bigint`, cela entraîne la création et le stockage en mémoire d'une table de correspondance entre chaque objet de la couche et le numéro d'arrivée de la ligne. Sur les tables volumineuses, cela peut être sensible.
400 |
401 | Pour trouver toutes les tables, on peut faire cette requête :
402 |
403 | ```sql
404 | SELECT
405 | nspname AS table_schema, relname AS table_name,
406 | a.attname AS column_name,
407 | format_type(a.atttypid, a.atttypmod) AS column_type
408 | FROM pg_index AS i
409 | JOIN pg_class AS c
410 | ON i.indrelid = c.oid
411 | JOIN pg_attribute AS a
412 | ON a.attrelid = c.oid
413 | AND a.attnum = any(i.indkey)
414 | JOIN pg_namespace AS n
415 | ON n.oid = c.relnamespace
416 | WHERE indisprimary AND nspname NOT LIKE 'pg_%' AND nspname NOT LIKE 'lizmap_%'
417 | AND format_type(a.atttypid, a.atttypmod) != 'integer';
418 | ```
419 |
420 | Ce qui donne par exemple :
421 |
422 | table_schema | table_name | column_name | column_type
423 | -------------------|----------------------------------|-------------|-------------------
424 | un_schema | une_table_a | id | bigint
425 | un_schema | une_table_b | id | bigint
426 | un_autre_schema | autre_table_c | id | character varying
427 | un_autre_schema | autre_table_d | id | character varying
428 |
429 |
430 | ## Trouver les tables spatiales avec une géométrie non typée
431 |
432 | Il est important lorsqu'on crée des champs de type géométrie `geometry` de préciser le type des objets (point, ligne, polygone, etc.) et la projection.
433 |
434 | On doit donc créer les champs comme ceci :
435 |
436 | ```sql
437 | CREATE TABLE test (
438 | id serial primary key,
439 | geom geometry(Point, 2154)
440 | );
441 | ```
442 |
443 | et non comme ceci :
444 |
445 | ```sql
446 | CREATE TABLE test (
447 | id serial primary key,
448 | geom geometry
449 | );
450 | ```
451 |
452 | C'est donc important lorsqu'on crée des tables à partir de requêtes SQL de toujours bien typer les géométries. Par exemple :
453 |
454 | ```sql
455 | CREATE TABLE test AS
456 | SELECT id,
457 | ST_Centroid(geom)::geometry(Point, 2154) AS geom
458 | -- ne pas faire :
459 | -- ST_Centroid(geom) AS geom
460 | FROM autre_table
461 | ```
462 |
463 | On peut trouver toutes les tables qui auraient été créées avec des champs de géométrie non typés via la requête suivante :
464 |
465 | ```sql
466 | SELECT *
467 | FROM geometry_columns
468 | WHERE srid = 0 OR lower(type) = 'geometry'
469 | ;
470 | ```
471 |
472 | Il faut corriger ces vues ou tables.
473 |
474 | ## Trouver les objets avec des géométries trop complexes
475 |
476 | ```sql
477 | SELECT count(*)
478 | FROM ma_table
479 | WHERE ST_NPoints(geom) > 10000
480 | ;
481 | ```
482 |
483 | Les trop gros polygones (zones inondables, zonages issus de regroupement de nombreux objets, etc.) peuvent poser de réels soucis de performance, notamment sur les opérations d'intersection avec les objets d'autres couches via `ST_Intersects`.
484 |
485 | On peut corriger cela via la fonction `ST_Subdivide`. Voir [Documentation de ST_Subdivide](https://postgis.net/docs/ST_Subdivide.html)
486 |
487 |
488 | ## Tester les différences entre 2 tables de même structure
489 |
490 | 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`.
491 |
492 | On peut utiliser une fonction qui utilise les possibilités du format hstore pour comparer les données entre elles.
493 |
494 | ```sql
495 | -- On ajoute le support du format hstore
496 | CREATE EXTENSION IF NOT EXISTS hstore;
497 |
498 | -- On crée la fonction de comparaison
499 | DROP FUNCTION compare_tables(text,text,text,text,text,text[]);
500 | CREATE OR REPLACE FUNCTION compare_tables(
501 | p_schema_name_a text,
502 | p_table_name_a text,
503 | p_schema_name_b text,
504 | p_table_name_b text,
505 | p_common_identifier_field text,
506 | p_excluded_fields text[]
507 |
508 | ) RETURNS TABLE(
509 | uid text,
510 | status text,
511 | table_a_values hstore,
512 | table_b_values hstore
513 | )
514 | LANGUAGE plpgsql
515 | AS $_$
516 | DECLARE
517 | sqltemplate text;
518 | BEGIN
519 |
520 | -- Compare data
521 | sqltemplate = '
522 | SELECT
523 | coalesce(ta."%1$s", tb."%1$s") AS "%1$s",
524 | CASE
525 | WHEN ta."%1$s" IS NULL THEN ''not in table A''
526 | WHEN tb."%1$s" IS NULL THEN ''not in table B''
527 | ELSE ''table A != table B''
528 | END AS status,
529 | CASE
530 | WHEN ta."%1$s" IS NULL THEN NULL
531 | ELSE (hstore(ta.*) - ''%6$s''::text[]) - (hstore(tb) - ''%6$s''::text[])
532 | END AS values_in_table_a,
533 | CASE
534 | WHEN tb."%1$s" IS NULL THEN NULL
535 | ELSE (hstore(tb.*) - ''%6$s''::text[]) - (hstore(ta) - ''%6$s''::text[])
536 | END AS values_in_table_b
537 | FROM "%2$s"."%3$s" AS ta
538 | FULL JOIN "%4$s"."%5$s" AS tb
539 | ON ta."%1$s" = tb."%1$s"
540 | WHERE
541 | (hstore(ta.*) - ''%6$s''::text[]) != (hstore(tb.*) - ''%6$s''::text[])
542 | OR (ta."%1$s" IS NULL)
543 | OR (tb."%1$s" IS NULL)
544 | ';
545 |
546 | RETURN QUERY
547 | EXECUTE format(sqltemplate,
548 | p_common_identifier_field,
549 | p_schema_name_a,
550 | p_table_name_a,
551 | p_schema_name_b,
552 | p_table_name_b,
553 | p_excluded_fields
554 | );
555 |
556 | END;
557 | $_$;
558 | ```
559 |
560 | Cette fonction attend en paramètres
561 |
562 | * le schéma de la **table A**. Ex: `referentiels`
563 | * le nom de la **table A**. Ex: `communes_2021`
564 | * le schéma de la **table B**. Ex: `referentiels`
565 | * le nom de la **table B**. Ex: `communes_2022`
566 | * le nom du champ qui identifie de manière unique la donnée. Ce n'est pas forcément la clé primaire. Ex `code_commune`
567 | * un tableau de champs pour lesquels ne pas vérifier les différences. Ex: `array['region', 'departement']`
568 |
569 | La requête à lancer est la suivantes
570 | ```sql
571 | SELECT "uid", "status", "table_a_values", "table_b_values"
572 | FROM compare_tables(
573 | 'referentiels', 'commune_2021',
574 | 'referentiels', 'commune_2022',
575 | 'code_commune',
576 | array['region', 'departement']
577 | )
578 | ORDER BY status, uid
579 | ;
580 | ```
581 |
582 | Exemple de données renvoyées:
583 |
584 | | uid | status | table_a_values | table_b_values |
585 | |-------|--------------------|-----------------------------------------------------------------------------|------------------------------------------------------------------------------|
586 | | 12345 | not in table A | NULL | "annee_ref"=>"2022", "nom_commune"=>"Nouvelle commune", "population"=>"5723" |
587 | | 97612 | not in table B | "annee_ref"=>"2021", "nom_commune"=>"Ancienne commune", "population"=>"840" | NULL |
588 | | 97602 | table A != table B | "annee_ref"=>"2021", "population"=>"1245" | "annee_ref"=>"2022", "population"=>"1322" |
589 |
590 | Dans l'affichage ci-dessus, je n'ai pas affiché le champ de géométrie, mais la fonction teste aussi les différences de géométries.
591 |
592 | *Attention, les performances de ce type de requête ne sont pas forcément assurées pour des volumes de données importants.*
593 |
594 |
595 | ## Trouver les valeurs distinctes des champs d'une table
596 |
597 | Pour comprendre quelles données sont présentes dans une table PostgreSQL,
598 | vous pouvez exploiter la puissance des fonctions de manipulation du `JSON`
599 | et récupérer automatiquement toutes les **valeurs distinctes** d'une table.
600 |
601 | Cela permet de lister les **champs** de cette table et de bien se représenter
602 | ce qu'ils contiennent.
603 |
604 | ```sql
605 | SELECT
606 | -- nom du champ de la table
607 | key AS champ,
608 |
609 | -- On regroupe les valeurs distinctes du champ
610 | -- depuis le JSON calculé plus bas via to_jsonb
611 | -- On compte les valeurs distinctes
612 | count(DISTINCT value) AS nombre,
613 |
614 | -- On récupère les valeurs uniques pour ce champ
615 | json_agg(DISTINCT value) AS valeurs
616 | FROM
617 | -- Table dans laquelle chercher les valeurs uniques
618 | velo.amenagement AS i,
619 | -- Transformation de chaque ligne de la table en JSON (paires clé/valeurs)
620 | jsonb_each(
621 | -- on utilise le - 'id' - 'geom' pour ne pas récupérer les valeurs de ces champs
622 | to_jsonb(i) - 'id' - 'geom'
623 | )
624 | -- On regroupe par clé, c'est-à-dire par champ
625 | GROUP BY key;
626 | ```
627 |
628 | ce qui donnera comme résultat
629 |
630 | ```sql
631 | champ | nombre | valeurs
632 | ------------+--------+--------------------------------------------------------------------------------------------------------
633 | commune | 8 | ["AMBON", "ARZAL", "BILLIERS", "LA ROCHE-BERNARD", "LE GUERNO", "MUZILLAC", "NIVILLAC", "SAINT-DOLAY"]
634 | gestionnai | 3 | ["Commune", "Département", "EPCI"]
635 | id_iti | 9 | ["iti_02", "iti_03", "iti_06", "iti_07", "iti_08", "iti_09", "iti_13", "iti_15", "iti_18"]
636 | insee | 9 | ["56002", "56004", "56018", "56077", "56143", "56147", "56149", "56195", "56212"]
637 | maitre_ouv | 3 | ["Commune", "Département", "EPCI"]
638 | rlv_chauss | 5 | ["Double sens", "Interdit à la circ.", "NC", "Rond-point", "Sens unique"]
639 | rlv_md_dx_ | 5 | ["Aucun aménagement", "Bande", "Contresens cyclable", "Voie uniquement piétonne", "Voie verte"]
640 | rlv_pente | 5 | ["Forte (ponctuelle)", "Forte (tronçon)", "Moyenne", "NC", "Nulle ou faible"]
641 | rlv_vitess | 7 | ["< 20", "20", "30", "50", "70", "80 et plus", "NC"]
642 | type_surfa | 3 | ["Lisse", "Meuble", "Rugueux"]
643 | vvv | 3 | ["V3", "V42", "V45"]
644 | ```
645 |
646 | Points d'attention:
647 |
648 | * Attention aux performances sur un très gros volume de données.
649 | * Bien penser à ne pas prendre en compte les champs qui contiennent
650 | des données différentes pour tous les objets (identifiants, longueur, etc.)
651 | au risque d'avoir une très longue liste de valeurs uniques.
652 |
653 | Continuer vers [Gestion des droits](./grant.md)
654 |
--------------------------------------------------------------------------------
/docs/validate_geometries.md:
--------------------------------------------------------------------------------
1 | # Correction des géométries
2 |
3 | Avec PostgreSQL on peut **tester la validité des géométries** d'une table, comprendre la raison et localiser les soucis de validité:
4 |
5 |
6 | ```sql
7 | SELECT
8 | id_parcelle,
9 | -- vérifier si la géom est valide
10 | ST_IsValid(geom) AS validite_geom,
11 | -- connaitre la raison d'invalidité
12 | st_isvalidreason(geom) AS validite_raison,
13 | -- sortir un point qui localise le souci de validité
14 | ST_SetSRID(location(st_isvaliddetail(geom)), 2154) AS geom
15 | FROM z_formation.parcelle_havre
16 | WHERE ST_IsValid(geom) IS FALSE
17 | ```
18 |
19 | qui renvoie 2 erreurs de polygones croisés.
20 |
21 | | id_parcelle | validite_geom | validite_raison | point_invalide |
22 | |-------------|---------------|------------------------------------------------------|--------------------------------------------|
23 | | 707847 | False | Self-intersection[492016.260004897 6938870.66384629] | 010100000041B93E0AC1071E4122757CAA3D785A41 |
24 | | 742330 | False | Self-intersection[489317.48266784 6939616.89391708] | 0101000000677A40EE95DD1D41FBEF3539F8785A41 |
25 |
26 | et qu'on peut ouvrir comme une nouvelle couche, avec le champ géométrie *point_invalide*, ce qui permet de visualiser dans QGIS les positions des erreurs.
27 |
28 | PostGIS fournir l'outil **ST_MakeValid** pour corriger automatiquement les géométries invalides. On peut l'utiliser pour les lignes et polygones.
29 |
30 | Attention, pour les polygones, cela peut conduire à des géométries de type différent (par exemple une polygone à 2 noeuds devient une ligne). On utilise donc aussi la fonction **ST_CollectionExtract** pour ne récupérer que les polygones.
31 |
32 | ```sql
33 | -- Corriger les géométries
34 | UPDATE z_formation.parcelle_havre
35 | SET geom = ST_Multi(ST_CollectionExtract(ST_MakeValid(geom), 3))
36 | WHERE NOT ST_isvalid(geom)
37 |
38 | -- Tester
39 | SELECT count(*)
40 | FROM z_formation.parcelle_havre
41 | WHERE NOT ST_isvalid(geom)
42 | ```
43 |
44 | Il faut aussi supprimer l'ensemble des lignes dans la table qui ne correspondent pas au type de la couche importée. Par exemple, pour les polygones, supprimer les objets dont le nombre de nœuds est inférieur à 3.
45 |
46 | * On les trouve:
47 |
48 | ```sql
49 | SELECT *
50 | FROM z_formation.parcelle_havre
51 | WHERE ST_NPoints(geom) < 3
52 | ```
53 |
54 | * On les supprime:
55 |
56 | ```sql
57 | DELETE
58 | FROM z_formation.parcelle_havre
59 | WHERE ST_NPoints(geom) < 3
60 | ```
61 |
62 |
63 | Continuer vers [Vérifier la topologie](./check_topology.md)
64 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Formation PostGIS
2 | site_author: 3Liz
3 | site_description: Formation PostGIS
4 | site_url: https://docs.3liz.org/formation-postgis/
5 | repo_url: https://github.com/3liz/formation-postgis/
6 | copyright: '© 3Liz'
7 |
8 | site_dir: build
9 |
10 | nav:
11 | - docs.3liz.org: '../'
12 | - Accueil: index.md
13 | - Liens et données: links_and_data.md
14 | - Gestion des données: postgresql_in_qgis.md
15 | - Import des données: import_data.md
16 | - Sélection: sql_select.md
17 | - Calcul & Fonctions: perform_calculation.md
18 | - Filtrer: filter_data.md
19 | - Regrouper: group_data.md
20 | - Rassembler: union.md
21 | - Enregistrer: save_queries.md
22 | - Jointures: join_data.md
23 | - Fusionner: merge_geometries.md
24 | - Triggers: triggers.md
25 | - Correction géométries: validate_geometries.md
26 | - Topologie: check_topology.md
27 | - Fonctions utiles: utils.md
28 | - Droits: grant.md
29 | - Données externes: fdw.md
30 | - Tutoriels en ligne: tutoriel.md
31 |
32 | plugins:
33 | - search
34 | - git-revision-date-localized
35 |
36 | markdown_extensions:
37 | - toc:
38 | permalink: "#"
39 | - meta:
40 | - pymdownx.highlight:
41 | linenums: true
42 | linenums_style: pymdownx.inline
43 | - pymdownx.superfences:
44 | custom_fences:
45 | - name: mermaid
46 | class: mermaid
47 | format: !!python/name:pymdownx.superfences.fence_div_format
48 | - pymdownx.tabbed:
49 | alternate_style: true
50 | - pymdownx.magiclink:
51 | - pymdownx.tasklist:
52 | - pymdownx.snippets:
53 | - pymdownx.keys:
54 | - attr_list:
55 | - admonition:
56 | - pymdownx.details:
57 |
58 | extra_javascript:
59 | - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
60 | - https://unpkg.com/mermaid@8.6.4/dist/mermaid.min.js
61 | extra_css:
62 | - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css
63 |
64 | theme:
65 | name: material
66 | font: false
67 | icon:
68 | repo: fontawesome/brands/github-alt
69 | language: 'en'
70 | logo: logo.svg
71 | favicon: logo.svg
72 | palette:
73 | accent: deep-orange
74 | primary: green
75 | scheme: default
76 | features:
77 | # - navigation.tabs
78 | - navigation.tabs.sticky
79 | - navigation.top
80 | - content.code.copy
81 |
82 | extra:
83 | social:
84 | - icon: fontawesome/brands/twitter
85 | link: https://twitter.com/3LIZ_news
86 |
87 | - icon: fontawesome/brands/linkedin
88 | link: https://www.linkedin.com/company/3liz
89 |
90 | - icon: fontawesome/brands/github
91 | link: https://github.com/3liz/
92 |
93 | - icon: fontawesome/brands/docker
94 | link: https://hub.docker.com/u/3liz
95 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs-material>=7.0.0,<10.0.0
2 | mkdocs-git-revision-date-localized-plugin
3 |
--------------------------------------------------------------------------------