Denormalize the data - mysql

I have three normalized Tables as follows
person_info person_phone phone_types
------------ ------------------------------- --------------
| id | Name | | p_id | typeid | value | | id | type |
|----|-------| |---------|-----------|--------| |----|---------|
| 1 | Sam | | 1 | 1 | 4455544| | 1 | landline|
| 2 | Bill | | 1 | 1 | 889898 | | 2 | mobile |
| 3 | Ted | | 1 | 2 | 8898999| | 3 | fax |
------------ ------------------------------ --------------
Every person can have more than one mobile or Landline or Fax. In the above example "SAM" has two landline numbers and 1 mobile number.
I am trying to retrieve the data for SAM as follows
SELECT
pi.id,
pi.name,
MAX(CASE WHEN pt.type = 'landline' THEN pp.value ELSE NULL END) AS landline,
MAX(CASE WHEN pt.type = 'mobile' THEN pp.value ELSE NULL END) AS mobile,
MAX(CASE WHEN pt.type = 'fax' THEN pp.value ELSE NULL END) AS fax,
FROM
person_phone as pp
INNER JOIN phone_types AS pt on pp.type_id=pt.id
INNER JOIN person_info AS pi on pi.id =pp.p_id
GROUP BY pi.id;
This gives me just the last added phone number for each time.
id name landline mobile fax
1 Sam 889898 8898999 NULL
but I want all the contact numbers which they enter(It can be 'n' number of landline/mobile numbers for each person) displayed as landline1, landline2, landline3 and so on.
If my method is wrong, please suggest some other proper method.
EDIT
I am changing the condition from 'n' to a fixed number. No person can have more than 5 Landline, 5 mobile and 2 fax numbers. So there is a Maximum number limit per person now.

The solution that comes to mind is to use group concat to get all the numbers, delimited with something, and then let the application handle the presentation.

Related

Get all person rows when left joined values missing [duplicate]

I have two tables and need to get all rows from the first one and then check which values from the second table match the first table.
My goal is to get all so called 'achievements' and then check which one the user has reached.
achievements
+---------------+-------------+
| achievementID | description |
+---------------+-------------+
| 1 | goal1 |
| 2 | goal2 |
| 3 | goal3 |
+---------------+-------------+
achievement_user
+---------------------+---------------+--------+
| achievementRecordID | achievementID | userID |
+---------------------+---------------+--------+
| 1 | 1 | 1 |
| 2 | 1 | 3 |
| 3 | 4 | 2 |
| 4 | 3 | 1 |
+---------------------+---------------+--------+
My desired result for a query where I check the results for userID = 1 would be something like
+---------------+---------------+--------------+
| achievementID | description | solvedByUser |
+---------------+---------------+--------------+
| 1 | goal1 | true |
| 2 | goal2 | false |
| 3 | goal3 | true |
+---------------+---------------+--------------+
The new column solvedByUser could be basically any datatype (boolean, int, ...).
I just need a list of all available achievements and then see which one the user has reached.
You can left join the achievments table with achievement_user:
select a.*, (au.userID is not null) solvedByUser
from achievements a
left join achievement_user au
on au.achievementID = a.achievementID
and au.userID = 1
solvedByUser is a 0/1 flag that indicates whether the given achievement was reached by the given user.
I think you need a left join -
SELECT A.achievementID, A.description, CASE WHEN AU.userID IS NOT NULL THEN 'true' ELSE 'false' solvedByUser
FROM achievements A
LEFT JOIN achievement_user AU ON A.achievementID = AU.achievementID
AND userID = 1; -- YOUR INPUT ID
You need a left join:
select a.*,
case when u.achievementID is null then 'false' else 'true' end solvedByUser
from achievements a left join achievement_user u
on u.achievementID = a.achievementID and u.userid = 1

How to join has many relation table and fetch result by type

I have a few tables which I am trying to join and fetch the results for a list
Interviews Table
+--------------+-----------+
| interview_id | Candidate |
+--------------+-----------+
| 1 | Ram |
| 2 | Rahim |
| 3 | Joseph |
+--------------+-----------+
Participant Ratings Table
+--------------+-----------+-------+
| interview_id | Rater Type|Rating |
+--------------+-----------+-------+
| 1 | Candidate | 4 |
| 2 | Candidate | 4 |
| 1 | Recruiter | 5 |
+--------------+-----------+-------+
System Ratings Table
+--------------+------------+-------+
| interview_id | Rating Type|Rating |
+--------------+------------+-------+
| 1 | Quality | 4 |
| 1 | Depth | 4 |
| 1 | Accuracy | 5 |
| 2 | Quality | 4 |
| 2 | Depth | 3 |
| 2 | Accuracy | 5 |
| 3 | Quality | 4 |
| 3 | Depth | 5 |
| 3 | Accuracy | 5 |
+--------------+------------+-------+
I need to fetch the result of average ratings for each interview given in the following manner.
+--------------+--------------+-----------------+-----------------+
| interview_id | System Rating|Recruiter Rating |Candidate Rating |
+--------------+--------------+-----------------+-----------------+
| 1 | 4.3 | 5 | 4 |
| 2 | 4.0 | 0 | 4 |
| 3 | 4.6 | 0 | 0 |
+--------------+--------------+-----------------+-----------------+
Each interview can will have one 1 candidate rating and 1 recruiter rating but that is optional. If given a record is created in participant rating with rating and type.
Need to get the average of system ratings of all the types and get one value as system rating and if rating provided by participants then display else display as 0 if any or both the participants not provided any rating.
Please ignore the values, if there is a mistake.
The SQL which I tried to get the result.
SELECT i.candidate, i.id AS interview_id,
AVG(sr.rating) AS system_rating,
AVG(CASE WHEN pr.rater_type = 'Candidate' THEN pr.rating END) AS candidate_rating,
AVG(CASE WHEN pr.rater_type = 'Recruiter' THEN pr.rating END) AS recruiter_rating
FROM system_ratings sr, participant_ratings pr, interviews i
WHERE sr.interview_id = i.id AND i.id = 2497 AND pr.interview_id = i.interview_id
The problem is whenever participant ratings are not present then results are missing as there is join.
Use LEFT JOIN to make sure if relation tables do not have any data, still we can have records from the main table.
Reference: Understanding MySQL LEFT JOIN
Issue(s):
Wrong field name: pr.interview_id = i.interview_id, it should be pr.interview_id = i.id as we don't have any interview_id field in interviews table, it would be id field - based on your query.
pr.interview_id = i.id in where clause: If participant_rating table does not have any records for a given interview, this will cause the removal of that interview from the result set. Use LEFT JOIN for participant_rating table.
sr.interview_id = i.id in where clause: If system_rating table does not have any records for a given interview, this will cause the removal of that interview from the result set. Use LEFT JOIN for system_rating table too.
Usage of AVG works but won't work for other aggregates functions like SUM, COUNT.. because if we have one to many relationships then join will make there will be multiple records for the same row.
Solution:
SELECT
i.id AS interview_id,
i.candidate,
AVG(sr.rating) AS system_rating,
AVG(CASE WHEN pr.rater_type = 'Candidate' THEN pr.rating END) AS candidate_rating,
AVG(CASE WHEN pr.rater_type = 'Recruiter' THEN pr.rating END) AS recruiter_rating
FROM interviews i
LEFT JOIN system_rating sr ON sr.interview_id = i.id
LEFT JOIN participant_rating pr ON pr.interview_id = i.id
-- WHERE i.id IN (1, 2, 3) -- use whenever required
GROUP BY i.id

MySQL count when value is not equal

I have few tables.
1.region
2.restaurant
3.restaurant_itmes
region
id | name
................
1 | NY
2 | Paris
3 | London
restaurant
id | name | region_id
.........................
1 | KFC | 1
2 | McDonals'| 1
3 | La res | 2
4 | Queen's | 3
restaurant_items
id | name | restaurant_id | pro_pic | featured_pic
...................................................
1 |Pizza |3 | null | defaut.jpg
2 |Pizza |4 | pizza.jpg | defaut.jpg
3 |Burger|1 | burger.jpg| burger.jpg
4 |Burger|2 | burger.jpg| burger.jpg
5 |Burger|3 | null | burger.jpg
6 |Burger|4 | null | default.jpg
7 |Donat |2 | null | default.jpg
8 |Fries |2 | null | default.jpg
I want to generate an query to populate this table
region |Number of restaurants |total items | items_with_pro_pic | items_with_featured_pic
............................................................................................
NY | 2 | 4 | 2 | 2
Paris | 1 | 2 | 0 | 1
London | 1 | 2 | 1 | 0
What I have done upto now is
SELECT region.name, count(restaurant_items.id) as total_items, count(restaurant_items.pro_pic)
INNER JOIN restaurant on restaurant_items.restaurant_id = restaurant.id
INNER JOIN region on restaurant.region_id = region.id
GROUP BY region.name;
Here I can get items_with_pro_pic by count(restaurant_items.pro_pic)
but I can't do that for items_with_featured_pic because featured_pic is not null able if there is no value
default value is default.jpg.
So I tried count(restaurant_items.featured_pic != 'defaut.jpg') but this doesn't work for me.
And how could I get number of restaurants since it is not a part of restaurant_items table?
How do I achieve these two using MySQL?
You can change the COUNT to a SUM and run it over an IF statement:
SUM(IF(restaurant_items.featured_pic != 'default.jpg',1,0))
Or alternatively you can specify it as a COUNT if you want, but the ELSE portion will need to be a NULL rather than a 0 since otherwise it will still count it:
COUNT(IF(restaurant_items.featured_pic != 'default.jpg',1,NULL))
To count number of restaurants, you can simply do a distinct count:
COUNT(DISTINCT restaurant.id)
A few small extra tips:
You may want to change the name of 'restaurant_items' to 'restaurant_item' since that suits the naming convention of the other tables
You should be aliasing your table names in the FROM and JOIN clauses, since it enhances code legibility
You can use a case expression to produce nulls instead of default.jpg in order to use in a count function:
SELECT region.name,
COUNT(restaurant_items.id) as total_items,
COUNT(restaurant_items.pro_pic),
COUNT(CASE WHEN restaurant_items.featured_pic != 'default.jpg' THEN 1 END)
INNER JOIN restaurant ON restaurant_items.restaurant_id = restaurant.id
INNER JOIN region ON restaurant.region_id = region.id
GROUP BY region.name;
use case when
SELECT region.name, count(restaurant_items.id) as total_items, count(restaurant_items.pro_pic),
count(case when restaurant_items.featured_pic != 'defaut.jpg' then 1 end) as
items_with_featured_pic,
count(case when pro_pic is null then 1 end) as
items_with_pro_pic_null
INNER JOIN restaurant on restaurant_items.restaurant_id = restaurant.id
INNER JOIN region on restaurant.region_id = region.id
GROUP BY region.name

GroupBy ID but keep multiple values

I have the following MySQL table:
+----------+----------+---------+-------------+------------+----------+----------+-----------+
| queue_id | email_id | user_id | customer_id | send_date | campaign | approved | scheduled |
+----------+----------+---------+-------------+------------+----------+----------+-----------+
| 1 | 1 | 1 | 1 | 2018-10-30 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 | 2018-10-30 | 1 | 1 | 1 |
| 3 | 2 | 1 | 1 | 2018-11-02 | 1 | 1 | 1 |
| 4 | 2 | 2 | 1 | 2018-11-02 | 1 | 0 | 1 |
| 5 | 2 | 3 | 1 | 2018-11-02 | 1 | 1 | 1 |
+----------+----------+---------+-------------+------------+----------+----------+-----------+
Where the email_id, user_id, and customer_id are all foreign keys.
What I need to do is return the send_date, subject (which is apart of the email table that the email_id references), and name (which is apart of the business table that the user_id references) but only for columns where the approved column is true. The idea is to ultimately display the data to a user in an HTML table where the table would look like the following (using the sample data provided):
+--------------------+--------------------------+---------------+
| October 30th, 2018 | Subject for email_id "1" | View Approved |
| November 2nd, 2018 | Subject for email_id "2" | View Approved |
+--------------------+--------------------------+---------------+
Whenever the user would click on the "View Approved" cell, then it would display all of the business names that approved that particular email.
I tried using the following query, but it is only returning one value in the name column:
SELECT
DATE_FORMAT(q.`send_date`, "%M %D, %Y") AS `date_visited`,
e.`subject`,
b.`name`
FROM
`email_queue` AS q
INNER JOIN
`email` AS e ON q.`email_id` = e.`email_id`
INNER JOIN
`user` AS u ON q.`user_id` = u.`user_id`
INNER JOIN
`business` AS b ON u.`business_id` = b.`business_id`
WHERE
q.`approved` = true
GROUP BY
e.`email_id`
ORDER BY
q.`send_date` DESC
How can I structure my query to where it would return all of the business names in the name column instead of just one?
You can get all the unqiue business names in a Comma separated string, using Group_Concat() function with Distinct clause.
Try:
GROUP_CONCAT(DISTINCT b.`name` SEPARATOR ',') AS name
instead of:
b.`name`
Note:
You can avoid the usage of Distinct clause, if there would not be any duplicate user_id (for a specific email_id), thus ensuring that b.name is also unique.
You can also use any separator, instead of comma. For eg: to use separator as pipe character |, you would write the query as:
GROUP_CONCAT(DISTINCT b.nameSEPARATOR '|') AS name

MySQL select a single record from highest join value from multiple tables with multiple records

I have the following tables:
members
This stores a list of members for our system.
---------------------
| member_id | name |
---------------------
| 1 | Bob |
---------------------
| 2 | Joe |
---------------------
| 3 | Tom |
---------------------
| 4 | Bill |
---------------------
| 5 | Will |
---------------------
categories
This stores the categories for our system. Categories are not visible to members by default. A member must have a valid licence to be able to access a category (see below).
----------------------
| cat_id | name |
----------------------
| 1 | Cat1 |
----------------------
| 2 | Cat2 |
----------------------
| 3 | Cat3 |
----------------------
licences
Stores the licences that a member has. One member can have many licences. Licences can have a life time and will expire. Once a licence expires, the member can no longer view the category.
------------------------------------------------------
| id | catid | subid | valid_from | valid_to |
------------------------------------------------------
| 1 | 1 | 1 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 2 | 1 | 2 | 1999-01-01 | 2001-01-02 |
------------------------------------------------------
| 3 | 1 | 3 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 4 | 1 | 4 | 1999-01-01 | 2000-01-01 |
------------------------------------------------------
| 5 | 1 | 5 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 6 | 2 | 1 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 7 | 2 | 2 | 1999-01-01 | 2001-01-02 |
------------------------------------------------------
| 8 | 2 | 3 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 9 | 2 | 4 | 1999-01-01 | 2000-01-01 |
------------------------------------------------------
| 10 | 2 | 5 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 11 | 3 | 1 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
| 12 | 3 | 2 | 2014-01-01 | 2020-12-01 |
------------------------------------------------------
preferences
The preferences table stores whether a member wishes to receive e-mails that are are sent relating to a category. The member can set a preference of '1' for 'wish to receive' or '0' for 'do not wish to receive'. A quirk is that if the member has no record (or a null value) we make an assumption that they wish to receive.
-----------------------------------
| id | catid | subid | pref |
-----------------------------------
| 1 | 1 | 1 | 0 |
-----------------------------------
| 2 | 2 | 1 | 1 |
-----------------------------------
| 3 | 3 | 1 | 1 |
-----------------------------------
| 4 | 1 | 2 | 0 |
-----------------------------------
| 5 | 1 | 3 | 1 |
-----------------------------------
| 6 | 2 | 3 | 0 |
-----------------------------------
recipients
When an email is sent out based on a category, the recipient is logged so we don't email them more than once.
-----------------------------
| id | emailid | subid |
-----------------------------
| 1 | 1 | 1 |
-----------------------------
| 2 | 1 | 2 |
-----------------------------
I'm trying to write a query the fetches all members, and their related licence for a range of category IDs, their preferences and also make sure that they don't have a record in the recipients table.
In pseudo query:
SELECT [all members, their licence info, and preference setting]
FROM [members table]
WHERE [member doesnt exist in the recipients table for a given emailID]
The issue is that I need to check against multiple categoryIDs, but return just one result and only if the preference is set to 1 (or null, or doesn't exist).
So with the example data, given we are searching for categoryIDs 1,2 and 3 (A member must have a licence for at least one of these categories) and checking against emailID of 1, the only result should be for member_id 3 (Tom) with preference ID of 6 (because it's set to 1) and licence ID of 3 (because it's valid and the preference ID of 6 corresponds to it and it is set to 1). The second result should be member_id 5 (Will) as he has a licence to catids 1 and 2, he hasn't received the email with ID of 1 and he has no specific preference set.
Reason being: Members 1 and 2 are in the recipient table for emailID 1, member 2's licence has also expired, member 4's licence has expired and member 5 has their preference set to 0.
The query I've written which isn't working quite right is:
SELECT
members.member_id,
members.name,
licence.catid as licencedToCat,
categories.cat_name as categoryName,
licence.valid_from as licenceStart,
licence.valid_to as licenceEnd,
preferences.pref
FROM (`members`)
JOIN `licence` ON `licence`.`subid`=`members`.`member_id`
JOIN `preferences` ON `preferences`.`subid`=`members`.`member_id`
JOIN `categories` ON `categories`.`cat_id`=`licence`.`catid`
WHERE `licence`.`catid` IN (1,2,3)
AND `start_date` <= '2014-12-16'
AND `end_date` >= '2014-12-16'
AND (pref='1' OR pref IS NULL)
AND `members`.`member_id` NOT IN (SELECT subid FROM `recipients` WHERE `recipients`.`emailid`='1')
GROUP BY `licence`.`subid`
The issue is that the query is returning results saying users have a preference set to 1 where they actually don't even have a record set for that category.
The desired output is any member(s) along with the licence they have for the category but only if their preference for that category is 1/null/doesn't exist AND only if they don't appear in the recipients table for a given emailID.
So, if a member has 2 licences
I appreciate this was a long read, so thanks if you're still here! Any ideas on how to tweak my query to solve this?
I think part of your problem here is that you're using all inner joins. Like you said, a user may not have a preference, so a row may not be returned in your query. That being said, it seems like you want to inner join most tables, as it appears you only want members who have licenses, but you want to see all licenses regardless of whether that user has a preference. So, I made preferences an outer joined table:
SELECT m.*, l.catid AS licenseCat, c.name AS categoryName,
l.valid_from AS licenseStart, l.valid_to AS licenseEnd, p.pref AS preference
FROM members m
JOIN licenses l ON l.subid = m.member_id
JOIN categories c ON c.cat_id = l.catid
LEFT JOIN preferences p ON p.catid = c.cat_id AND p.subid = l.subid;
Once I had done that, I wrote the subquery that pulled the member_id of all members who are in the recipients table with the specified email:
SELECT subid
FROM recipients
WHERE emailid = 1;
Now you can insert that into your original query, and add your other requirements:
SELECT m.*, l.catid AS licenseCat, c.name AS categoryName,
l.valid_from AS licenseStart, l.valid_to AS licenseEnd, IFNULL(p.pref, 0) AS preference
FROM members m
JOIN licenses l ON l.subid = m.member_id
JOIN categories c ON c.cat_id = l.catid
LEFT JOIN preferences p ON p.catid = c.cat_id AND p.subid = l.subid
WHERE c.cat_id IN (1, 2, 3) AND
l.valid_from <= '2014-12-06' AND l.valid_to >= '2014-12-06' AND
m.member_id NOT IN (SELECT subid FROM recipients WHERE emailid = 1)
AND (p.pref = 1 OR p.pref IS NULL);
You said in your question that this should return member_id 3 (which is Tom) but that does not match your results because member 5 has no preferences, so we should assume they want an email right? I'm also not sure how to group this for you. If a member has multiple subscriptions, which one do you want to keep?
I built an SQL Fiddle and tested what I have and it's really close. I hope this can at least push you in the right direction and I will edit the answer as needed.
EDIT
The following will give you what you want, but it is not always recommended. If you really don't care about the subscription dates (as long as it meets the criteria in the where clause) and you really don't care about the category for the user, just add GROUP BY m.member_id to get one row for each member.
So, the final query is like these, tested and working:
SELECT
m.member_id,
m.email,
l.catid as licencedToCat,
c.cat_name as categoryName,
l.valid_from as licenceStart,
l.valid_to as licenceEnd,
COALESCE(p.pref, 1) pref
FROM members m
JOIN licence l ON l.subid = m.member_id
JOIN categories c ON c.cat_id = l.catid
LEFT JOIN preferences p ON p.subid= m.member_id AND p.cat_id = l.cat_id
LEFT JOIN recipients r ON r.subid = m.member_id
WHERE l.catid IN (1,2,3)
AND start_date <= '2014-12-16' AND end_date >= '2014-12-16'
AND COALESCE(p.pref, 1) = 1
AND COALESCE(r.emailid, 0) = 0-- assuming with emailid = 0 it remains valid as recipient
GROUP BY m.member_id
However, for the purpose of the query it should only have DISTINCT m.* in the SELECT clause which would discard the GROUP BY