Verbessern Sie die Leistung von COUNT / GROUP-BY in einer großen PostgresSQL-Tabelle?

24

Ich führe PostgresSQL 9.2 aus und habe eine 12-Spalten-Beziehung mit ungefähr 6.700.000 Zeilen. Es enthält Knoten in einem 3D-Raum, von denen jeder auf einen Benutzer verweist (der ihn erstellt hat). Um abzufragen, welcher Benutzer wie viele Knoten erstellt hat, gehe ich wie folgt vor ( explain analyzefür weitere Informationen hinzugefügt ):

EXPLAIN ANALYZE SELECT user_id, count(user_id) FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                    QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253668.70..253669.07 rows=37 width=8) (actual time=1747.620..1747.623 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220278.79 rows=6677983 width=8) (actual time=0.019..886.803 rows=6677983 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1747.653 ms

Wie Sie sehen, dauert dies ungefähr 1,7 Sekunden. In Anbetracht der Datenmenge ist das nicht schlecht, aber ich frage mich, ob dies verbessert werden kann. Ich habe versucht, einen BTree-Index für die Benutzerspalte hinzuzufügen, aber das hat in keiner Weise geholfen.

Haben Sie alternative Vorschläge?


Der Vollständigkeit halber ist dies die vollständige Tabellendefinition mit allen Indizes (ohne Fremdschlüsseleinschränkungen, Referenzen und Trigger):

    Column     |           Type           |                      Modifiers                    
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 location      | double3d                 | not null
 reviewer_id   | integer                  | not null default (-1)
 review_time   | timestamp with time zone |
 editor_id     | integer                  |
 parent_id     | bigint                   |
 radius        | double precision         | not null default 0
 confidence    | integer                  | not null default 5
 skeleton_id   | bigint                   |
Indexes:
    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)
    "skeleton_id_treenode_index" btree (skeleton_id)
    "treenode_editor_index" btree (editor_id)
    "treenode_location_x_index" btree (((location).x))
    "treenode_location_y_index" btree (((location).y))
    "treenode_location_z_index" btree (((location).z))
    "treenode_parent_id" btree (parent_id)
    "treenode_user_index" btree (user_id)

Bearbeiten: Dies ist das Ergebnis, wenn ich die von @ypercube vorgeschlagene Abfrage (und den Index) verwende (Abfrage dauert ungefähr 5,3 Sekunden ohne EXPLAIN ANALYZE):

EXPLAIN ANALYZE SELECT u.id, ( SELECT COUNT(*) FROM treenode AS t WHERE t.project_id=1 AND t.user_id = u.id ) AS number_of_nodes FROM auth_user As u;
                                                                        QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on auth_user u  (cost=0.00..6987937.85 rows=46 width=4) (actual time=29.934..5556.147 rows=46 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=151911.65..151911.66 rows=1 width=0) (actual time=120.780..120.780 rows=1 loops=46)
           ->  Bitmap Heap Scan on treenode t  (cost=4634.41..151460.44 rows=180486 width=0) (actual time=13.785..114.021 rows=145174 loops=46)
                 Recheck Cond: ((project_id = 1) AND (user_id = u.id))
                 Rows Removed by Index Recheck: 461076
                 ->  Bitmap Index Scan on treenode_user_index  (cost=0.00..4589.29 rows=180486 width=0) (actual time=13.082..13.082 rows=145174 loops=46)
                       Index Cond: ((project_id = 1) AND (user_id = u.id))
 Total runtime: 5556.190 ms
(9 rows)

Time: 5556.804 ms

Edit 2: Dies ist das Ergebnis, wenn ich ein indexOn project_id, user_id(aber noch keine Schemaoptimierung) verwende, wie von @ erwin-brandstetter vorgeschlagen (die Abfrage läuft mit 1,5 Sekunden mit der gleichen Geschwindigkeit wie meine ursprüngliche Abfrage):

EXPLAIN ANALYZE SELECT user_id, count(user_id) as ct FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                        QUERY PLAN                                                      
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253670.88..253671.24 rows=37 width=8) (actual time=1807.334..1807.339 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220280.62 rows=6678050 width=8) (actual time=0.183..893.491 rows=6678050 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1807.368 ms
(4 rows)
Tomka
quelle
Haben Sie auch eine Tabelle Usersmit user_idals Primärschlüssel?
Ypercubeᵀᴹ
Ich habe gerade gesehen, dass es für Postgres ein Columnstore-Addon von Drittanbietern gibt. Außerdem wollte ich nur von der neuen
iOS-
2
Vielen Dank für die gute, klare, vollständige Frage - Versionen, Tabellendefinitionen usw.
Craig Ringer
@ypercube Ja, ich habe eine Benutzertabelle.
Tomka
Wie viele verschiedene project_idund user_id? Wird die Tabelle ständig aktualisiert oder können Sie (für einige Zeit) mit einer materialisierten Ansicht arbeiten?
Erwin Brandstetter

Antworten:

25

Hauptproblem ist der fehlende Index. Aber es gibt noch mehr.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • Sie haben viele bigintSpalten. Wahrscheinlich übertrieben. In der Regel integerist mehr als genug für Spalten wie project_idund user_id. Dies würde auch beim nächsten Punkt helfen.
    Berücksichtigen Sie beim Optimieren der Tabellendefinition diese verwandte Antwort, wobei der Schwerpunkt auf der Datenausrichtung und dem Auffüllen liegt . Aber auch das meiste gilt:

  • Der Elefant im Raum : Es gibt keinen Indexproject_id . Erstelle einen. Dies ist wichtiger als der Rest dieser Antwort.
    Machen Sie dabei einen mehrspaltigen Index:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);

    Wenn Sie meinen Rat befolgen, integerwäre hier perfekt:

  • user_idist definiert NOT NULL, count(user_id)entspricht also count(*), letztere ist jedoch etwas kürzer und schneller. (In dieser speziellen Abfrage würde dies sogar ohne user_idDefinition zutreffen NOT NULL.)

  • idist bereits der Primärschlüssel, die zusätzliche UNIQUEEinschränkung ist nutzloses Vorschaltgerät . Lass es fallen:

    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)

    Nebenbei: Ich würde nicht idals Spaltenname verwenden. Verwenden Sie etwas Beschreibendes wie treenode_id.

Informationen hinzugefügt

F: How many different project_id and user_id?
A: not more than five different project_id.

Das bedeutet, dass Postgres ungefähr 20% der gesamten Tabelle lesen muss , um Ihre Anfrage zu beantworten. Sofern kein Nur-Index-Scan verwendet werden kann , ist ein sequentieller Scan in der Tabelle schneller als das Einbeziehen von Indizes. Hier ist keine Leistung mehr zu erzielen - außer durch die Optimierung der Tabellen- und Servereinstellungen.

Was den Nur-Index-Scan betrifft : Um zu sehen, wie effektiv das sein kann, führen VACUUM ANALYZESie ihn aus, wenn Sie es sich leisten können (sperrt die Tabelle ausschließlich). Dann versuchen Sie es erneut. Es sollte jetzt einigermaßen schneller sein, wenn nur der Index verwendet wird. Lesen Sie diese Antwort zuerst:

Sowie die mit Postgres 9.6 und dem Postgres-Wiki hinzugefügte Handbuchseite für Nur-Index-Scans .

Erwin Brandstetter
quelle
1
Erwin, danke für deine Vorschläge. Sie haben Recht, denn user_idund project_id integersollten mehr als genug sein. Es ist gut zu wissen, hier etwa 70 ms zu verwenden count(*)anstatt zu count(user_id)sparen. Ich habe die EXPLAIN ANALYZEder Abfrage hinzugefügt, nachdem ich Ihren Vorschlag indexzum ersten Beitrag hinzugefügt habe . Es verbessert die Leistung jedoch nicht (tut aber auch nicht weh). Es scheint, dass das überhaupt indexnicht verwendet wird. Ich werde die Schemaoptimierungen bald testen.
Tomka
1
Wenn ich deaktiviere seqscan, wird der Index verwendet ( Index Only Scan using treenode_project_id_user_id_index on treenode), aber die Abfrage dauert dann ungefähr 2,5 Sekunden (was ungefähr 1 Sekunde länger ist als bei seqscan).
Tomka
1
Danke für dein Update. Diese fehlenden Teile hätten Teil meiner Frage sein sollen, das ist richtig. Ich war mir ihrer Auswirkungen einfach nicht bewusst. Ich werde mein Schema optimieren, wie Sie es vorgeschlagen haben. Lassen Sie uns sehen, was ich daraus ziehen kann. Vielen Dank für Ihre Erklärung, es macht für mich Sinn und deshalb werde ich Ihre Antwort als die akzeptierte markieren.
Tomka
7

Ich würde zuerst einen Index hinzufügen (project_id, user_id)und dann in Version 9.3 diese Abfrage versuchen:

SELECT u.user_id, c.number_of_nodes 
FROM users AS u
   , LATERAL
     ( SELECT COUNT(*) AS number_of_nodes 
       FROM treenode AS t
       WHERE t.project_id = 1 
         AND t.user_id = u.user_id
     ) c 
-- WHERE c.number_of_nodes > 0 ;   -- you probably want this as well
                                   -- to show only relevant users

Versuchen Sie in 9.2 Folgendes:

SELECT u.user_id, 
       ( SELECT COUNT(*) 
         FROM treenode AS t
         WHERE t.project_id = 1 
           AND t.user_id = u.user_id
       ) AS number_of_nodes  
FROM users AS u ;

Ich nehme an, Sie haben einen usersTisch. Wenn nicht, ersetzen Sie usersdurch:
(SELECT DISTINCT user_id FROM treenode)

ypercubeᵀᴹ
quelle
Ich danke Ihnen sehr für Ihre Antwort. Sie haben Recht, ich habe eine Benutzertabelle. Bei Verwendung Ihrer Abfrage in 9.2 dauert es jedoch ungefähr 5 Sekunden, bis das Ergebnis angezeigt wird - unabhängig davon, ob der Index erstellt wurde oder nicht. Ich habe den Index folgendermaßen erstellt:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);aber ich habe es auch ohne die USINGKlausel versucht . Vermisse ich etwas
Tomka
Wie viele usersZeilen enthält die Tabelle und wie viele Zeilen gibt die Abfrage zurück (also wie viele Benutzer haben sie project_id=1?) Können Sie die Erklärung dieser Abfrage anzeigen, nachdem Sie den Index hinzugefügt haben?
Ypercubeᵀᴹ
1
Erstens habe ich mich in meinem ersten Kommentar geirrt. Ohne Ihren vorgeschlagenen Index dauert es ungefähr 40s (!), Um das Ergebnis abzurufen. Es dauert ca. 5s mit dem indexin Ort. Entschuldigung für die Verwirrung. In meiner usersTabelle habe ich 46 Einträge. Die Abfrage gibt nur 9 Zeilen zurück. Überraschenderweise werden SELECT DISTINCT user_id FROM treenode WHERE project_id=1;38 Zeilen zurückgegeben. Ich habe den explainzu meinem ersten Beitrag hinzugefügt . Und um Verwirrung zu vermeiden: Mein usersTisch heißt eigentlich auth_user.
Tomka
Ich frage mich, wie SELECT DISTINCT user_id FROM treenode WHERE project_id=1;38 Zeilen zurückgegeben werden können, während die Abfragen nur 9 zurückgeben.
Ypercubeᵀᴹ
Können Sie das versuchen ?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ