Die Unterabfrage SELECT DISTINCT ON verwendet einen ineffizienten Plan

8

Ich habe eine Tabelle progresses(enthält derzeit Hunderttausende von Datensätzen):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

und eine Ansicht, v_latest_progressesdie nach den neuesten progressvon user_idund fragt lesson_id:

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

Ein Benutzer kann für eine bestimmte Lektion viele Fortschritte erzielen. Wir möchten jedoch häufig eine Reihe der zuletzt erstellten Fortschritte für eine bestimmte Gruppe von Benutzern oder Lektionen (oder eine Kombination aus beiden) abfragen.

Die Ansicht v_latest_progressesmacht das gut und ist sogar performant, wenn ich eine Reihe von user_ids spezifiziere :

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

Wenn ich jedoch versuche, dieselbe Abfrage user_iddurchzuführen und die Menge von s durch eine Unterabfrage zu ersetzen, wird dies sehr ineffizient:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

Ich versuche herauszufinden, warum PostgreSQL die DISTINCTAbfrage für die gesamte progressesTabelle ausführen möchte, bevor sie im zweiten Beispiel nach der Unterabfrage gefiltert wird.

Hätte jemand einen Rat, wie man diese Abfrage verbessern kann?

Aaron
quelle

Antworten:

11

Aaron,

In meiner letzten Arbeit habe ich einige ähnliche Fragen mit PostgreSQL untersucht. PostgreSQL ist fast immer ziemlich gut darin, den richtigen Abfrageplan zu generieren, aber es ist nicht immer perfekt.

Einige einfache Vorschläge wären , sicherzustellen, dass Sie eine ANALYZEauf Ihrem progressesTisch ausführen , um sicherzustellen, dass Sie Statistiken aktualisiert haben, aber dies kann nicht garantiert werden , um Ihre Probleme zu beheben!

Aus Gründen, die für diesen Beitrag wahrscheinlich zu langwierig sind, habe ich in der Statistikerfassung ANALYZEund im Abfrageplaner einige merkwürdige Verhaltensweisen festgestellt , die möglicherweise langfristig gelöst werden müssen. Kurzfristig besteht der Trick darin, Ihre Abfrage neu zu schreiben, um zu versuchen, den gewünschten Abfrageplan zu hacken.

Ohne Zugriff auf Ihre Daten zum Testen zu haben, mache ich die folgenden zwei möglichen Vorschläge.

1) Verwenden ARRAY()

PostgreSQL behandelt Arrays und Sätze von Datensätzen in seinem Abfrageplaner unterschiedlich. Manchmal erhalten Sie einen identischen Abfrageplan. In diesem Fall, wie in vielen meiner Fälle, tun Sie dies nicht.

In Ihrer ursprünglichen Anfrage hatten Sie:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

Versuchen Sie es als ersten Versuch, das Problem zu beheben

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

Beachten Sie die Änderung der Unterabfrage von INnach =ANY(ARRAY()).

2) Verwenden Sie CTEs

Ein weiterer Trick besteht darin, separate Optimierungen zu erzwingen, wenn mein erster Vorschlag nicht funktioniert. Ich weiß, dass viele Leute diesen Trick verwenden, weil Abfragen innerhalb eines CTE getrennt von der Hauptabfrage optimiert und materialisiert werden.

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

Wenn Sie den CTE user_selectionmithilfe der WITHKlausel erstellen , fordern Sie PostgreSQL im Wesentlichen auf, eine separate Optimierung für die Unterabfrage durchzuführen

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

und dann diese Ergebnisse zu materialisieren. Ich benutze dann noch einmal den =ANY(ARRAY())Ausdruck, um zu versuchen, den Plan manuell zu manipulieren.

In diesen Fällen können Sie wahrscheinlich nicht nur den Ergebnissen von vertrauen EXPLAIN, da bereits angenommen wurde, dass die kostengünstigste Lösung gefunden wurde. Stellen Sie sicher, dass Sie ein ausführen EXPLAIN (ANALYZE,BUFFERS)..., um herauszufinden, was es in Bezug auf Zeit und Seitenlesungen wirklich kostet.

Chris
quelle
Wie sich herausstellt, wirkt Ihr erster Vorschlag Wunder. Die Kosten für diese Abfrage liegen weit 144.07..144.6unter den 70.000, die ich bekommen habe! Vielen Dank.
Aaron
1
Ha! Froh, dass ich helfen konnte. Ich kämpfe viel durch diese "Abfrageplan-Hacking" -Probleme; Es ist ein bisschen Kunst über der Wissenschaft.
Chris
Ich habe im Laufe der Jahre links und rechts Tricks gelernt, um Datenbanken dazu zu bringen, das zu tun, was ich will, und ich muss sagen, dass dies eine der seltsamsten Situationen war, mit denen ich mich befasst habe. Es ist wirklich eine Kunst. Ich schätze Ihre gut durchdachte Erklärung sehr!
Aaron