web-dev-qa-db-fra.com

Structure d'arborescence postgreSQL et optimisation de la CTE récursive

J'essaie de représenter une structure d'arbres dans PostgreSQL (8.4) pour pouvoir interroger le chemin de la racine à un nœud donné ou pour trouver tous les nœuds dans une sous-branche.

Voici une table d'essai:

CREATE TABLE tree_data_1 (
    forest_id TEXT NOT NULL,
    node_id TEXT NOT NULL,
    parent_id TEXT,
    node_type TEXT,
    description TEXT,
    PRIMARY KEY (forest_id, node_id),
    FOREIGN KEY (forest_id, parent_id) REFERENCES tree_data_1 (forest_id, node_id)
);
CREATE INDEX tree_data_1_forestid_parent_idx ON tree_data_1(forest_id, parent_id);
CREATE INDEX tree_data_1_forestid_idx ON tree_data_1(forest_id);
CREATE INDEX tree_data_1_nodeid_idx ON tree_data_1(node_id);
CREATE INDEX tree_data_1_parent_idx ON tree_data_1(parent_id);

Chaque nœud est identifié par (forest_id, node_id) (Il peut y avoir un autre nœud avec le même nom dans une autre forêt). Chaque arbre commence à un nœud racine (où parent_id est null), bien que je n'attends que par une seule par forêt.

Voici la vue qui utilise un CTE récursif:

CREATE OR REPLACE VIEW tree_view_1 AS
    WITH RECURSIVE rec_sub_tree(forest_id, node_id, parent_id, depth, path, cycle) AS (
        SELECT td.forest_id, td.node_id, td.parent_id, 0, ARRAY[td.node_id], FALSE FROM tree_data_1 td
        UNION ALL
        SELECT td.forest_id, rec.node_id, td.parent_id, rec.depth+1, td.node_id || rec.path, td.node_id = ANY(rec.path)
            FROM tree_data_1 td, rec_sub_tree rec
            WHERE td.forest_id = rec.forest_id AND rec.parent_id = td.node_id AND NOT cycle
     )
     SELECT forest_id, node_id, parent_id, depth, path
         FROM rec_sub_tree;

Il s'agit d'une version légèrement modifiée du exemple dans la documentation , pour prendre en compte le forest_id, et cela renvoie rec.node_id dans la récursive SELECT au lieu de ce qui serait td.node_id.

Le chemin obtient le chemin de la racine à un nœud donné, cette requête peut être utilisée:

SELECT * FROM tree_view_1 WHERE forest_id='Forest A' AND node_id='...' AND parent_id IS NULL

Obtenez un sous-arbre, cette requête peut être utilisée:

SELECT * FROM tree_view_1 WHERE forest_id='Forest A' AND parent_id='...'

Le fait d'obtenir un arbre complet dans une forêt donnée:

SELECT * FROM tree_view_1 WHERE forest_id='Forest A' AND parent_id IS NULL

La dernière requête utilise le plan de requête suivant (visible sur expliquer.depesz.com ):

 CTE Scan on rec_sub_tree  (cost=1465505.41..1472461.19 rows=8 width=132) (actual time=0.067..62480.876 rows=133495 loops=1)
   Filter: ((parent_id IS NULL) AND (forest_id = 'Forest A'::text))
   CTE rec_sub_tree
     ->  Recursive Union  (cost=0.00..1465505.41 rows=309146 width=150) (actual time=0.048..53736.585 rows=1645992 loops=1)
           ->  Seq Scan on tree_data_1 td  (cost=0.00..6006.16 rows=247316 width=82) (actual time=0.034..975.796 rows=247316 loops=1)
           ->  Hash Join  (cost=13097.90..145331.63 rows=6183 width=150) (actual time=2087.065..5842.870 rows=199811 loops=7)
                 Hash Cond: ((rec.forest_id = td.forest_id) AND (rec.parent_id = td.node_id))
                 ->  WorkTable Scan on rec_sub_tree rec  (cost=0.00..49463.20 rows=1236580 width=132) (actual time=0.017..915.814 rows=235142 loops=7)
                       Filter: (NOT cycle)
                 ->  Hash  (cost=6006.16..6006.16 rows=247316 width=82) (actual time=1871.964..1871.964 rows=247316 loops=7)
                       ->  Seq Scan on tree_data_1 td  (cost=0.00..6006.16 rows=247316 width=82) (actual time=0.017..872.725 rows=247316 loops=7)
 Total runtime: 62978.883 ms
(12 rows)

Comme prévu, ce n'est pas très efficace. Je suis en partie surpris qu'il ne semble pas utiliser d'index.

Considérant que ces données seraient relues souvent mais rarement modifiées (peut-être une petite modification toutes les deux semaines), quelles techniques possibles sont là pour optimiser ces requêtes et/ou représentation des données?

Edit: Je voudrais également récupérer l'arborescence en profondeur-première commande. En utilisant ORDER BY path se dégrade également sensiblement la vitesse de la requête ci-dessus.


Échantillon Python Programme pour renseigner la table avec les données de test (nécessite psycopg2 ), probablement un peu plus que je ne m'attends à avoir dans une situation plus réaliste:

from uuid import uuid4
import random
import psycopg2

random.seed(1234567890)
min_depth = 3
max_depth = 6
max_sub_width = 10
next_level_prob = 0.7

db_connection = psycopg2.connect(database='...')
cursor = db_connection.cursor()
query = "INSERT INTO tree_data_1(forest_id, node_id, parent_id) VALUES (%s, %s, %s)"

def generate_sub_tree(forest_id, parent_id=None, depth=0, node_ids=[]):
    if not node_ids:
        node_ids = [ str(uuid4()) for _ in range(random.randint(1, max_sub_width)) ]
    for node_id in node_ids:
        cursor.execute(query, [ forest_id, node_id, parent_id ])
        if depth < min_depth or (depth < max_depth and random.random() < next_level_prob):
            generate_sub_tree(forest_id, node_id, depth+1)

generate_sub_tree('Forest A', node_ids=['Node %d' % (i,) for i in range(10)])
generate_sub_tree('Forest B', node_ids=['Node %d' % (i,) for i in range(10)])

db_connection.commit()
db_connection.close()
6
Bruno

Si vous devez vraiment modifier ces données rarement, vous pouvez simplement stocker le résultat du CTE dans une table et exécuter des requêtes contre ce tableau. Vous pouvez définir des index en fonction de vos requêtes typiques.
[.____] alors TRUNCATE et repeuplez (et ANALYZE) si nécessaire.

D'autre part, si vous pouvez mettre le CTE dans des procédures stockées séparées plutôt que sur une vue, vous pouvez facilement mettre vos conditions dans la partie CTE plutôt que la finale SELECT (qui est essentiellement ce que vous avez interrogé contre tree_view_1), de sorte que beaucoup moins de lignes seront impliquées dans la récursivité. Du plan de requête, on dirait que PostgreSQL estime les numéros de ligne basés sur certaines hypothèses de loin des véritables, produisant probablement des plans sous-optimaux - cet effet peut être quelque peu réduit avec la solution SP Solution.

ÉDITER Je peux manquer quelque chose, mais je viens de remarquer que dans le terme non récursif, vous ne filtrez pas les rangées. Vous voulez peut-être inclure uniquement des nœuds de la racine (WHERE parent_id IS NULL) - J'attendrais beaucoup moins de lignes et de récursions de cette façon.

éditer 2 car il est lentement devenu clair pour moi des commentaires, j'ai mal essentiellement la récursion dans la question initiale qui aille l'autre sens. Ici, je veux dire à partir des nœuds de la racine et aller plus loin dans la récursion.

2
dezso