MySQL query limit per join - mysql

I tried asking question before, but it's hard to ask in specific without right terminology I am not quite familiar with. So here is an example
Take this query for example:
(
SELECT *
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
)
UNION ALL
(
SELECT c.*
FROM comments c JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.parent_id = p.id
LIMIT 5
)
id parent_id depth title
1 0 0 Title 1
2 0 0 Title 2
3 1 1 Title 3
4 1 1 Title 4
5 1 1 Title 5
6 1 1 Title 6
7 1 1 Title 7
I get two depth 0 rows and in join I get 5 child elements of those two returned queries as well. What I would like to get is to get 5 child elements of each of those two queries, total of 10 rows (of depth 1). For example:
id parent_id depth title
1 0 0 Title 1
2 0 0 Title 2
3 1 1 Title 3
4 1 1 Title 4
5 1 1 Title 5
6 1 1 Title 6
7 1 1 Title 7
8 2 1 Title 8
9 2 1 Title 9
10 2 1 Title 10
11 2 1 Title 11
12 2 1 Title 12
Is that even possible with adjacency list and a requirement to return everything as union (flat)?
edit:
Thanks to Bill Karwin's answer, I got it working now. I wonder still if there is a shorter way to write this. I have 6 (0-5) depth levels, so my query is rather long (and probably not optimal). Here is what it looks like for three levels (you can imagine what the full one looks like).
-- DEPTH LEVEL 0
(
SELECT * FROM (
SELECT *, 1 as _rn, #parent:=0
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) as D0
)
union all
-- DEPTH LEVEL 1
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 5
)
union all
-- DEPTH LEVEL 2
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 2
)
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 2
)

You can't do this with LIMIT, because LIMIT is applied after the result set is completely finished, after all joining, grouping, sorting, etc.
You're using a variation of the greatest-n-per-group type of query. It's tricky to do this in MySQL because MySQL doesn't support the ROW_NUMBER() window function supported by many other SQL databases.
Here's a workaround for MySQL, in which user-defined variables can take the place of partitioned row numbers:
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.parent_id,#row+1,1) AS _rn, #parent:=c.parent_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.parent_id = p.id
) AS _ranked
WHERE _ranked._rn <= 5

Related

Unknown column in WHERE clause in subquery

I have pages table which I'm trying to truncate.
I need to leave last 10 versions of each page (page is identified by uid).
id----|----uid----|----version
1 ----|---- 1 ----|---- 1
2 ----|---- 1 ----|---- 2
3 ----|---- 1 ----|---- 3
4 ----|---- 1 ----|---- 4
5 ----|---- 2 ----|---- 1
6 ----|---- 2 ----|---- 2
...........
55 ----|---- 1 ----|---- 23
56 ----|---- 2 ----|---- 14
57 ----|---- 2 ----|---- 15
I've tried with this MySQL query:
DELETE FROM pages AS p WHERE p.id IN (
SELECT Versions.id FROM
(SELECT q.id FROM pages AS q WHERE p.uid = q.uid ORDER BY q.version DESC LIMIT 10)
AS Versions
)
but above SQL returns:
Unknown column 'p.uid' in 'where clause'
On Stackoverflow is loads of answers for similar questions like mine but as I'm not SQL ninja I wasn't able to convert any of these to fit my problem.
I don't want to overkill my query as well.
There must be some simple answer to this.
I can achieve needed truncation in MSSQL Server with query below:
DELETE FROM pages WHERE id NOT IN (
SELECT Versions.id FROM
(SELECT TOP (10) q.id FROM pages AS q WHERE pages.uid = q.uid ORDER BY q.version DESC)
AS Versions
)
Thanks
You don't need to co-ordinate the queries together here; just select a list of IDs to delete- the tables are the same
DELETE FROM pages WHERE id NOT IN
(SELECT TOP (10) id FROM pages ORDER BY q.version DESC)
You can't "order by" a delete query..
If youre after the MySQL version, and your version of MySQL doesnt support LIMIT for subqueries, and it doesn't support window functions like this:
delete from pages where id in
(select id from
(select id, row_number() over(order by dt desc) as rown from pages) a where a.rown > 10)
)
then you could consider something like:
CREATE table x as SELECT * FROM pages ORDER BY q.Version DESC LIMIT 10
TRUNCATE /*or delete from*/ pages
INSERT INTO pages SELECT * FROM x
DELETE PG FROM pages PG
LEFT JOIN
(
SELECT q.id FROM pages AS q ORDER BY q.version DESC LIMIT 10 OFFSET 0
) Versions ON Versions.ID=PG.ID
WHERE Versions.ID IS NULL
You can try above query.
DELETE p FROM pages AS p WHERE p.id IN (
SELECT Versions.id FROM
(SELECT q.id FROM pages AS q WHERE p.uid = q.uid ORDER BY q.version DESC LIMIT 10)
AS Versions
)
Check if this works..

Usage of COALESCE

I have a query making use of COALESCE() to count the combination of 2 columns:
SELECT method, main_ingredient, COUNT(*) AS cnt FROM `recipes`
GROUP BY COALESCE( method, main_ingredient )
The result is useful. Sample result:
method main_ingredient cnt
================================
1 4 10
2 1 6
3 6 3
4 6 5
5 2 4
6 8 2
However, how can I obtain the results that has COUNT(*) equals to 0 ?
UPDATE with expected output:
method main_ingredient cnt
================================
1 2 0
1 3 0
1 5 0
1 6 0
2 2 0
2 3 0
.
.
.
.
etc
UPDATE added the tbl_methods and tbl_main_ingredients:
Schema of tbl_methods:
id method_name
=================
1 Method 1
2 Method 2
.
.
.
6 Method 6
Schema of tbl_main_ingredients:
id ingredient_name
======================
1 Ingredient 1
2 Ingredient 2
.
.
.
8 Ingredient 8
Both id are the primary key of their table, auto-increment.
First you need to make a CROSS JOIN between tbl_methods and tbl_main_ingredients table in order to obtain the all possible combination of method and ingredient.
Later make a left join between the above cross joined table and your reipes table on matching method and main_ingredient.
Thus you will obtain a result for all possible combination of method and main_ingredient. If any combination exists in recipes table then you will get the corresponding count otherwise you will obtain 0 as count.
SELECT
method_ingredients.method_id,
method_ingredients.ingredients_id,
COUNT(R.method) AS cnt
FROM
(
SELECT
TM.id AS method_id,
TMI.id AS ingredients_id
FROM tbl_methods TM
CROSS JOIN tbl_main_ingredients TMI
) AS method_ingredients
LEFT JOIN `recipes` R ON R.method = method_ingredients.method_id AND R.main_ingredient = method_ingredients.ingredients_id
GROUP BY method_ingredients.method_id, method_ingredients.ingredients_id
ORDER BY method_ingredients.method_id, method_ingredients.ingredients_id;
Or
you can prefer the shorter version of this query:
SELECT
TM.id AS method_id,
TMI.id AS ingredients_id,
COUNT(R.method) AS cnt
FROM tbl_methods TM
CROSS JOIN tbl_main_ingredients TMI
LEFT JOIN `recipes` R ON R.method = TM.id AND R.main_ingredient = TMI.id
GROUP BY TM.id, TMI.id
ORDER BY TM.id, TMI.id;
More:
Some subtleties regarding COUNT:
SELECT COUNT(0); Result: 1
SELECT COUNT(-1); Result: 1
SELECT COUNT(NULL); Result: 0
SELECT COUNT(71); Result: 1
SQL FIDDLE
BTW there's nothing to do with COALESCE in your use case. COALESCE returns the first non-NULL element from the list if there's any otherwise NULL.
Example:
SELECT COALESCE(NULL,NULL,NULL,'abc',NULL,'def'); returns abc
SELECT COALESCE(NULL,NULL,NULL); returns NULL
Could be you need to check if the main_ingredient is null
SELECT method, ifnull(main_ingredient,0), COUNT(*) AS cnt FROM `recipes`
GROUP BY method
Cross join your 2 base tables, then left join on recipes. Then, if you count any of the left joined columns, you will get the desired result:
select m.id, i.id, count(r.method) as cnt
from tbl_methods m
cross join tbl_main_ingredients i
left join recipes r
on r.method = m.id
and r.main_ingredient = i.id
group by m.id, i.id
order by m.id, i.id

SQL - How to calculate column value and join with another table

As I am not good with MySQL query's so I wish someone help me for creating this kind of sql query.
I having two MySQL tables which is describe bellow:
Table Name: rating
-------------------
property_id user_id area_rate_count safety_rate_count friendly_rate_count walkability_rate_count
4 28 1 1 1 2
5 38 2 3 4 1
5 40 2 2 3 1
6 40 2 3 1 4
10 43 2 2 3 1
Table Name: listing
-------------------
property_id title
4 Sample 1
5 Sample 2
6 Sample 3
10 Sample 4
11 Sample 5
12 Sample 6
Now first I want to sum each column and divide. (area_rate_count, safety_rate_count, friendly_rate_count, walkability_rate_count). For example In property_id:5 having two times so first calculate column sum and divide by 2.
After calculation we will get this output:
Table Name: rating (After Calculation)
--------------------------------------
property_id rate
4 5
5 9 (Divided by 2 because this property_id is two times in table)
6 10
10 8
And Finally I want join this result to my listing table and result looks something like this:
Table Name: listing
-------------------
property_id title rate
4 Sample 1 5
5 Sample 2 9 (Divided by 2 becouse property_id is two times in table)
6 Sample 3 10
10 Sample 4 8
11 Sample 5 0
12 Sample 6 0
Thanks.
I think you want the avg() aggregation function along with a join:
select l.property_id, l.title,
coalesce(avg(area_rate_count + safety_rate_count + friendly_rate_count + walkability_rate_count
), 0) as rate
from listing l left outer join
property_id p
on l.property_id = p.property_id
group by l.property_id, l.title ;
If I understood it right I think you need this:
select l.property_id, l.title, coalesce(r.ssum/if(r.ct=0,1,r.ct), 0) as rate
from listing l LEFT JOIN
(select property_id,
sum(area_rate_count+safety_rate_count
+friendly_rate_count+walkability_rate_count) ssum,
count(*) ct
from rating
group by property_id ) r
ON l.property_id = r.property_id
order by l.property_id
See it here on fiddle: http://sqlfiddle.com/#!2/589d6/5
Edit
As OP asked on the comments that he wants all columns from listing here is what he want:
select l.*, coalesce(r.ssum/if(r.ct=0,1,r.ct), 0) as rate
from listing l LEFT JOIN
(select property_id,
sum(area_rate_count+safety_rate_count
+friendly_rate_count+walkability_rate_count) ssum,
count(*) ct
from rating
group by property_id ) r
ON l.property_id = r.property_id
order by l.property_id
CREATE TEMPORARY TABLE IF NOT EXISTS
temp_table ( INDEX(col_2) )
ENGINE=MyISAM
AS (
SELECT
property_id,
AVG(area_rate_count) as area_rate_count,
AVG(safety_rate_count) as safety_rate_count,
AVG(friendly_rate_count) as friendly_rate_count,
AVG(walkability_rate_count) as walkability_rate_count
FROM rating
GROUP BY property_id
)
SELECT * FROM listing L
JOIN temp_table T
ON L.property_id = T.property_id
Use the below statement to get distinct property_id with its own rate
select property_id, sum(separaterating)/count(property_id) from (
select property_id,sum(area_rate_count , safety_rate_count , friendly_rate_count , walkability_rate_count) as separaterating from rating group by property_id AS temp ) group by
property_id
you can then join with the other table to get the final result as below
select * from ( select property_id, sum(separaterating)/count(property_id) from (
select property_id,sum(area_rate_count , safety_rate_count , friendly_rate_count , walkability_rate_count) as separaterating from rating group by property_id AS temp ) group by
property_id) AS A inner join listing AS B on A.property_id = B.property_id
try this:
select a.prop_id as property_id, l.title, a.allratings / b.numberofreviews as rate
from
(
select property_id as prop_id, SUM(coalesce(area_rate_count,0) + coalesce(safety_rate_count,0) + coalesce(friendly_rate_count,0) + coalesce(walkability_rate_count,0)) as allratings
from rating
group by property_id
) a inner join
(
select property_id, count(distinct user_id) as numberofreviews
from rating
group by property_id
) b on a.property_id = b.property_id
inner join listing l on a.property_id = l.property_id
Try This Query
select ls.property_id,ls.title,inr.rate from listing as ls
left join
(select r.property_id as pid,r.rate/r.cnt as rate from
(select property_id,user_id,(area_rate_count+safefty_rate_count+friendly_rate_count+walkability_rate_count) as rate,count(*) as cnt from rating group by property_id) as r) as inr on inr.pid=ls.property_id

How to rank MySQL results, based on different values?

I have 2 different tables in my database by the name of: rank, settings.
Here is how each table looks like with a few records in them:
Table #rank:
id points userid
-- ----- ------
1 500 1
2 300 2
3 900 3
4 1500 4
5 100 5
6 700 6
7 230 7
8 350 8
9 850 9
10 150 10
Table #settings:
userid active
------ ------
1 0
2 1
3 1
4 1
5 1
6 0
7 1
8 1
9 0
10 1
What I basically want to achieve is to select a specific row from #rank by ID, sort it by points and select 3 rows above the specific ID and 3 row below the specific ID but only for rows where the active column (from #settings) for the user equals 1.
For example:
I would like to select from #rank the ID of 8, and it should return me the following:
rank points userid
---- ----- ------
2 150 10
3 230 7
4 300 2
5 350 8
6 900 3
7 1500 4
I have created quite an extensive query for this, but the problem is, that it is ranking the columns before it decides that the user is active or not. However I need to rank the columns after it is decided that the user is active or not.
SELECT sub2.sort, sub2.points, sub2.userid
FROM
(
SELECT #sort1 := #sort1 + 1 AS sort, puu.points, puu.userid
FROM rank as puu,
(SELECT #sort1 := 0) s
LEFT JOIN
(
settings as p11
)
ON puu.userid = p11.userid,
WHERE p11.active = 1
ORDER BY puu.points DESC
) sub1
INNER JOIN
(
SELECT #sort2:=#sort2+1 AS sort, p2.points, p2.userid
FROM rank as p2,
(SELECT #sort2 := 0) s
LEFT JOIN
(
settings as p12
)
ON p2.userid = p12.userid,
WHERE p12.active = 1
ORDER BY points DESC
) sub2
ON sub1.userid = :userid
AND sub2.sort BETWEEN (sub1.sort - 5) AND (sub1.sort + 5)
Can you guys find any solution for my problem? If you can provide an SQLfiddle demo, that would be really awesome!
SELECT sort, points, user_id, active FROM (
SELECT #pos := #pos + 1 AS sort, id, points, r.user_id, s.active,
IF(user_id = :userid, #userpos := #pos, 0)
FROM rank r
JOIN settings s USING(user_id)
JOIN (SELECT #pos := 0, #userpos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE sort BETWEEN #userpos - 3 AND #userpos + 3
I made a fiddle here: sqlfiddle
Possibly using 2 unioned queries to get the values before the ranking:-
SELECT #rank:=#rank+1 AS rank, points, userid
FROM
(
SELECT id, points, userid
FROM
(
SELECT rank.id, rank.points, rank.userid
FROM rank
INNER JOIN
(
SELECT points
FROM rank
WHERE id = 8
) sub0
ON rank.points >= sub0.points
INNER JOIN settings
ON rank.userid = settings.userid
WHERE settings.active = 1
ORDER BY rank.points LIMIT 3
) sub1
UNION ALL
SELECT id, points, userid
FROM
(
SELECT rank.id, rank.points, rank.userid
FROM rank
INNER JOIN
(
SELECT points
FROM rank
WHERE id = 8
) sub0
ON rank.points < sub0.points
INNER JOIN settings
ON rank.userid = settings.userid
WHERE settings.active = 1
ORDER BY rank.points DESC LIMIT 3
) sub1
) sub2
CROSS JOIN (SELECT #rank:=0) sub3
ORDER BY points

Join with limit on right table

Say I have two tables I want to join.
Categories:
id name
----------
1 Cars
2 Games
3 Pencils
4 Books
And items:
id categoryid itemname
---------------------------
1 1 Ford
2 1 BMW
3 1 VW
4 2 Tetris
5 2 Pong
6 3 Foobar Pencil Factory
I want a query that returns the category and the last maximum N (for example: 2) itemname:
category.id category.name item.id item.itemname
-------------------------------------------------
1 Cars 2 BMW
1 Cars 3 VW
2 Games 4 Tetris
2 Games 5 Pong
3 Pencils 6 Foobar Pencil Factory
4 Books NULL NULL
I want write a query like below:
Select * From categories c
Left Join (select * from items order by id desc) i
On c.id=i=categoryid
AND LIMIT 2 #comment: N=2 this line not supported
Where i.categoryid = c.id
Group By c.id
Thanks!
I'm not saying it's efficient, but it should work:
SELECT c.*, i.id, i.itemname
FROM categories c
LEFT JOIN
(SELECT i.*
FROM items i
LEFT JOIN items i2 ON i.categoryid = i2.categoryid AND i2.id > i.id
GROUP BY i.id
HAVING COUNT(*) < 2 # this 2 = N
) i ON c.id = i.categoryid
Check http://sqlfiddle.com/#!2/9a132/1
SELECT *
FROM Categories c
LEFT JOIN -- Items i
(
SELECT * FROM Items WHERE LOCATE(id,
(
SELECT GROUP_CONCAT(it.itemids) AS its
FROM (
SELECT (SUBSTRING_INDEX(GROUP_CONCAT(CONVERT(id, CHAR(8))
ORDER BY id DESC), ',', 2)) AS itemids
FROM Items
GROUP BY categoryid
) it
)) <> 0
) i
ON i.categoryid = c.id;
where N=2 is: SUBSTRING_INDEX(GROUP_CONCAT(CONVERT(id, CHAR(8)) ORDER BY id DESC), ',', 2)
Because GROUP_CONCAT is by default sort ASC ; above will become SUBSTRING_INDEX(GROUP_CONCAT(CONVERT(id, CHAR(8)) ), ',', -2). But GROUP_CONCAT result is truncated to max len 1024 (group_concat_max_len).