매우 느린 간단한 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사용자 이름을 선택하려면 그룹화해야한다고 말합니다 ). 그룹화하면 usernamePostgres의 실행 시간에 약간의 ms 가 추가되거나 아무것도 수행되지 않습니다.

과학을 위해 Microsoft SQL Server를 동일한 서버 (archlinux, 8 core xeon, 24 기가 바이트 램, ssd 실행)에 설치하고 Postgres의 모든 데이터를 동일한 테이블 구조, 동일한 인덱스 및 동일한 데이터 로 마이그레이션했습니다 . 0.3 초 안에 상위 100 개 포스터를 얻는 동일한 쿼리 :

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 배 빠르게 작업을 수행합니다. 그리고 그것은 리눅스에서 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

PostgreSQL을위한 가장 빠른 방법 인 GIN 및 GIST까지 사용 된 모든 인덱스를 사용해 보았습니다.

MS SQL Server 14.0.405.200-1, 기본 conf.

API에서 (분석없이 일반 선택으로) 이것을 사용하고 크롬 으로이 API 엔드 포인트를 호출하면 2500ms +-, 50ms의 HTTP 및 웹 서버 오버 헤드 오버 헤드 추가 (API 및 SQL이 동일한 서버에서 실행 됨) - 그것은 동일합니다. 나는 여기 또는 저기에 100ms를 신경 쓰지 않고, 내가 관심있는 것은 2 초 전체입니다.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;700ms가 걸립니다. posts테이블 크기 는 2154MB입니다.


2
들리 겠지만, 사용자들의 뚱뚱한 게시물이 있습니다 (평균 1kB ~). 그런 식으로 posts같은 테이블을 사용하여 나머지 테이블 에서 분리하는 것이 이치에 맞을 CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text);수 있습니다. 이러한 유형의 쿼리에서 '낭비되는'대부분의 I / O가 절약 될 수 있습니다. 게시물이 이보다 작 으면 VACUUM FULLon posts이 도움 이 될 수 있습니다.
dezso

예, 게시물에는 게시물의 모든 HTML이 포함 된 콘텐츠 열이 있습니다. 당신의 제안에 감사합니다, 내일 그것을 시도 할 것입니다. 문제는 MSSQL 게시물 테이블의 무게가 1.5GB를 넘고 내용이 동일하지만 훨씬 더 빠르다는 이유는 무엇입니까?
Lars

2
SQL Server에서 실제 실행 계획을 게시 할 수도 있습니다. 나 같은 Postgres 사람들에게도 정말 흥미로울 수 있습니다.
dezso

흠, 빠른 추측, 이것을 이것 GROUP BY u.id으로 바꾸고 GROUP BY p.user_id시도해 볼 수 있습니까? 내 생각에, Postgres는 사용자 테이블 식별자별로 그룹화하기 때문에 먼저 N2 행을 조인합니다. 사용자 N_ 행을 가져 오기 위해 user_id 만 게시해야합니다.
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가 너무 작아서 해시 조인이 여러 배치 (임시 파일로 작성 됨)를 사용해야합니다.


너무 작지 않습니다. 240MB로 늘리면 아무 효과가 없습니다. 무엇을 postgresql.conf에 도움이 될 것은이 두 줄을 추가하여 병렬 쿼리 가능하다 : max_parallel_workers_per_gather = 4max_worker_processes = 16
라스
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.