SQL 조인 : 일대 다 관계에서 마지막 레코드 선택


298

고객 테이블과 구매 테이블이 있다고 가정합니다. 각 구매는 하나의 고객에 속합니다. 하나의 SELECT 문에서 마지막 구매와 함께 모든 고객 목록을 얻고 싶습니다. 모범 사례는 무엇입니까? 인덱스 작성에 대한 조언이 있습니까?

답에 다음 표 / 열 이름을 사용하십시오.

  • 고객 : 아이디, 이름
  • 구매 : id, customer_id, item_id, 날짜

더 복잡한 상황에서 마지막 구매를 고객 테이블에 넣어 데이터베이스를 비정규 화하는 것이 (성능 측면에서) 유리합니까?

(구매) id가 날짜순으로 정렬되는 것이 보장된다면 LIMIT 1?


예, 비정규화할 가치가 있습니다 (성능이 많이 향상되는 경우 두 버전을 모두 테스트하여 확인할 수 있음). 그러나 비정규 화의 단점은 대개 피할 가치가 있습니다.
빈스 Bowdren

답변:


449

이것은 greatest-n-per-groupStackOverflow에서 정기적으로 나타나는 문제 의 예입니다 .

일반적으로 해결하는 것이 좋습니다.

SELECT c.*, p1.*
FROM customer c
JOIN purchase p1 ON (c.id = p1.customer_id)
LEFT OUTER JOIN purchase p2 ON (c.id = p2.customer_id AND 
    (p1.date < p2.date OR (p1.date = p2.date AND p1.id < p2.id)))
WHERE p2.id IS NULL;

설명 : 행이 지정된 경우 동일한 고객 및 이후 날짜를 가진 p1행이 없어야합니다 p2(또는 관계가있는 경우 나중 id). 이 사실을 발견하면 p1해당 고객에 대한 가장 최근 구매입니다.

인덱스에 대해서, 나는에 복합 인덱스를 만들 것 purchase열 이상 ( customer_id, date, id). 이는 외부 색인이 포함 색인을 사용하여 수행되도록 할 수 있습니다. 최적화는 구현에 따라 다르므로 플랫폼에서 테스트해야합니다. RDBMS의 기능을 사용하여 최적화 계획을 분석하십시오. 예 EXPLAIN를 들어 MySQL에서.


어떤 사람들은 위에서 보여준 솔루션 대신 하위 쿼리를 사용하지만 솔루션을 통해 관계를 쉽게 해결할 수 있습니다.


3
유리하게는 일반적으로. 그러나 이는 사용하는 데이터베이스 브랜드와 데이터베이스의 데이터 수량 및 배포에 따라 다릅니다. 정확한 답변을 얻는 유일한 방법은 데이터에 대해 두 솔루션을 모두 테스트하는 것입니다.
Bill Karwin

27
구매 한 적이없는 고객을 포함하려면 구매 p1 ON (c.id = p1.customer_id) JOIN 구매 p1 ON (c.id = p1.customer_id)
GordonM

5
@russds, 넥타이를 해결하는 데 사용할 수있는 고유 한 열이 필요합니다. 관계형 데이터베이스에서 두 개의 동일한 행을 갖는 것은 의미가 없습니다.
Bill Karwin 23시 26 분

6
"WHERE p2.id IS NULL"의 목적은 무엇입니까?
clu

3
이 솔루션은 둘 이상의 구매 레코드가있는 경우에만 작동합니다. 1 : 1 링크가 없으면 작동하지 않습니다. "WHERE (p2.id IS NULL 또는 p1.id = p2.id) 여야합니다.
Bruno Jennrich

126

하위 선택을 사용 하여이 작업을 시도 할 수도 있습니다

SELECT  c.*, p.*
FROM    customer c INNER JOIN
        (
            SELECT  customer_id,
                    MAX(date) MaxDate
            FROM    purchase
            GROUP BY customer_id
        ) MaxDates ON c.id = MaxDates.customer_id INNER JOIN
        purchase p ON   MaxDates.customer_id = p.customer_id
                    AND MaxDates.MaxDate = p.date

선택은 모든 고객과 마지막 구매 날짜 에 참여해야 합니다.


4
감사합니다. 방금 저를 구했습니다.이 솔루션은 더 재사용 가능하고 유지 관리가 용이 ​​한 것 같습니다. 목록에있는 다른 것들은 + 제품별로 다릅니다.
Daveo

구매가 없더라도 고객을 확보하려면 어떻게해야합니까?
clu

3
@clu : 변경 INNER JOINA와 LEFT OUTER JOIN.
Sasha Chedygov

3
이 날짜에 구매가 하나만 있다고 가정합니다. 두 개가 있다면 한 고객에 대해 두 개의 출력 행을 얻게 될 것입니다.
artfulrobot 2016 년

1
@IstiaqueAhmed-마지막 INNER JOIN은 Max (date) 값을 가져 와서 소스 테이블에 다시 연결합니다. 해당 조인이 없으면 purchase테이블 에서 얻을 수있는 정보 는 날짜와 customer_id 뿐이지 만 쿼리는 테이블의 모든 필드를 요청합니다.
웃음 Vergil

26

데이터베이스를 지정하지 않았습니다. 분석 기능을 허용하는 것이면 GROUP BY보다이 방법을 사용하는 것이 더 빠를 수 있습니다 (Oracle에서는 확실히 더 빠르며, SQL Server 최신 버전에서는 더 빠를 수 있습니다).

SQL Server의 구문은 다음과 같습니다.

SELECT c.*, p.*
FROM customer c INNER JOIN 
     (SELECT RANK() OVER (PARTITION BY customer_id ORDER BY date DESC) r, *
             FROM purchase) p
ON (c.id = p.customer_id)
WHERE p.r = 1

10
"ROW_NUMBER ()"대신 "RANK ()"를 사용하고 있기 때문에 질문에 대한 정답이 아닙니다. RANK는 두 번의 구매 날짜가 정확히 동일한 경우에도 동일한 문제를 제공합니다. 이것이 순위 함수의 기능입니다. 상위 2 개 일치하는 경우 둘 다 1의 값이 지정되고 3 번째 레코드의 값은 3이됩니다. Row_Number를 사용하면 동점이 없으며 전체 파티션에 고유합니다.
MikeTeeVee 2019 년

4
SQL Server 2008에서 실행 계획을 사용하여 Madalina의 접근 방식에 대한 Bill Karwin의 접근 방식을 시도해 보니 Bill Karwin의 접근 방식이 57 %를 사용하는 Madalina의 접근 방식과 달리 쿼리 비용이 43 % 인 것으로 나타났습니다. 여전히 Bill 버전을 선호합니다!
Shawson

26

또 다른 방법은 NOT EXISTS가입 조건에 조건을 사용하여 이후 구매를 테스트하는 것입니다.

SELECT *
FROM customer c
LEFT JOIN purchase p ON (
       c.id = p.customer_id
   AND NOT EXISTS (
     SELECT 1 FROM purchase p1
     WHERE p1.customer_id = c.id
     AND p1.id > p.id
   )
)

AND NOT EXISTS쉬운 말로 그 부분 을 설명 할 수 있습니까 ?
Istiaque Ahmed

하위 선택은 더 높은 ID를 가진 행이 있는지 확인합니다. 더 높은 ID를 가진 것이 없으면 결과 집합에 행만 표시됩니다. 그것은 유일한 가장 높은 것이어야합니다.
Stefan Haberl

2
이것은 나를 위해 가장 읽기 쉬운 솔루션입니다. 이것이 중요하다면.
fguillen

:) 감사. 그 때문에 나는 항상 가장 읽을 수있는 솔루션을 위해 노력 입니다 중요합니다.
Stefan Haberl

19

이 스레드를 내 문제에 대한 해결책으로 찾았습니다.

그러나 내가 시도했을 때 성능이 떨어졌습니다. Bellow는 더 나은 성능을위한 제안입니다.

With MaxDates as (
SELECT  customer_id,
                MAX(date) MaxDate
        FROM    purchase
        GROUP BY customer_id
)

SELECT  c.*, M.*
FROM    customer c INNER JOIN
        MaxDates as M ON c.id = M.customer_id 

이것이 도움이 되길 바랍니다.


내가 사용 top 1하고 ordered it byMaxDatedesc
Roshna Omer

1
이 MY 경우 쉽고 간단 솔루션, (많은 고객, 몇 구매) 10 % 빠른 다음 @Stefan Haberl의 솔루션 및 허용 대답보다 10 배 이상이다
유라이 Bezručka

이 문제를 해결하기 위해 공통 테이블 표현식 (CTE)을 사용하는 것이 좋습니다. 많은 상황에서 쿼리 성능이 크게 향상되었습니다.
AdamsTips

MAX () 절은 ORDER BY + LIMIT 1
mrj

10

PostgreSQL을 사용 DISTINCT ON하는 경우 그룹에서 첫 번째 행을 찾는 데 사용할 수 있습니다 .

SELECT customer.*, purchase.*
FROM customer
JOIN (
   SELECT DISTINCT ON (customer_id) *
   FROM purchase
   ORDER BY customer_id, date DESC
) purchase ON purchase.customer_id = customer.id

PostgreSQL 문서-구별

참고 그 DISTINCT ON필드 (들) - 여기 customer_id-에서 가장 왼쪽 필드 (들)과 일치해야합니다 ORDER BY절.

주의 사항 : 이것은 비표준 조항입니다.


8

이것을 시도하십시오, 그것은 도움이 될 것입니다.

나는 이것을 내 프로젝트에서 사용했다.

SELECT 
*
FROM
customer c
OUTER APPLY(SELECT top 1 * FROM purchase pi 
WHERE pi.customer_id = c.Id order by pi.Id desc) AS [LastPurchasePrice]

별명 "p"는 어디에서 오는가?
TiagoA

이것은 잘 수행되지 않습니다 .... 내가 가지고있는 데이터 세트에서 여기에 다른 예제가 2 초 걸린 곳에 영원히 걸렸습니다 ....
Joel_J

3

SQLite에서 테스트되었습니다.

SELECT c.*, p.*, max(p.date)
FROM customer c
LEFT OUTER JOIN purchase p
ON c.id = p.customer_id
GROUP BY c.id

max()집계 함수는 최신 구매가 각 그룹에서 선택 (- 경우 일반적이다하지만 날짜 열이) (최대 최신 제공함으로써 형식으로되어 있다고 가정)되어 있는지 확인합니다. 같은 날짜의 구매를 처리하려면을 사용할 수 있습니다 max(p.date, p.id).

인덱스 측면에서 (customer_id, date, [선택한 항목으로 반환하려는 다른 구매 열])과 함께 구매시 인덱스를 사용합니다.

LEFT OUTER JOIN(반대 INNER JOIN) 확인하여 구매 한 적이없는 고객도 포함되어 있는지 확인합니다.


select c. *에 group by 절에없는 열이 있으므로 t-sql에서 실행되지 않음
Joel_J

1

이것을 시도하십시오

SELECT 
c.Id,
c.name,
(SELECT pi.price FROM purchase pi WHERE pi.Id = MAX(p.Id)) AS [LastPurchasePrice]
FROM customer c INNER JOIN purchase p 
ON c.Id = p.customerId 
GROUP BY c.Id,c.name;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.