Truncate and concatenate mysql results based on number of results - mysql

I would like to concatenate all the item names of an order; however, if the total number of unique item names exceeds a certain number, then I want to to truncate each name before concatenating the names. Below are the conditions:
If the total number of unique item names in the order is less than 5, then use the full-length item name and concatenate the names; else if the total number of unique item names is greater than 5, then truncate each item name to 20 characters and concatenate the truncated names. For example, below is my table:
order_id | item_name | item_name_len
---------|-------------------------------------|--------------
1 | "pampers diapers ultra sensitive" | 31
1 | "cabbage salad pure organic greens" | 33
1 | "milky way" | 9
1 | "sea salt" | 8
1 | "cool waters fruit juice" | 23
| |
2 | "pure clear glass crystals" | 25
2 | "simple sugar edible paper" | 25
I want the following results:
order_id | all_item_names
---------|-----------------------------------------------------------
1 | "pampers diapers ultr ; cabbage salad pure o ; milky way ;
| sea salt ; cool waters fruit ju"
|
2 | "pure clear glass crystals ; simple sugar edible paper"
For Order #1, since there are 5 unique item names in the order, we truncate each of the item names to 20 characters and concatenate the truncated names. For Order #2, since there are only 2 unique item names in the order, we take the full-length of the name and concatenate them. (I've included the item name strlen in the table above for illustration.)
I'm trying to use a ternary condition, but it's not working.
IF( COUNT(DISTINCT item_name) < 5, item_name, SUBSTRING(item_name, 1, 20) )
See query below. I get Error code: 1111. Invalid use of group function
SELECT
w.order_id,
(SELECT GROUP_CONCAT( IF ( COUNT(DISTINCT o.item_name) < 5 ,
o.item_name, SUBSTRING(o.item_name, 1, 20) ) ) separator ' ; ' )
FROM order_items o WHERE o.order_id = w.order_id)AS all_item_names
FROM order_items w
GROUP BY order_id

You can do this with one aggregation and no join:
select oi.order_id,
(case when count(*) < 5
then group_concat(oi.item_name separator '; ')
else group_concat(left(oi.item_name, 20) separator ';')
end) as all_item_names
from order_items oi
group by oi.order_id

Group once to get the number of items and join to the table for the final group_concat:
select
o.order_id,
group_concat(
case
when counter < 5 then item_name
else left(item_name, 20)
end SEPARATOR ' ; '
) all_item_names
from order_items o inner join (
select
order_id, count(*) counter
from order_items
group by order_id
) g on g.order_id = o.order_id
group by o.order_id
See the demo
Results:
| order_id | all_item_names |
| -------- | ----------------------------------------------------------------------------------------- |
| 1 | pampers diapers ultr ; cabbage salad pure o ; milky way ; sea salt ; cool waters fruit ju |
| 2 | pure clear glass crystals ; simple sugar edible paper |

Related

MySQL Relational Division with multiple IDs

Please take a look at the question described here: MySQL ONLY IN() equivalent clause , regarding Relational Division in MySQL.
My database structure is very similar to the one described, but in the "Chocolate Boys Table", I have an additional ID field - let's call it milk ID.
Chocolates Boys Table
+----+---------+-----------------------+
| id | chocolate_id | milk id | boy_id |
+----+--------------+---------+--------+
| 1 | 1000 | 2000 | 10007 |
| 2 | 1003 | 2001 | 10007 |
| 3 | 1006 | 2005 | 10007 |
| 4 | 1000 | 2001 | 10009 |
| 5 | 1001 | 2000 | 10009 |
| 6 | 1005 | 2008 | 10009 |
+----+--------------+---------+--------|
The objective is to run a query that retrieves the boy ID that contains the exact chocolate and milk IDs that I pass in. Here are some examples of my expected results:
Example #1:
Chocolate IDs Passed In (in order) - 1000,1003,1006.
Milk IDs Passed In (in order) - 2000,2001,2005.
Expected Result: Query returns boy ID of 10007.
Example #2:
Chocolate IDs Passed In (in order) - 1000,1003.
Milk IDs Passed In (in order) - 2000,2001.
Expected Result: Empty result set.
Example #3:
Chocolate IDs Passed In (in order) - 1003,1000,1006.
Milk IDs Passed In (in order) - 2000,2001,2005.
Expected Result: Empty result set - The passed in IDs are included in boy ID 10007, but the order is wrong. The values of Chocolate ID and Milk ID don't match up if examined on a row by row basis.
I am attempting to use a slightly modified version of John Woo's solution in order to incorporate the added ID field:
SELECT boy_id
FROM boys_chocolates a
WHERE chocolate_id IN (1003,1000,1006) AND milk_id IN (2000,2001,2005) AND
EXISTS
(
SELECT 1
FROM boys_chocolates b
WHERE a.boy_ID = b.boy_ID
GROUP BY boy_id
HAVING COUNT(DISTINCT chocolate_id) = 3
)
GROUP BY boy_id
HAVING COUNT(*) = 3
The problem that I'm having is that the IN function does not enforce order, as seen in example #3. I would like the above query to return an empty result set. What needs to be changed in order to address this problem? Thank you!
Try this approach:
SELECT a.boy_id
FROM
(SELECT id, boy_id FROM boys_chocolates WHERE chocolate_id = 1000) a
JOIN
(
(SELECT id, boy_id FROM boys_chocolates WHERE chocolate_id = 1003) b,
(SELECT id, boy_id FROM boys_chocolates WHERE chocolate_id = 1006) c,
(SELECT id, boy_id FROM boys_chocolates WHERE milk_id = 2000) d,
(SELECT id, boy_id FROM boys_chocolates WHERE milk_id = 2001) e,
(SELECT id, boy_id FROM boys_chocolates WHERE milk_id = 2005) f
)
ON a.boy_id = b.boy_id AND a.boy_id = c.boy_id AND a.boy_id = d.boy_id
AND a.boy_id = e.boy_id AND a.boy_id = f.boy_id AND b.id > a.id
AND c.id > b.id AND e.id > d.id AND f.id > e.id;
Replace 1000 1003 1006 with your first chocolate_id, second chocolate_id, third chocolate_id respectively. Also replace 2000 2001 2005 with your first milk_id, second milk_id, third milk_id.

SQL Select count of categories accross multiple columns

I have a data structure in the table of these columns
ID | Title | Category_level_1 | Category_level_2 | Category_level_3
1 | offer 1 | Browns | Greens | White
2 | offer 1 | Browns | White |
3 | offer 2 | Greens | Yellow |
4 | offer 3 | Browns | Greens |
5 | offer 4 | Browns | Yellow | White
Without the ability to change the table structure I need to "count the number for Offers per Category across the 3 columns"
There is also columns for date range of the offer, to limit to the current ones, but I want to work out the query first.
I need to get a list of all the Categories and then put offers against them.
Offer can be in the table more than once.
As far as I have got is do a temp table first with a UNION.
CREATE TEMPORARY TABLE IF NOT EXISTS Cats AS
( SELECT DISTINCT(opt) FROM (
SELECT Category_level_1 AS opt FROM a_table
UNION
SELECT Category_level_2 AS opt FROM a_table
UNION
SELECT Category_level_3 AS opt FROM a_table
) AS Temp
) ;
SELECT
Cats.opt AS "Joint Cat",
(
SELECT count(*)
FROM a_table
WHERE a_table.`Category_level_1` = Cats.opt
OR a_table.`Category_level_2` = Cats.opt
OR a_table.`Category_level_3` = Cats.opt
GROUP BY a_table.Title
) As Total
FROM Cats
WHERE Category_level_1 != ''
ORDER BY Category_level_1 ASC;
ISSUE:
a) so the union works well and I get my values. DONE
b) the Total subselect though is not grouping correctly.
I just want a count of all the rows returned but it is grouping with a count of the row titles not all rows.
So trying to work out how to figure this should work and the SQL could be totally different with the answer:
Joint Category | Total Count of offers
Browns | 3
White | 3
Greens | 2
Yellow | 2
plan
take a union of all distinct categories, alias to Joint Category
aggregate count over Joint Category ( where not null or blank - not clear from your rendering if those fields are null or blank.. )
grouping by Joint Category
query
select `Joint Category`, count(*) as `Total Count of offers`
from
(
select Title, Category_level_1 as `Joint Category`
from a_table
union
select Title, Category_level_2
from a_table
union
select Title, Category_level_3
from a_table
) allcats
where `Joint Category` is not null
and `Joint Category` <> ''
group by `Joint Category`
;
output
+----------------+-----------------------+
| Joint Category | Total Count of offers |
+----------------+-----------------------+
| Browns | 3 |
| Greens | 3 |
| White | 2 |
| Yellow | 2 |
+----------------+-----------------------+
sqlfiddle
Your results are a bit confusing . . . I cannot tell why browns and whites both have a count of 3. I think you are counting the combination of level and category.
I would be inclined to approach this using union all and then use count() or count(distinct), depending on what the counting logic really is. For the combination of level and category:
SELECT cat, COUNT(DISTINCT level, title) as numtitles
FROM ((SELECT title, 1 as level, category_level1 as cat FROM a_table) union all
(SELECT title, 2 as level, category_level2 as cat FROM a_table) union all
(SELECT title, 3 as level, category_level3 as cat FROM a_table)
) tc
WHERE cat is not null
GROUP BY cat;
You can include the date column in each of the subqueries and then include a condition in the WHERE clause.

MySQL Query: 2 records with different count totals possible to combine when using COALESCE.. WITH ROLLUP?

I'm currently developing a MySQL query that counts different totals of people who bet in certain categories. Say Pro Baseball, Flex, World Cup, ULeague.
The thing is Flex and World Cup is counted as one.
My SQL code so far
SELECT COALESCE(`category_desc`,'Total') AS ALLGAMES,
COUNT(DISTINCT `player_id`) AS 'No. of Person',COUNT(`bet_id`) AS 'No. of Bets' FROM `bet`
WHERE `category_desc` IN ('World Cup','Flexi','ULeague','Pro Baseball')
GROUP BY ALLGAMES WITH ROLLUP
Result:
+----------+---------------+-----------+
| ALLGAMES | No. of Persons| Bet Count |
+----------+---------------+-----------+
|Flexi | 723 | 100,100|
|ProBasebal| 247 | 400,000 |
|World Cup | 709 | 20,375 |
|ULeague | 1000 | 5,311 |
+----------+---------------+-----------+
Is the following result possible thru sql statement?
+----------+---------------+-----------+
| ALLGAMES | No. of Persons| Bet Count |
+----------+---------------+-----------+
|Flexi/WCup| 1432 | 120,475|
|ProBasebal| 247 | 400,000 |
|ULeague | 1000 | 5,311 |
+----------+---------------+-----------+
Say We'd like to combine the data of Flexi and World Cup.
And Also is it possible to arrange the games by custom order?
Like ULeague comes first, next would be ProBasebal.
You just need to convert category_desc Flex and World Cup to Flex/WCup, otherwise use the value as-is:
SELECT COALESCE(ALLGAMES, 'Total') AS ALLGAMES, No_of_Person, No_of_Bets FROM (
SELECT
if(category_desc in ('Flexi', 'World Cup'), 'Flexi/WCup', category_desc) AS ALLGAMES,
COUNT(DISTINCT player_id) AS No_of_Person,
COUNT(bet_id) AS No_of_Bets
FROM bet
WHERE category_desc IN ('World Cup','Flexi','ULeague','Pro Baseball')
GROUP BY ALLGAMES WITH ROLLUP) x
ORDER BY FIND_IN_SET(COALESCE(ALLGAMES, 'Total'), 'Flexi/WCup,ProBaseball,ULeague,Total')
See SQLFiddle
This should work, although it can surely be improved.
SELECT
COALESCE(allgames, 'Total') AS ALLGAMES,
player_count AS 'No. of Person',
bet_count AS 'No. of Bets'
FROM
(SELECT
CASE
WHEN `category_desc` in ('Flexi' , 'World Cup') THEN 'Flexi/WCup'
ELSE `category_desc`
END AS ALLGAMES,
COUNT(DISTINCT `player_id`) player_count,
COUNT(`bet_id`) bet_count,
CASE `category_desc`
WHEN 'ULeague' THEN 1
WHEN 'Pro Baseball' THEN 2
WHEN 'World Cup' THEN 3
WHEN 'Flexi' THEN 3
END AS sort
FROM
`bet`
WHERE
`category_desc` IN ('World Cup' , 'Flexi', 'ULeague', 'Pro Baseball')
GROUP BY ALLGAMES WITH ROLLUP) a
ORDER BY (allgames <> 'Total') DESC, sort;
Sample SQL Fiddle
Sample output:
| ALLGAMES | No_of_Persons | Bet_count |
|--------------|---------------|-----------|
| ULeague | 1 | 4 |
| Pro Baseball | 1 | 4 |
| Flexi/WCup | 2 | 8 |
| Total | 4 | 16 |
Both of your requests (Customer sort order, and combination of two Categories) is achievable with CASE Statements.
SELECT
CASE
WHEN `category_desc` IN ('World Cup', 'Flexi')
THEN 1
WHEN `Uleague`
THEN 2
WHEN `Pro Baseball`
THEN 3
END AS SORTORDER
CASE
WHEN `category_desc` IN ('World Cup', 'Flexi')
THEN 'Flexi/WCup'
ELSE
COALESCE(`category_desc`, 'Total')
END AS ALLGAMES,
COUNT(DISTINCT `player_id`) AS 'No. of Person',
COUNT(`bet_id`) AS 'No. of Bets'
FROM `bet`
WHERE `category_desc` IN (
'World Cup',
'Flexi',
'ULeague',
'Pro Baseball'
)
GROUP BY SORTORDER, ALLGAMES
WITH ROLLUP
ORDER BY SORTORDER;
Using this logic you can make whatever type of sort order you wish using any combination of your fields. You can also combine any of your categories to make a multi-category row in your recordset.

Outliers of data by groups

I want to analyse outliers a of grouped data. Lets say I have data:
+--------+---------+-------+
| fruit | country | price |
+--------+---------+-------+
| apple | UK | 1 |
| apple | USA | 3 |
| apple | LT | 2 |
| apple | LV | 5 |
| apple | EE | 4 |
| pear | SW | 6 |
| pear | NO | 2 |
| pear | FI | 3 |
| pear | PL | 7 |
+--------+---------+-------+
Lets take pears. If my method of finding outliers would be to take 25% highest prices of pears and lowest 25%, outliers of pears would be
+--------+---------+-------+
| pear | NO | 2 |
| pear | PL | 7 |
+--------+---------+-------+
As for apples:
+--------+---------+-------+
| apple | UK | 1 |
| apple | LV | 5 |
+--------+---------+-------+
That I want is to create a view, which would show table of all fruits outliers union. If I had this view, I could analyse only tails, also intersect view with main table to get table without outliers - that's my goal. Solution to this would be:
(SELECT * FROM fruits f WHERE f.fruit = 'pear' ORDER BY f.price ASC
LIMIT (SELECT ROUND(COUNT(*) * 0.25,0)
FROM fruits f2
WHERE f2.fruit = 'pear')
)
union all
(SELECT * FROM fruits f WHERE f.fruit = 'pear' ORDER BY f.price DESC
LIMIT (SELECT ROUND(COUNT(*) * 0.25,0)
FROM fruits f2
WHERE f2.fruit = 'pear')
)
union all
(SELECT * FROM fruits f WHERE f.fruit = 'apple' ORDER BY f.price ASC
LIMIT (SELECT ROUND(COUNT(*) * 0.25,0)
FROM fruits f2
WHERE f2.fruit = 'apple')
)
union all
(SELECT * FROM fruits f WHERE f.fruit = 'apple' ORDER BY f.price DESC
LIMIT (SELECT ROUND(COUNT(*) * 0.25,0)
FROM fruits f2
WHERE f2.fruit = 'apple')
)
This would give me a table I want, however code after LIMIT doesn't seem to be correct... Another problem is number of groups. In this example there are only two groups(pears,apples), but in my actual data there are around 100 groups. So 'union all' should somehow automatically go thru all unique fruits without writing code for each unique fruit, find number of outliers of each unique fruit, take only that numbe of rows and show it all in another table(view).
You can't supply LIMIT with a value from a subquery, in any RDBMS I'm aware of. Some dbs don't even allow host variables/parameters in their versions of the clause (I'm thinking of iSeries DB2).
This is essentially a greatest-n-per-group problem. Similar queries in most other RDBMSs are solved with what are called Windowing functions - essentially, you're looking at a movable selection of data.
MySQL doesn't have this functionality, so we have to counterfeit it. The actual mechanics of the query will depend on the actual data you need, so I can only speak to what you're attempting here. The techniques should be generally adaptable, but may require rather more creativity than otherwise.
To start with you want a function that will return a number indicating it's position - I'm assuming duplicate prices should be given the same rank (ties), and that doing so won't create a gap in the number. This is essentially the DENSE_RANK() windowing function. We can get these results by doing the following:
SELECT fruit, country, price,
#Rnk := IF(#last_fruit <> fruit, 1,
IF(#last_price = price, #Rnk, #Rnk + 1)) AS Rnk,
#last_fruit := fruit,
#last_price := price
FROM Fruits
JOIN (SELECT #Rnk := 0) n
ORDER BY fruit, price
Example Fiddle
... Which generates the following for the 'apple' group:
fruit country price rank
=============================
apple UK 1 1
apple LT 2 2
apple USA 3 3
apple EE 4 4
apple LV 5 5
Now, you're trying to get the top/bottom 25% of rows. In this case, you need a count of distinct prices:
SELECT fruit, COUNT(DISTINCT price)
FROM Fruits
GROUP BY fruit
... And now we just need to join this to the previous statement to limit the top/bottom:
SELECT RankedFruit.fruit, RankedFruit.country, RankedFruit.price
FROM (SELECT fruit, COUNT(DISTINCT price) AS priceCount
FROM Fruits
GROUP BY fruit) CountedFruit
JOIN (SELECT fruit, country, price,
#Rnk := IF(#last_fruit <> fruit, 1,
IF(#last_price = price, #Rnk, #Rnk + 1)) AS rnk,
#last_fruit := fruit,
#last_price := price
FROM Fruits
JOIN (SELECT #Rnk := 0) n
ORDER BY fruit, price) RankedFruit
ON RankedFruit.fruit = CountedFruit.fruit
AND (RankedFruit.rnk > ROUND(CountedFruit.priceCount * .75)
OR RankedFruit.rnk <= ROUND(CountedFruit.priceCount * .25))
SQL Fiddle Example
...which yields the following:
fruit country price
=======================
apple UK 1
apple LV 5
pear NN 2
pear NO 2
pear PL 7
(I duplicated a pear row to show "tied" prices.)
Does round not need 2 / 3 arguments? I.e. do you not need to put in, to what decimal place you wish to round?
so
...
LIMIT (SELECT ROUND(COUNT(*) * 0.25)
FROM #fruits f2
WHERE f2.fruit = 'apple')
becomes
...
LIMIT (SELECT ROUND(COUNT(*) * 0.25,2)
FROM #fruits f2
WHERE f2.fruit = 'apple')
also, just having a quick look at lunch, but it looks like you're just expecting the min / max values. Could you not just use those functions instead?

Retrieve data from a complex table

While searches the date range, start date (date_reg) and end date (date_reg)
, the mysql result should be have each main_table rows contains latest return, received, balance of each products.
E.g.: Between 10-01-2014 and 10-05-2014, should retrieve values of each product within the date
Client Id | Return | Received | Balance
| prod 1 prod 2 | prod 1 prod 2 | prod 1 prod 2
--------------------------------------------------------------
1 | 2 [3] 2 [7] | 5 5 | 8 5
2 | 1 [5] 0 [8] | 5 5 | 9 3
3 | 0 [6] 1 [10]| 5 5 | 7 6
[id], where id is the primary key of sub_table
I have tried mysql query
SELECT p.product_name, ipd.id as ipd_id, i.id as i_id, ipd.*, i.*
FROM main_table i
LEFT JOIN sub_table ipd ON ipd.main_table_id=i.id AND ipd.product_id IN (1,2)
LEFT JOIN product p ON ipd.product_id=p.id
WHERE ipd.date_reg IN (SELECT MAX(ipd1.date_reg)
FROM sub_table ipd1
WHERE ipd1.main_table_id=i.id AND
date_reg BETWEEN '10-01-2014' AND '10-05-2014')
ORDER BY cl.id ASC LIMIT 0, 20
it only return single product of return, received and balance of each client
When you use the subquery WHERE 'ipd.date_reg IN 'SELECT MAX...' you're only going to get 1 entry based on your data - 10-04-2014. Working correctly.
Try use GROUP BY in the sub query
Also GROUP_CONCAT(expr); helps to do many-to-many info's which can be be used to concatenate column values into a single string.
I got the output. Thanks everyone for the helps.
I have used GROUP_CANCAT to concatenate the results into one string with comma seperated
SELECT p.product_name, ipd.id as ipd_id, i.id as i_id, ipd.*, i.*,
GROUP_CONCAT(product_id SEPARATOR ',') as group_product_id,
GROUP_CONCAT(ipd.return SEPARATOR ',') as group_return,
GROUP_CONCAT(ipd.received SEPARATOR ',') as group_received,
GROUP_CONCAT(ipd.balance SEPARATOR ',') as group_balance
FROM main_table i
LEFT JOIN sub_table ipd ON ipd.main_table_id=i.id AND ipd.product_id IN (1,2)
LEFT JOIN product p ON ipd.product_id=p.id
WHERE ipd.date_reg IN (SELECT MAX(ipd1.date_reg)
FROM sub_table ipd1
WHERE ipd1.main_table_id=i.id AND
date_reg BETWEEN '10-01-2014' AND '10-05-2014'
GROUP BY ipd1.product_id)
ORDER BY cl.id ASC LIMIT 0, 20
The Result
Client Id | group_product_id | group_return | group_received | group_balance
--------------------------------------------------------------------------
1 | 1, 2 | 2, 2 | 5,5 | 8,5
2 | 1, 2 | 1, 0 | 5,5 | 9,3
3 | 1, 2 | 0, 1 | 5,5 | 7,6
Then the strings can be exploded into an array.