PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Da Oracle a PostgreSQL:cursori ed espressioni di tabelle comuni

Fa male quando lo fai, quindi non farlo.

In Oracle, i cursori vengono insegnati come parte della programmazione 101. In molti (se non nella maggior parte) casi, i cursori sono la prima cosa che lo sviluppatore Oracle impara. La prima lezione di solito inizia con:"Ci sono 13 strutture logiche, la prima delle quali è il ciclo, che va così..."

PostgreSQL, d'altra parte, non fa molto affidamento sui cursori. Sì, esistono. Esistono diversi tipi di sintassi su come usarli. Tratterò tutti i principali progetti ad un certo punto in questa serie di articoli. Ma la prima lezione sui cursori PostgreSQL è che ci sono alcune (e molto migliori) alternative algoritmiche all'uso dei cursori in PostgreSQL. In effetti, in una carriera di 23 anni con PostgreSQL, ho effettivamente riscontrato la necessità di utilizzare i cursori solo due volte. E me ne pento uno.

I cursori sono un'abitudine costosa.

L'iterazione è meglio del ciclo. "Qual è la differenza?", potresti chiedere. Bene, la differenza riguarda O(N) contro O(N^2). Ok, lo ripeto in inglese. La complessità dell'utilizzo dei cursori consiste nel fatto che eseguono il ciclo dei set di dati utilizzando lo stesso schema di un ciclo for nidificato. Ogni set di dati aggiuntivo aumenta la complessità del totale tramite l'esponenziazione. Questo perché ogni set di dati aggiuntivo crea effettivamente un altro ciclo più interno. Due set di dati sono O(N^2), tre set di dati sono O(N^3) e così via. Prendere l'abitudine di usare i cursori quando ci sono algoritmi migliori tra cui scegliere può essere costoso.

Lo fanno senza nessuna delle ottimizzazioni che sarebbero disponibili per le funzioni di livello inferiore del database stesso. Cioè, non possono utilizzare gli indici in modo significativo, non possono trasformarsi in sottoselezioni, eseguire il pull up in join o utilizzare letture parallele. Inoltre, non trarranno vantaggio da eventuali ottimizzazioni future che il database ha a disposizione. Spero che tu sia un grande maestro di programmazione che ottiene sempre l'algoritmo corretto e lo codifica perfettamente la prima volta, perché hai appena sconfitto uno dei vantaggi più importanti di un database relazionale. Prestazioni basandosi sulle migliori pratiche o almeno sul codice di qualcun altro.

Tutti sono migliori di te. Forse non individualmente, ma collettivamente quasi certamente. A parte l'argomento dichiarativo vs. imperativo, la codifica in un linguaggio che è stato rimosso una volta dalla libreria di funzioni sottostante consente a tutti gli altri di tentare di far funzionare il tuo codice più velocemente, meglio e in modo più efficiente senza consultarti. E questo è molto, molto buono per te.

Facciamo alcuni dati con cui giocare.

Inizieremo impostando alcuni dati con cui giocare nei prossimi articoli.

Contenuto di cursors.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

Questo ci dà poco più di 600.000 record con cui giocare nella tabella itis.hierarchy, che contiene una tassonomia del mondo naturale. Utilizzeremo questi dati per illustrare vari metodi per gestire interazioni di dati complessi.

La prima alternativa.

Il mio modello di progettazione preferito per camminare lungo i recordset mentre si eseguono operazioni costose è la Common Table Expression (CTE).

Ecco un esempio del modulo 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;

Che produce i seguenti risultati:

┌─────────┬────────┬──────────────────────────────────────────────────────────┐
│   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)

Questa query è facilmente modificabile per eseguire qualsiasi calcolo. Ciò include l'arricchimento dei dati, funzioni complesse o qualsiasi altra cosa il tuo cuore desidera.

“Ma guarda!”, esclami. "Dice RECURSIVE proprio lì nel nome! Non sta facendo esattamente quello che avevi detto di non fare?" Beh, in realtà no. Sotto il cofano, non usa la ricorsione nel senso nidificato o il looping per eseguire la "ricorsione". È solo una lettura lineare della tabella finché la query subordinata non restituisce alcun nuovo risultato. E funziona anche con gli indici.

Diamo un'occhiata al piano di esecuzione:

┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                              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                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘

Andiamo avanti e creiamo un indice e vediamo come funziona.

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                                                               │
└─────────────────────────────────────────────────────────────────────────────┘

Bene, è stato soddisfacente, vero? E sarebbe stato proibitivo creare un indice in combinazione con un cursore per fare lo stesso lavoro. Questa struttura ci porta abbastanza lontano da essere in grado di percorrere una struttura ad albero abbastanza complessa e utilizzarla per ricerche semplici.

Nella prossima puntata parleremo di un altro metodo per ottenere lo stesso risultato ancora più velocemente. Per il nostro prossimo articolo, parleremo dell'estensione ltree e di come esaminare i dati gerarchici in modo sorprendentemente rapido. Resta sintonizzato.