Cross-multiplying table - sql

I have this SQL code and I want to show the sum of each item on its charge slip and on their receipt:
select item_description, sum(receipt_qty) as Samp1, sum(chargeSlip_qty) as Samp2
from Items inner join Receipt_Detail on (Receipt_Detail.item_number =
Items.item_number)
inner join ChargeSlip_Detail on (ChargeSlip_Detail.item_number =
Items.item_number)
group by item_description
It produces this output:
Acetazolamide 2681 1730
Ascorbic Acid 1512 651
Paracetamol 1370 742
Silk 576 952
But it should be:
Acetazolamide 383 173
Ascorbic Acid 216 93
Paracetamol 274 106
Silk 96 238
What's wrong with my code?

Since you are joining tables, you might have a one-to-many relationship that is causing the problem when you then get the sum(). So you can use subqueries to get the result. This will get the sum() for the receipt and chargeslip for each item_number and then you join that back to your items table to get the final result:
select i.item_description,
r.Samp1,
c.Samp2
from Items i
inner join
(
select sum(receipt_qty) Samp1,
item_number
from Receipt_Detail
group by item_number
) r
on r.item_number = i.item_number
inner join
(
select sum(chargeSlip_qty) Samp2,
item_number
from ChargeSlip_Detail
group by item_number
) c
on c.item_number = i.item_number

Do the GROUP BYs first, per Item_Number, so you don't multiply out rows from Receipt_Detail and ChargeSlip_Detail. That is, you generate the SUM values per Item_Number before JOINing back to Items
select
I.item_description,
R.Samp1,
C.Samp2
from
Items I
inner join
(SELECT item_number, sum(receipt_qty) as Samp1
FROM Receipt_Detail
GROUP BY item_number
) R
on (R.item_number = I.item_number)
inner join
(SELECT item_number, sum(chargeSlip_qty) as Samp2
FROM ChargeSlip_Detail
GROUP BY item_number
) C
on (C.item_number = I.item_number)

A left join returns rows from the left table, and for each row in the left table, all matching rows in the right table.
So for example:
create table Customers (name varchar(50));
insert Customers values
('Tim'),
('John'),
('Spike');
create table Orders (customer_name varchar(50), product varchar(50));
insert Orders values (
('Tim', 'Guitar'),
('John', 'Drums'),
('John', 'Trumpet');
create table Addresses (customer_name varchar(50), address varchar(50));
insert Addresses values (
('Tim', 'Penny Lane 1'),
('John', 'Abbey Road 1'),
('John', 'Abbey Road 2');
Then if you run:
select c.name
, count(o.product) as Products
, count(a.address) as Addresses
from Customers c
left join Orders o on o.customer_name = c.name
left join Addresses a on a.customer_name = c.name
group by name
You get:
name Products Addresses
Tim 1 1
John 4 4
Spike 0 0
But John doesn't have 4 products!
If you run without the group by, you can see why the counts are off:
select *
from Customers c
left join Orders o on o.customer_name = c.name
left join Addresses a on a.customer_name = c.name
You get:
name customer_name product customer_name address
Tim Tim Guitar Tim Penny Lane 1
John John Drums John Abbey Road 1
John John Drums John Abbey Road 2
John John Trumpet John Abbey Road 1
John John Trumpet John Abbey Road 2
Spike NULL NULL NULL NULL
As you can see, the joins end up repeating each other. For each product, the list of addresses is repeated. That gives you the wrong counts. To solve this problem, use one of the excellent other answers:
select c.name
, o.order_count
, a.address_count
from Customers c
left join
(
select customer_name
, count(*) as order_count
from Orders
group by
customer_name
) o
on o.customer_name = c.name
left join
(
select customer_name
, count(*) as address_count
from Addresses
group by
customer_name
) a
on a.customer_name = c.name
The subqueries ensure only one row is joined per customer. The result is much better:
name order_count address_count
Tim 1 1
John 2 2
Spike NULL NULL

Related

SQL MAX aggregate function not bringing the latest date

Purpose: I am trying to find the max date of when the teachers made a purchase and type.
Orders table
ID
Ordertype
Status
TeacherID
PurchaseDate
SchoolID
TeacherassistantID
1
Pencils
Completed
1
1/1/2021
1
1
2
Paper
Completed
1
3/5/2021
1
1
3
Notebooks
Completed
1
4/1/2021
1
1
4
Erasers
Completed
2
2/1/2021
2
2
Teachers table
TeacherID
Teachername
1
Mary Smith
2
Jason Crane
School table
ID
schoolname
1
ABC school
2
PS1
3
PS2
Here is my attempted code:
SELECT o.ordertype, o.status, t.Teachername, s.schoolname
,MAX(o.Purchasedate) OVER (PARTITION by t.ID) last_purchase
FROM orders o
INNER JOIN teachers t ON t.ID=o.TeacherID
INNER JOIN schools s ON s.ID=o.schoolID
WHERE o.status in ('Completed','In-progress')
AND o.ordertype not like 'notebook'
It should look like this:
Ordertype
Status
teachername
last_purchase
schoolname
Paper
Completed
Mary Smith
3/5/2021
ABC School
Erasers
Completed
PS1
2/1/2021
ABC school
It is bringing multiple rows instead of just the latest purchase date and its associated rows. I think i need a subquery.
Aggregation functions are not appropriate for what you are trying to do. Their purpose is to summarize values in multiple rows, not to choose a particular row.
Just a window function does not filter any rows.
You want to use window functions with filtering:
SELECT ordertype, status, Teachername, schoolname, Purchasedate
FROM (SELECT o.ordertype, o.status, t.Teachername, s.schoolname,
o.Purchasedate,
ROW_NUMBER() OVER (PARTITION by t.ID ORDER BY o.PurchaseDate DESC) as seqnum
FROM orders o JOIN
teachers t
ON t.ID = o.TeacherID
schools s
ON s.ID = o.schoolID
WHERE o.status in ('Completed', 'In-progress') AND
o.ordertype not like 'notebook'
) o
WHERE seqnum = 1;
You can use it in different way. it's better to use Group By for grouping the other columns and after that use Order by for reorder all records just like bellow.
SELECT top 1 o.ordertype, o.status, t.Teachername, s.schoolname
,o.Purchasedate
FROM orders o
INNER JOIN teachers t ON t.ID=o.TeacherID
INNER JOIN schools s ON s.ID=o.schoolID
having o.status in ('Completed','In-progress')
AND o.ordertype not like 'notebook'
group by o.ordertype, o.status, t.Teachername, s.schoolname
order by o.Purchasedate Desc

Eliminate database rows based on mod dates

I have an issue grabbing data from two tables base on a single field in one of the tables The field does not exist in both tables.
Example:
Table 1 (call it invoices):
invoice email customer_name original_bill invoice_status
1 a#g.com bob 5.00 P
2 a#g.com harry 23.00 P
3 a#g.com sally 4.00 P
4 b#g.com loretta 14.00 P
5 b#g.com hamish 74.00 P
Table 2 (customer invoice edits):
invoice email date_last_modified_timestamp mod_status mod_amount
1 a#g.com 2019-05-01 A 3.00
1 a#g.com 2019-04-01 D
3 b#g.com 2019-10-25 A
What I want
a list of all invioces and their mods but I only wish each invoice to appear once in the list. based on the latest date in Table two.
example:
invoice email customer_name original_bill invoice_status date_last_modified_timestamp mod_status mod_amount
1 a#g.com bob 5.00 P 2019-05-01 A 3.00
2 a#g.com harry 23.00 P
3 a#g.com sally 4.00 P 2019-10-25 A
4 b#g.com loretta 14.00 P
5 b#g.com hamish 74.00 P
How im pulling it now:
select invoice, email, customer_name, original bill, invoice status, max(date_last_modified_timestamp), mod status, mod_amount
from table one
left join on table1.invoice = table 2.invoice
group by
invoice, email, customer_name, original bill, invoice status, max(date_last_modified_timestamp), mod status, mod_amount
I've tried a gazzillion variation to no avail.
What i get
i do get accurate results, but those results inclues a duplicate row containing each invoice that has been modified. I only want one invoice per row. I want the one that's been modified last. Is this even possible? What am I doing wrong?
You can join and use a correlated subquery for filtering:
select i.*, e.*
from invoices i
inner join customer_invoice_edits e
on e.invoice = i.invoice
and e.date_last_modified_timestamp = (
select max(date_last_modified_timestamp)
from customer_invoice_edits e1
where e1.invoice = e.invoice
)
This will give you one record per row in the invoices table that has a match in customer_invoice_edits, along with the latest corresponding record in customer_invoice_edits.
If some invoices have no corresponding record in the dependant table, they will not appear in the resultset. If you do want to see them, then you can use a left join instead.
Is this what you want?
select
invoice,
email
from (
select
invoice,
email,
mod_status,
mod_amount,
date_last_modified_timestamp,
row_number() over (
partition by invoice, email
order by date_last_modified_timestamp desc
) rn
from table2
)
where rn=1
I'm pretty sure you want this field-by-field. Here is an example:
select i.invoice,
nz( (select top (1)
from invoice_edits as ie
where ie.invoice = i.invoice and ie.email is not null
), i.email
) as email,
nz( (select top (1)
from invoice_edits as ie
where ie.invoice = i.invoice and ie.mod_amount is not null
), i.original_bill
) as original_bill,
. . .
from invoices as i;

How to remove duplicate columns from join in SQL

I have the following code
SELECT *
FROM customer
INNER JOIN
(SELECT
customerid, newspapername, enddate, n.publishedby
FROM
newspapersubscription ns, newspaper n
WHERE
publishedby IN (SELECT publishedby
FROM newspaper
WHERE ns.newspapername = n.NewspaperName)
UNION
SELECT
customerid, Magazinename, enddate, m.publishedby
FROM
magazinesubscription ms, magazine m
WHERE
publishedby IN (SELECT publishedby
FROM magazine
WHERE ms.Magazinename = m.MagazineName)) ON customer.customerid = customerid
ORDER BY
customer.customerid;
The customer table has the following:
customerid | customername | customersaddress
This query returns the following result:
customerid | customername | customersaddress | customerid | newspapername | enddate| publishedby
What I actually want is
customerid | customername | customersaddress | newspapername | magazinename | enddate| publishedby
Here, the newspapername field should be blank if the magazinename is present and vice versa. Also, the duplicate field of customerid from the union operations should not be present, while in my result, the value of both the newspapername and the magazinename are put under newspapername title.
How can I do that?
Since you are querying the table with '*', you will always get all the columns in both tables. In order to omit this column, you will have to manually name all columns you DO want to query. To address your other need, you need to simply insert a dummy column to each clause in the union query. Below is an example that should work to allow for what you want -
SELECT customer.customerid, customer.customername, customer.customeraddress, newspapername, magazinename, enddate, publishedby
FROM customer
INNER JOIN
(select customerid, newspapername, null Magazinename, enddate, n.publishedby
from newspapersubscription ns, newspaper n
where publishedby in(select publishedby
from newspaper
where ns.newspapername = n.NewspaperName)
UNION
select customerid, null newspapername, Magazinename, enddate, m.publishedby
from magazinesubscription ms, magazine m
where publishedby in(select publishedby
from magazine
where ms.Magazinename = m.MagazineName))
on customer.customerid = customerid
ORDER BY customer.customerid;
To get the projection you want, build sub-queries of the right shape and UNION them to get the result set. UNION ALL is better than UNION because it avoids a sort: you know you'll get a distinct set because you're joining on two different tables.
select * from (
select customer.*
, n.newspapername
, null as magazinename
, ns.enddate
, n.publishedby
from customer
join newspapersubscription ns
on ns.customerid = customer.customerid
join newspaper n
on n.newspapername = ns.newspapername
union all
select customer.*
, null as newspapername
, m.magazinename
, ms.enddate
, m.publishedby
from customer
join magazinesubscription ms
on ms.customerid = customer.customerid
join magazine m
on m.magazinename = ms.magazinename
)
order by customerid, newspapername nulls last, magazinename ;
Here is the output from my toy data set (which lacks publishedby columns:
CUSTOMERID CUSTOMERNAME NEWSPAPERNAME MAGAZINENAME ENDDATE
---------- -------------------- ---------------------- ---------------------- ---------
10 DAISY-HEAD MAISIE THE DAILY BUGLE 30-SEP-17
30 FOX-IN-SOCKS THE DAILY BUGLE 30-SEP-17
30 FOX-IN-SOCKS THE WHOVILLE TIMES 30-SEP-16
30 FOX-IN-SOCKS GREEN NEWS 31-DEC-17
30 FOX-IN-SOCKS TWEETLE BEETLE MONTHLY 31-DEC-16
40 THE LORAX GREEN NEWS 31-DEC-18
6 rows selected.
SQL>

Completing a table with rows not existing

I have a table like this:
buyer product quantity
tom skirt 2
anna skirt 3
tom jeans 5
The distinct(product) = skirt and jeans
I want a table that inserts another column with quantity = 0 when the <buyer, product> tuple does not exist for all possible products.
So the result would be:
buyer product quantity
tom skirt 2
anna skirt 3
tom jeans 5
anna jeans 0
It does not look very complicated but I don't know how to do it.
UPDATE
I have found one extra complication.
My product is actually defined by two fields: class and product. Product can be null and I need not to lose the information quantity when the product field is null (now it is happening with the cross join).
So if I have this:
buyer class product quantity
tom clothes skirt 2
anna clothes skirt 3
tom clothes jeans 5
jim shoes NULL 7
I would need:
buyer class product quantity
tom clothes skirt 2
anna clothes skirt 3
tom clothes jeans 5
anna clothes jeans 0
jim shoes NULL 7
jim clothes skirt 0
jim clothes jeans 0
tom shoes NULL 0
anna shoes NULL 0
Thank you all, I know I am complicating things!
You can use a cross join to generate all possible combinations of buyers and products. Then use a left join (or not exists) to filter out the ones already in the table:
insert into table(buyer, product, quantity)
select b.buyer, p.product, 0
from (select distinct buyer from table) b cross join
(select distinct product p from table) p left join
table t
on t.buyer = b.buyer and t.product = p.product
where t.buyer is null;
EDIT:
If you want a query that returns all the rows, then you would use something quite similar:
select b.buyer, p.product, coalesce(t.qty, 0) as qty
from (select distinct buyer from table) b cross join
(select distinct product p from table) p left join
table t
on t.buyer = b.buyer and t.product = p.product;
EDIT II:
If you have NULL values for buyer and/or product, then use NULL safe comparisons:
select b.buyer, p.product, coalesce(t.qty, 0) as qty
from (select distinct buyer from table) b cross join
(select distinct product p from table) p left join
table t
on t.buyer is not distinct from b.buyer and
t.product is not distinct from p.product;
(As a minor side note: I really do not like the use of distinct in this construct. Why did Postgres (ANSI ?) give it such a complicated name?)
The solution of #Gordon is almost full, I edit like this:
declare #tb table (buyer varchar(150), product varchar(150), quantity int)
insert into #tb
values('tom','skirt',2),
('anna','skirt',3),
('tom','jeans',5)
select *
from #tb a
left join( select
distinct(product)
from #tb) b on a.product = a.product
select b.buyer, p.p, isnull(t.quantity,0)
from (select distinct buyer from #tb) b cross join
(select distinct product p from #tb) p left join
#tb t
on t.buyer = b.buyer and t.product = p.p
--where t.buyer is null
Try it.

show result from one table

good day, i have these 3 tables...i.e.;
customer table
cust_id cust_name sales_employee
1 abc 1
2 cde 1
3 efg 2
transaction table
order_num cust_id sales_employee
1001 1 1
1002 2 2
sales_employee table
sales_employee employee name
1 john doe
2 jane doe
how can i show the employee name on both customer table and transaction table?
notice how the sales_employee can change per transaction, it does not necessarily have to be the same per customer.
please help.
To select customers with sales person name
select
C.*, E.employee_name
from
Customers as C
inner join Sales_Employees as E on E.sales_employee = C.sales_employee
To select transactions with customer name and salesperson name (at the point in time of the transaction)
select
T.*,
E.employee_name as Trans_employee,
C.cust_name,
EC.employee_name as Cust_employee
from
Transactions as T
inner join Sales_Employees as E on E.sales_employee = T.sales_employee
inner join Customers as C on C.cust_id= T.cust_id
inner join Sales_Employees as EC on EC.sales_employee = C.sales_employee
This code is meant to guide you, you will need to adjust it to match your table and field names.