2

나는 적어도 (나는 적어도 생각할 때) 데이터베이스 구조를 가지고있다. 뉴스는 (News(id, source_id))이고, 각 뉴스에는 소스 (Source(id, url))가있다. 소스는 TopicSource(source_id, topic_id)을 통해 주제 (Topic(id, title))로 집계됩니다. 또한 NewsRead(news_id, user_id)을 통해 뉴스를 읽음으로 표시 할 수있는 사용자 (User(id, name))가 있습니다. db diagram커다란 테이블에 읽지 않은 뉴스 계산하기

나는이 주제에 수를 읽지 않은 뉴스를 원하는 특정 사용자에 대한 : 여기에 물건을 정리하기위한 도면이다. 문제는 News 테이블이 큰 테이블 (10^6 - 10^7 행)입니다. 다행스럽게도 정확히 카운트를 알 필요가 없습니다.이 임계 값을 계산 된 값으로 반환하는 임계 값 이후에 카운트를 중지하는 것이 좋습니다.

하나의 주제에 대한 this answer에 따라 나는 다음 쿼리를 내놓았다 :

SELECT t.topic_id, count(1) as unread_count 
FROM (
SELECT 1, topic_id 
FROM news n 
    JOIN topic_source t ON n.source_id = t.source_id 
    -- join news_read to filter already read news 
    LEFT JOIN news_read r 
    ON (n.id = r.news_id AND r.user_id = 1) 
WHERE t.topic_id = 3 AND r.user_id IS NULL 
LIMIT 10 -- Threshold 
) t GROUP BY t.topic_id; 

(query plan 1). 이 쿼리는 테스트 데이터베이스에서 약 50ms가 걸립니다.

이제 개의 여러 주제에 대해 읽지 않은 메일을 선택하고 싶습니다.. 나는 그런 선택을 시도 :

SELECT 
    t.topic_id, 
    (SELECT count(1) 
    FROM (SELECT 1 FROM news n 
      JOIN topic_source tt ON n.source_id = tt.source_id 
      LEFT JOIN news_read r 
      ON (n.id = r.news_id AND r.user_id = 1) 
      WHERE tt.topic_id = t.topic_id AND r.user_id IS NULL 
      LIMIT 10 -- Threshold 
     ) t) AS unread_count 
FROM topic_source t WHERE t.topic_id IN (1, 2) GROUP BY t.topic_id; 

(query plan 2). 그러나 나에게 알려지지 않은 이유 때문에 테스트 데이터에 대해 약 1.5 초가 걸리고 개별 쿼리의 합계는 약 0.2-0.3 초가 걸립니다.

나는 분명히 뭔가를 여기에서 놓치고있다. 두 번째 쿼리에 실수가 있습니까? 읽지 않은 뉴스를 선택하는 더 좋은 (더 빠른) 방법이 있습니까?

추가 정보 : 여기

테이블 크기 :

News - 10^6 - 10^7 
User - 10^3 
Source - 10^4 
Topic - 10^3 
TopicSource - 10^5 
NewsRead - 10^6 

UPD : 쿼리 계획은 분명히 내가 두 번째 쿼리를 엉망으로 보여줍니다. 모든 단서는 높이 평가됩니다.

UPD2 :

SELECT 
    id, 
    count(*) 
FROM topic t 
    LEFT JOIN LATERAL (
    SELECT ts.topic_id 
    FROM news n 
     LEFT JOIN news_read r 
     ON (n.id = r.news_id AND r.user_id = 1) 
     JOIN topic_source ts ON n.source_id = ts.source_id 
    WHERE ts.topic_id = t.id AND r.user_id IS NULL 
    LIMIT 10 
) p ON TRUE 
WHERE t.id IN (4, 10, 12, 16) 
GROUP BY t.id; 

(query plan 3) :는 단순히 먼저 각 topic_id에 대한 (가장 빠른) 쿼리를 실행하도록되어있는 가입 횡 방향이 쿼리를 시도했다. 그러나 Pg planner는 이에 대해 다른 견해를 가지고있는 것으로 보입니다. 인덱스 스캔과 조인 조인 대신 매우 느린 seq 스캔과 해시 조인을 실행합니다.

+0

내가 [이] (https://paste.ofcode.org/TMhZbxCGqiSgc3ijhzZwfX) 쿼리가 데이터에 운임 얼마나 궁금합니다. 비슷한 볼륨의 샘플 데이터를 만들려고 시도했지만 배포본이 너무 다르기 때문에 원래 쿼리에 대해서도 매우 다른 결과를 얻었습니다. 예를 들어, 다중 주제 쿼리는 ~ 19ms 만 소요됩니다 (여러 항목 중에서 가장 좋음). –

+0

@ IljaEverilä, 의견을 보내 주셔서 감사합니다! 이 쿼리는 내 데이터에서 ~ 3.5 초 걸립니다. 나는 우리의 배포판이 꺼져있는 것 같아요. [여기에 설명이 있습니다.] (https://explain.depesz.com/s/q740). 갑자기 여러 UNION ALL이 매우 빠르다. 나는 짧은 연구 끝에 나의 포스트를 업데이트 할 것이다. – 9dogs

+0

가장 안쪽의 하위 쿼리에서 LIMIT 10을 잊어 버렸습니다. 그것은 많은 시도 중 일부를 복사하기 위해 얻은 것입니다. 아마 그 자리에서 그걸로 조금 더 빨리 달릴 것입니다. –

답변

0
마침내 모든 쿼리 간단한 UNION에서 중지 한 일부 벤치마킹 후

그리고 측면 내 데이터에 가입보다 10 배 빠르다 :

SELECT 
    p.topic_id, 
    count(*) 
FROM (
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 4 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
     UNION ALL 
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 10 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
     UNION ALL 
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 12 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
     UNION ALL 
     SELECT * 
     FROM (
       SELECT fs.topic_id 
       FROM news n 
       LEFT JOIN news_read r 
        ON (n.id = r.news_id AND r.user_id = 1) 
       JOIN topic_source fs ON n.source_id = fs.source_id 
       WHERE fs.topic_id = 16 AND r.user_id IS NULL 
       LIMIT 100 
      ) t1 
    ) p 
GROUP BY p.topic_id; 

(execute plan)

여기 직관에 의한 것입니다 topic_id`를 명시 적으로 지정하면 Pg 플래너에게 효과적인 계획을 세우는데 필요한 충분한 정보를 제공합니다.

보기의 SQLAlchemy 관점에서 그것은 매우 간단합니다 :

# topic_ids, user_id are defined elsewhere, e.g. 
# topic_ids = [4, 10, 12, 16] 
# user_id = 1 
for topic_id in topic_ids: 
    topic_query = (
     db.session.query(News.id, TopicSource.topic_id) 
     .join(TopicSource, TopicSource.source_id == News.source_id) 
     # LEFT JOIN NewsRead table to filter only unreads 
     # (where News.user_id IS NULL) 
     .outerjoin(NewsRead, 
        and_(NewsRead.news_id == News.id, 
         NewsRead.user_id == user_id)) 
     .filter(TopicSource.topic_id == topic_id, 
       NewsRead.user_id.is_(None)) 
     .limit(100)) 
    topic_queries.append(topic_query) 
# Unite queries with UNION ALL 
union_query = topic_queries[0].union_all(*topic_queries[1:]) 
# Groups query by `topic_id` and count unreads 
counts = (union_query 
      # Using `with_entities(func.count())` to avoid 
      # a subquery. See link below for info: 
      # https://gist.github.com/hest/8798884 
      .with_entities(TopicSource.topic_id.label('topic_id'), 
         func.count().label('unread_count')) 
      .group_by(TopicSource.topic_id)) 
result = counts.all()