How to do a subquery in group_concat - mysql

I have the following data model:
`title`
- id
- name
`version`
- id
- name
- title_id
`version_price`
- id
- version_id
- store
- price
And here is an example of the data:
`title`
- id=1, name=titanic
- id=2, name=avatar
`version`
- id=1, name="titanic (dubbed in spanish)", title_id=1
- id=2, name="avatar directors cut", title_id=2
- id=3, name="avatar theatrical", title_id=2
`version_price`
- id=1, version_id=1, store=itunes, price=$4.99
- id=1, version_id=1, store=google, price=$4.99
- id=1, version_id=2, store=itunes, price=$5.99
- id=1, version_id=3, store=itunes, price=$5.99
I want to construct a query that will give me all titles that have a version_price on iTunes but not on Google. How would I do this? Here is what I have so far:
select
title.id, title.name, group_concat(distinct store order by store)
from
version inner join title on version.title_id=title.id inner join version_price on version_price.version_id=version.id
group by
title_id
This gives me a group_concat which shows me what I have:
id name group_concat(distinct store order by store)
1 Titanic Google,iTunes
2 Avatar iTunes
But how would I construct a query to include whether the item is on Google (using a case statement or whatever's needed)
id name group_concat(distinct store order by store) on_google
1 Titanic Google,iTunes true
2 Avatar iTunes false
It would basically be doing a group_concat LIKE '%google%' instead of a normal where clause.
Here's a link for a SQL fiddle of the current query I have: http://sqlfiddle.com/#!9/e52b53/1/0

Use conditional aggregation to determine if the title is in a specified store.
select title.id, title.name, group_concat(distinct version_price.store order by store),
if(count(case when store = 'google' then 1 end) >= 1,'true','false') as on_google
from version
inner join title on version.title_id=title.id
inner join version_price on version_price.version_id=version.id
group by title.id, title.name
count(case when store = 'google' then 1 end) >= 1 counts all the rows for a given title after assigning 1 to the rows which have google in them. (Or else they would be assigned null and the count ignores nulls.) Thereafter, the if checks for the countand classifies a title if it has atleast one google store on it.

http://sqlfiddle.com/#!9/b8706/2
you can just:
SELECT
title.id,
title.name,
group_concat(distinct version_price.store),
MAX(IF(version_price.store='google',1,0)) on_google
FROM version
INNER JOIN title
ON version.title_id=title.id
INNER JOIN version_price
ON version_price.version_id=version.id
GROUP BY title_id;
and add HAVING to the query if need records to be filtered:
SELECT
title.id,
title.name,
group_concat(distinct version_price.store),
MAX(IF(version_price.store='google',1,0)) on_google
FROM version
INNER JOIN title
ON version.title_id=title.id
INNER JOIN version_price
ON version_price.version_id=version.id
GROUP BY title_id
HAVING on_google;

This will give you the number of version prices not on google, and the number on google. (COUNT does not count null values.)
SELECT t.id, t.name
, COUNT(DISTINCT vpNotG.id) > 0 AS onOtherThanGoogle
, COUNT(DISTINCT vpG.id) > 0 AS onGoogle
FROM title AS t
INNER JOIN version AS v ON t.id=v.title_id
LEFT JOIN version_price AS vpNotG
ON v.id=vpNotG.version_id
AND vpNotG.store <> 'Google'
LEFT JOIN version_price AS vpG
ON v.id=vpG.version_id
AND vpG.store = 'Google'
GROUP BY t.id
or for another solution similar to vkp's:
SELECT t.id, t.name
, COUNT(DISTINCT CASE WHEN store = 'Google' THEN vp.id ELSE NULL END) AS googlePriceCount
, COUNT(DISTINCT CASE WHEN store = 'iTunes' THEN vp.id ELSE NULL END) AS iTunesPriceCount
, COUNT(DISTINCT CASE WHEN store <> 'Google' THEN vp.id ELSE NULL END) AS nonGooglePriceCount
FROM title AS t
INNER JOIN version AS v ON t.id = v.title_id
INNER JOIN version_price AS vp ON v.id = vp.version_id
GROUP BY t.id
Note: The ELSE NULL can be omitted, because if no ELSE is provided it is implied; but I included for clarity.

I would do it like below
SELECT
*
FROM
title t
INNER JOIN
version v ON
v.title_id = t.id
CROSS APPLY (
SELECT
*
FROM
version_price vp
WHERE
vp.store <> 'google'
) c ON c.version_id == v.id
Syntax may be just a little off as I can't test it right now, but I believe this is the spirit of what you would want. Cross apply is also a very efficient join which is always helpful!

This might be the most inefficient of the above answers, but the following subquery would work, using a %like% condition:
select *, case when stores like '%google%' then 1 else 0 end on_google
from (select title.id, title.name, group_concat(distinct store order by store) stores
from version inner join title on version.title_id=title.id inner join version_price
on version_price.version_id=version.id group by title_id) x
id name stores on_google
1 Titanic Google,iTunes 1
2 Avatar iTunes 0

Related

MySQL using LIKE operator on result of CASE statement and reformatted DATETIME-column

I have a MySQL query where I also need to take a user search-input as a parameter. I was originally going to use the FULLTEXT index to efficiently search all the columns in the table to provide the best search results, but due to limitations in the FULLTEXT index I had to opt for just the LIKE operator. The reason for this is I also need the user to be able to search in the ID column of the table and the DATETIME column, and to my understanding, the FULLTEXT index can only search in columns having a sort of text or character field. Please correct me if I am wrong.
Anyways it works all fine, but in the SELECT part of the query I have 1 CASE function and 2 DATE_FORMAT functions, which the user needs to be able to search in. This is the query I am working with:
SELECT i.itemID, d.dictionaryID, m.modelName, b.brandName, c.categoryName, p1.fname, p1.lname, p2.fname, p2.lname,
date_format(u.startDato, "%W %e. %M - %H:%i") "dateAdded", date_format(u.returDato, "%W %e. %M - %H:%i") "datePlannedReturn",
CASE
WHEN u.accoutPermission = 1 THEN "Moderator"
WHEN u.accoutPermission = 2 THEN "Administrator"
ELSE "User"
END AS permission
FROM item i
INNER JOIN model m ON m.modelID = i.modelID
INNER JOIN dictionaryItem d ON d.dictionaryID = i.dictionaryID
INNER JOIN brand b ON b.brandID = d.brandID
INNER JOIN category c ON c.categoryID = d.categoryID
INNER JOIN person1 p1 ON p1.personID = i.personID
INNER JOIN userAccount u ON u.accountID = d.accountID
INNER JOIN person2 p2 ON p2.personID = d.personID
WHERE u.accountID = x AND i.state = 1 AND (i.itemID LIKE "%search term%" OR c.brandName LIKE "%search term%" OR c.categoryName LIKE "%search term%" OR p1.fname LIKE "%search term%" OR p1.lname LIKE "%search term%"
OR p2.fname LIKE "%search term%" OR p2.lname LIKE "%search term%" OR dateAdded LIKE "%search term%" OR datePlannedReturn LIKE "%search term%"
OR permission LIKE "%search term%")
The problem here is that the LIKE operator on the "permission" column is only searching for the value of the u.accountPermission and not the result of the CASE-function. The same goes for the "dateAdded" and "datePlannedReturn" columns, it compares the search term to the original DATETIME--format and not the reformated date-format. Does anyone know why this happens?
Also, is there any better way to search in all of the returned columns, or do I have to spesify each one individually?
EDIT:
I managed to fix both my issues by using HAVING instead of WHERE for the search-operation and used LOCATE('search term', CONCAT_WS(column1, column2, column3, ...)). This ended up being much cleaner, faster and reliable. Thanks for the help!
Here is the updated Query:
SELECT i.itemID, d.dictionaryID, m.modelName, b.brandName, c.categoryName, p1.fname, p1.lname, p2.fname, p2.lname,
date_format(u.startDato, "%W %e. %M - %H:%i") "dateAdded", date_format(u.returDato, "%W %e. %M - %H:%i") "datePlannedReturn",
CASE
WHEN u.accoutPermission = 1 THEN "Moderator"
WHEN u.accoutPermission = 2 THEN "Administrator"
ELSE "User"
END AS permission
FROM item i
INNER JOIN model m ON m.modelID = i.modelID
INNER JOIN dictionaryItem d ON d.dictionaryID = i.dictionaryID
INNER JOIN brand b ON b.brandID = d.brandID
INNER JOIN category c ON c.categoryID = d.categoryID
INNER JOIN person1 p1 ON p1.personID = i.personID
INNER JOIN userAccount u ON u.accountID = d.accountID
INNER JOIN person2 p2 ON p2.personID = d.personID
WHERE b.brukerID = x AND u.state = 1
HAVING LOCATE('search term', CONCAT_WS(i.itemID, b.brandname, c.categoryName, p1.fname, p1.lname, p2.fname, p2.lname, dateAdded, datePlannedReturn, permission))
Avoid using such a shotgun approach. Instead, craft the search query based on what the user enters. ("If it quacks like a duck, assume it is a duck".)
Pseudo app code:
if it looks like an account number
SELECT id WHERE acct_num = ?
else if numeric input
SELECT id WHERE id = ?
else if it looks like dddd-dd-dd,
SELECT id WHERE date_col = ?
else if ...
...
else if it is long enough to be used in fulltext
SELECT id WHERE MATCH(text1, text2, ...)
AGAINST ("+$string" IN BOoLEAN MODE)
else
your current SELECT (but getting only `id`)
Then put that as a derived table thus:
SELECT lots of columns from lots of tables
FROM (whichever of the above you picked) AS ids
JOINs to get other columns
ORDER BY ...
LIMIT ...
Then declare a FULLTEXT index on just the text columns showing in that MATCH.

Selection of user's vote column based on ID

In my public voting database, I have pages and pages_votes tables with structure below:-
Page
Page Votes
i am querying mysql database to get results for all pages with their total sum of positive and negative votes
positive vote is 1, and negative vote is 0.
my query is as below:-
SELECT pages.id, pages.title, COUNT(pv.page_id) AS `total_count`, SUM(pv.vote=1) AS `likes`, SUM(pv.vote=0) AS `dislikes`
FROM `pages`
LEFT JOIN `pages_votes` AS `pv` ON `pages`.`id` = `pv`.`page_id`
GROUP BY `pages`.`id`, `pages`.`title`, `pages`.`slug`, `pages`.`image`
ORDER BY `total_count` DESC;
Results look like this (no issue here):-
Now I want to include a new custom column in this result called 'my_vote', my vote will show me my votes (user_id = 3) as 1 or 0 for like/dislikes if i have voted, and NULL if i have not voted.
There is already a user_id in page_votes table recording which user voted. How do I use that to get votes of a specific user with say ID = 10?
I suggest writing your query with table aliases. Then the answer to your question is a CASE expression:
SELECT p.id, p.title, COUNT(pv.page_id) AS total_count,
SUM(pv.vote =1 ) AS likes, SUM(pv.vote = 0) AS dislikes,
SUM(CASE WHEN pv.vote = 1 AND p.user_id = 3 THEN 1
WHEN pv.vote = 0 AND p.user_id = 3 THEN 0
END) as user_3_vote
FROM pages p LEFT JOIN
pages_votes pv
ON p.id = pv.page_id
GROUP BY p.id, p.title
ORDER BY total_count DESC;
Note that this uses SUM(CASE . . .) rather than SUM( <boolean expression> ). This is important so you can get a NULL value when there is no vote.
will you consider a small change here in your query? to get specified output.
SELECT pages.id, pages.title, COUNT(pv.page_id) AS `total_count`,
SUM(case when pv.vote = 1 then 1 else 0 end) AS `likes`,
SUM(case when pv.vote = 0 then 1 else 0 end) AS `dislikes`
FROM `pages`
LEFT JOIN `pages_votes` AS `pv`
ON `pages`.`id` = `pv`.`page_id`
GROUP BY `pages`.`id`, `pages`.`title`, `pages`.`slug`, `pages`.`image`
ORDER BY `total_count` DESC;

How do I get the MIN value from two columns along with corresponding id?

My query does return the lowest price from the two columns (price_base, price_special) but it is not returning the correct store_id that corresponds to the lowest price found.
My Query:
SELECT grocery_item.id, grocery_item.category,
grocery_category.name AS cat, grocery_item.name AS itemName,
MIN( if( grocery_price.price_special>0,
grocery_price.price_base)) AS price,
grocery_price.store_id,
grocery_store.name AS storeName
FROM grocery_item
LEFT JOIN grocery_category ON
grocery_category.id=grocery_item.category
LEFT JOIN grocery_price
ON grocery_price.item_id = grocery_item.id
LEFT JOIN grocery_store
ON grocery_store.id=grocery_price.store_id
WHERE grocery_price.selection='no'
AND buy='yes'
GROUP BY grocery_price.item_id
ORDER BY store_id, grocery_item.category, grocery_item.name
Returns this:
ID category cat itemName price store_id storeName
92 3 Bread/Bakery Arnold Bread 2.14 1 Food Lion
But the grocery_price table holds this info:
item_id price_base price_special store_id
92 4.29 2.14 9
92 3.99 0.00 1
so the store_id I need to be returned is 9 (the storeName returned would NOT then be Food Lion)
EDIT: WORKING QUERY based on Uueerdo's comments (thank you!)
SELECT minP.item_id, gi.category, gc.name AS cat,
gi.name as itemName, gp.store_id,
gs.name AS storeName, minP.price
FROM
(SELECT p.item_id, MIN(IF(p.price_special >0,
p.price_special,p.price_base)) AS price
FROM grocery_item AS i
INNER JOIN grocery_price AS p ON (i.id = p.item_id)
WHERE i.buy = 'yes'
GROUP BY p.item_id) AS minP
INNER JOIN grocery_item AS gi ON minP.item_id = gi.id
INNER JOIN grocery_category AS gc on gi.category = gc.id
LEFT JOIN grocery_price AS gp
ON minP.price = IF(gp.price_special > 0,
gp.price_special,gp.price_base)
AND gp.item_id = gi.id
INNER JOIN grocery_store AS gs ON gp.store_id = gs.id
GROUP BY gi.id
ORDER BY gs.id, gi.category,gi.name
The values returned for non-grouped, non-aggregated fields are an (effectively) random selection from the values encountered with the grouped fields' values. Most RDBMS do not even consider such a query valid, and even newer versions of MySQL default to disallowing such queries.
In cases like yours, where you need the non-grouped value(s) associated with the aggregate result (min in this case); the aggregating query must be converted into a subquery, that can be joined back to the aggregated tables to find the source row(s) that correspond to the aggregated value.
Edit: Basically, you need to look at the problem slightly differently. You're currently finding the lowest price for an item and a store that item is listed for; you need to find the lowest price for an item, and use that to find the store(s) that have the item at that price.
This gets you the lowest price for item's marked "buy":
SELECT p.item_id, MIN(IF(p.price_special > 0,p.price_special,p.price_base)) AS price
FROM grocery_item AS i
INNER JOIN grocery_price AS p ON (i.id = p.item_id)
WHERE i.buy = 'yes'
GROUP BY p.item_id
You can then take that to get the rest of the results:
SELECT minP.item_id
, gi.name
, gi.category, gc.category_name AS cat, gi.Name as itemName, gi.buy
, gp.store_id, gp.name AS storeName
, minP.price
FROM ([the query above]) AS minP
INNER JOIN grocery_item AS gi ON minP.item_id = gi.id
INNER JOIN grocery_category AS gc on gi.category = gc.grocery_category_id
/* Guessing on this join since grocery_category_id
was not qualified with it's table name */
INNER JOIN grocery_price AS gp
ON minP.price = IF(gp.price_special > 0,gp.price_special,gp.price_base)
/* Alternatively: ON minP.price IN (gp.price_special, gp.price_base)
... though this could cause false positives if the minP.price is 0
from one store's base price being "free"
*/
INNER JOIN grocery_store AS gs ON gp.store_id = gs.id
;

Joining product attributes table with the product table to display product

I have three tables for listing products with product attributes
Product Table with dummy data
Product_Attributes with dummy data
Attributes with dummy data
Kespersky antivirus (productid = 1) has no attributes but the iPhone (productid =2) has two attributes applicable to it, memory and resolution both in Attribute table which has its value stored in Product_Attribute table.
How do I join these tables to show/display both the products with there corresponding attributes?
EDIT
I need to display these products as
The following will work for any number of attributes:
select product.productId, product.name,
group_concat(concat(attr.attributeName, ":", pa.attributeValue))
from product
left outer join product_attributes pa
on (pa.productId = product.productId)
left outer join attributes attr
on (attr.attributeId = pa.attributeId)
group by product.productId, product.name
Your question requires a pivot, which needs to be predefined. Meaning, if you want to include 2 extra COLUMNS in your result set, your query can then only store up to 2 attributes. This is a PRESENTATION layer problem, not query layer. But alas, I have a general solution for you. It assumes you will have a max number of 2 attributes (for the reasons states above). Here is the query:
SELECT
P.ProductName,
A.AttributeName,
PA.AttributeValue,
B.AttributeName,
PB.AttributeValue
FROM lb_products P
LEFT JOIN (select row_number() over (partition by productID order by AttributeID asc) rn, *
from lb_product_attributes x) PA
ON P.ProductID = PA.ProductID and PA.rn = 1
LEFT JOIN (select row_number() over (partition by productID order by AttributeID asc) rn, *
from lb_product_attributes x) PB
ON P.ProductID = PB.ProductID and PB.rn = 2
LEFT JOIN lb_attributes A
ON PA.AttributeID = A.AttributeID
LEFT JOIN lb_attributes B
ON PB.AttributeID = B.AttributeID;
And the SQL Fiddle for you to play around. Good luck! And feel free to ask any questions :)
http://sqlfiddle.com/#!6/49a9e0/5
You can try this:
SELECT
P.ProductName,
P.Price,
-- Add other Column Here
A.AttributeName,
PA.AttributeValue
FROM Product P
LEFT JOIN Product_Attributes PA
ON P.ProductID = PA.ProductID
LEFT JOIN Attributes A
ON PA.AttributeID = A.AttributeID
Output
ProductName Price AttbituteName AttributeValue
Kaspersky 380 NULL NULL
IPHONE 45000 Memory 64 gb
IPHONE 45000 Resolution 21500 pi
Philip's answer is certainly good, and he spells out the problem in that you need to define a static number of attributes if you're doing a pivot. I'm not sure the windowed functions are necessary, though, so here's how I'd do it:
select
prd.productId
,max(case when lpa.attributeId = 1 then attributeName else null end) attributeName1
,max(case when lpa.attributeId = 1 then attributeValue else null end) attributeValue1
,max(case when lpa.attributeId = 2 then attributeName else null end) attributeName2
,max(case when lpa.attributeId = 2 then attributeValue else null end) attributeValu2
from
lb_products prd
left outer join lb_product_attributes lpa on lpa.productId = prd.productId
left outer join lb_attributes atr on atr.attributeId = lpa.attributeId
group by
prd.productId

MySQL GROUP BY on a calculated value gives an error - using other values produces unexpected results

This query returns all 23 of these music.id's:
39,64,1327,1608,1644,1657,1666,1676,1681,1686,1691,1711,1726,1730,1811,1851,2346,2440,2967,2968,2996,2998,3110
But... I want to group on a calculated value, then group by category (since 'name' is unique to each record and category isn't)
SELECT
music.id,
music.name AS name,
music.filename,
music.url_name,
music.file_path_high,
music.filesize,
music.categories AS category,
music.duration,
music.folder AS folder,
SUM(active0.weight) + SUM(active1.weight) AS total_weight
FROM (music)
INNER JOIN linked_tags AS active0
ON active0.track_id = music.id AND active0.tag_id = 1
INNER JOIN linked_tags AS active1
ON active1.track_id = music.id AND active1.tag_id = 11
GROUP BY name
ORDER BY total_weight DESC
But... when I try to GROUP BY total_weight I get an error (it says I "can't group on total_weight"). Can I not GROUP BY a calculated value? Also, if I try to GROUP BY any other value, the results that are returned are unique for that GROUP BY parameter, so depending on the parameter, I may get 0 results. GROUP BY 'music.categories' for instance only returns 7 items, 7 items all with different categories. Each item's 'name' value is unique, so it really can't group anything anyway, so I figured that by grouping by 'categories' would at least group them by their like integers of a category they belong to, but that's not what I'm seeing.
My table structure is like so:
//music table
ID name categories
1 Hopeful 02 1
2 Organic 01b 3
3 Organic 01c 3
4 Instrumental 01 8
// linked_tags table
track_id tag_id weight
1 1 3
2 2 4
2 3 5
2 1 2
You can't group by an aggregate function. It has to calculate the sum using the group and it gets the group using the sum, it's a catch 22 situation.
You may have meant to group on active0.weight + active1.weight
I think you have misunderstood what group by does in SQL - it ensures that only one row is returned for each combination of groups.
In order to retrieve multiple rows in a specific order, you need to use order by - like so:
SELECT
music.id,
music.name AS name,
music.filename,
music.url_name,
music.file_path_high,
music.filesize,
music.categories AS category,
music.duration,
music.folder AS folder,
SUM(active.weight) AS total_weight
FROM (music)
INNER JOIN linked_tags AS active
ON active.track_id = music.id AND active.tag_id in (1, 11)
GROUP BY music.id
HAVING COUNT(DISTINCT active.tag_id)=2
ORDER BY category, total_weight DESC
I think this is what you want:
SELECT
music.id,
music.name AS name,
music.filename,
music.url_name,
music.file_path_high,
music.filesize,
music.categories AS category,
music.duration,
music.folder AS folder,
total_weights.weight as total_weight
FROM music
JOIN (SELECT
music.categories as category,
SUM(active0.weight + active1.weight) as weight
FROM (music)
INNER JOIN linked_tags AS active0
ON active0.track_id = music.id AND active0.tag_id = 1
INNER JOIN linked_tags AS active1
ON active1.track_id = music.id AND active1.tag_id = 11
GROUP BY music.categories
) total_weights
ON music.categories = total_weights.category
INNER JOIN linked_tags AS active0
ON active0.track_id = music.id AND active0.tag_id = 1
INNER JOIN linked_tags AS active1
ON active1.track_id = music.id AND active1.tag_id = 11
ORDER BY total_weight DESC