PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Oracle vers PostgreSQL — Curseurs et expressions de table communes

Ça fait mal quand tu fais ça, alors ne fais pas ça.

Dans Oracle, les curseurs sont enseignés dans le cadre de la programmation 101. Dans de nombreux cas (sinon la plupart), les curseurs sont la première chose que le développeur Oracle apprend. La première classe commence généralement par :"Il y a 13 structures logiques, dont la première est la boucle, qui se déroule comme ceci…"

PostgreSQL, d'autre part, ne repose pas beaucoup sur les curseurs. Oui, ils existent. Il existe plusieurs saveurs de syntaxe pour savoir comment les utiliser. Je couvrirai tous les principaux modèles à un moment donné dans cette série d'articles. Mais la première leçon sur les curseurs PostgreSQL est qu'il existe de nombreuses alternatives algorithmiques (et bien meilleures) à l'utilisation des curseurs dans PostgreSQL. En fait, au cours d'une carrière de 23 ans avec PostgreSQL, je n'ai trouvé le besoin d'utiliser les curseurs que deux fois. Et j'en regrette un.

Les curseurs sont une habitude coûteuse.

Itérer vaut mieux que boucler. "Quelle est la différence?", Vous pourriez demander. Eh bien, la différence est d'environ O(N) vs. O(N^2). Ok, je vais le redire en anglais. La complexité de l'utilisation des curseurs réside dans le fait qu'ils parcourent des ensembles de données en utilisant le même modèle qu'une boucle for imbriquée. Chaque ensemble de données supplémentaire augmente la complexité du total par exponentiation. En effet, chaque ensemble de données supplémentaire crée effectivement une autre boucle la plus interne. Deux ensembles de données sont O(N^2), trois ensembles de données sont O(N^3) et ainsi de suite. Prendre l'habitude d'utiliser des curseurs lorsqu'il existe de meilleurs algorithmes parmi lesquels choisir peut s'avérer coûteux.

Ils le font sans aucune des optimisations qui seraient disponibles pour les fonctions de niveau inférieur de la base de données elle-même. Autrement dit, ils ne peuvent pas utiliser les index de manière significative, ne peuvent pas se transformer en sous-sélections, remonter dans des jointures ou utiliser des lectures parallèles. Ils ne bénéficieront pas non plus des futures optimisations dont dispose la base de données. J'espère que vous êtes un codeur grand maître qui obtient toujours le bon algorithme et le code parfaitement la première fois, car vous venez de vaincre l'un des avantages les plus importants d'une base de données relationnelle. Performances en s'appuyant sur les meilleures pratiques, ou au moins sur le code de quelqu'un d'autre.

Tout le monde est meilleur que toi. Peut-être pas individuellement, mais collectivement presque certainement. Mis à part l'argument déclaratif vs impératif, coder dans un langage qui est une fois supprimé de la bibliothèque de fonctions sous-jacente permet à tout le monde d'essayer de faire fonctionner votre code plus rapidement, mieux et plus efficacement sans vous consulter. Et c'est très, très bon pour vous.

Créons des données avec lesquelles jouer.

Nous commencerons par configurer quelques données avec lesquelles jouer dans les prochains articles.

Contenu de curseurs.bash :

set -o nounset                              # Treat unset variables as an error
# This script assumes that you have PostgreSQL running locally,
#  that you have a database with the same name as the local user,
#  and that you can create all this structure.
#  If not, then:
#   sudo -iu postgres createuser -s $USER
#   createdb

# Clean up from the last run
[[ -f itisPostgreSql.zip ]] && rm itisPostgreSql.zip
subdirs=$(ls -1 itisPostgreSql* | grep : | sed -e 's/://')
for sub in ${subdirs[@]}
do
    rm -rf $sub
done

# Get the newest file
wget https://www.itis.gov/downloads/itisPostgreSql.zip
# Unpack it
unzip itisPostgreSql.zip
# This makes a directory with the stupidest f-ing name possible
#  itisPostgreSqlDDMMYY
subdir=$(\ls -1 itisPostgreSql* | grep : | sed -e 's/://')
# The script wants to create an "ITIS" database.  Let's just make that a schema.
sed -i $subdir/ITIS.sql -e '/"ITIS"/d'  # Cut the lines about making the db
sed -i $subdir/ITIS.sql -e '/-- PostgreSQL database dump/s/.*/CREATE SCHEMA IF NOT EXISTS itis;/'
sed -i $subdir/ITIS.sql -e '/SET search_path = public, pg_catalog;/s/.*/SET search_path TO itis;/'
# ok, we have a schema to put the data in, let's do the import.
#  timeout if we can't connect, fail on error.
PG_TIMEOUT=5 psql -v "ON_ERROR_STOP=1" -f $subdir/ITIS.sql

Cela nous donne un peu plus de 600 000 enregistrements avec lesquels jouer dans la table itis.hierarchy, qui contient une taxonomie du monde naturel. Nous utiliserons ces données pour illustrer diverses méthodes de traitement des interactions de données complexes.

La première alternative.

Mon modèle de conception préféré pour parcourir les jeux d'enregistrements tout en effectuant des opérations coûteuses est l'expression de table commune (CTE).

Voici un exemple du formulaire de base :

WITH RECURSIVE fauna AS (
    SELECT tsn, parent_tsn, tsn::text taxonomy
    FROM itis.hierarchy
    WHERE parent_tsn = 0
    UNION ALL
    SELECT h1.tsn, h1.parent_tsn, f.taxonomy || '.' || h1.tsn
    FROM itis.hierarchy h1
    JOIN fauna f
    ON h1.parent_tsn = f.tsn
    )
SELECT *
FROM fauna
ORDER BY taxonomy;

Ce qui produit les résultats suivants :

┌─────────┬────────┬──────────────────────────────────────────────────────────┐
│   tsn   │ parent │             taxonomy                                     │
│         │ tsn    │                                                          │
├─────────┼────────┼──────────────────────────────────────────────────────────┤
│  202422 │      0 │202422                                                    │
│  846491 │ 202422 │202422.846491                                             │
│  660046 │ 846491 │202422.846491.660046                                      │
│  846497 │ 660046 │202422.846491.660046.846497                               │
│  846508 │ 846497 │202422.846491.660046.846497.846508                        │
│  846553 │ 846508 │202422.846491.660046.846497.846508.846553                 │
│  954935 │ 846553 │202422.846491.660046.846497.846508.846553.954935          │
│    5549 │ 954935 │202422.846491.660046.846497.846508.846553.954935.5549     │
│    5550 │   5549 │202422.846491.660046.846497.846508.846553.954935.5549.5550│
│  954936 │ 846553 │202422.846491.660046.846497.846508.846553.954936          │
│  954904 │ 660046 │202422.846491.660046.954904                               │
│  846509 │ 954904 │202422.846491.660046.954904.846509                        │
│   11473 │ 846509 │202422.846491.660046.954904.846509.11473                  │
│   11474 │  11473 │202422.846491.660046.954904.846509.11473.11474            │
│   11475 │  11474 │202422.846491.660046.954904.846509.11473.11474.11475      │
│   ...   │        │...snip...                                                │
└─────────┴────────┴──────────────────────────────────────────────────────────┘
(601187 rows)

Cette requête est facilement modifiable pour effectuer des calculs. Cela inclut l'enrichissement des données, les fonctions complexes ou tout ce que votre cœur désire.

« Mais regardez ! », vous exclamez-vous. "Il dit RECURSIVE juste là dans le nom! Ne fait-il pas exactement ce que vous avez dit de ne pas faire ? » Eh bien, en fait non. Sous le capot, il n'utilise pas la récursivité au sens imbriqué ou en boucle pour effectuer la "récursivité". Il s'agit simplement d'une lecture linéaire de la table jusqu'à ce que la requête subordonnée ne renvoie aucun nouveau résultat. Et cela fonctionne aussi avec les index.

Regardons le plan d'exécution :

┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                              QUERY PLAN                                              │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Sort  (cost=211750.51..211840.16 rows=35858 width=40)                                                │
│   Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy                                                │
│   Sort Key: fauna.taxonomy                                                                           │
│   CTE fauna                                                                                          │
│     ->  Recursive Union  (cost=1000.00..208320.69 rows=35858 width=40)                               │
│           ->  Gather  (cost=1000.00..15045.02 rows=18 width=40)                                      │
│                 Output: hierarchy.tsn, hierarchy.parent_tsn, ((hierarchy.tsn)::text)                 │
│                 Workers Planned: 2                                                                   │
│                 ->  Parallel Seq Scan on itis.hierarchy  (cost=0.00..14043.22 rows=8 width=40)       │
│                       Output: hierarchy.tsn, hierarchy.parent_tsn, (hierarchy.tsn)::text             │
│                       Filter: (hierarchy.parent_tsn = 0)                                             │
│           ->  Hash Join  (cost=5.85..19255.85 rows=3584 width=40)                                    │
│                 Output: h1.tsn, h1.parent_tsn, ((f.taxonomy || '.'::text) || (h1.tsn)::text)         │
│                 Hash Cond: (h1.parent_tsn = f.tsn)                                                   │
│                 ->  Seq Scan on itis.hierarchy h1  (cost=0.00..16923.87 rows=601187 width=8)         │
│                       Output: h1.hierarchy_string, h1.tsn, h1.parent_tsn, h1.level, h1.childrencount │
│                 ->  Hash  (cost=3.60..3.60 rows=180 width=36)                                        │
│                       Output: f.taxonomy, f.tsn                                                      │
│                       ->  WorkTable Scan on fauna f  (cost=0.00..3.60 rows=180 width=36)             │
│                             Output: f.taxonomy, f.tsn                                                │
│   ->  CTE Scan on fauna  (cost=0.00..717.16 rows=35858 width=40)                                     │
│         Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy                                          │
│ JIT:                                                                                                 │
│   Functions: 13                                                                                      │
│   Options: Inlining false, Optimization false, Expressions true, Deforming true                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘

Continuons et créons un index, et voyons comment cela fonctionne.

CREATE UNIQUE INDEX taxonomy_parents ON itis.hierarchy (parent_tsn, tsn);

┌─────────────────────────────────────────────────────────────────────────────┐
│                             QUERY PLAN                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│Sort  (cost=135148.13..135237.77 rows=35858 width=40)                        │
│  Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy                        │
│  Sort Key: fauna.taxonomy                                                   │
│  CTE fauna                                                                  │
│    ->  Recursive Union  (cost=4.56..131718.31 rows=35858 width=40)          │
│          ->  Bitmap Heap Scan on itis.hierarchy  (cost=4.56..74.69 rows=18) │
│              Output: hierarchy.tsn, hierarchy.parent_tsn, (hierarchy.tsn)   │
│                Recheck Cond: (hierarchy.parent_tsn = 0)                     │
│                ->  Bitmap Index Scan on taxonomy_parents                    │
│                                                   (cost=0.00..4.56 rows=18) │
│                      Index Cond: (hierarchy.parent_tsn = 0)                 │
│          ->  Nested Loop  (cost=0.42..13092.65 rows=3584 width=40)          │
│                Output: h1.tsn, h1.parent_tsn,((f.taxonomy || '.')||(h1.tsn))│
│                ->  WorkTable Scan on fauna f  (cost=0.00..3.60 rows=180)    │
│                      Output: f.tsn, f.parent_tsn, f.taxonomy                │
│                ->  Index Only Scan using taxonomy_parents on itis.hierarchy │
│                                   h1  (cost=0.42..72.32 rows=20 width=8)    │
│                      Output: h1.parent_tsn, h1.tsn                          │
│                      Index Cond: (h1.parent_tsn = f.tsn)                    │
│  ->  CTE Scan on fauna  (cost=0.00..717.16 rows=35858 width=40)             │
│        Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy                  │
│JIT:                                                                         │
│  Functions: 6                                                               │
└─────────────────────────────────────────────────────────────────────────────┘

Eh bien, c'était satisfaisant, n'est-ce pas? Et il aurait été extrêmement difficile de créer un index en combinaison avec un curseur pour faire le même travail. Cette structure nous amène assez loin pour pouvoir parcourir une structure arborescente assez complexe et l'utiliser pour des recherches simples.

Dans le prochain épisode, nous parlerons d'une autre méthode pour obtenir le même résultat encore plus rapidement. Pour notre prochain article, nous parlerons de l'extension ltree et de la façon d'examiner les données hiérarchiques avec une rapidité incroyable. Restez à l'écoute.