Count non-matching rows as zero - how to? [duplicate] - mysql

This question already has an answer here:
MySql Count cannot show 0 values
(1 answer)
Closed 5 years ago.
The thing, that I'm trying to do is:
Fetch room availability count based on supplied checkin and checkout dates.
Here are the tables:
categories
id | name
1 | Luxe
2 | Deluxe
reservations
id | category_id | checkin | checkout
1 | 1 | 2018-01-06 | 2018-01-10
1 | 2 | 2018-01-11 | 2018-01-13
3 | 2 | 2018-01-13 | 2018-01-17
Expected output
Since category_id = 2 is available from 2018-01-06 up to 2018-01-10 (see the very first row in reservations table), I'm trying to include its availability count, when fetching total count.
So when running this query
SELECT
categories.name AS category,
COUNT(reservations.id) AS free_count
FROM reservations
INNER JOIN categories ON categories.id = reservations.category_id
WHERE checkin BETWEEN '2018-01-06' AND '2018-01-10'
GROUP BY categories.name, reservations.id
The output is:
category | free_count
Luxe | 1
However I'm willing the output to be:
category | free_count
Luxe | 1
Deluxe | 0
i.e replace non-matching rows to zero. How this can be done? A solution without nested queries (due to their slow performance) is very encouraged.

Use LEFT JOIN instead of INNER JOIN
SELECT
c.name AS category,
COUNT(r.id) AS free_count
FROM categories c
LEFT JOIN reservations r
ON c.id = r.category_id
AND checkin BETWEEN '2018-01-06' AND '2018-01-10'
GROUP BY c.name
Its important to keep the checkin filter in ON condition. When you keep the right table filter in Where clause, the non matching records will have NULL values which will be filtered in result due the where condition(basically Left join will be converted to Inner join)

Related

SQL left join: how to return the newest from tableB and grouped by another field

I've been trying for two days, without luck.
I have the following simplified tables in my database:
customers:
| id | name |
| 1 | andrea |
| 2 | marco |
| 3 | giovanni |
access:
| id | name_id | date |
| 1 | 1 | 5000 |
| 2 | 1 | 4000 |
| 3 | 2 | 1500 |
| 4 | 2 | 3000 |
| 5 | 2 | 1000 |
| 6 | 3 | 6000 |
| 7 | 3 | 2000 |
I want to return all the names with their last access date.
At first I tried simply with
SELECT * FROM customers LEFT JOIN access ON customers.id =
access.name_id
But I got 7 rows instead of 3 as expected. So I understood I need to use GROUP BY statemet as the following:
SELECT * FROM customers LEFT JOIN access ON customers.id =
access.name_id GROUP BY customers.id
As far I know, GROUP BY combines using a random row. In fact I got unordered access dates with several tests.
Instead I need to group every customer id with its corresponding latest access! How this can be done?
You have to get the latest date from the access table with a group by on the the name_id, then join this result with the customer table. Here is the query:
select c.id, c.name, a.last_access_date from customers c left join
(select id, name_id, max(access_date) last_access_date from access group by name_id) a
on c.id=a.name_id;
Here is a DEMO on sqlfiddle.
I think this is what you'd like to achieve:
SELECT c.id, c.name, max(a.date) last_access
FROM customers c
LEFT JOIN access a ON c.id = a.name_id
GROUP BY c.id, c.name
The LEFT join will return all entries in table customers regardless if the join criteria (c.id = a.name_id) is satisfied. This means that you might get some NULL entries.
Example:
Simply add a new row in the customers table (id: 4, name: manuela). The output will have 4 rows and the newest row will be (id: 4, last_access: null)
I would do this using a correlated subquery in the ON clause:
SELECT a.*, c.*
FROM customers c LEFT JOIN
access a
ON c.id = a.name_id AND
a.DATE = (SELECT MAX(a2.date) FROM access a2 WHERE a2.name_id = a.name_id);
If this statement is true:
I need to group every customer id with its corresponding latest access! How this can be done?
Then you can simply do:
select a.name_id, max(a2.date)
from access a
group by a.name_id;
You do not need the customers table because:
All customers are in access, so the left join is not necessary.
You need no columns from customers.

Select data from another table if exists, if not display null

I have two tables.
Invoices
ID | Amount
-----------
1 | 123.54
2 | 553.46
3 | 431.34
4 | 321.31
5 | 983.12
Credit Memos
ID | invoice_ID | Amount
------------------------
1 | 3 | 25.50
2 | 95 | 65.69
3 | 51 | 42.50
I want to get a result set like this out of those two tables
ID | Amount | Cr_memo
---------------------
1 | 123.54 |
2 | 553.46 |
3 | 431.34 | 25.50
4 | 321.31 |
5 | 983.12 |
I've been messing with joins and whatnot all morning with no real luck.
Here is the last query I tried, which pulled everything from the Credit Memo table...
SELECT A.ID, A.Amount FROM Invoices AS A
LEFT JOIN Credit_Memos AS B ON A.ID = B.invoice_ID
Any help or pointers are appreciated.
Your query would work fine. Just add Credit_memo.Amount with an alias:
SELECT Inv.ID,Inv.Amount,IFNULL(C.Amount,'') AS Cr_memo
FROM Invoices Inv LEFT JOIN
Credit_Memos C ON Inv.ID=C.invoice_ID
Result:
ID AMOUNT CR_MEMO
1 124
2 553
3 431 25.50
4 321
5 983
See result in SQL FIDDLE.
You almost got the answer Left Outer Join is what you need but you missed to select Cr_memo from Credit_Memos table. Since you don't want to show Null values when there is no Invoices_ID in Credit Memos table use IFNULL to make NULL's as Empty string
SELECT A.ID, A.Amount, IFNULL(B.Cr_memo,'') AS Cr_memo
FROM Invoices AS A
LEFT JOIN Credit_Memos AS B
ON A.ID = B.invoice_ID
The LEFT JOIN keyword returns all rows from the left table (table1), with the matching rows in the right table (table2). The result is NULL in the right side when there is no match.
SELECT A.ID, A.Amount, IFNULL(B.amount,0) AS Cr_memo FROM Invoices AS A
LEFT JOIN Credit_Memos AS B ON A.ID = B.invoice_ID
here is some useful link about left join link and another

Duplicated Data When Joining 4 Tables in MySql [duplicate]

This question already has answers here:
Sum total of table with two related tables
(2 answers)
Closed 9 years ago.
I have 4 tables, with the relevant columns summarized here:
customers:
id
name
credits:
id
customer_id # ie customers.id
amount
sales:
id
customer_id # ie customers.id
sales_items:
id
sale_id # ie sales.id
price
discount
The idea is that customers lists all of our customers, credits lists each time they have paid us, sales lists each time they have bought things from us (but not what things they bought) and sales_items lists all of the items they bought at each of those sales. So you can see that credits and sales both relate back to customers, but sales_items only relates back to sales.
As an example dataset, consider:
customers:
id | name
5 | Carter
credits:
id | customer_id | amount
1 | 5 | 100
sales:
id | customer_id
3 | 5
sales_items:
id | sale_id | price | discount
7 | 3 | 5 | 0
8 | 3 | 0 | 0
9 | 3 | 10 | 0
I have tried this in MySQL:
SELECT c.*,
SUM( cr.amount ) AS paid,
SUM( i.price + i.discount ) AS bought
FROM customers AS c
LEFT JOIN sales AS s ON s.customer_id = c.id
LEFT JOIN sales_items AS i ON i.sale_id = s.id
LEFT JOIN credits AS cr ON cr.customer_id = c.id
WHERE c.id = 5
But it returns:
id | name | paid | bought
5 | Carter | 300 | 15
If I omit the SUM() functions, it returns:
id | name | paid | bought
5 | Carter | 100 | 5
5 | Carter | 100 | 0
5 | Carter | 100 | 15
So it looks like it's returning one row for every record matched in sales_items, but it's filling in the amount column with same value from credits each time. I see that this is happening, but I'm not understanding why it's happening.
So, two questions:
1. What is happening that it's smearing that one value through all of the rows?
2. What SQL can I throw at MySQL so that I can get this back:
id | name | paid | bought
5 | Carter | 100 | 15
I know that I could break it all up in subqueries, but is there a away to do it just with joins? I was hoping to learn a thing or two about joins as I tackled this problem. Thank you.
Edit: I created an SQL Fiddle for this: http://sqlfiddle.com/#!2/0051b/1/0
select distinct (c.id, c.name), sum(i.price+i.discount) AS bought, cr.amount AS paid
from customer c, credits cr, sales s, sales_items i
where s.customer_id = c.id
and i.sale_id = s.id
and cr.customer_id = c.id and c.id = 5
group by c.id, c.name;
I'm not very sure, but try this. Use group by; that is surely the solution.
Please try this
SELECT c.*,( SELECT SUM( cr.amount ) FROM customer c INNER JOIN credits cr ON
cr.customer_id = c.id WHERE c.id = 5 GROUP BY cr.id ) AS paid
,SUM( i.price + i.discount ) AS bought
FROM customers AS c INNER JOIN sales s ON s.customer_id = c.id
INNER JOIN sales_items i ON i.sale_id = s.id
INNER JOIN credits cr ON cr.customer_id = c.id
WHERE c.id = 5 GROUP BY s.id,cr.id

Exclusive mysql select query, two tables

I have the following tables (they all got more columns but I'm just showing the ones of interest):
Product Order details Orders
---------------------------- ---------------------------- --------------
| id_product | id_supplier | | id_order | id_product | | id_order |
| 12 | 2 | | 1 | 56 | | 1 |
| 32 | 4 | | 2 | 32 | | 2 |
| 56 | 2 | | 2 | 56 | | 3 |
| 10 | 1 | | 4 | 56 | | 4 |
---------------------------- | 3 | 12 | --------------
----------------------------
What I want to do is select all orders which have products from ONLY one or more suppliers. So lets say I want all orders that only have products from the supplier with id 2 (id_supplier = 2) I should get the orders with id 1, 3 and 4.
If I want all orders that ONLY have products from the supplier with id 4 (id_supplier = 4) I should get an empty result.
If I want all orders that ONLY have products from the suppliers with id 2 AND 4 I should get the order with id 2.
I've read the following question: mySQL exclusive records but I can't get a grip of that query to work when I have two tables like I have. I just need another pair of eyes to help me out here! :)
Do you have any idea on how I'll do this?
EDIT: To clearify, I want to fetch all orders that ONLY contains products from one or more specified suppliers. Orders with products from other suppliers than is specified, should not be included.
per the questions I've listed, I think THIS is what you want, and can be done with a LEFT join.
select
od.id_order,
sum( if( p.id_supplier in ( 2, 4 ), 1, 0 )) as HasSupplierLookingFor,
sum( if( p.id_supplier in ( 2, 4 ), 0, 1 )) as HasOtherSuppliers
from
order_Details od
join product p
on od.id_product = p.id_product
group by
od.id_order
having
HasSupplierLookingFor > 0
AND HasOtherSuppliers = 0
Sometimes, just answering a question that can be somewhat ambiguous as presented leads to misrepresented answers. This query will by a per order basis, join to the products to find the suppliers and group by the order id.
For each product ordered, the first SUM() asks if its one of the suppliers you ARE looking for, if so, sum a value of 1, otherwise 0... The next SUM() asks the same thing... but if it IS the supplier, use zero, thus all OTHER suppliers gets the 1.
So, now, the HAVING clause is looking for any order that at a minimum of 1 of your suppliers qualified AND it had no other suppliers represented.
So you could have an order with 30 items, and 20 from supplier 2, and 10 from supplier 4. The HasSupplierLookingFor would = 30, and HasOtherSuppliers = 0, the order would be included.
Another order could have 5 items. One from supplier 2, and 4 others from supplier 9. This would have HasSupplierLookingFor = 1, and HasOtherSuppliers = 4, thus exclude this as a qualified order.
You should inner join all those tables, like this:
SELECT o.* from Orders o
INNER JOIN Details d ON o.id_order = d.id_order
INNER JOIN Products p ON d.id_product = p.id_product
WHERE p.id_supplier = 4
That will give you the orders which include products from that supplier.
SELECT o.id_order
FROM Orders o
INNER JOIN `Order details` od
ON o.id_order = od.id_order
INNER JOIN Product p
ON p.id_product = od.id_product
WHERE p.id_supplier IN (2,4)
the (2,4) are the suppliers you want to fetch. you can also ask for only 1 by saying (2)

Using SUM to count three tables with relevant records

I'm trying to use the SUM function to count rows from 3 tables, which is however, not working effectively since when the total_files and total_notes are returned, they both are the same when there is at least one file and then total_files will take the same value as total_notes which I don't understand why it's doing that.
It should count the number of rows which is relevant to each record that will get return as a record list with a count of total files, total notes and total contacts assigned to the record per record row (the data of files, notes and contacts do not get displayed only counted).
My query is shown below:
SELECT rec.street_number,
rec.street_name,
rec.city,
rec.state,
rec.country,
rec.latitude,
rec.longitude,
LEFT(rec.description, 250) AS description,
usr.username,
usr.full_name,
ppl.person_id,
ppl.first_name,
ppl.last_name,
SUM(IF(rlk.record_id = rec.record_id, 1, 0)) AS total_contacts,
SUM(IF(files.record_id = rec.record_id, 1, 0)) AS total_files,
SUM(IF(notes.record_id = rec.record_id, 1, 0)) AS total_notes,
(
SELECT COUNT(DISTINCT rec.record_id)
FROM records rec
WHERE rec.marked_delete = 0 AND rec.is_archive = 0
) AS total_records
FROM
(
records rec
INNER JOIN members usr ON rec.user_id = usr.user_id
LEFT OUTER JOIN record_links rlk ON rec.record_id = rlk.record_id
LEFT OUTER JOIN people ppl ON ppl.person_id = rlk.person_id AND rlk.record_id = rec.record_id
LEFT OUTER JOIN files files ON files.record_id = rec.record_id
LEFT OUTER JOIN notes notes ON notes.record_id = rec.record_id
)
WHERE rec.marked_delete = 0 AND rec.is_archive = 0
GROUP BY rec.record_id
ORDER BY rec.submit_date DESC
LIMIT 0, 25
Basically as you can see there is three SUM which will count relevant rows that comes from those tables, but I seriously don't understand how total_files would be taking the same value as total_notes is there something wrong I'm doing here?
It's because rec is joined to both notes and files.
Suppose record 1 has 2 notes and 1 file, record 2 has two note and two files, and record 3 has a note but no files.
Then the table rec LEFT OUTER JOIN files ... LEFT OUTER JOIN notes will look like this:
+-----------+---------+---------+
| record_id | file_id | note_id |
+-----------+---------+---------+
| 1 | 1 | 1 |
| 1 | 2 | 1 |
| 2 | 3 | 2 |
| 2 | 3 | 3 |
| 2 | 4 | 2 |
| 2 | 4 | 3 |
| 3 | NULL | 4 |
+-----------+---------+---------+
Note how every file_id gets joined to every note_id (within the same record_id). Also, since you have SUM(IF(files.record_id = rec.record_id,1,0)) and the join condition is files.record_id = rec.record_id, you are actually counting COUNT(files)*COUNT(notes) per record_id.
I'd recommend you instead COUNT(DISTINCT files.id) and COUNT(DISTINCT records.id). The column in the COUNT would be your primary key on files/notes, not files.record_id:
SELECT rec.record_id,
COUNT(DISTINCT files.id) AS total_files,
COUNT(DISTINCT notes.id) AS total_notes
FROM rec
-- note: LEFT OUTER JOIN is the same as LEFT JOIN in MySQL
LEFT JOIN files ON files.record_id=rec.record_id
LEFT JOIN notes ON notes.record_id=rec.record_id
GROUP BY record_id
+-----------+-------------+-------------+
| record_id | total_files | total_notes |
+-----------+-------------+-------------+
| 1 | 2 | 1 |
| 2 | 2 | 2 |
| 3 | 0 | 1 |
+-----------+-------------+-------------+
Of course, adjust to your query as necessary (add in those extra columns/joins).