非常に遅い単純なJOINクエリ


12

シンプルなDB構造(オンラインフォーラム用):

CREATE TABLE users (
    id integer NOT NULL PRIMARY KEY,
    username text
);
CREATE INDEX ON users (username);

CREATE TABLE posts (
    id integer NOT NULL PRIMARY KEY,
    thread_id integer NOT NULL REFERENCES threads (id),
    user_id integer NOT NULL REFERENCES users (id),
    date timestamp without time zone NOT NULL,
    content text
);
CREATE INDEX ON posts (thread_id);
CREATE INDEX ON posts (user_id);

約80kのエントリusersと2,6百万のエントリのpostsテーブル。投稿で上位100人のユーザーを取得するこの簡単なクエリには、2,4秒かかります。

EXPLAIN ANALYZE SELECT u.id, u.username, COUNT(p.id) AS PostCount FROM users u
                    INNER JOIN posts p on p.user_id = u.id
                    WHERE u.username IS NOT NULL
                    GROUP BY u.id
ORDER BY PostCount DESC LIMIT 100;
Limit  (cost=316926.14..316926.39 rows=100 width=20) (actual time=2326.812..2326.830 rows=100 loops=1)
  ->  Sort  (cost=316926.14..317014.83 rows=35476 width=20) (actual time=2326.809..2326.820 rows=100 loops=1)
        Sort Key: (count(p.id)) DESC
        Sort Method: top-N heapsort  Memory: 32kB
        ->  HashAggregate  (cost=315215.51..315570.27 rows=35476 width=20) (actual time=2311.296..2321.739 rows=34608 loops=1)
              Group Key: u.id
              ->  Hash Join  (cost=1176.89..308201.88 rows=1402727 width=16) (actual time=16.538..1784.546 rows=1910831 loops=1)
                    Hash Cond: (p.user_id = u.id)
                    ->  Seq Scan on posts p  (cost=0.00..286185.34 rows=1816634 width=8) (actual time=0.103..1144.681 rows=2173916 loops=1)
                    ->  Hash  (cost=733.44..733.44 rows=35476 width=12) (actual time=15.763..15.763 rows=34609 loops=1)
                          Buckets: 65536  Batches: 1  Memory Usage: 2021kB
                          ->  Seq Scan on users u  (cost=0.00..733.44 rows=35476 width=12) (actual time=0.033..6.521 rows=34609 loops=1)
                                Filter: (username IS NOT NULL)
                                Rows Removed by Filter: 11335

Execution time: 2301.357 ms

set enable_seqscan = falseさらに悪いことに:

Limit  (cost=1160881.74..1160881.99 rows=100 width=20) (actual time=2758.086..2758.107 rows=100 loops=1)
  ->  Sort  (cost=1160881.74..1160970.43 rows=35476 width=20) (actual time=2758.084..2758.098 rows=100 loops=1)
        Sort Key: (count(p.id)) DESC
        Sort Method: top-N heapsort  Memory: 32kB
        ->  GroupAggregate  (cost=0.79..1159525.87 rows=35476 width=20) (actual time=0.095..2749.859 rows=34608 loops=1)
              Group Key: u.id
              ->  Merge Join  (cost=0.79..1152157.48 rows=1402727 width=16) (actual time=0.036..2537.064 rows=1910831 loops=1)
                    Merge Cond: (u.id = p.user_id)
                    ->  Index Scan using users_pkey on users u  (cost=0.29..2404.83 rows=35476 width=12) (actual time=0.016..41.163 rows=34609 loops=1)
                          Filter: (username IS NOT NULL)
                          Rows Removed by Filter: 11335
                    ->  Index Scan using posts_user_id_index on posts p  (cost=0.43..1131472.19 rows=1816634 width=8) (actual time=0.012..2191.856 rows=2173916 loops=1)
Planning time: 1.281 ms
Execution time: 2758.187 ms

usernamePostgresにはGroup by は必要ありません(SQL Serverでは、usernameユーザー名を選択する場合はグループ化する必要があると言われています)。でグループ化するとusername、Postgresでの実行時間が少し増えるか、何もしません。

科学のために、Microsoft SQL Serverを同じサーバー(archlinux、8コアxeon、24 GB RAM、SSDを実行)にインストールし、Postgresからすべてのデータを移行しました- 同じテーブル構造、同じインデックス、同じデータ。トップ100のポスターを取得するための同じクエリが実行されます 0,3秒で

SELECT TOP 100 u.id, u.username, COUNT(p.id) AS PostCount FROM dbo.users u
                    INNER JOIN dbo.posts p on p.user_id = u.id
                    WHERE u.username IS NOT NULL
                    GROUP BY u.id, u.username
ORDER BY PostCount DESC

利回り 同じ同じデータから得られた結果は、しかし、8倍速いそれをしません。そして、それはLinux上のMS SQLのベータ版です。それは「ホーム」OS(Windows Server)で実行されていると思います。

私のPostgreSQLクエリは完全に間違っていますか、それともPostgreSQLが遅いだけですか?

追加情報

バージョンはほぼ最新です(9.6.1、現在最新は9.6.2、ArchLinuxは古いパッケージがあり、更新が非常に遅いです)。構成:

max_connections = 75
shared_buffers = 3584MB       
effective_cache_size = 10752MB
work_mem = 24466kB         
maintenance_work_mem = 896MB   
dynamic_shared_memory_type = posix  
min_wal_size = 1GB
max_wal_size = 2GB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100

EXPLAIN ANALYZE 出力: https //pastebin.com/HxucRgnk

GINとGISTを使用したすべてのインデックスを試してみました。PostgreSQLの最速の方法(およびグーグルは多くの行で確認します)は、順次スキャンを使用することです。

MS SQL Server 14.0.405.200-1、デフォルト設定

私はこれをAPIで使用し(分析せずにプレーンセレクトで)、このAPIエンドポイントをクロームで呼び出すと、2500ミリ秒かかります-50ミリ秒のHTTPとWebサーバーオーバーヘッドオーバーヘッド(APIとSQLは同じサーバーで実行されます) - それは同じだ。私はここで100 msを気にしません。私が気にするのは2秒間です。

explain analyze SELECT user_id, count(9) FROM posts group by user_id;700ミリ秒かかります。postsテーブルのサイズは2154 MBです。


2
どうやら、ユーザーからの素敵な太った投稿があります(平均1 KBまで)。 そのようpostsなテーブルを使用して、テーブルの残りの部分からそれらを切り離すのが理にかなっているかもしれませんCREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text);。このタイプのクエリで「無駄になった」I / Oの大部分は省くことができます。投稿がこれよりも小さい場合は、VACUUM FULLon postsが役立ちます。
-dezso

はい。投稿には、投稿のすべてのhtmlを含むコンテンツ列があります。あなたの提案をありがとう、明日それを試してみます。質問です-MSSQL投稿テーブルも1.5 GBを超え、コンテンツに同じエントリがありますが、かなり高速になります-なぜですか?
ラース

2
SQL Serverから実際の実行計画を投稿することもできます。私のようなPostgresの人々にとっても、本当に面白いかもしれません。
dezso

うーん、すぐに推測して、これGROUP BY u.idをこれに変更してGROUP BY p.user_id試してみてください。私は、上位N-行を取得するためにuser_idの投稿のみが必要な場合でも、Postgresは最初に参加し、2番目にグループ化するのではないかと推測しています。
-UldisK

回答:


1

別の優れたクエリバリアントは次のとおりです。

SELECT p.user_id, p.cnt AS PostCount
FROM users u
INNER JOIN (
    select user_id, count(id) as cnt from posts group by user_id
) as p on p.user_id = u.id
WHERE u.username IS NOT NULL          
ORDER BY PostCount DESC LIMIT 100;

CTEを悪用せず、正しい答えを提供します(理論的には、CTEの例では100行未満しか生成されない可能性があり、最初に制限してからユーザーと結合します)。

私は、MSSQLはクエリオプティマイザーでそのような変換を実行でき、PostgreSQLは結合中の集約をプッシュダウンできないと思います。または、MSSQLのハッシュ結合の実装がはるかに高速です。


8

これはうまくいくかもしれないし、うまくいかないかもしれない-私はこれがグループとフィルターの前にあなたのテーブルに参加しているという直感に基づいている。私は次のことを試みることをお勧めします:参加を試みる前に、CTEを使用してフィルターおよびグループ化します。

with
    __posts as(
        select
            user_id,
            count(1) as num_posts
        from
            posts
        group by
            user_id
        order by
            num_posts desc
        limit 100
    )
select
    users.username,
    __posts.num_posts
from
    users
    inner join __posts on(
        __posts.user_id = users.id
    )
order by
    num_posts desc

クエリプランナーは、少しのガイダンスが必要な場合があります。このソリューションはここではうまく機能しますが、状況によってはCTEが恐ろしい可能性があります。CTEはメモリに排他的に保存されます。この結果、大量のデータがPostgresに割り当てられたメモリを超えて、スワップ(MSでのページング)を開始する可能性があります。CTEのインデックスも作成できないため、CTEを照会する際に十分に大きなクエリを実行しても、大幅に速度が低下する可能性があります。

実際に持ち帰れる最善のアドバイスは、複数の方法で試して、クエリプランを確認することです。


-1

work_memを増やしようとしましたか?24Mbは小さすぎると思われるため、ハッシュ結合では複数のバッチ(一時ファイルに書き込まれます)を使用する必要があります。


小さすぎません。240メガバイトに増やしても何も起こりません。何postgresql.conf内役立つだろうことは、これらの2行を追加することで、並列クエリを有効にしますmax_parallel_workers_per_gather = 4max_worker_processes = 16
ラース
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.