2016-03-30 7 views
0

Ich versuche zu bestimmen, wie viele Menschen ein Paar Waren aus allen verschiedenen Paaren von Produkten gekauft haben, die existieren können. Z.B. Ich habe drei Produkte, A, B, C und ich möchte bestimmen, wie viele% der Kunden gekauft A und B, B und C und A und C von der Anzahl der Kunden, die jedes Produkt innerhalb jedes Paares nach Land besitzen.Prozentsatz der Cross Produkt Kauf

Meine Tabelle sieht wie folgt aus.

Customer | Country | Product 
1  | US | A 
1  | US | B 
2  | CA | A 
2  | CA | C 
3  | US | A 
3  | US | C 
4  | US | B 
5  | US | A 

Beachten Sie, dass ein Kunde nur zu einem Land gehören kann.

My gewünschte Ausgabe ist:

Country | Pair | % 
US  | A_B | 25%  
US  | B_C | 0% 
US  | A_C | 33% 
CA  | A_B | 0%  
CA  | B_C | 0% 
CA  | A_C | 100% 

Das% im Wesentlichen das Verhältnis

(# of unique customers who bought Product1 and Product2)/ 
(# of unique customers who bought Product1 or Product2) 

nach Land unterschiedlich.

So zum Beispiel in den USA für A_B haben wir 4 Kunden, die A oder B gekauft, aber nur 1 davon ist 1/4 sowohl A und B so das Verhältnis gekauft.

Gibt es eine schöne Lösung, die skalieren würde, wenn ich eine große Anzahl, beliebig viele Paare hätte?

+0

Sollte der US A_C-Wert nicht 33%, nicht 25% betragen? Drei Kunden (1, 3, 5) kauften entweder A oder C, und einer von ihnen (3) kaufte beide. – APH

+0

@APH du bist richtig. Es sollte 33% sein – Black

Antwort

2

Iterative Abfrage Entwicklung ...

Wenn Sie keinen product Tisch haben, und nur haben die customer_country_product Tabelle, können Sie einen Inline-View verwenden, um eine eindeutige Liste von Produkten für jedes Land zu schaffen .

Um Produkte nach Land ...

SELECT ccp.product_id 
     , ccp.country_id 
    FROM customer_country_product ccp 
    GROUP 
     BY ccp.product_id 
     , ccp.country_id 

Wir diese Abfrage als rowsource verwenden können, indem sie eine Inline-Ansicht zu machen. Umbrechen Sie diese Abfrage in Parens, weisen Sie einen Alias ​​zu und verweisen Sie in der FROM-Klausel einer anderen Abfrage darauf. Um "Paare" von Produkten zu erhalten, können wir die Inline-Ansicht mit sich selbst verbinden (Vermeidung von Paaren desselben Produkts (A_A) und Vermeidung von doppelten Paaren) (nur eines von A_C und C_A zurückgeben).

SELECT a.country_id 
     , a.product_id AS a_product_id 
     , b.product_id AS b_product_id 
    FROM (SELECT ccpa.product_id 
       , ccpa.country_id 
      FROM customer_country_product ccpa 
      GROUP 
       BY ccpa.product_id 
       , ccpa.country_id 
     ) a 
    JOIN (SELECT ccpb.product_id 
       , ccpb.country_id 
      FROM customer_country_product ccpb 
      GROUP 
       BY ccpb.product_id 
       , ccpb.country_id 
     ) b 
     ON b.country_id = a.country_id 
    AND b.product_id > a.product_id 
    ORDER 
     BY a.country_id 
     , a.product_id 
     , b.product_id 

Das sollte Ihnen alle Produkt "Paare" für jedes Land erhalten. HINWEIS: Dadurch werden Produkte ausgeschlossen, wenn kein Kunde das Produkt besitzt. Wenn wir alle möglichen Produktpaare wollen, für jedes Land, würden wir anders, dass ein wenig schreiben müssen ...

SELECT c.country_id 
     , a.product_id AS a_product_id 
     , b.product_id AS b_product_id 
    FROM (SELECT ccpa.product_id 
      FROM customer_country_product ccpa 
      GROUP BY ccpa.product_id 
     ) a 
    JOIN (SELECT ccpb.product_id 
      FROM customer_country_product ccpb 
      GROUP BY ccpb.product_id 
     ) b 
     ON b.product_id > a.product_id 
    CROSS 
    JOIN (SELECT ccpc.country_id 
      FROM customer_country_product ccpc 
      GROUP BY ccpc.country_id 
     ) c 
    ORDER 
     BY c.country_id 
     , a.product_id 
     , b.product_id 

Wenn Sie product und country Tabellen haben, können Sie die Inline-Ansichten in den Abfragen ersetzen könnte oben mit Verweisen auf diese Tabellen.

Um die "Zählungen" des Kunden zu erhalten, könnten wir entweder korrelierte Unterabfragen in der SELECT-Liste verwenden, oder wir können Join-Operationen und Aggregate in der SELECT-Liste durchführen. (Mit dem beitritt, wenn wir nicht aufpassen, dann ist es ein Potential zu generieren und zählen „Duplikate“.)

Um eine Zählung der verschiedenen Kunden in einem bestimmten Land zu bekommen, die ein bestimmtes Produkt hat

SELECT COUNT(DISTINCT ccp.customer_id) AS cnt_cust 
    FROM customer_country_product ccp 
WHERE ccp.country_id = ? 
    AND ccp.product_id = ? 

Um eine Zählung von verschiedenen Kunden aus einem bestimmten Land zu erhalten, die mindestens eine von zwei bestimmten Produkten

SELECT COUNT(DISTINCT ccp.customer_id) AS cnt_cust_have_either 
    FROM customer_country_product ccp 
WHERE ccp.country_id = ? 
    AND ccp.product_id IN (? , ?) 

hat, um eine Anzahl von Kunden in einem bestimmten Land zu bekommen, die zwei bestimmte Produkte hat:

Da diese Abfragen eine einzelne Zeile mit einer einzelnen Spalte zurückgeben, können wir diese als Ausdrücke in der SELECT-Liste einer anderen Abfrage verwenden. Wir beginnen mit der Abfrage "Produktpaare" und fügen sie zur SELECT-Liste hinzu. Wir ersetzen diese Fragezeichen-Platzhalter mit Verweisen auf Spalten aus der äußeren Abfrage:

SELECT c.country_id 
     , a.product_id AS a_product_id 
     , b.product_id AS b_product_id 
     , (SELECT COUNT(DISTINCT ccp1.customer_id) 
      FROM customer_country_product ccp1 
      JOIN customer_country_product ccp2 
       ON ccp2.country_id = ccp1.country_id 
       AND ccp2.customer_id = ccp1.customer_id 
      WHERE ccp1.country_id = c.country_id 
       AND ccp1.product_id = a.product_id 
       AND ccp2.product_id = b.product_id 
     ) AS cnt_cust_have_both 
     , (SELECT COUNT(DISTINCT ccp.customer_id) 
      FROM customer_country_product ccp 
      WHERE ccp.country_id = c.country_id 
       AND ccp.product_id IN (a.product_id,b.product_id) 
     ) AS cnt_cust_have_either 
    FROM (SELECT ccpa.product_id 
      FROM customer_country_product ccpa 
      GROUP BY ccpa.product_id 
     ) a 
    JOIN (SELECT ccpb.product_id 
      FROM customer_country_product ccpb 
      GROUP BY ccpb.product_id 
     ) b 
     ON b.product_id > a.product_id 
    CROSS 
    JOIN (SELECT ccpc.country_id 
      FROM customer_country_product ccpc 
      GROUP BY ccpc.country_id 
     ) c 
    ORDER 
     BY c.country_id 
     , a.product_id 
     , b.product_id 

, nun den „Prozentsatz“ berechnen wir brauchen nur eine Divisionsoperation zu tun. Bei MySQL wird eine "Division durch Null" NULL zurückgeben. (Wir würden nicht damit befassen müssen, wenn unsere äußere Abfrage nur die Zeilen zurück, wo wir einen Kunden aus dem Land kennen hat eines der Produkte ... dh das Ergebnis der ersten Abfrage zurückgegeben

SELECT c.country_id 
     , a.product_id AS a_product_id 
     , b.product_id AS b_product_id 
     , (SELECT COUNT(DISTINCT ccp1.customer_id) 
      FROM customer_country_product ccp1 
      JOIN customer_country_product ccp2 
       ON ccp2.country_id = ccp1.country_id 
       AND ccp2.customer_id = ccp1.customer_id 
      WHERE ccp1.country_id = c.country_id 
       AND ccp1.product_id = a.product_id 
       AND ccp2.product_id = b.product_id 
     ) 
    /(SELECT COUNT(DISTINCT ccp.customer_id) 
      FROM customer_country_product ccp 
      WHERE ccp.country_id = c.country_id 
       AND ccp.product_id IN (a.product_id,b.product_id) 
     ) 
     * 100.00 AS percent_cust_have_both 
    FROM (SELECT ccpa.product_id 
      FROM customer_country_product ccpa 
      GROUP BY ccpa.product_id 
     ) a 
    JOIN (SELECT ccpb.product_id 
      FROM customer_country_product ccpb 
      GROUP BY ccpb.product_id 
     ) b 
     ON b.product_id > a.product_id 
    CROSS 
    JOIN (SELECT ccpc.country_id 
      FROM customer_country_product ccpc 
      GROUP BY ccpc.country_id 
     ) c 
    ORDER 
     BY c.country_id 
     , a.product_id 
     , b.product_id 

Soweit "aufwärts" zu skalieren, müssen wir für jede nicht-triviale Tabelle geeignete Indizes zur Verfügung haben, insbesondere für die korrelierten Unterabfragen. Diese werden für jede Zeile ausgeführt, die von der äußeren Abfrage zurückgegeben wird.

Diese letzte Abfrage hat das Potenzial, NULL zurückzugeben, wenn im Nenner eine Zählung von Null vorhanden ist.Wir können eine Null ersetzen, indem wir dies während der Division in einen Cond einbetten itional Test

IFNULL(<expr> , 0) * 100.00 AS 

(Wahrscheinlich gibt es einen Fehler irgendwo in diesen Abfragen, um eine vermisste paren, eine ungültige Referenz, eine falsche Qualifier, usw. Diese Abfragen werden nicht geprüft. Ich empfehle Ihnen dringend, ein jeder zu testen, und zwar nicht nur, dass die letzte Heimelf.)


Followup

Eine Tabelle zum Testen ...

CREATE TABLE customer_country_product 
(customer_id INT 
, country_id VARCHAR(2) 
, product_id VARCHAR(2) 
) 
; 
INSERT INTO customer_country_product (customer_id, country_id, product_id) VALUES 
('1','US','A') 
,('1','US','B') 
,('2','CA','A') 
,('2','CA','C') 
,('3','US','A') 
,('3','US','C') 
,('4','US','B') 
,('5','US','A') 
; 

Schluss Abfrage gibt:

country_id a_product_id b_product_id percent_cust_have_both 
---------- ------------ ------------ ---------------------- 
CA   A    B    0.000000 
CA   A    C    100.000000 
CA   B    C    0.000000 
US   A    B    25.000000 
US   A    C    33.333333 
US   B    C    0.000000 

Es wäre eine triviale Änderung a.product_id und b.product_id in einer einzigen Spalte zu verketten. Die zweite und dritte Spalte in der SELECT-Liste könnte durch etwas wie CONCAT(a.product_id,'_',b.product_id) AS a_b ersetzt werden.

+0

Whoops. Ich hatte MySQL im Hinterkopf, als ich diese Antwort schrieb, nicht SQL Server. Mein Fehler. Einige der Syntax sind möglicherweise spezifisch für MySQL. – spencer7593

+0

Vielen Dank für Ihre Hilfe. Genau das habe ich gebraucht und ist wirklich klar. – Black

+1

@Black: Das stellte sich als ziemlich langwierige Abfrage heraus. Ich habe versucht, einen schrittweisen, inkrementellen Ansatz zum Erstellen dieser Abfrage zu demonstrieren und dabei zu testen. (Es gibt keine Möglichkeit, die letzte Abfrage auf einen Schlag zu erstellen; ich bin einfach nicht schlau/talentiert genug, um das zu tun.) Für SQL Server kann diese Division einen Fehler ausgeben, wenn es sich um eine Division durch Null handelt. Betrieb. Ich empfehle, den Nenner-Ausdruck in eine Funktion einzufügen, die NULL zurückgibt, wenn sie null ergibt. – spencer7593

1

Sie müssen alle Paare von Produkten zusammen mit dem Land generieren. Dann müssen Sie die Anzahl der übereinstimmenden Kunden, die beide gekauft haben, und die Anzahl, die beide gekauft hat, berechnen.

Lassen Sie mich annehmen, dass Sie eine Produkttabelle und eine Ländertabelle haben. Dann denke ich, dass Subqueries vielleicht die einfachste Lösung sein:

select p1.product as product1, p2.product as p2, 
     (select count(*) 
     from (select cp.customer 
       from customerproducts cp 
       where cp.product in (p1.product, p2.product) and 
        cp.country = c.country 
       group by cp.customer 
       having count(distinct product) = 2 
      ) cp 
     ) as numWithBoth, 
     (select count(*) 
     from (select cp.customer 
       from customerproducts cp 
       where cp.product in (p1.product, p2.product) and 
        cp.country = c.country 
       group by cp.customer 
      ) cp 
     ) as numWithEither 
from countries c cross join 
    products p1 cross join 
    products p2 ; 

Die endgültige Antwort das Verhältnis der beiden Werte ist.

+0

Das sieht ziemlich gut aus. Aber benötigt die Inline-Ansicht 'cp' keine' GROUP BY'-Klausel? Wird das Aggregat ("COUNT") in der "HAVING" -Klausel nicht auf eine einzelne Zeile reduziert und nur ein Kunde zurückgegeben? (Oder gibt es einen Fehler, wenn sql_mode 'ONLY_FULL_GROUP_BY' enthält?) Beschränkt MySQL die Tiefe, mit der eine korrelierte Unterabfrage auf Spalten von einer äußeren Abfrage verweisen kann? (Vielleicht ist das nur in einer älteren Version oder einer anderen Datenbank, an die ich denke.) Wird dies nicht doppelte Paare, z. 'A_C' und' C_A' sowie 'A_A'? – spencer7593

+0

Leider habe ich nur diese eine Tabelle. – Black

0

Hier ist eine alternative Methode mit CTEs, und vorausgesetzt, Sie haben nur die Tabelle, die Sie gebucht haben (also gibt es einige zusätzliche Arbeit, um die Länder-/Produktkombinationsliste zu extrahieren). Es ist wahrscheinlich möglich, dies in weniger Schritten zu tun, aber ich wollte die Lösung so beschreiben, dass es einfacher ist zu sehen, was passiert.

drop table #test 

create table #test (customer int, country varchar(2), product char(1)) 
insert into #test values (1, 'US','A') 
insert into #test values (1, 'US','B') 
insert into #test values (2, 'CA','A') 
insert into #test values (2, 'CA','C') 
insert into #test values (3, 'US','A') 
insert into #test values (3, 'US','C') 
insert into #test values (4, 'US','B') 
insert into #test values (5, 'US','A') 

; with CTE as (--Count the number of customers ordering each item 
    select country, product, count(distinct customer) as TotalOrders 
    from #test 
    group by country, product 
    ) 
, CTE2 as (--Join the order counts back to the original data set (can do this in CTE as a windowed function if you don't have customers ordering the same product more than once) 
    select a.*, b.TotalOrders from #test a 
    left join cte b 
    on a.country = b.country and a.product = b.product 
    ) 
, combinations as (--Generate all possible country/product combinations 
    Select * from 
     (Select distinct Country from #test) a 
    cross join 
     (Select distinct a.product + '_' + b.product as ProductCombination from #test a 
     left join #test b 
     on a.product < b.product) b 
    where b.ProductCombination is not null 
    ) 
, calculations as (--count purchasers of combinations, and use this combined with the earlier purchaser counts to generate an unduplicated total purchasers count 
    select a.country, a.product + '_' + b.product as ProductCombination 
     , cast(count(distinct a.customer)*100.0/(a.totalorders + b.totalorders - count(distinct a.customer)) as decimal(5,0)) as PctOfTotal 
    from cte2 a 
    inner join cte2 b 
    on a.country = b.country 
     and a.customer = b.customer 
     and a.product < b.product 
    group by a.country, a.product, b.product, a.totalorders + b.totalorders) 

select a.*, isnull(b.PctOfTotal, 0) as PercentOfTotal from combinations a 
left join calculations b 
on a.country = b.country 
    and a.ProductCombination = b.ProductCombination 
order by a.country, a.ProductCombination 
+0

Danke für Ihre Hilfe. Ich habe versucht, dies auszuführen, aber leider kann ich das Ergebnis nicht sehen, da die Abfrage noch läuft. Es scheint ziemlich langsam. – Black