J'ai écrit deux fonctions pour répondre à la première question de devoirs de la journée 3 de sept bases de données en sept semaines .
Créez une procédure stockée dans laquelle vous pouvez saisir un titre de film ou le nom de l'acteur que vous le souhaitez, et il retournera les cinq principales suggestions basées sur les deux films que l'acteur a joué dans des genres ou des films similaires.
Ma première tentative est correcte mais lente. Cela peut prendre jusqu'à 2000MS pour renvoyer un résultat.
CREATE OR REPLACE FUNCTION suggest_movies(IN query text, IN result_limit integer DEFAULT 5)
RETURNS TABLE(movie_id integer, title text) AS
$BODY$
WITH suggestions AS (
SELECT
actors.name AS entity_term,
movies.movie_id AS suggestion_id,
movies.title AS suggestion_title,
1 AS rank
FROM actors
INNER JOIN movies_actors ON (actors.actor_id = movies_actors.actor_id)
INNER JOIN movies ON (movies.movie_id = movies_actors.movie_id)
UNION ALL
SELECT
searches.title AS entity_term,
suggestions.movie_id AS suggestion_id,
suggestions.title AS suggestion_title,
RANK() OVER (PARTITION BY searches.movie_id ORDER BY cube_distance(searches.genre, suggestions.genre)) AS rank
FROM movies AS searches
INNER JOIN movies AS suggestions ON
(searches.movie_id <> suggestions.movie_id) AND
(cube_enlarge(searches.genre, 2, 18) @> suggestions.genre)
)
SELECT suggestion_id, suggestion_title
FROM suggestions
WHERE entity_term = query
ORDER BY rank, suggestion_id
LIMIT result_limit;
$BODY$
LANGUAGE sql;
Ma deuxième tentative est correcte et rapide. Je l'optimisa en appuyant sur le filtre de la CTE dans chaque partie de l'Union.
J'ai enlevé cette ligne de la requête extérieure:
WHERE entity_term = query
J'ai ajouté cette ligne à la première requête intérieure:
WHERE actors.name = query
J'ai ajouté cette ligne à la deuxième requête interne:
WHERE movies.title = query
La deuxième fonction prend environ 10 ms pour renvoyer le même résultat.
Rien ne diffère dans la base de données à part les définitions de la fonction.
Pourquoi PostgreSQL produit-il de tels plans différents pour ces deux requêtes logiquement équivalentes?
Le EXPLAIN ANALYZE
Le plan de la première fonction ressemble à ceci:
Limit (cost=7774.18..7774.19 rows=5 width=44) (actual time=1738.566..1738.567 rows=5 loops=1)
CTE suggestions
-> Append (cost=332.56..7337.19 rows=19350 width=285) (actual time=7.113..1577.823 rows=383024 loops=1)
-> Subquery Scan on "*SELECT* 1" (cost=332.56..996.80 rows=11168 width=33) (actual time=7.113..22.258 rows=11168 loops=1)
-> Hash Join (cost=332.56..885.12 rows=11168 width=33) (actual time=7.110..19.850 rows=11168 loops=1)
Hash Cond: (movies_actors.movie_id = movies.movie_id)
-> Hash Join (cost=143.19..514.27 rows=11168 width=18) (actual time=4.326..11.938 rows=11168 loops=1)
Hash Cond: (movies_actors.actor_id = actors.actor_id)
-> Seq Scan on movies_actors (cost=0.00..161.68 rows=11168 width=8) (actual time=0.013..1.648 rows=11168 loops=1)
-> Hash (cost=80.86..80.86 rows=4986 width=18) (actual time=4.296..4.296 rows=4986 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 252kB
-> Seq Scan on actors (cost=0.00..80.86 rows=4986 width=18) (actual time=0.009..1.681 rows=4986 loops=1)
-> Hash (cost=153.61..153.61 rows=2861 width=19) (actual time=2.768..2.768 rows=2861 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 146kB
-> Seq Scan on movies (cost=0.00..153.61 rows=2861 width=19) (actual time=0.003..1.197 rows=2861 loops=1)
-> Subquery Scan on "*SELECT* 2" (cost=6074.48..6340.40 rows=8182 width=630) (actual time=1231.324..1528.188 rows=371856 loops=1)
-> WindowAgg (cost=6074.48..6258.58 rows=8182 width=630) (actual time=1231.324..1492.106 rows=371856 loops=1)
-> Sort (cost=6074.48..6094.94 rows=8182 width=630) (actual time=1231.307..1282.550 rows=371856 loops=1)
Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
Sort Method: external sort Disk: 21584kB
-> Nested Loop (cost=0.27..3246.72 rows=8182 width=630) (actual time=0.047..909.096 rows=371856 loops=1)
-> Seq Scan on movies searches (cost=0.00..153.61 rows=2861 width=315) (actual time=0.003..0.676 rows=2861 loops=1)
-> Index Scan using movies_genres_cube on movies suggestions_1 (cost=0.27..1.05 rows=3 width=315) (actual time=0.016..0.277 rows=130 loops=2861)
Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
Filter: (searches.movie_id <> movie_id)
Rows Removed by Filter: 1
-> Sort (cost=436.99..437.23 rows=97 width=44) (actual time=1738.565..1738.566 rows=5 loops=1)
Sort Key: suggestions.rank, suggestions.suggestion_id
Sort Method: top-N heapsort Memory: 25kB
-> CTE Scan on suggestions (cost=0.00..435.38 rows=97 width=44) (actual time=1281.905..1738.531 rows=43 loops=1)
Filter: (entity_term = 'Die Hard'::text)
Rows Removed by Filter: 382981
Total runtime: 1746.623 ms
Le EXPLAIN ANALYZE
Plan de la deuxième requête ressemble à ceci:
Limit (cost=43.74..43.76 rows=5 width=44) (actual time=1.231..1.234 rows=5 loops=1)
CTE suggestions
-> Append (cost=4.86..43.58 rows=5 width=391) (actual time=1.029..1.141 rows=43 loops=1)
-> Subquery Scan on "*SELECT* 1" (cost=4.86..20.18 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
-> Nested Loop (cost=4.86..20.16 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
-> Nested Loop (cost=4.58..19.45 rows=2 width=18) (actual time=0.045..0.045 rows=0 loops=1)
-> Index Scan using actors_name on actors (cost=0.28..8.30 rows=1 width=18) (actual time=0.045..0.045 rows=0 loops=1)
Index Cond: (name = 'Die Hard'::text)
-> Bitmap Heap Scan on movies_actors (cost=4.30..11.13 rows=2 width=8) (never executed)
Recheck Cond: (actor_id = actors.actor_id)
-> Bitmap Index Scan on movies_actors_actor_id (cost=0.00..4.30 rows=2 width=0) (never executed)
Index Cond: (actor_id = actors.actor_id)
-> Index Scan using movies_pkey on movies (cost=0.28..0.35 rows=1 width=19) (never executed)
Index Cond: (movie_id = movies_actors.movie_id)
-> Subquery Scan on "*SELECT* 2" (cost=23.31..23.40 rows=3 width=630) (actual time=0.982..1.081 rows=43 loops=1)
-> WindowAgg (cost=23.31..23.37 rows=3 width=630) (actual time=0.982..1.064 rows=43 loops=1)
-> Sort (cost=23.31..23.31 rows=3 width=630) (actual time=0.963..0.971 rows=43 loops=1)
Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
Sort Method: quicksort Memory: 28kB
-> Nested Loop (cost=4.58..23.28 rows=3 width=630) (actual time=0.808..0.916 rows=43 loops=1)
-> Index Scan using movies_title on movies searches (cost=0.28..8.30 rows=1 width=315) (actual time=0.025..0.027 rows=1 loops=1)
Index Cond: (title = 'Die Hard'::text)
-> Bitmap Heap Scan on movies suggestions_1 (cost=4.30..14.95 rows=3 width=315) (actual time=0.775..0.844 rows=43 loops=1)
Recheck Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
Filter: (searches.movie_id <> movie_id)
Rows Removed by Filter: 1
-> Bitmap Index Scan on movies_genres_cube (cost=0.00..4.29 rows=3 width=0) (actual time=0.750..0.750 rows=44 loops=1)
Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
-> Sort (cost=0.16..0.17 rows=5 width=44) (actual time=1.230..1.231 rows=5 loops=1)
Sort Key: suggestions.rank, suggestions.suggestion_id
Sort Method: top-N heapsort Memory: 25kB
-> CTE Scan on suggestions (cost=0.00..0.10 rows=5 width=44) (actual time=1.034..1.187 rows=43 loops=1)
Total runtime: 1.410 ms
PostgreSQL 9.3 ne fait pas Predicate Browdown pour CTES.
Un optimiseur qui prédit le poussoir peut se déplacer dans les cas de clauses dans des requêtes internes. L'objectif est de filtrer des données non pertinentes le plus tôt possible. Tant que la nouvelle requête est logiquement équivalente, le moteur récupère toujours toutes les données pertinentes, ce qui produit le résultat correct, seulement plus rapidement.
Le développeur de base Tom Lane fait allusion à la difficulté de déterminer l'équivalence logique sur la liste de diffusion PGSQL-Performances .
Les CTES sont également traités comme des clôtures d'optimisation; Ce n'est pas tellement une limitation d'optimisation de garder la sémantique sain d'esprit lorsque le CTE contient une requête écritable.
L'optimiseur ne distingue pas les CTES en lecture seule de l'écriture écrite, de sorte que trop conservatrice lors de la prise en compte des plans. Le traitement "clôture" empêche l'optimiseur de déplacer la clause Where à l'intérieur du CTE, bien que nous puissions le voir, il est prudent de le faire.
Nous pouvons attendre que l'équipe PostgreSQL améliore l'optimisation CTE, mais pour maintenant obtenir de bonnes performances, vous devez changer votre style d'écriture.
La question montre déjà une façon d'obtenir un meilleur plan. Dupliquer la condition de filtre essentiellement des codes papier L'effet de la poussée du prédicat.
Dans les deux plans, les copies du moteur résultent des rangées à une table de travail afin de pouvoir les trier. Plus la table de travail est grande, plus la requête est plus lente.
Le premier plan copie toutes les lignes des tables de base à la table de travail et scanne que pour trouver le résultat. Pour rendre les choses encore plus lentes, le moteur doit numériser toute la table de travail car il n'a pas d'index.
C'est une quantité ridicule de travaux inutiles. Il lit toutes les données des tables de base deux fois pour trouver la réponse, une fois que des lignes correspondantes estimées de 5 lignes d'apparition estimées de 19350 lignes dans les tables de base.
Le deuxième plan utilise les index pour trouver les lignes correspondantes et les copies exactement celles à la table de travail. L'indice a effectivement filtré les données pour nous.
Sur page 85 de l'art de SQL, Stéphane Faroult nous rappelle les attentes des utilisateurs.
Dans une très large mesure, les utilisateurs finaux ajustent leur patience au nombre de rangées qu'elles attendent: lorsqu'ils demandent une aiguille, elles paient peu d'attention à la taille de la botte de foin.
La deuxième échelle de plan avec l'aiguille est plus susceptible de garder vos utilisateurs heureux.
La nouvelle requête est plus difficile à maintenir car vous pouvez introduire un défaut en modifiant une époxe filtrante mais pas l'autre.
Ne serait-ce pas formidable si nous pouvions tout écrire une fois et que nous obtenions toujours de bonnes performances?
Nous pouvons. L'optimiseur prédit la poussée pour les subqeries.
Un exemple plus simple est plus facile à expliquer.
CREATE TABLE a (c INT);
CREATE TABLE b (c INT);
CREATE INDEX a_c ON a(c);
CREATE INDEX b_c ON b(c);
INSERT INTO a SELECT 1 FROM generate_series(1, 1000000);
INSERT INTO b SELECT 2 FROM a;
INSERT INTO a SELECT 3;
Cela crée deux tables chacune avec une colonne indexée. Ensemble, ils contiennent un million 1
s, un million 2
s, et un 3
.
Vous pouvez trouver l'aiguille 3
Utiliser l'une de ces questions.
-- CTE
EXPLAIN ANALYZE
WITH cte AS (
SELECT c FROM a
UNION ALL
SELECT c FROM b
)
SELECT c FROM cte WHERE c = 3;
-- Subquery
EXPLAIN ANALYZE
SELECT c
FROM (
SELECT c FROM a
UNION ALL
SELECT c FROM b
) AS subquery
WHERE c = 3;
Le plan pour le CTE est lent. Le moteur scanne trois tables et se lit sur environ quatre millions de lignes. Il faut près de 1000 millisecondes.
CTE Scan on cte (cost=33275.00..78275.00 rows=10000 width=4) (actual time=471.412..943.225 rows=1 loops=1)
Filter: (c = 3)
Rows Removed by Filter: 2000000
CTE cte
-> Append (cost=0.00..33275.00 rows=2000000 width=4) (actual time=0.011..409.573 rows=2000001 loops=1)
-> Seq Scan on a (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.010..114.869 rows=1000001 loops=1)
-> Seq Scan on b (cost=0.00..18850.00 rows=1000000 width=4) (actual time=5.530..104.674 rows=1000000 loops=1)
Total runtime: 948.594 ms
Le plan de la sous-requête est rapide. Le moteur cherche juste chaque index. Il faut moins qu'un milliseconde.
Append (cost=0.42..8.88 rows=2 width=4) (actual time=0.021..0.038 rows=1 loops=1)
-> Index Only Scan using a_c on a (cost=0.42..4.44 rows=1 width=4) (actual time=0.020..0.021 rows=1 loops=1)
Index Cond: (c = 3)
Heap Fetches: 1
-> Index Only Scan using b_c on b (cost=0.42..4.44 rows=1 width=4) (actual time=0.016..0.016 rows=0 loops=1)
Index Cond: (c = 3)
Heap Fetches: 0
Total runtime: 0.065 ms
Voir sqlfiddle pour une version interactive.
La question posée sur Postgres 9.3. Cinq ans plus tard, cette version est obsolète, mais ce qui est changé?
PostgreSQL 12 Maintenant inline CTES comme celles-ci.
Inline de requêtes (expressions de table communes)
Expressions de table communes (AKA
WITH
requêtes) peut désormais être automatiquement inlinité dans une requête si elles a) ne sont pas récursives, b) ne pas avoir d'effets secondaires et c) ne sont référencés que dans une partie ultérieure de une requête. Cela supprime une "clôture d'optimisation" qui existe depuis l'introduction de la clauseWITH
dans PostgreSQL 8.4Si besoin d'être, vous pouvez forcer A avec requête pour se matérialiser à l'aide de la clause matérialisée, par ex.
WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;