복붙노트

[SQL] BY 쿼리 최적화 그룹은 사용자 당 최신 행을 검색하기

SQL

BY 쿼리 최적화 그룹은 사용자 당 최신 행을 검색하기

나는 포스트 그레스 9.2에서 사용자 메시지 (단순화 된 형태)에 대해 다음 로그 테이블이 있습니다

CREATE TABLE log (
    log_date DATE,
    user_id  INTEGER,
    payload  INTEGER
);

그것은 사용자 당 하루에 한 기록까지 포함되어 있습니다. 3백일을 위해 하루에 약 50 만 기록이있을 것이다. (이 중요한 경우) 페이로드는 지금까지 각 사용자에 대해 증가하고있다.

나는 효율적으로 특정 날짜 이전에 각 사용자에 대한 최신 기록을 검색 할 수 있습니다. 내 질문은 :

SELECT user_id, max(log_date), max(payload) 
FROM log 
WHERE log_date <= :mydate 
GROUP BY user_id

이는 매우 느립니다. 나는 또한 시도했다 :

SELECT DISTINCT ON(user_id), log_date, payload
FROM log
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC;

의 같은 계획을 가지고 있으며, 동일하게 느립니다.

지금까지 나는 로그 (log_date)에 하나의 인덱스를 가지고 있지만, 많은 도움이되지 않습니다.

그리고 나는 모든 사용자와 사용자 테이블이 포함되어 있습니다. 또한 일부 일부 사용자에 대한 결과 (: 값 페이로드에 그들을>) 검색 할 수 있습니다.

나는이 위로, 또는 내가 원하는 것을 달성하기 위해 다른 방법으로 속도를 사용해야하는 다른 인덱스가 있습니까?

해결법

  1. ==============================

    1.최적의 읽기 성능을 당신은 멀티 컬럼 인덱스가 필요합니다 :

    최적의 읽기 성능을 당신은 멀티 컬럼 인덱스가 필요합니다 :

    CREATE INDEX log_combo_idx
    ON log (user_id, log_date DESC NULLS LAST);
    

    메이크업 지수는 가능한 스캔하려면 INCLUDE 절 (나중에 포스트 그레스 11)와 커버 인덱스에 달리 필요하지 않은 열 페이로드를 추가 :

    CREATE INDEX log_combo_covering_idx
    ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
    

    보다:

    이전 버전에 대한 폴백 (fallback) :

    CREATE INDEX log_combo_covering_idx
    ON log (user_id, log_date DESC NULLS LAST, payload);
    

    왜 DESC NULLS LAST?

    USER_ID 당 몇 행이나 작은 테이블의 경우 DISTINCT ON는 일반적으로 가장 빠르고 간단합니다 :

    인덱스 스캔 (또는 느슨한 인덱스 스캔)을 건너 USER_ID 당 행 수를 들어 (훨씬) 더 효율적입니다. 즉하지 포스트 그레스 (12)까지 구현 것 - 작업은 포스트 그레스 (13)에 대해 진행되고 그러나 효율적으로 에뮬레이션 할 수있는 방법이 있습니다.

    공통 테이블 표현식은 포스트 그레스 8.4+이 필요합니다. 측면은 포스트 그레스 9.3+가 필요합니다. 다음 솔루션은 포스트 그레스 위키에 덮여 것 이상으로 이동합니다.

    별도의 사용자 테이블로, 2에서 솔루션은 아래에 일반적으로 간단하고 빠릅니다. 앞서 건너 뜁니다.

    WITH RECURSIVE cte AS (
       (                                -- parentheses required
       SELECT user_id, log_date, payload
       FROM   log
       WHERE  log_date <= :mydate
       ORDER  BY user_id, log_date DESC NULLS LAST
       LIMIT  1
       )
       UNION ALL
       SELECT l.*
       FROM   cte c
       CROSS  JOIN LATERAL (
          SELECT l.user_id, l.log_date, l.payload
          FROM   log l
          WHERE  l.user_id > c.user_id  -- lateral reference
          AND    log_date <= :mydate    -- repeat condition
          ORDER  BY l.user_id, l.log_date DESC NULLS LAST
          LIMIT  1
          ) l
       )
    TABLE  cte
    ORDER  BY user_id;
    

    이는 현재 포스트 그레스에서 아마 가장 좋은 임의의 열과를 검색 할 간단합니다. 장 2A에 더 설명. 이하.

    WITH RECURSIVE cte AS (
       (                                           -- parentheses required
       SELECT l AS my_row                          -- whole row
       FROM   log l
       WHERE  log_date <= :mydate
       ORDER  BY user_id, log_date DESC NULLS LAST
       LIMIT  1
       )
       UNION ALL
       SELECT (SELECT l                            -- whole row
               FROM   log l
               WHERE  l.user_id > (c.my_row).user_id
               AND    l.log_date <= :mydate        -- repeat condition
               ORDER  BY l.user_id, l.log_date DESC NULLS LAST
               LIMIT  1)
       FROM   cte c
       WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
       )
    SELECT (my_row).*                              -- decompose row
    FROM   cte
    WHERE  (my_row).user_id IS NOT NULL
    ORDER  BY (my_row).user_id;
    

    단일 열 또는 전체 행을 검색하기 편리합니다. 이 예에서는 테이블의 전체 행의 형태를 사용한다. 다른 변형이 가능하다.

    이전 반복에서 발견 된 행을 주장하려면 (기본 키와 같은) 하나의 NOT NULL 열을 테스트합니다.

    장 2B에서이 쿼리에 대한 자세한 설명. 이하.

    관련 :

    테이블 레이아웃은 거의만큼 정확히 관련 USER_ID 당 하나 개의 행이 보장으로 중요하지 않습니다. 예:

    CREATE TABLE users (
       user_id  serial PRIMARY KEY
     , username text NOT NULL
    );
    

    이상적으로, 테이블은 물리적으로 로그 테이블과 동기화 정렬됩니다. 보다:

    또는 그것이 거의 문제 없다고 정도로 작은 (낮은 카디널리티)입니다. 다른 쿼리의 행을 정렬하는 것은 더 성능 최적화에 도움이 될 수 있습니다. 갱 리앙의 추가를 참조하십시오. 사용자 테이블의 물리적 정렬 순서가 로그에 인덱스를 일치 발생하면이 관련이있을 수 있습니다.

    SELECT u.user_id, l.log_date, l.payload
    FROM   users u
    CROSS  JOIN LATERAL (
       SELECT l.log_date, l.payload
       FROM   log l
       WHERE  l.user_id = u.user_id         -- lateral reference
       AND    l.log_date <= :mydate
       ORDER  BY l.log_date DESC NULLS LAST
       LIMIT  1
       ) l;
    

    가입 측면은 동일한 쿼리 수준에서 항목 FROM 이전 참조 할 수 있습니다. 보다:

    사용자 당 하나의 인덱스의 결과 (오닐) 룩업.

    사용자 테이블에없는 사용자에 대한 행을 반환합니다. 일반적으로, 참조 무결성을 시행 외래 키 제약 조건은 배제한다.

    또한, 로그 항목이 일치하지 않고 사용자를위한 어떤 행 - 원래의 질문에 부합. 결과를 사용하여 왼쪽에서 해당 사용자를 유지하려면 ... 대신 십자가의 진정한 ON 측면 가입 측면 가입 :

    N 대신 LIMIT 1의 사용 제한은 사용자 당 하나 이상의 행 (전부는 아니지만)을 검색 할 수 있습니다.

    효과적으로, 이들 모두는 동일한 작업을 수행 :

    JOIN LATERAL ... ON true
    CROSS JOIN LATERAL ...
    , LATERAL ...
    

    마지막 하나는하지만, 우선 순위가 낮습니다. 명시 쉼표 ​​전에 바인딩 가입하세요. 그 미묘한 차이는 더 많은 것을 가진 문제는 테이블에 가입하실 수 있습니다. 보다:

    좋은 선택은 단일 행에서 하나의 열을 검색 할 수 있습니다. 코드 예제 :

    같은 여러 열에 가능하지만, 당신은 더 많은 현명함이 필요합니다 :

    CREATE TEMP TABLE combo (log_date date, payload int);
    
    SELECT user_id, (combo1).*              -- note parentheses
    FROM (
       SELECT u.user_id
            , (SELECT (l.log_date, l.payload)::combo
               FROM   log l
               WHERE  l.user_id = u.user_id
               AND    l.log_date <= :mydate
               ORDER  BY l.log_date DESC NULLS LAST
               LIMIT  1) AS combo1
       FROM   users u
       ) sub;
    

    관련 :

    100,000 로그 항목 및 1K 사용자와 4 개 쿼리를 입증 : DB <> 바이올린 여기 - 11 페이지 올드 sqlfiddle - 페이지 9.6

  2. ==============================

    2.이것은 독립 답변이 아니라 어윈의 대답 @에 대한 의견이 아닙니다. 2A의 경우, 측면, 예를 조인 쿼리 로그에 인덱스의 지방을 이용하기 위해 사용자 테이블을 정렬하여 개선 될 수있다.

    이것은 독립 답변이 아니라 어윈의 대답 @에 대한 의견이 아닙니다. 2A의 경우, 측면, 예를 조인 쿼리 로그에 인덱스의 지방을 이용하기 위해 사용자 테이블을 정렬하여 개선 될 수있다.

    SELECT u.user_id, l.log_date, l.payload
      FROM (SELECT user_id FROM users ORDER BY user_id) u,
           LATERAL (SELECT log_date, payload
                      FROM log
                     WHERE user_id = u.user_id -- lateral reference
                       AND log_date <= :mydate
                  ORDER BY log_date DESC NULLS LAST
                     LIMIT 1) l;
    

    이론적 근거 USER_ID 값이 임의 인 경우 고가 조회하는 인덱스이다. 제 USER_ID를 정렬하여, 후속하는 측의 로그 지수 간단한 주사 같을 것이다 조인. 두 쿼리 계획이 닮았에도 불구하고, 실행 시간은 큰 테이블 훨씬 특히 다를 것이다.

    USER_ID 필드에 대한 인덱스가있는 경우 정렬 비용은 특히 최소이다.

  3. ==============================

    3.아마도 테이블에 다른 인덱스는 도움이 될 것이다. 로그 (USER_ID, log_date) :이 일을보십시오. 나는 포스트 그레스는 별개의 최적 사용을 만들 것입니다 긍정적 아닙니다.

    아마도 테이블에 다른 인덱스는 도움이 될 것이다. 로그 (USER_ID, log_date) :이 일을보십시오. 나는 포스트 그레스는 별개의 최적 사용을 만들 것입니다 긍정적 아닙니다.

    그래서, 그 인덱스와 스틱이 버전을 시도 할 것입니다 :

    select *
    from log l
    where not exists (select 1
                      from log l2
                      where l2.user_id = l.user_id and
                            l2.log_date <= :mydate and
                            l2.log_date > l.log_date
                     );
    

    이 정렬을 교체해야 / 인덱스보기 업으로 그룹화. 그것은 더 빠를 수 있습니다.

  4. from https://stackoverflow.com/questions/25536422/optimize-group-by-query-to-retrieve-latest-row-per-user by cc-by-sa and MIT license