web-dev-qa-db-fra.com

SQL JOIN, GROUP BY sur trois tables pour obtenir des totaux

J'ai hérité de la conception de base de données suivante. Les tableaux sont:

customers
---------
customerid  
customernumber

invoices
--------
invoiceid  
amount

invoicepayments
---------------
invoicepaymentid  
invoiceid  
paymentid

payments
--------
paymentid  
customerid  
amount

Ma requête doit renvoyer facture (facture), le montant de la facture (dans la table des factures) et le montant dû (montant de la facture moins les paiements effectués pour la facture) pour un numéro personnalisé donné. Un client peut avoir plusieurs factures.

La requête suivante me donne des enregistrements en double lorsque plusieurs paiements sont effectués sur une facture:

SELECT i.invoiceid, i.amount, i.amount - p.amount AS amountdue
FROM invoices i
LEFT JOIN invoicepayments ip ON i.invoiceid = ip.invoiceid
LEFT JOIN payments p ON ip.paymentid = p.paymentid
LEFT JOIN customers c ON p.customerid = c.customerid
WHERE c.customernumber = '100'

Comment puis-je résoudre ça?

10
Nick

Je ne suis pas sûr de t'avoir, mais c'est peut-être ce que tu cherches:

SELECT i.invoiceid, sum(case when i.amount is not null then i.amount else 0 end), sum(case when i.amount is not null then i.amount else 0 end) - sum(case when p.amount is not null then p.amount else 0 end) AS amountdue
FROM invoices i
LEFT JOIN invoicepayments ip ON i.invoiceid = ip.invoiceid
LEFT JOIN payments p ON ip.paymentid = p.paymentid
LEFT JOIN customers c ON p.customerid = c.customerid
WHERE c.customernumber = '100'
GROUP BY i.invoiceid

Cela vous donnerait les montants au cas où il y aurait plusieurs lignes de paiement pour chaque facture.

13
Saggi Malachi

Merci beaucoup pour les réponses! 

Saggi Malachi, cette requête résume malheureusement le montant de la facture dans les cas où plusieurs paiements sont effectués. Supposons qu'il y ait deux paiements sur une facture de 39 $, soit 18 $ et 12 $. Donc plutôt que de finir avec un résultat qui ressemble à:

1   39.00   9.00

Vous allez vous retrouver avec:

1   78.00   48.00

Charles Bretana, en train de réduire ma requête à la requête la plus simple possible, j’ai (bêtement) omis une table supplémentaire, factures client, qui établit un lien entre les clients et les factures. Cela peut être utilisé pour voir les factures pour lesquelles aucun paiement n'a été effectué.

Après beaucoup de difficultés, je pense que la requête suivante renvoie ce dont j'ai besoin:

SELECT DISTINCT i.invoiceid, i.amount, ISNULL(i.amount - p.amount, i.amount) AS amountdue
FROM invoices i
LEFT JOIN invoicepayments ip ON i.invoiceid = ip.invoiceid
LEFT JOIN customerinvoices ci ON i.invoiceid = ci.invoiceid
LEFT JOIN (
  SELECT invoiceid, SUM(p.amount) amount
  FROM invoicepayments ip 
  LEFT JOIN payments p ON ip.paymentid = p.paymentid
  GROUP BY ip.invoiceid
) p
ON p.invoiceid = ip.invoiceid
LEFT JOIN payments p2 ON ip.paymentid = p2.paymentid
LEFT JOIN customers c ON ci.customerid = c.customerid
WHERE c.customernumber='100'

Seriez-vous d'accord?

5
Nick Silberstein

J'ai un conseil pour ceux qui veulent obtenir différentes valeurs agrégées à partir du même tableau. 

Disons que j'ai un tableau avec les utilisateurs et un tableau avec les points que les utilisateurs acquièrent. La connexion entre eux est donc 1: N (un utilisateur, plusieurs enregistrements de points).

Maintenant, dans la table 'points', je stocke également les informations sur les points obtenus par l'utilisateur (connexion, clic sur une bannière, etc.). Et je veux lister tous les utilisateurs classés par SUM(points) AND puis par SUM(points WHERE type = x). C'est-à-dire classés par tous les points que l'utilisateur a puis par points obtenus par l'utilisateur pour une action spécifique (par exemple, connexion).

Le SQL serait:

SELECT SUM(points.points) AS points_all, SUM(points.points * (points.type = 7)) AS points_login
FROM user
LEFT JOIN points ON user.id = points.user_id
GROUP BY user.id

La beauté de ceci réside dans SUM(points.points * (points.type = 7)) où la parenthèse interne est évaluée à 0 ou 1, multipliant ainsi la valeur de points donnée par 0 ou 1, selon que cela correspond au type de points que nous voulons.

2
PunchyRascal

Tout d'abord, ne devrait-il pas y avoir un CustomerId dans la table des factures? Dans l'état actuel des choses, vous ne pouvez pas effectuer cette requête pour les factures pour lesquelles aucun paiement n'a encore été effectué. S'il n'y a pas de paiement sur une facture, celle-ci n'apparaîtra même pas dans la sortie de la requête, même s'il s'agit d'une jointure externe ... 

En outre, lorsqu'un client effectue un paiement, comment savoir à quelle facture le joindre? Si le seul moyen consiste à utiliser l'IdvoiceId sur le talon qui arrive avec le paiement, vous associez (peut-être de manière inappropriée) des factures au client qui les a payées, plutôt qu'au client qui les a commandées .... (Parfois, une facture peut être payée par une personne autre que le client qui a commandé les services) 

2
Charles Bretana

Je sais que c'est tard, mais cela répond à votre question initiale. 

/*Read the comments the same way that SQL runs the query
    1) FROM 
    2) GROUP 
    3) SELECT 
    4) My final notes at the bottom 
*/
SELECT 
        list.invoiceid
    ,   cust.customernumber 
    ,   MAX(list.inv_amount) AS invoice_amount/* we select the max because it will be the same for each payment to that invoice (presumably invoice amounts do not vary based on payment) */
    ,   MAX(list.inv_amount) - SUM(list.pay_amount)  AS [amount_due]
FROM 
Customers AS cust 
    INNER JOIN 
Payments  AS pay 
    ON 
        pay.customerid = cust.customerid
INNER JOIN  (   /* generate a list of payment_ids, their amounts, and the totals of the invoices they billed to*/
    SELECT 
            inpay.paymentid AS paymentid
        ,   inv.invoiceid AS invoiceid 
        ,   inv.amount  AS inv_amount 
        ,   pay.amount AS pay_amount 
    FROM 
    InvoicePayments AS inpay
        INNER JOIN 
    Invoices AS inv 
        ON  inv.invoiceid = inpay.invoiceid 
        INNER JOIN 
    Payments AS pay 
        ON pay.paymentid = inpay.paymentid
    )  AS list
ON 
    list.paymentid = pay.paymentid
    /* so at this point my result set would look like: 
    -- All my customers (crossed by) every paymentid they are associated to (I'll call this A)
    -- Every invoice payment and its association to: its own ammount, the total invoice ammount, its own paymentid (what I call list) 
    -- Filter out all records in A that do not have a paymentid matching in (list)
     -- we filter the result because there may be payments that did not go towards invoices!
 */
GROUP BY
    /* we want a record line for each customer and invoice ( or basically each invoice but i believe this makes more sense logically */ 
        cust.customernumber 
    ,   list.invoiceid 
/*
    -- we can improve this query by only hitting the Payments table once by moving it inside of our list subquery, 
    -- but this is what made sense to me when I was planning. 
    -- Hopefully it makes it clearer how the thought process works to leave it in there
    -- as several people have already pointed out, the data structure of the DB prevents us from looking at customers with invoices that have no payments towards them.
*/
0
Edward