"가장 최근에 해당하는 행"을 효율적으로 얻는 방법은 무엇입니까?


53

매우 일반적인 쿼리 패턴이 있지만 효율적인 쿼리를 작성하는 방법을 모르겠습니다. 다른 테이블의 "행 이후"가장 최근 날짜에 해당하는 테이블의 행을 조회하려고합니다.

inventory예를 들어, 특정 날짜에 보유하고있는 재고를 나타내는 표가 있습니다.

date       | good | quantity
------------------------------
2013-08-09 | egg  | 5
2013-08-09 | pear | 7
2013-08-02 | egg  | 1
2013-08-02 | pear | 2

특정 일에 상품의 가격을 유지하는 "가격"이라고 말하는 표

date       | good | price
--------------------------
2013-08-07 | egg  | 120
2013-08-06 | pear | 200
2013-08-01 | egg  | 110
2013-07-30 | pear | 220

인벤토리 테이블의 각 행에 대해 "최신"가격을 효율적으로 얻을 수있는 방법 , 즉

date       | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07   | egg  | 5        | 120
2013-08-09 | 2013-08-06   | pear | 7        | 200
2013-08-02 | 2013-08-01   | egg  | 1        | 110
2013-08-02 | 2013-07-30   | pear | 2        | 220

나는 이것을하는 한 가지 방법을 알고있다.

select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good

이 쿼리를 다시 인벤토리에 결합하십시오 . 큰 테이블의 경우 첫 번째 쿼리 ( 재고 에 다시 조인하지 않음 )도 매우 느립니다. 그러나 프로그래밍 언어를 사용 하여 인벤토리 테이블에서 max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1각각에 대해 하나의 쿼리 를 발행하면 동일한 문제가 신속하게 해결 date_of_interest되므로 계산 장애가 없음을 알고 있습니다. 그러나 단일 SQL 쿼리로 전체 문제를 해결하는 것이 좋습니다. 쿼리 결과에 대해 추가 SQL 처리를 수행 할 수 있기 때문입니다.

이를 효율적으로 수행하는 표준 방법이 있습니까? 자주 와야하고 빠른 쿼리를 작성하는 방법이 있어야한다고 생각합니다.

Postgres를 사용하고 있지만 SQL 일반 답변을 주시면 감사하겠습니다.


3
효율성 문제이므로 DBA.SE로 마이그레이션하기로 결정했습니다. 우리는 몇 가지 다른 방법으로 쿼리를 작성할 수 있지만 훨씬 빠르지는 않습니다.
ypercubeᵀᴹ

5
단일 쿼리에서 하루 종일 모든 상품이 실제로 필요합니까? 가능성이없는 것 같습니까? 보다 일반적으로 특정 날짜의 가격 또는 특정 상품의 가격 (특정 날짜)을 검색합니다. 이러한 대안 쿼리는 (적절한) 인덱스로부터 훨씬 더 쉽게 혜택을 얻을 수 있습니다. 또한 카디널리티 (각 테이블에 몇 개의 행이 있습니까?), 전체 테이블 정의 등 을 알아야 합니다 . 데이터 유형, 제약 조건, 색인, ... ( \d tblpsql에서 사용 ), Postgres 버전 및 최소. / 최대 상품당 가격 수
Erwin Brandstetter

@ErwinBrandstetter 답변을 요청 하시겠습니까? 나는 당신이 가장 찬성하는 사람이 많으므로 기꺼이 받아 들일 수는 있지만 어떤 것이 가장 좋은지 알 수는 없습니다.
Tom Ellis

그것이 귀하의 질문에 대답하거나 귀하에게 효과가있는 경우에만 수락하십시오. 관련 사례에 도움이 될 경우 진행 방법에 대한 의견을 남길 수도 있습니다 . 질문에 대한 답변이 없다고 생각되면 알려주십시오.
Erwin Brandstetter

1
나는 훌륭한 답변으로 보이는 것을 받았지만 더 이상 질문을 일으킨 문제에 대해 노력하고 있지 않으므로 가장 적합한 답변인지 또는 실제로 그 중 하나라도 판단 할 수있는 곳이 아니기 때문에 사과해야합니다. 내 유스 케이스에 실제로 적합합니다 (있는 그대로). DBA.Stackexchange 에티켓이 있으면이 경우 따라야합니다.
Tom Ellis

답변:


42

그것은 매우 의존 상황과 정확한 요구 사항에. 질문에 대한 나의 의견을 고려하십시오 .

간단한 솔루션

DISTINCT ONPostgres 와 함께 :

SELECT DISTINCT ON (i.good, i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good, i.the_date, p.the_date DESC;

주문 결과.

또는 NOT EXISTS표준 SQL에서 (내가 알고있는 모든 RDBMS에서 작동) :

SELECT i.the_date, p.the_date AS pricing_date, i.good, i.quantity, p.price
FROM   inventory  i
LEFT   JOIN price p ON p.good = i.good AND p.the_date <= i.the_date
WHERE  NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good = p.good
   AND p1.the_date <= i.the_date
   AND p1.the_date >  p.the_date
   );

추가하지 않는 한 동일한 결과이지만 임의의 정렬 순서가 있습니다 ORDER BY.
데이터 배포, 정확한 요구 사항 및 지수에 따라 이들 중 하나가 더 빠를 수 있습니다.
일반적으로 DISTINCT ON승자이며 그 위에 정렬 된 결과를 얻습니다. 그러나 어떤 경우에는 다른 쿼리 기술이 훨씬 빠릅니다. 아래를 참조하십시오.

최대 / 최소값을 계산하기위한 하위 쿼리가있는 솔루션은 일반적으로 느립니다. CTE가있는 변형은 일반적으로 더 느립니다.

일반 답변 (다른 답변에서 제안한 것처럼)은 Postgres의 성능에 전혀 도움이되지 않습니다.

SQL 바이올린.


적절한 솔루션

문자열과 콜 레이션

우선, 최적이 아닌 테이블 레이아웃으로 어려움을 겪습니다. 사소한 것처럼 보이지만 스키마 정규화는 먼 길을 갈 수 있습니다.

로케일 , 특히 COLLATION 에 따라 문자 유형 ( text,, varchar...)으로 정렬 해야합니다 . 대부분의 DB는 로컬 규칙 세트를 사용합니다 (예 :) . 다음으로 알아보십시오.de_AT.UTF-8

SHOW lc_collate;

정렬 및 인덱스 조회 속도가 느려집니다 . 줄 (상품명)이 길수록 나빠집니다. 실제로 출력 (또는 정렬 순서)에서 데이터 정렬 규칙을 신경 쓰지 않으면 다음을 추가하면 더 빠를 수 있습니다 COLLATE "C".

SELECT DISTINCT ON (i.good COLLATE "C", i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good COLLATE "C", i.the_date, p.the_date DESC;

콜 레이션을 두 곳에 어떻게 추가했는지 주목하십시오.
테스트에서 각각 20k 개의 행과 매우 기본적인 이름 ( 'good123')으로 두 번 빠릅니다.

인덱스

쿼리에서 인덱스를 사용해야하는 경우 문자 데이터가있는 열은 일치하는 데이터 정렬을 사용해야합니다 ( good예제에서).

CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);

SO에 대한이 관련 답변의 마지막 두 장을 읽으십시오.

다른 쿼리에서 다른 (또는 기본) 데이터 정렬에 따라 정렬 된 상품이 필요한 경우 동일한 열에서 다른 데이터 정렬을 가진 여러 인덱스를 가질 수도 있습니다.

정규화

중복 문자열 (좋은 이름)도 부풀게 도 느린 모든 것을 만드는 당신의 테이블과 인덱스를. 적절한 테이블 레이아웃을 사용하면 대부분의 문제를 피할 수 있습니다. 다음과 같이 보일 수 있습니다 :

CREATE TABLE good (
  good_id serial PRIMARY KEY
, good    text   NOT NULL
);

CREATE TABLE inventory (
  good_id  int  REFERENCES good (good_id)
, the_date date NOT NULL
, quantity int  NOT NULL
, PRIMARY KEY(good_id, the_date)
);

CREATE TABLE price (
  good_id  int     REFERENCES good (good_id)
, the_date date    NOT NULL
, price    numeric NOT NULL
, PRIMARY KEY(good_id, the_date));

기본 키는 필요한 거의 모든 인덱스를 자동으로 제공합니다.
누락 된 세부 사항 에 따라 두 번째 컬럼에서 내림차순 으로 다중 컬럼 인덱스price성능을 향상시킬 수 있습니다.

CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);

다시, 데이터 정렬 (위 참조) 쿼리와 일치해야합니다.

Postgres 9.2 이상 에서 인덱스 전용 스캔을위한 "커버 인덱스"는 특히 테이블에 추가 열이있어 테이블이 커버링 인덱스보다 훨씬 큰 경우에 도움이 될 수 있습니다.

이 결과 쿼리는 훨씬 빠릅니다.

존재하지 않음

SELECT i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
AND    NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good_id = p.good_id
   AND    p1.the_date <= i.the_date
   AND    p1.the_date >  p.the_date
   );

ON ON

SELECT DISTINCT ON (i.the_date)
       i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
ORDER  BY i.the_date, p.the_date DESC;

SQL 바이올린.


빠른 솔루션

그래도 여전히 빠르지 않으면 더 빠른 솔루션이있을 수 있습니다.

재귀 CTE / JOIN LATERAL/ 상관 하위 쿼리

특히 상품당 가격많은 데이터 배포의 경우 :

구체화 된 뷰

이것을 자주 그리고 빠르게 실행해야하는 경우 구체화 된보기를 작성하는 것이 좋습니다. 과거 날짜의 가격과 재고는 거의 변하지 않는다고 가정하는 것이 안전하다고 생각합니다. 결과를 한 번 계산하고 스냅 샷을 구체화 된보기로 저장하십시오.

Postgres 9.3+는 구체화 된 뷰를 자동으로 지원합니다. 이전 버전에서는 기본 버전을 쉽게 구현할 수 있습니다.


3
price_good_date_desc_idx귀하가 권장 하는 색인은 유사한 광산 쿼리에 대한 성능을 크게 향상 시켰습니다. 내 쿼리 계획은 42374.01..42374.86다운 비용 에서 0.00..37.12!
cimmanon

@cimmanon : 니스! 핵심 검색어 기능은 무엇입니까? 존재하지 않습니까? 계속 하시겠습니까? GROUP BY?
Erwin Brandstetter

DISTINCT ON 사용
cimmanon

6

참고로, mssql 2008을 사용 했으므로 Postgres에는 "include"인덱스가 없습니다. 그러나 아래에 표시된 기본 색인을 사용하면 Postgres의 해시 조인에서 병합 조인으로 변경됩니다. http://explain.depesz.com/s/eF6 (색인 없음) http://explain.depesz.com/s/j9x ( 조인 기준에 색인이있는 경우)

쿼리를 두 부분으로 나눌 것을 제안합니다. 먼저, 재고 날짜와 가격 책정 날짜의 관계를 나타내는 다양한 다른 컨텍스트에서 사용할 수 있는보기 (성능을 향상시키려는 것이 아님) 입니다.

create view mostrecent_pricing_dates_per_good as
select i.good,i.date i_date,max(p.date)p_date
  from inventory i
  join price p on i.good = p.good and i.date >= p.date
 group by i.good,i.date;

그런 다음 문의가있는 경우 쿼리가 다른 종류 (예 : 최근 가격 날짜없이 인벤토리를 찾기 위해 왼쪽 조인을 사용하는 경우)를보다 간단하고 쉽게 조작 할 수 있습니다.

select i.good
       ,i.date inventory_date
       ,i.quantity
       ,p.date pricing_date
       ,p.price       
  from inventory i
  join price p on i.good = p.good
  join mostrecent_pricing_dates_per_good x 
    on i.good = x.good 
   and p.date = x.p_date
   and i.date = x.i_date

다음과 같은 실행 계획이 생성됩니다. http://sqlfiddle.com/#!3/24f23/1 인덱싱 없음

... 모든 종류의 스캔. 해시 일치의 성능 비용은 총 비용의 대부분을 차지합니다 ... 그리고 테이블 스캔 및 정렬이 느립니다 (목표와 비교 : 인덱스 탐색).

이제 조인에 사용 된 기준을 돕기 위해 기본 색인을 추가하십시오 (최적의 색인이라고 주장하지는 않지만 요점을 설명합니다) : http://sqlfiddle.com/#!3/5ec75/1 기본 색인 생성

이것은 개선을 보여줍니다. 중첩 루프 (내부 조인) 작업은 더 이상 쿼리에 관련된 총 비용을 차지하지 않습니다. 나머지 비용은 이제 인덱스 검색 (모든 재고 행을 가져 오기 때문에 재고 스캔)으로 분산됩니다. 그러나 쿼리가 수량과 가격을 가져 오기 때문에 여전히 더 잘할 수 있습니다. 해당 데이터를 얻으려면 결합 기준을 평가 한 후 조회를 수행해야합니다.

최종 반복은 인덱스에서 "include"를 사용하여 계획을 쉽게 슬라이드하고 인덱스 자체에서 추가 요청 된 데이터를 쉽게 가져올 수 있습니다. 따라서 조회가 사라졌습니다 : http://sqlfiddle.com/#!3/5f143/1 여기에 이미지 설명을 입력하십시오

이제 쿼리의 총 비용이 매우 빠른 인덱스 검색 작업에 균등하게 분산되는 쿼리 계획이 있습니다. 이것은 get-as-it-gets에 가깝습니다. 분명히 다른 전문가들이이를 더욱 개선 할 수 있지만이 솔루션은 몇 가지 주요 관심사를 해결합니다.

  1. 데이터베이스의 이해하기 쉬운 데이터 구조를 만들어 응용 프로그램의 다른 영역에서 작성 및 재사용하기가 더 쉽습니다.
  2. 가장 비싼 쿼리 연산자는 모두 기본 인덱싱을 사용하여 쿼리 계획에서 제외되었습니다.

3
이것은 훌륭하지만 (SQL-Server의 경우) 유사하지만 다른 DBMS에 대해 최적화하면 심각한 차이점도 있습니다.
ypercubeᵀᴹ

@ypercube는 사실입니다. Postgres에 대한 자격을 추가했습니다. 필자의 의도는 여기에 설명 된 대부분의 사고 과정이 DBMS 관련 기능에 관계없이 적용된다는 것입니다.
cocogorilla

답은 매우 깊기 때문에 시험해 보는 데 약간의 시간이 걸립니다. 내가 어떻게되는지 알려 줄게
Tom Ellis

5

PostgreSQL 9.3 (오늘 출시)이 발생하면 LATERAL JOIN을 사용할 수 있습니다.

나는 이것을 테스트 할 방법이 없으며 이전에 사용한 적이 없지만 문서 에서 알 수 있는 구문은 다음과 같습니다.

SELECT  Inventory.Date,
        Inventory.Good,
        Inventory.Quantity,
        Price.Date,
        Price.Price
FROM    Inventory
        LATERAL
        (   SELECT  Date, Price
            FROM    Price
            WHERE   Price.Good = Inventory.Good
            AND     Price.Date <= Inventory.Date
            ORDER BY Price.Date DESC
            LIMIT 1
        ) p;

이 중 기본적으로 동일합니다 SQL은-서버의이 적용 하고,이 SQL-바이올린에 이것의 작동 예 데모 목적은.


5

Erwin과 다른 사람들이 지적했듯이 효율적인 쿼리는 많은 변수에 의존하며 PostgreSQL은 이러한 변수를 기반으로 쿼리 실행을 최적화하기 위해 매우 열심히 노력합니다. 일반적으로 명확성을 위해 먼저 작성한 다음 병목 현상을 식별 한 후에 성능을 수정 하려고합니다 .

또한 PostgreSQL에는 작업을 좀 더 효율적으로 만들기 위해 사용할 수있는 많은 트릭이 있습니다 (부분 인덱스). 읽기 / 쓰기로드에 따라 신중하게 인덱싱을 조사하여이를 최적화 할 수 있습니다.

가장 먼저 시도하는 것은보기를하고 참여하는 것입니다.

CREATE VIEW most_recent_rows AS
SELECT good, max(date) as max_date
FROM inventory
GROUP BY good;

이것은 다음과 같은 일을 할 때 잘 수행되어야합니다.

SELECT price 
  FROM inventory i
  JOIN goods g ON i.goods = g.description
  JOIN most_recent_rows r ON i.goods = r.goods
 WHERE g.id = 123;

그런 다음 참여할 수 있습니다. 쿼리는 기본 테이블에 대해 뷰를 조인하지만 결국 (date, good 순서대로 ) 고유 인덱스가 있다고 가정하면 갈 수 있어야합니다 (간단한 캐시 조회이기 때문에). 이것은 몇 개의 행을 찾은 경우에는 잘 작동하지만 수백만 달러의 제품 가격을 소화하려고하면 매우 비효율적입니다.

두 번째로 할 수있는 것은 인벤토리 테이블에 most_recent bool 열을 추가하고

create unique index on inventory (good) where most_recent;

그런 다음 상품에 대한 새 행을 삽입 할 때 트리거를 사용하여 most_recent를 false로 설정하려고합니다. 이렇게하면 더 복잡하고 버그가 발생할 가능성이 높아지지만 도움이됩니다.

다시 말하지만이 중 많은 부분이 적절한 인덱스가 있어야합니다. 가장 최근의 날짜 쿼리의 경우 날짜에 대한 색인이 있어야하며 날짜로 시작하고 조인 기준을 포함하는 여러 열이있을 수 있습니다.

아래 Per Erwin의 의견을 업데이트 하면이 사실을 오해 한 것 같습니다. 질문을 다시 읽으면서 나는 무엇을 요구하는지 전혀 확신하지 못합니다. 업데이트에서 내가 볼 수있는 잠재적 인 문제와 이것이 왜 이것이 명확하지 않은지 언급하고 싶습니다.

제공되는 데이터베이스 설계는 ERP 및 회계 시스템에서 실제 IME를 사용하지 않습니다. 특정 제품의 특정 일에 판매 된 모든 제품의 가격이 동일한 가상의 완벽한 가격 모델로 작동합니다. 그러나 항상 그런 것은 아닙니다. 환전과 같은 경우조차 해당되지 않습니다 (일부 모델에서는 그렇게하는 척). 이것이 좋은 예라면 불분명합니다. 실제 사례라면 데이터 수준의 설계에 더 큰 문제가 있습니다. 여기서는 이것이 실제 예라고 가정하겠습니다.

당신은 할 수없는 그 날 혼자 주어진 이익을 위해 가격을 지정 가정합니다. 모든 비즈니스의 가격은 거래 당 또는 때로는 거래 당 협상 될 수 있습니다. 이러한 이유로 실제로 재고를 처리하는 테이블 (재고 테이블)에 가격을 저장 해야합니다 . 이 경우, 날짜 / 상품 / 가격표는 기본 가격을 지정하며 협상에 따라 변경 될 수 있습니다. 이 경우이 문제는보고 문제에서 한 번에 각 테이블의 한 행씩 트랜잭션으로 작동하는 문제가됩니다. 예를 들어, 주어진 날짜에 특정 제품의 기본 가격을 다음과 같이 검색 할 수 있습니다.

 SELECT price 
   FROM prices p
   JOIN goods g ON p.good = g.good
  WHERE g.id = 123 AND p."date" >= '2013-03-01'
  ORDER BY p."date" ASC LIMIT 1;

가격 (좋은 날짜)에 대한 인덱스를 사용하면 성능이 좋아집니다.

나는 이것이 당신이 작업하고있는 것에 더 가까운 무언가가 도움이 될 수있는 고안된 예이다.


most_recent접근 방식은 가장 최근 가격에 완벽하게 적용 됩니다. 그러나 OP에는 각 재고 날짜와 관련 하여 가장 최근 가격이 필요한 것 같습니다 .
Erwin Brandstetter

좋은 지적. 제안 된 데이터로 실제적인 결함이 있음을 다시 읽었지만 그것이 단지 예의가 된 것인지 알 수는 없습니다. 고안된 예로서, 무엇이 빠졌는지 알 수 없습니다. 아마도 이것을 지적하기위한 업데이트도 순서가있을 것입니다.
Chris Travers

@ChrisTravers : 이것은 좋은 예이지만, 내가 작업하고있는 실제 스키마를 게시 할 자유는 없습니다. 아마도 당신은 당신이 발견 한 실제적인 결함에 대해 조금 말할 수있을 것입니다.
Tom Ellis

나는 그것이 정확해야한다고 생각하지 않지만 우화에서 잃어버린 문제에 대해 걱정했습니다. 조금 더 가까운 것이 도움이 될 것입니다. 문제는 가격 책정을 사용하면 특정 날짜의 가격이 기본값이 될 가능성이 높으므로 결과적으로 거래 항목의 기본값으로보고하는 데 사용하지 않으므로 흥미로운 쿼리는 일반적으로 시각.
Chris Travers

3

다른 방법은 창 함수를 사용 lead()하여 테이블 가격의 모든 행에 대한 날짜 범위를 얻은 다음 between인벤토리에 참여할 때 사용하는 것 입니다. 나는 실제로 이것을 실제 생활에서 사용했지만 주로 이것이 이것을 해결하는 첫 번째 아이디어 였기 때문에 주로 사용되었습니다.

with cte as (
  select
    good,
    price,
    date,
    coalesce(lead(date) over(partition by good order by date) - 1
            ,Now()::date) as ndate
  from
    price
)

select * from inventory i join cte on
  (i.good = cte.good and i.date between cte.date and cte.ndate)

SqlFiddle


1

가격 표의 레코드를 재고 날짜 또는 그 이전 날짜로 제한하는 결합 조건으로 재고에서 가격으로 결합을 사용한 다음 최대 날짜를 추출하고 날짜가 해당 서브 세트에서 가장 높은 날짜를 추출하십시오.

재고 가격은 다음과 같습니다.

 Select i.date, p.Date pricingDate,
    i.good, quantity, price        
 from inventory I join price p 
    on p.good = i.good
        And p.Date = 
           (Select Max(Date from price
            where good = i.good
               and date <= i.Date)

지정된 상품에 대한 가격이 같은 날에 두 번 이상 변경되어 실제로이 열에 날짜와 시간이없는 경우, 가격 변경 레코드 중 하나만 선택하기 위해 조인에 더 많은 제한 사항을 적용해야 할 수도 있습니다.


불행히도 속도를 높이 지 않는 것 같습니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.