SQL birleşimi: bire çok ilişkisindeki son kayıtları seçme


298

Bir müşteri tablom ve satın alma tablom olduğunu varsayalım. Her satın alma bir müşteriye aittir. Bir SELECT deyimiyle son satın alımlarıyla birlikte tüm müşterilerin bir listesini almak istiyorum. En iyi uygulama nedir? Endeks oluşturma konusunda tavsiyeleriniz var mı?

Lütfen cevabınızda şu tablo / sütun adlarını kullanın:

  • müşteri: kimlik, isim
  • satın alma: kimlik, müşteri_kimliği, öğe_kimliği, tarih

Ve daha karmaşık durumlarda, son satın alma işlemini müşteri masasına koyarak veritabanını denormalize etmek faydalı olur mu?

(Satın alma) kimliğinin tarihe göre sıralanması garanti edilirse, ifadeler benzer bir şey kullanılarak basitleştirilebilir LIMIT 1mi?


Evet, denormalize etmeye değer olabilir (performansı çok geliştirirse, yalnızca her iki sürümü de test ederek öğrenebilirsiniz). Ancak denormalizasyonun dezavantajları genellikle kaçınılmalıdır.
Vince Bowdren

Yanıtlar:


451

Bu bir örnektir greatest-n-per-group StackOverflow üzerinde düzenli olarak ortaya çıkan sorunun .

Genellikle bunu çözmeyi tavsiye ederim:

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;

Açıklama: Bir satır verildiğinde , aynı müşteriyle ve daha sonraki bir tarihle (veya daha sonraki bağlarda ) bir satır p1olmamalıdır . Bunun doğru olduğunu bulduğumuzda, o müşteri için en son satın alma işlemidir.p2idp1

Endeksler ile ilgili olarak, ben bir bileşik endeksi oluşturmak istiyorum purchasesütunlar üzerinde ( customer_id, date, id). Bu, dış birleştirmenin bir kaplama endeksi kullanılarak yapılmasına izin verebilir. Optimizasyon uygulamaya bağlı olduğundan platformunuzda test ettiğinizden emin olun. Optimizasyon planını analiz etmek için RDBMS'nizin özelliklerini kullanın. Örneğin EXPLAINMySQL üzerinde.


Bazı insanlar yukarıda gösterdiğim çözüm yerine alt sorgular kullanıyor, ancak çözümümün bağları çözmeyi kolaylaştırdığını düşünüyorum.


3
Genel olarak olumlu. Ancak bu, kullandığınız veritabanı markasına ve veritabanınızdaki verilerin miktarına ve dağıtımına bağlıdır. Kesin bir cevap almanın tek yolu, her iki çözümü de verilerinize karşı test etmenizdir.
Bill Karwin

27
Daha önce hiç satın alma işlemi gerçekleştirmemiş müşterileri dahil etmek istiyorsanız, JOIN satın alma p1 AÇIK (c.id = p1.customer_id) öğesini LEFT JOIN satın alma p1 ON (c.id = p1.customer_id) olarak değiştirin
GordonM,

5
@russds, bağı çözmek için kullanabileceğiniz benzersiz bir sütuna ihtiyacınız var. İlişkisel bir veritabanında iki özdeş satır olması mantıklı değildir.
Bill Karwin

6
"NEREDİ p2.id NULL NEDİR" amacı nedir?
clu

3
bu çözüm yalnızca 1'den fazla satın alma kaydı varsa çalışır. 1: 1 bağlantı varsa, işe yaramaz. orada "NEREDE (p2.id IS NULL veya p1.id = p2.id)
Bruno Jennrich

126

Bunu bir alt seçim kullanarak da deneyebilirsiniz

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

Seçim tüm müşterilere ve Son satın alma tarihlerine katılmalıdır .


4
Teşekkürler bu beni kurtardı - bu çözüm listelenen diğerlerinden daha güvenilir ve sürdürülebilir görünüyor + ürüne özgü değil
Daveo

Satın alma işlemi olmasa bile müşteri elde etmek istersem bunu nasıl değiştirebilirim?
clu

3
@clu: Değişim INNER JOINbir etmek LEFT OUTER JOIN.
Sasha Chedygov

3
Görünüşe göre, o gün sadece bir satın alma var. Eğer iki tane olsaydı, bir müşteri için iki çıkış satırı alırsınız, sanırım?
artfulrobot

1
@IstiaqueAhmed - son INNER JOIN bu Max (tarih) değerini alır ve onu kaynak tabloya bağlar. Bu birleştirme olmadan purchasetablodan alacağınız tek bilgi tarih ve customer_id olur, ancak sorgu tablodaki tüm alanları ister.
Vergil Gülüyor

26

Veritabanını belirtmediniz. Analitik fonksiyonlara izin verenlerden biri ise bu yaklaşımı kullanmak GROUP BY ile olandan daha hızlı olabilir (Oracle'da kesinlikle daha hızlı, geç SQL Server sürümlerinde büyük olasılıkla daha hızlı, başkalarını bilmiyorum).

SQL Server'daki sözdizimi şöyle olur:

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 ()" yerine "RANK ()" kullandığınız için bu sorunun cevabı yanlış. İki satın alma işlemi aynı tarihte olduğunda RANK size aynı bağlantı sorununu verecektir. Sıralama işlevi bunu yapar; ilk 2 eşleşmeye her ikisi de 1 değerini ve 3. kayıt 3 değerini alır. Row_Number ile beraberlik yoktur, tüm bölüm için benzersizdir.
MikeTeeVee

4
Bill Karwin'in Madalina'nın yaklaşımına karşı yaklaşımını denerken, 2008 sql sunucusu altında yürütme planları ile Bill Karwin'in yaklaşımının Madalina'nın% 57 kullanan yaklaşımının aksine% 43'lük bir sorgu maliyeti olduğunu gördüm - bu yüzden bu cevabın daha zarif sözdizimine rağmen, hala Bill'in versiyonunu tercih ederdi!
Shawson

26

Başka bir yaklaşım, NOT EXISTSdaha sonraki satın alımları test etmek için katılma koşulunuzdaki bir koşulu kullanmak olacaktır :

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
   )
)

Parçayı AND NOT EXISTSkolay kelimelerle açıklayabilir misiniz ?
Istiaque Ahmed

Alt seçim yalnızca daha yüksek kimliğe sahip bir satır olup olmadığını kontrol eder. Sonuç kümenizde yalnızca daha yüksek kimliğe sahip bir satır bulunmazsa bir satır alırsınız. Bu eşsiz en yüksek olan olmalı.
Stefan Haberl

2
Bu benim için en okunabilir çözüm. Bu önemliyse.
fguillen

:) Teşekkürler. Çünkü ben her zaman, en okunabilir çözümü için çaba olduğunu önemli.
Stefan Haberl

19

Ben bu konu benim sorunum için bir çözüm olarak bulundu.

Ama onları denediğimde performans düşüktü. Daha iyi performans için önerim körük.

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 

Umarım bu yardımcı olacaktır.


sadece 1 kullanılan top 1ve ordered it byMaxDate almak içindesc
Roshna Omer

1
bu kolay ve anlaşılır bir çözümdür, MY davasında (birçok müşteri, az sayıda satın alma) @Stefan Haberl'in çözümünden% 10 daha hızlı ve kabul edilen yanıttan 10 kat daha iyi
Juraj Bezručka

Bu sorunu çözmek için ortak tablo ifadeleri (CTE) kullanarak harika bir öneri. Bu, birçok durumda sorguların performansını önemli ölçüde geliştirmiştir.
AdamsTips

En iyi yanıt imo, okunması kolay MAX () cümlesi, ORDER BY + LIMIT 1
mrj

10

PostgreSQL kullanıyorsanız DISTINCT ONbir gruptaki ilk satırı bulmak için kullanabilirsiniz .

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 Dokümanları - Farklı Açık

DISTINCT ONBurada customer_id- alanların , alandaki en soldaki alanlarla eşleşmesi gerektiğini unutmayın.ORDER BY maddesi.

Uyarı: Bu standart dışı bir maddedir.


8

Bunu deneyin, yardımcı olacaktır.

Bunu projemde kullandım.

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" takma adı nereden geliyor?
TiagoA

Bu iyi performans değil .... burada diğer örnekler ben var veri kümesi üzerinde 2 saniye sürdü sonsuza aldı ....
Joel_J

3

SQLite üzerinde test edilmiştir:

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()Toplama işlevi son alım her gruptan seçilen (- vaka normalde olduğu ancak tarih kolonu) (maks son verir, burada bir formatta olduğunu varsayar) emin yapacaktır. Aynı tarihte satın alma işlemleri yapmak istiyorsanız,max(p.date, p.id) .

Dizinler açısından, satın alma işleminde bir dizin (müşteri_kimliği, tarih, [seçiminizde iade etmek istediğiniz diğer tüm satın alma sütunları]) ile kullanırım.

LEFT OUTER JOIN(Aksine INNER JOIN) emin bir mal veya hizmet satın asla müşteriler de dahil olduğunu yapacaktır.


t-
sql'de

1

Lütfen bunu deneyin,

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;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.