PostgreSQL-열에 대한 Max 값이있는 행 가져 오기


96

time_stamp, usr_id, transaction_id 및 lives_remaining에 대한 열이있는 레코드가 포함 된 Postgres 테이블 ( "lives"라고 함)을 다루고 있습니다. 각 usr_id에 대한 가장 최근의 lives_remaining 합계를 제공하는 쿼리가 필요합니다.

  1. 여러 명의 사용자가 있습니다 (별도의 usr_id).
  2. time_stamp는 고유 식별자가 아닙니다. 때때로 사용자 이벤트 (테이블의 행별로)가 동일한 time_stamp로 발생합니다.
  3. trans_id는 매우 작은 시간 범위에서만 고유합니다. 시간이 지남에 따라 반복됩니다.
  4. (주어진 사용자에 대해) 남은 수명은 시간이 지남에 따라 증가 및 감소 할 수 있습니다.

예:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 | 2 | 3 | 삼    
  10:00 | 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

주어진 각 usr_id에 대한 최신 데이터가있는 행의 다른 열에 액세스해야하므로 다음과 같은 결과를 제공하는 쿼리가 필요합니다.

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

언급했듯이 각 usr_id는 생명을 얻거나 잃을 수 있으며 때로는 이러한 타임 스탬프가있는 이벤트가 너무 가깝게 발생하여 동일한 타임 스탬프를 갖습니다! 따라서이 쿼리는 작동하지 않습니다.

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

대신 time_stamp (첫 번째)와 trans_id (두 번째)를 모두 사용하여 올바른 행을 식별해야합니다. 그런 다음 하위 쿼리의 해당 정보를 해당 행의 다른 열에 대한 데이터를 제공하는 기본 쿼리로 전달해야합니다. 이것은 내가 일하게 된 해킹 된 쿼리입니다.

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

좋아,이게 효과가 있지만 나는 그것을 좋아하지 않는다. 쿼리 내에서 쿼리, 자체 조인이 필요하며 MAX가 가장 큰 타임 스탬프와 trans_id를 가진 것으로 확인 된 행을 잡아서 훨씬 더 간단 할 수있는 것 같습니다. "lives"테이블에는 구문 분석 할 수천만 개의 행이 있으므로이 쿼리가 가능한 한 빠르고 효율적 이길 바랍니다. 특히 RDBM과 Postgres를 처음 접했기 때문에 적절한 인덱스를 효과적으로 사용해야한다는 것을 알고 있습니다. 최적화하는 방법에 대해 약간 잃었습니다.

여기 에서 비슷한 토론을 찾았 습니다 . Oracle 분석 기능에 해당하는 일부 유형의 Postgres를 수행 할 수 있습니까?

집계 함수 (예 : MAX)에서 사용하는 관련 열 정보에 액세스하고, 인덱스를 만들고, 더 나은 쿼리를 만드는 방법에 대한 조언을 주시면 감사하겠습니다!

추신 다음을 사용하여 예제 케이스를 만들 수 있습니다.

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);

Josh, 쿼리가 자체 조인된다는 사실이 마음에 들지 않을 수도 있지만 RDBMS에 관한 한 괜찮습니다.
vladr

1
셀프 조인이 실제로 변환되는 것은 내부 SELECT (MAX가있는 항목)가 관련없는 항목을 버리고 인덱스를 스캔하고 외부 SELECT가 테이블에서 나머지 열을 가져 오는 간단한 인덱스 매핑입니다. 축소 된 인덱스에 해당합니다.
vladr

Vlad, 팁과 설명에 감사드립니다. 데이터베이스의 내부 동작을 이해하고 쿼리를 최적화하는 방법에 대해 눈을 뜨게되었습니다. Quassnoi, 훌륭한 쿼리와 기본 키에 대한 팁에 감사드립니다. 빌도. 매우 유용합니다.
Joshua Berry

MAX BY2 개의 열 을 얻는 방법을 보여 주셔서 감사합니다 !

답변:


90

158k 의사 랜덤 행이있는 테이블 (usr_id는 0에서 10k trans_id사이에 균일하게 분포 , 0에서 30 사이에 균일하게 분포),

아래에서 쿼리 비용 xxx_cost은 필요한 I / O 및 CPU 리소스에 대한 가중치 함수 추정 인 Postgres의 비용 기반 최적화 프로그램의 비용 추정 (Postgres의 기본값 포함 )을 참조합니다. PgAdminIII를 시작하고 "Query / Explain options"를 "Analyze"로 설정 한 쿼리에서 "Query / Explain (F7)"을 실행하여이를 얻을 수 있습니다.

  • Quassnoy의 쿼리는 1.3 초에서 비용 745k의 추정 (!), 그리고 완료가 (에 화합물 지수 부여를 ( usr_id, trans_id, time_stamp))
  • Bill의 쿼리의 예상 비용은 93k이며 2.9 초 만에 완료됩니다 (( usr_id, trans_id) 에 대한 복합 인덱스를 제공함 ).
  • 쿼리 # 1 아래 16K의 비용 추정치를 가지며, 800ms의 완료가 (화합물에 주어진 인덱스 ( usr_id, trans_id, time_stamp))
  • 쿼리 # 2 아래 14K의 비용 추정치를 가지며, 800ms의 완료는 (ON 화합물 함수 인덱스를 부여 ( usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • 이것은 Postgres 전용입니다.
  • 아래 쿼리 # 3 (포스트 그레스가 8.4+) 쿼리 2 비교 (또는 더 이상) 비용 추정치 및 종료 시간을 갖는다 (복합 지표 (에 기재를 usr_id, time_stamp, trans_id)); lives테이블을 한 번만 스캔하는 이점이 있으며 메모리에서 정렬을 수용하기 위해 임시로 (필요한 경우) work_mem늘리면 모든 쿼리 중에서 훨씬 빠릅니다.

위의 모든 시간에는 전체 10k 행 결과 집합 검색이 포함됩니다.

목표는 예상 비용에 중점을두고 최소 비용 예상 최소 쿼리 실행 시간입니다. 쿼리 실행은 런타임 조건 (예 : 관련 행이 이미 메모리에 완전히 캐시되었는지 여부)에 크게 좌우 될 수 있지만 비용 추정은 그렇지 않습니다. 다른 한편으로, 비용 견적은 정확히 견적이라는 것을 명심하십시오.

최적의 쿼리 실행 시간은로드없이 전용 데이터베이스에서 실행될 때 얻을 수 있습니다 (예 : 개발 PC에서 pgAdminIII로 플레이). 쿼리 시간은 실제 머신로드 / 데이터 액세스 확산에 따라 프로덕션에 따라 다릅니다. 한 쿼리가 다른 쿼리보다 약간 빠르지 만 (<20 %) 비용 이 훨씬 높은 경우 일반적으로 실행 시간 은 높지만 비용 은 낮은 쿼리 를 선택하는 것이 더 현명합니다.

쿼리가 실행될 때 프로덕션 시스템의 메모리에 대한 경쟁이 없을 것으로 예상되는 경우 (예 : RDBMS 캐시 및 파일 시스템 캐시는 동시 쿼리 및 / 또는 파일 시스템 활동에 의해 스 래싱되지 않음) 얻은 쿼리 시간 독립형 (예 : 개발 PC의 pgAdminIII) ​​모드가 대표적입니다. 프로덕션 시스템에 경합이있는 경우 비용이 낮은 쿼리는 캐시에 많이 의존하지 않는 반면 비용이 높은 쿼리는 동일한 데이터를 반복해서 다시 방문 하므로 쿼리 시간이 예상 비용 비율에 비례하여 저하 됩니다 (트리거링 안정적인 캐시가없는 경우 추가 I / O), 예 :

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

ANALYZE lives필요한 인덱스를 만든 후 한 번 실행하는 것을 잊지 마십시오 .


쿼리 # 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

쿼리 # 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

2013/01/29 갱신

마지막으로 버전 8.4부터 Postgres는 Window 함수를 지원하므로 다음과 같이 간단하고 효율적으로 작성할 수 있습니다.

쿼리 # 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

(usr_id, trans_id, times_tamp)의 복합 인덱스에 의해 "CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp)"와 같은 의미입니까? 아니면 각 열에 대해 세 개의 개별 인덱스를 만들어야합니까? 기본값 인 "USING btree"를 고수해야합니다. 그렇죠?
Joshua Berry

1
첫 번째 선택에 예 : CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp)를 의미합니다. :) 건배.
vladr

비용 비교 vladr를 해주셔서 감사합니다! 아주 완전한 대답!
Adam

@vladr 방금 당신의 대답을 보았습니다. 쿼리 1의 비용은 16k이고 쿼리 2의 비용은 14k입니다. 그러나 테이블 아래에서 쿼리 1의 비용은 5k이고 쿼리 2의 비용은 50k입니다. 그렇다면 어떤 쿼리가 선호되는 쿼리입니까? :) 감사합니다
Houman 2012

1
@Kave, 테이블은 OP의 두 쿼리가 아니라 예제를 설명하기위한 가상 쿼리 쌍입니다. 혼란을 줄이기 위해 이름을 바꿉니다.
vladr

77

다음을 기반으로 깨끗한 버전을 제안합니다 DISTINCT ON( docs 참조 ).

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

6
이것은 매우 짧고 건전한 대답입니다. 또한 좋은 참조가 있습니다! 이것은 받아 들여진 대답이어야합니다.
Prakhar Agrawal

이것은 다른 것들이 없을 약간 다른 응용 프로그램에서 나를 위해 작동하는 것처럼 보였습니다. 더 많은 가시성을 위해 확실히 올려야합니다.
짐 요인

8

여기에 상관 된 하위 쿼리 나 GROUP BY를 사용하지 않는 또 다른 방법이 있습니다. 저는 PostgreSQL 성능 조정 전문가가 아니므로이 방법과 다른 사람들이 제공 한 솔루션을 모두 시도하여 어떤 것이 더 나은지 확인하는 것이 좋습니다.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

나는 그것이 trans_id적어도 주어진 가치에 대해 고유 하다고 가정하고 time_stamp있습니다.


4

나는 당신이 언급 한 다른 페이지에서 Mike Woodhouse의 답변 스타일이 마음에 듭니다 . 그것은이 경우 하위 쿼리 그냥 사용할 수 있습니다, 단지 하나의 열이 특히 간결 것은 이상 최대화 될 때의 MAX(some_col)GROUP BY다른 열을하지만 경우에 당신 극대화 할 수있는 두 부분으로 수량이 여전히 사용하여 수행 할 수 있습니다 ORDER BY더하기 LIMIT 1대신 (Quassnoi가 수행 한대로) :

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

행 생성자 구문을 사용하면 WHERE (a, b, c) IN (subquery)필요한 말의 양이 줄어들 기 때문에 좋습니다.


3

이 문제에 대한 해키 솔루션이 있습니다. 한 지역의 각 숲에서 가장 큰 나무를 선택한다고 가정 해 보겠습니다.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

숲별로 나무를 그룹화하면 분류되지 않은 나무 목록이 있으며 가장 큰 나무를 찾아야합니다. 가장 먼저해야 할 일은 행을 크기별로 정렬하고 목록 중 첫 번째 행을 선택하는 것입니다. 비효율적으로 보일 수 있지만 수백만 개의 행이 있으면 JOIN의 및 WHERE조건 을 포함하는 솔루션보다 훨씬 빠릅니다 .

BTW, ORDER_BYfor array_agg는 Postgresql 9.0에 도입되었습니다.


오류가 있습니다. ORDER BY tree_size.size DESC를 작성해야합니다. 또한 작성자의 작업에 대한 코드는 다음과 같습니다. SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky 2014-08-21

2

Postgressql 9.5에는 DISTINCT ON이라는 새로운 옵션이 있습니다.

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

중복 행을 제거하고 ORDER BY 절에 정의 된 첫 번째 행만 남깁니다.

공식 문서 참조


1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

색인을 만들면 (usr_id, time_stamp, trans_id)이 쿼리가 크게 향상됩니다.

당신은 항상 PRIMARY KEY당신의 테이블에 어떤 종류의 것을 가지고 있어야 합니다.


0

여기에 한 가지 중요한 문제가 있다고 생각합니다. 주어진 행이 다른 행보다 늦게 발생했음을 보장하기 위해 단조롭게 증가하는 "카운터"가 없습니다. 이 예를 보자 :

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

이 데이터에서 가장 최근 항목을 확인할 수 없습니다. 두 번째입니까 아니면 마지막입니까? 정답을 제공하기 위해이 데이터에 적용 할 수있는 sort 또는 max () 함수가 없습니다.

타임 스탬프의 해상도를 높이는 것은 큰 도움이 될 것입니다. 데이터베이스 엔진은 요청을 직렬화하므로 충분한 해상도로 두 개의 타임 스탬프가 동일하지 않음을 보장 할 수 있습니다.

또는 매우 오랫동안 롤오버되지 않는 trans_id를 사용하십시오. 롤오버되는 trans_id가 있다는 것은 복잡한 수학을 수행하지 않는 한 trans_id 6이 trans_id 1보다 최신인지 (동일한 타임 스탬프에 대해) 알 수 없음을 의미합니다.


예, 이상적으로는 순서 (자동 증가) 열이 순서대로 정렬됩니다.
vladr

위의 가정은 작은 시간 증가의 경우 trans_id가 롤오버되지 않는다는 것입니다. 나는 테이블에 반복되지 않는 trans_id와 같은 고유 한 기본 인덱스가 필요하다는 데 동의합니다. (PS 내가 지금 코멘트에 충분 카르마 / 명성 포인트를 가지고 행복 해요!)
여호수아 베리

Vlad는 trans_id가 자주 바뀌는 다소 짧은주기를 가지고 있다고 말합니다. 내 테이블에서 중간 두 행 (trans_id = 6 및 1) 만 고려하더라도 가장 최근 행이 무엇인지 알 수 없습니다. 따라서 주어진 타임 스탬프에 대해 max (trans_id)를 사용하면 작동하지 않습니다.
Barry Brown

네, (time_stamp, trans_id) 튜플이 주어진 사용자에게 고유하다는 응용 프로그램 작성자의 보증에 의존하고 있습니다. 그렇지 않은 경우 "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..."는 "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM. .. WHERE ... GROUP BY l1.usr_id, ...
vladr

0

유용하다고 생각되는 또 다른 솔루션입니다.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.