How to list unused items from database - sql

MDW_CUSTOMER_ACCOUNTS has the following fields: ACCOUNT_ID, MEAL_ID.
MDW_MEALS_MENU has the following fields: MEAL_ID, MEAL_NAME.
I am trying to generate a report on the number of times a particular meal has been subscribed to by a customer using the query,
SELECT count(a.account_id), b.meal_id, b.meal_name
FROM mdw_meals_menu b LEFT JOIN mdw_customer_accounts a
on b.meal_id=a.meal_id
WHERE
a.start_date BETWEEN to_date('01-APR-2013','DD-MON-YYYY')
AND to_date('30-JUN-2013','DD-MON-YYYY')
GROUP BY b.meal_id, b.meal_name
ORDER BY count(a.account_id) desc, b.meal_id;
This only lists the MEAL_IDs that has been subscribed to at least once. But it is not displaying the Ids that have not been subscribed to.
How do I get these MEAL_IDs to print with the count being 0?
i have modified the code, but still i get the same result.

Your where clause is effectively turning your outer join back into an inner join - conditions on an outer-joined table should generally be in the join clause, like so:
SELECT count(a.account_id), b.meal_id, b.meal_name
FROM mdw_meals_menu b
LEFT JOIN mdw_customer_accounts a
on b.meal_id=a.meal_id and
a.start_date BETWEEN to_date('01-APR-2013','DD-MON-YYYY')
AND to_date('30-JUN-2013','DD-MON-YYYY')
GROUP BY b.meal_id, b.meal_name
ORDER BY count(a.account_id) desc, b.meal_id;

You should use a left outer join .

Related

Select the first row of a LEFT JOIN

I'm trying to do a left join. But I only want the first row of the joined table.
When I do :
SELECT DISTINCT
c.reference
FROM contracts as c
output : 7400 rows
But when I try to do the left join I have a lot of duplicates.
I already tried to only get the first row but it does not work. Here is my code :
SELECT DISTINCT
c.reference,
contract_premiums.start_date
FROM contracts as c
LEFT OUTER JOIN contract_premiums ON contract_premiums.contract_id=(
SELECT contract_id FROM contract_premiums
WHERE contract_premiums.contract_id = c.id
ORDER BY contract_premiums.created_at ASC
LIMIT 1)
output : 11500 rows
Note the database in Postgresql and I'm using this request in klipfolio.
If you just want the latest start_date per reference, you can use aggregation:
select c.reference, max(cp.start_date) max_start_date
from contracts c
left join contracts_premiums cp on cp.contract_id = c.id
group by c.reference
This guarantees that you will only get one row per reference.
If you want more columns from contracts_premiums, or if you want to sort by a column other than start_date (possibly, you want created_at instead), then another option is distinct on:
select distinct on (c.reference) c.reference, cp.start_date, cp.created_at
from contracts c
left join contracts_premiums cp on cp.contract_id = c.cid
order by c.reference, cp.created_at desc

SQL: Left Join with three tables

I have three tables
Products (idProduct, name)
Invoices(typeinvoice, numberinvoice, date)
Item-invoices(typeinvoice, numberinvoice, idProduct)
My query has to select all the products not selled in the year 2019. I can use a function to obtain the year from the date, for example year(i.date). I know that the products that don't appear in the Item-invoice table are the not selled products. So I have tried with this two codes and obtain a good output.
SELECT p.name
FROM Products p
EXECPT
SELECT ii.idProduct
FROM Item-invoices ii, Invoices i
WHERE ii.typeinvoice=i.typeinvoice
AND ii.numberinvoice=i.numberinvocice
AND year(i.date)=2019
And the other code use a sub-query:
SELECT p.name
FROM Products p
WHERE p.idProduct NOT IN
(SELECT ii.idProduct
FROM Item-invoices ii, Invoices i
WHERE ii.typeinvoice=i.typeinvoice
AND ii.numberinvoice=i.numberinvocice
AND year(i.date)=2019)
The answer is how can i use the left join command to have the same output. I've tried with
SELECT p.name
FROM Products p
LEFT JOIN Item-invoices ii ON
p.IdProduct=ii.idProduct
LEFT JOIN Invoices i ON
ii.typeinvoice=i.typeinvoice
AND ii.numberinvoice=i.numberinvocice
WHERE year(i.date)=2019
AND ii.idProduct IS NULL
I know this is wrong but can't find the solution
Any help?
You are almost there. You just need to move the condition on the invoice date to from the from clause to the on clause of the join.
Conditions in the WHERE clause are mandatory, so what you did actually turned the LEFT JOI to an INNER JOIN, which can never be fulfilled (since both conditions in the WHERE clause cannot be true at the same time).
SELECT p.name
FROM Products p
LEFT JOIN Item-invoices ii ON
p.IdProduct=ii.idProduct
LEFT JOIN Invoices i ON
ii.typeinvoice=i.typeinvoice
AND ii.numberinvoice=i.numberinvocice
AND i.date >= '2019-01-01'
AND i.date < '2020-01-01'
WHERE ii.idProduct IS NULL
Note that I changed your date filter to a half-open filter that operates directly on the stored date, without using date functions; this is a more efficient way to proceed (since it allows the database to use an existing index).

SQL Get aggregate as 0 for non existing row using inner joins

I am using SQL Server to query these three tables that look like (there are some extra columns but not that relevant):
Customers -> Id, Name
Addresses -> Id, Street, StreetNo, CustomerId
Sales -> AddressId, Week, Total
And I would like to get the total sales per week and customer (showing at the same time the address details). I have come up with this query
SELECT a.Name, b.Street, b.StreetNo, c.Week, SUM (c.Total) as Total
FROM Customers a
INNER JOIN Addresses b ON a.Id = b.CustomerId
INNER JOIN Sales c ON b.Id = c.AddressId
GROUP BY a.Name, c.Week, b.Street, b.StreetNo
and even if my SQL skill are close to none it looks like it's doing its job. But now I would like to be able to show 0 whenever the one customer don't have sales for a particular week (weeks are just integers). And I wonder if somehow I should get distinct values of the weeks in the Sales table, and then loop through them (not sure how)
Any help?
Thanks
Use CROSS JOIN to generate the rows for all customers and weeks. Then use LEFT JOIN to bring in the data that is available:
SELECT c.Name, a.Street, a.StreetNo, w.Week,
COALESCE(SUM(s.Total), 0) as Total
FROM Customers c CROSS JOIN
(SELECT DISTINCT s.Week FROM sales s) w LEFT JOIN
Addresses a
ON c.CustomerId = a.CustomerId LEFT JOIN
Sales s
ON s.week = w.week AND s.AddressId = a.AddressId
GROUP BY c.Name, a.Street, a.StreetNo, w.Week;
Using table aliases is good, but the aliases should be abbreviations for the table names. So, a for Addresses not Customers.
You should generate a week numbers, rather than using DISTINCT. This is better in terms of performance and reliability. Then use a LEFT JOIN on the Sales table instead of an INNER JOIN:
SELECT a.Name
,b.Street
,b.StreetNo
,weeks.[Week]
,COALESCE(SUM(c.Total),0) as Total
FROM Customers a
INNER JOIN Addresses b ON a.Id = b.CustomerId
CROSS JOIN (
-- Generate a sequence of 52 integers (13 x 4)
SELECT ROW_NUMBER() OVER (ORDER BY a.x) AS [Week]
FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) a(x)
CROSS JOIN (SELECT x FROM (VALUES(1),(1),(1),(1)) b(x)) b
) weeks
LEFT JOIN Sales c ON b.Id = c.AddressId AND c.[Week] = weeek.[Week]
GROUP BY a.Name
,b.Street
,b.StreetNo
,weeks.[Week]
Please try the following...
SELECT Name,
Street,
StreetNo,
Week,
SUM( CASE
WHEN Total IS NULL THEN
0
ELSE
Total
END ) AS Total
FROM Customers a
JOIN Addresses b ON a.Id = b.CustomerId
RIGHT JOIN Sales c ON b.Id = c.AddressId
GROUP BY a.Name,
c.Week,
b.Street,
b.StreetNo;
I have modified your statement in three places. The first is I changed your join to Sales to a RIGHT JOIN. This will join as it would with an INNER JOIN, but it will also keep the records from the table on the right side of the JOIN that do not have a matching record or group of records on the left, placing NULL values in the resulting dataset's fields that would have come from the left of the JOIN. A LEFT JOIN works in the same way, but with any extra records in the table on the left being retained.
I have removed the word INNER from your surviving INNER JOIN. Where JOIN is not preceded by a join type, an INNER JOIN is performed. Both JOIN and INNER JOIN are considered correct, but the prevailing protocol seems to be to leave the INNER out, where the RDBMS allows it to be left out (which SQL-Server does). Which you go with is still entirely up to you - I have left it out here for illustrative purposes.
The third change is that I have added a CASE statement that tests to see if the Total field contains a NULL value, which it will if there were no sales for that Customer for that Week. If it does then SUM() would return a NULL, so the CASE statement returns a 0 instead. If Total does not contain a NULL value, then the SUM() of all values of Total for that grouping is performed.
Please note that I am assuming that Total will not have any NULL values other than from the RIGHT JOIN. Please advise me if this assumption is incorrect.
Please also note that I have assumed that either there will be no missing Weeks for a Customer in the Sales table or that you are not interested in listing them if there are. Again, please advise me if this assumption is incorrect.
If you have any questions or comments, then please feel free to post a Comment accordingly.

i want to modify this SQL statement to return only distinct rows of a column

select
picks.`fbid`,
picks.`time`,
categories.`name` as cname,
options.`name` as oname,
users.`name`
from
picks
left join categories
on (categories.`id` = picks.`cid`)
left join options
on (options.`id` = picks.oid)
left join users
on (users.fbid = picks.`fbid`)
order by
time desc
that query returns a result that like:
my question is.... I would like to modify the query to select only DISTINCT fbid's. (perhaps the first row only sorted by time)
can someone help with this?
select
p2.fbid,
p2.time,
c.`name` as cname,
o.`name` as oname,
u.`name`
from
( select p1.fbid,
min( p1.time ) FirstTimePerID
from picks p1
group by p1.fbid ) as FirstPerID
JOIN Picks p2
on FirstPerID.fbid = p2.fbid
AND FirstPerID.FirstTimePerID = p2.time
LEFT JOIN Categories c
on p2.cid = c.id
LEFT JOIN Options o
on p2.oid = o.id
LEFT JOIN Users u
on p2.fbid = u.fbid
order by
time desc
I don't know why you originally had LEFT JOINs, as it appears that all picks must be associated with a valid category, option and user... I would then remove the left, and change them to INNER joins instead.
The first inner query grabs for each fbid, the FIRST entry time which will result in a single entity for the FBID. From that, it re-joins to the picks table for the same ID and timeslot... then continues for the rest of the category, options, users join criteria of that single entry.
2 options, you could write a group by clause.
Or you could write a nested query joined back to itself to get pertinent info.
Nested aliased table:
SELECT
n.fBids
FROM
MyTable t
INNER JOIN
(SELECT DISTINCT fBids
FROM MyTable) n
ON n.ID = t.ID
Or group by option
SELECT fBId from MyTable
GROUP BY fBID
select picks.`fbid`, picks.`time`, categories.`name` as cname,
options.`name` as oname, users.`name` from picks left join categories
on (categories.`id` = picks.`cid`) left join options on (options.`id` = picks.oid)
left join users on (users.fbid = picks.`fbid`)
order by time desc GROUP BY picks.`fbid`
select
picks.fbid,
MIN(picks.time) as first_time,
MAX(picks.time) as last_time
from
picks
group by
picks.fbid
order by
MIN(picks.time) desc
However, if you want only distinct fbid's you cannot display cname and other columns at the same time.

PostgreSQL: how do you SELECT DISTINCT relations and order by different fields depending on WHERE clause?

Each account is associated with one person and one type of account. I want to SELECT a distinct subset of accounts. In order to be selected the accounts have meet at least one of two criteria. If an account occurs twice
I want to order this result set based on two different fields. This was my attempt:
Select DISTINCT a.*
FROM people AS p
JOIN accounts AS a
ON a.people_id = p.id
JOIN type_account AS t
ON t.type_id = a.id
WHERE t.id IN(1,3,5)
OR p.id IN(2,4,6)
ORDER BY(CASE
WHEN p.id IN(2,4,6) THEN p.updated_at
WHEN t.id IN(1,3,5) THEN p.created_at) AS position
And I got this error: SELECT DISTINCT, ORDER BY expressions must appear in select list
If I move the case statement to the select it possible for one account (associated with different people) to appear in the results twice, i.e. once when the first where clause is met and twice when the second where clause is met. In this case the accounts will be appearing twice in the result set.
I am having trouble wrapping my head around this one. Any help would be appreciated :)
Move your CASE statement(s) into the SELECT clause, then order on their position:
SELECT
CASE
WHEN p.id IN(2,4,6) THEN p.updated_at
WHEN t.id IN(1,3,5) THEN p.created_at
END AS position,
DISTINCT a.*
FROM people AS p
JOIN accounts AS a
ON a.people_id = p.id
JOIN type_account AS t
ON t.type_id = a.id
WHERE t.id IN(1,3,5)
OR p.id IN(2,4,6)
ORDER BY 1 DESC
LIMIT 1;