Select all items and count in related table by criteria - mysql

I have tables Match and Reaction as following:
REACTION
+----------+----------+----------+----------+
| user_id | game_id | item_id | reaction |
+----------+----------+----------+----------+
| 1 | 1 | 1 | 1 |
| 1 | 1 | 2 | 1 |
| 2 | 1 | 1 | 1 |
| 2 | 1 | 2 | 0 |
+----------+----------+----------+----------+
MATCH:
+----------+----------+
| game_id | item_id |
+----------+----------+
| 1 | 1 |
| 1 | 2 |
+----------+----------+
Now I want (if possible without subqueries) to select ALL item_ids from MATCH table AND count of rows where field reaction in table Reaction is equal to 1 for user with id = 2. For example, for defined tables I want to get following results:
+----------+----------+
| item_id | count |
+----------+----------+
| 1 |  1 |
| 2 | 0 |
+----------+----------+
I've tried something like
SELECT match.item_id, COUNT(reaction.user_id) as c
FROM match
LEFT JOIN reaction ON reaction.item_id = match.item_id
WHERE reaction.reaction = 1 AND match.game_id = 2
GROUP BY match.item_id
HAVING c > 0
but it didn't work as expected. I cannot get count for particular user.

I think you are close. I think you just need to move conditions on the second table to the ON clause:
SELECT m.item_id, COUNT(r.user_id) as c
FROM match m LEFT JOIN
reaction r
ON r.item_id = m.item_id AND
r.reaction = 1 AND
r.user_id = 2
WHERE m.game_id = 2
GROUP BY m.item_id;
I'm not sure what the HAVING clause is for, because you seem to want counts of 0.
Note that this also introduces table aliases so the query is easier to write and to read.

SELECT match.item_id, COUNT(reaction.user_id) as c
FROM match JOIN reaction ON (reaction.item_id = match.item_id and reaction.reaction = 1 AND match.game_id = 2)
GROUP BY match.item_id
HAVING COUNT(reaction.user_id)
I think you need to filter 'before' join -> so use the 'on' clause.
Filters in where are applied after the join is made while filter applied on on clause are applied before the join is made

You have not game_id = 2 so this should return no value
and you should not use left joined table columns in where condition otherwise these wprk as inner join ... in these cases you shou move the related condition in ON clause
SELECT match.item_id, COUNT(reaction.user_id) as c
FROM match
LEFT JOIN reaction ON reaction.item_id = match.item_id
AND reaction.reaction = 1
WHERE match.game_id = 2
GROUP BY match.item_id
HAVING c > 0
but try also
SELECT match.item_id, COUNT(reaction.user_id) as c
FROM match
LEFT JOIN reaction ON reaction.item_id = match.item_id
AND reaction.reaction = 1
GROUP BY match.item_id

Related

How to exclude rows when a certain condition is met in MySQL

I have two datatables. One table contains information of participants (Participants). And a table which contains all registrations for the event (Registrars).
+----+-------+
| id | name |
+----+-------+
| 1 | Peter |
+----+-------+
| 2 | John |
+----+-------+
+-----------+----------+
| person_id | event_id |
+-----------+----------+
| 1 | 1 |
+-----------+----------+
| 2 | 2 |
+-----------+----------+
| 2 | 1 |
+-----------+----------+
I run a MySQL query to get the information of registrars of an event. The query looks like this:
SELECT name
FROM Participants
INNER JOIN Registrars
ON Participants.id=Registrars.person_id
WHERE
event_id = 1
Now I want to build a query where if the participant also registered for event 2 it will not be returned in the result of the query the reg. How can I achieve this in one query?
You can group by participant and use conditional aggregation in the HAVING clause to set the conditions:
SELECT p.id, p.name
FROM Participants p INNER JOIN Registrars r
ON p.id = r.person_id
WHERE r.event_id IN (1, 2)
GROUP BY p.id, p.name
HAVING MAX(r.event_id = 1) = 1
AND MAX(r.event_id = 2) = 0
You can extend the code by adding more conditions in a similar way.
SELECT name
FROM Participants as p
INNER JOIN Registrars as reg
ON P.id=Reg.person_id
WHERE
reg.event_id = 1
and not exists (select 1 from Registrars as strr where strr.event_id=2 and p.id=strr.person_id )

MySQL left join that shows NULL for missing rows

I have 2 tables
Table:
recip
recipid | recipname
1 | Recip1
2 | Recip2
And table:
recipuser
recipid | userid
1 | 1
2 | 1
1 | 2
So userid 2 has 1 recip
The result I'm trying to achieve is to show all "recip" rows with matching or null for given user id, EG:
SELECT r.recipid, r.recipname, ru.userid
FROM recip r
left JOIN recipuser ru ON r.recipid = ru.recipid
WHERE ru.userid = 2 OR ru.userid IS NULL
Results in:
recipid | recipname | userid
1 | Recip1 | 2
I want to get:
recipid | recipname | userid
1 | Recip1 | 2
2 | Recip2 | NULL
How do I show all rows from recip with the userid or NULL for every row given a user id??
Thanks for your help.
Move the WHERE logic to the ON clause:
SELECT r.recipid, r.recipname, ru.userid
FROM recip r
LEFT JOIN recipuser ru
ON r.recipid = ru.recipid AND ru.userid = 2;
The problem with your current query is that the WHERE clause is filtering off the non matching record which you want to appear.

SELECT not working when using != in WHERE clause (using GROUP BY and HAVING COUNT)

This question is based on: Select row from left join table where multiple conditions are true
I am now trying to select rows from Table 1, which do not have a connection in Table 2 to a certain property ID.
These are the tables:
Table 1
| ID | Name |
| 1 | test |
| 2 | hello |
Table 2
| ID | PropertyID |
| 1 | 3 |
| 1 | 6 |
| 1 | 7 |
| 2 | 6 |
| 2 | 1 |
I am using the following query (which is working with '='):
SELECT tab1ID
FROM table2
WHERE propertyID != 3 OR propertyID = 6
GROUP BY tab1ID
HAVING COUNT(*) = 2;
This query should return ID=2, but it returns zero rows. What I am doing wrong?
Any help is greatly appreciated!
Edit: I had given a MWE but this is my actual query:
SELECT transactionline.total FROM transactionline
LEFT JOIN product_variant ON product_variant.SKU = transactionline.SKU
LEFT JOIN product ON product_variant.productID = product.productID
LEFT JOIN connect_option_product ON connect_option_product.productID = product.productID
LEFT JOIN productattribute_option ON productattribute_option.optionID = connect_option_product.optionID
WHERE productattribute_option.optionID = 4 OR productattribute_option.optionID = 9
GROUP BY transactionline.lineID
HAVING COUNT(*) = 1
AND SUM(productattribute_option.optionID = 4) = 0
AND SUM(productattribute_option.optionID = 9) > 0
A product can have multiple connections to the optionID's. The goal of this query is to select the total amount where some filters are true or false.
Your grouping is correct. But you need to count how many times the value you do not want is in your group. That count must be zero.
SELECT tab1ID
FROM table2
GROUP BY tab1ID
HAVING sum(propertyID = 6) > 0
AND sum(propertyID = 3) = 0

Mysql search on multiple join results

I've a table "products" and a table where are store some attributes of a product:
zd_products
----------
|ID|title|
----------
| 1| Test|
| 2| Prod|
| 3| Colr|
zd_product_attached_attributes
------------------
|attrid|pid|value|
------------------
|1 | 1 | A |
|2 | 1 | 10 |
|3 | 1 | AB |
|1 | 2 | B |
|2 | 2 | 22 |
|3 | 2 | BB |
|1 | 3 | A |
|2 | 3 | 10 |
|3 | 3 | CC |
I want to search in zd_products only the products that have some attributes values, for exam place
Get the product when the attribute 1 is A and the attribute 3 is AB
Get the product when the attribute 2 is 10 and the attribute 3 is CC
etc
How can i do this using a join ?
Oh, the Joys of the EAV model!
One way is to use a separate JOIN operation for each attribute value. For example:
SELECT p.id
, p.title
FROM zd_products p
JOIN zd_product_attached_attributes a1
ON a1.pid = p.id
AND a1.attrid = 1
AND a1.value = 'A'
JOIN zd_product_attached_attributes a3
ON a3.pid = p.id
AND a3.attrid = 3
AND a3.value = 'AB'
With appropriate indexes, that's likely going to be the most efficient approach. This isn't the only query that will return the specified result, but this one does make use of JOIN operations.
Another, less intuitive approach
If id is unique in the zd_products table, and we have guarantee that the (attrid,pid,value) tuple is unique in the zd_product_attached_attributes table, then this:
SELECT p.id
, p.title
FROM zd_products p
JOIN zd_product_attached_attributes a
ON a.pid = p.id
AND ( (a.attrid = 1 AND a.value = 'A')
OR (a.attrid = 3 AND a.value = 'AB')
)
GROUP
BY p.id
, p.title
HAVING COUNT(1) > 1
will return an equivalent result. The latter query is of a form that is particularly suitable for matching two criteria out of three, where we don't need a match on ALL of the attributes, but just some of them. For example, finding a product that matches any two of:
color = 'yellow'
size = 'bigger'
special = 'on fire'
And of course there are other approaches that don't make use of a JOIN.
FOLLOWUP
Q: And if I want to the same but using OR operator? I mean get ONLY if the attribute 1 is A or the attribute 2 is AB otherwise don't select the record.
A: A query of the form like the second one in my answer (above) is more conducive to the OR condition.
If you want XOR (exclusive OR), where one of the attributes has a matching value but the other one doesn't, just change the HAVING COUNT(1) > 1 to HAVING COUNT(1) = 1. Only rows from products that find one "matching" row in the attributes table will be returned. To match exactly 2 (out of several), HAVING COUNT(1) = 2, etc.
A query like the first one in my answer can be modified to use OUTER joins, to find matches, and then do a conditional test in the WHERE clause, to determine if a match was found.
SELECT p.id
, p.title
FROM zd_products p
LEFT
JOIN zd_product_attached_attributes a1
ON a1.pid = p.id
AND a1.attrid = 1
AND a1.value = 'A'
LEFT
JOIN zd_product_attached_attributes a3
ON a3.pid = p.id
AND a3.attrid = 3
AND a3.value = 'AB'
WHERE a1.pid IS NOT NULL
OR a3.pid IS NOT NULL
I've just added the LEFT keyword, to specify an outer join; rows from products will be returned with matching rows from a1 and a3, along with rows from products that don't have any matching rows found in a1 or a3.
The WHERE clause tests a column from a1 and a3 to see whether a matching row was returned. If a matching row was found in a1, we are guaranteed that the pid column from a1 will be non-NULL. That column will be returned as NULL only if a matching row was not found.
If we replaced the OR with an AND, we'd be negating the "outerness" of both joins, making it essentially equivalent to the first query above.
To get an XOR type operation (exclusive OR) where we find one matching attribute but not the other, we could change the WHERE clause to read:
WHERE (a1.pid IS NOT NULL AND a3.pid IS NULL)
OR (a3.pid IS NOT NULL AND a1.pid IS NULL)
Use a pivot
You can do this type of query using a pivot. As far as I know, MySQL doesn't have a native, built in pivot, but you can achieve this by transposing the rows and columns of your zd_product_attached_attributes table using:
SELECT pid,
MAX(CASE WHEN attrid = 1 THEN value END) `attrid_1`,
MAX(CASE WHEN attrid = 2 THEN value END) `attrid_2`,
MAX(CASE WHEN attrid = 3 THEN value END) `attrid_3`
FROM zd_product_attached_attributes
GROUP BY pid
This will pivot your table as shown:
+----+---------+-------+ +----+----------+----------+----------+
| attrid | pid | value | | pid| attrid_1 | attrid_2 | attrid_3 |
+----+---+-------------+ +----+----------+----------+----------+
| 1 | 1 | A | | 1 | A | 10 | AB |
| 2 | 1 | 10 | => | 2 | B | 22 | BB |
| 3 | 1 | AB | | 3 | A | 10 | CC |
| 1 | 2 | B | +----+----------+----------+----------+
| 2 | 2 | 22 |
| 3 | 2 | BB |
| 1 | 3 | A |
| 2 | 3 | 10 |
| 3 | 3 | CC |
+--------+---------+---+
So you can select the products id and title using:
SELECT id, title FROM zd_products
LEFT JOIN
(
SELECT pid,
MAX(CASE WHEN attrid = 1 THEN value END) `attrid_1`,
MAX(CASE WHEN attrid = 2 THEN value END) `attrid_2`,
MAX(CASE WHEN attrid = 3 THEN value END) `attrid_3`
FROM zd_product_attached_attributes
GROUP BY pid
) AS attrib_search
ON id = pid
WHERE ( attrib_1 = 'A' AND attrib_3 = 'AB' )
OR ( attrib_2 = 10 AND attrib_3 = 'CC' )
Note: You can use this type of query when you have guaranteed uniqueness on (pid, attrid)
(thanks #spencer7593)
I haven't tested this, but I think it should work:
select title
from zd_products p
join zd_product_attached_attributes a ON a.pid = p.id
where ( attrid = 1 and value = 'A' )
or ( attrid = 3 and value = 'AB' );
If you want to tack on more "searches" you could append more lines similar to the last one (ie. or "or" statements)

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).