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

Da Oracle a PostgreSQL:START WITH/CONNECT BY

E ora arriviamo al secondo articolo della nostra migrazione dalla serie Oracle a PostgreSQL. Questa volta daremo un'occhiata a START WITH/CONNECT BY costruire.

In Oracle, START WITH/CONNECT BY viene utilizzato per creare una struttura di elenchi collegati singolarmente a partire da una determinata riga sentinella. L'elenco collegato può assumere la forma di un albero e non ha requisiti di bilanciamento.

Per illustrare, iniziamo con una query e supponiamo che la tabella contenga 5 righe.

SELECT * FROM person;
 last_name  | first_name | id | parent_id
------------+------------+----+-----------
 Dunstan    | Andrew     |  1 |    (null)
 Roybal     | Kirk       |  2 |         1
 Riggs      | Simon      |  3 |         1
 Eisentraut | Peter      |  4 |         1
 Thomas     | Shaun      |  5 |         3
(5 rows)

Ecco la query gerarchica della tabella utilizzando la sintassi Oracle.

select id, parent_id
from person
start with parent_id IS NULL
connect by prior id = parent_id;
 id | parent_id
----+-----------
  1 |    (null)
  4 |         1
  3 |         1
  2 |         1
  5 |         3

Ed eccolo di nuovo usando PostgreSQL.

WITH RECURSIVE a AS (
SELECT id, parent_id
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id
FROM person d
JOIN a ON a.id = d.parent_id )
SELECT id, parent_id FROM a;
 id | parent_id
----+-----------
  1 |    (null)
  4 |         1
  3 |         1
  2 |         1
  5 |         3
(5 rows)

Questa query utilizza molte funzionalità di PostgreSQL, quindi esaminiamola lentamente.

WITH RECURSIVE

Questa è una "Espressione di tabella comune" (CTE). Definisce un insieme di query che verranno eseguite nella stessa istruzione, non solo nella stessa transazione. Potresti avere un numero qualsiasi di espressioni tra parentesi e un'affermazione finale. Per questo utilizzo, ne abbiamo solo bisogno. Dichiarando tale affermazione come RECURSIVE , verrà eseguito in modo iterativo fino a quando non verranno restituite più righe.

SELECT
UNION ALL
SELECT

Questa è una frase prescritta per una query ricorsiva. È definito nella documentazione come il metodo per distinguere il punto di partenza e l'algoritmo di ricorsione. In termini Oracle, puoi considerarli come la clausola START WITH unita alla clausola CONNECT BY.

JOIN a ON a.id = d.parent_id

Questo è un self-join all'istruzione CTE che fornisce i dati della riga precedente all'iterazione successiva.

Per illustrare come funziona, aggiungiamo un indicatore di iterazione alla query.

WITH RECURSIVE a AS (
SELECT id, parent_id, 1::integer recursion_level
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, a.recursion_level +1
FROM person d
JOIN a ON a.id = d.parent_id )
SELECT * FROM a;

 id | parent_id | recursion_level
----+-----------+-----------------
  1 |    (null) |               1
  4 |         1 |               2
  3 |         1 |               2
  2 |         1 |               2
  5 |         3 |               3
(5 rows)

Inizializziamo l'indicatore del livello di ricorsione con un valore. Si noti che nelle righe restituite, il primo livello di ricorsione si verifica una sola volta. Questo perché la prima clausola viene eseguita una sola volta.

La seconda clausola è dove avviene la magia iterativa. Qui abbiamo visibilità dei dati della riga precedente, insieme ai dati della riga corrente. Questo ci consente di eseguire i calcoli ricorsivi.

Simon Riggs ha un video molto carino su come usare questa funzione per la progettazione di database di grafici. È altamente informativo e dovresti dare un'occhiata.

Potresti aver notato che questa query potrebbe portare a una condizione circolare. È corretto. Spetta allo sviluppatore aggiungere una clausola limitante alla seconda query per evitare questa ricorsione infinita. Ad esempio, ricorrendo solo a 4 livelli di profondità prima di arrendersi.

WITH RECURSIVE a AS (
SELECT id, parent_id, 1::integer recursion_level  --<-- initialize it here
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, a.recursion_level +1    --<-- iteration increment
FROM person d
JOIN a ON a.id = d.parent_id
WHERE d.recursion_level <= 4  --<-- bail out here
) SELECT * FROM a;

I nomi delle colonne e i tipi di dati sono determinati dalla prima clausola. Si noti che l'esempio utilizza un operatore di cast per il livello di ricorsione. In un grafico molto profondo, questo tipo di dati potrebbe anche essere definito come 1::bigint recursion_level .

Questo grafico è molto facile da visualizzare con un piccolo script di shell e l'utilità graphviz.

#!/bin/bash -
#===============================================================================
#
#          FILE: pggraph
#
#         USAGE: ./pggraph
#
#   DESCRIPTION:
#
#       OPTIONS: ---
#  REQUIREMENTS: ---
#          BUGS: ---
#         NOTES: ---
#        AUTHOR: Kirk Roybal (), [email protected]
#  ORGANIZATION:
#       CREATED: 04/21/2020 14:09
#      REVISION:  ---
#===============================================================================

set -o nounset                              # Treat unset variables as an error

dbhost=localhost
dbport=5432
dbuser=$USER
dbname=$USER
ScriptVersion="1.0"
output=$(basename $0).dot

#===  FUNCTION  ================================================================
#         NAME:  usage
#  DESCRIPTION:  Display usage information.
#===============================================================================
function usage ()
{
cat <<- EOT

  Usage :  ${0##/*/} [options] [--]

  Options:
  -h|host     name Database Host Name default:localhost
  -n|name     name Database Name      default:$USER
  -o|output   file Output file        default:$output.dot
  -p|port   number TCP/IP port        default:5432
  -u|user     name User name          default:$USER
  -v|version    Display script version

EOT
}    # ----------  end of function usage  ----------

#-----------------------------------------------------------------------
#  Handle command line arguments
#-----------------------------------------------------------------------

while getopts ":dh:n:o:p:u:v" opt
do
  case $opt in

    d|debug    )  set -x ;;

    h|host     )  dbhost="$OPTARG" ;;

    n|name     )  dbname="$OPTARG" ;;

    o|output   )  output="$OPTARG" ;;

    p|port     )  dbport=$OPTARG ;;

    u|user     )  dbuser=$OPTARG ;;

    v|version  )  echo "$0 -- Version $ScriptVersion"; exit 0   ;;

    \? )  echo -e "\n  Option does not exist : $OPTARG\n"
          usage; exit 1   ;;

  esac    # --- end of case ---
done
shift $(($OPTIND-1))

[[ -f "$output" ]] && rm "$output"

tee "$output" <<eof< span="">
digraph g {
    node [shape=rectangle]
    rankdir=LR
EOF

psql -h $dbhost -U $dbuser -d $dbname -p $dbport -qtAf cte.sql |
    sed -e 's/^/node/' -e 's/.*(null)|/node/' -e 's/^/\t/' -e 's/|[[:digit:]]*$//' |
    sed -e 's/|/ -> node/' | tee -a "$output"

tee -a "$output" <<eof< span="">
}
EOF

dot -Tpng "$output" > "${output/dot/png}"

[[ -f "$output" ]] && rm "$output"

open "${output/dot/png}"</eof<></eof<>

Questo script richiede questa istruzione SQL in un file chiamato cte.sql

WITH RECURSIVE a AS (
SELECT id, parent_id, 1::integer recursion_level
FROM person
WHERE parent_id IS NULL
UNION ALL
SELECT d.id, d.parent_id, a.recursion_level +1
FROM person d
JOIN a ON a.id = d.parent_id )
SELECT parent_id, id, recursion_level FROM a;

Quindi lo invochi in questo modo:

chmod +x pggraph
./pggraph

E vedrai il grafico risultante.

INSERT INTO person (id, parent_id) VALUES (6,2);

Esegui di nuovo l'utilità e osserva le modifiche immediate al grafico diretto:

Ora, non è stato così difficile ora, vero?