I have a query like this:
SELECT
p.title,
a.title
FROM
pages p
LEFT JOIN
articles_pages ap on ap.p_id = p.id
LEFT JOIN
articles a ON a.id = p.a_id
WHERE
[...]
LIMIT 10
How can I limit to only 3 articles for each page?
There are few ways to do that. You can use sub-queries but I wouldn't recommend that as its slower and not scalable. So.. I like the one I show you bellow:
I would create a variable and save on it the row number(#num).
We also need another variable to save the "page" id as its necessary to restart the row count when a different page appears.
Finally we filter by #num with as much rows you want. Remember matchNO is a calculated field so you cant filter in a where statment, use having instead.
set #num := 0, #page := 0;
SELECT p.title, a.title,IF(#page = p.id,#num:=#num+1,#num:=1) as matchNO,#page:=p.id
FROM pages p
LEFT JOIN articles_pages ap on ap.p_id = p.id
LEFT JOIN articles a ON a.id = p.a_id
WHERE [...]
having matchNO <= 3
LIMIT 10
The row number is necessary to figure out in what row you are.
The page variable tell you the page in the last row so you can compare and then set #num to 1 again when necesary.
I hope it helps
Another possible way to do it. Whether this would be better or worse than the others would depend on the relative quantities of pages and articles.
You could use a sub query to get 3 articles per page and then join that back to your tables. You can cheat a bit and use GROUP_CONCAT to get the ids of all the articles per per, then use SUBSTRING_INDEX to get the first 3 (you can add an order clause to GROUP_CONCAT if you want), and then JOIN this using FIND_IN SET.
SELECT
p.title,
a.title
FROM pages p
LEFT OUTER JOIN
(
SELECT ap.p_id
SUBSTRING_INDEX(GROUP_CONCAT(ap.a_id), ',', 3) AS three_articles_per_page
FROM articles_pages ap
GROUP BY ap.p_id
) sub0 ON p.id = sub0.p_id
LEFT JOIN articles_pages ap on ap.p_id = p.id AND FIND_IN_SET(ap.aid, sub0.three_articles_per_page)
LEFT JOIN articles a ON a.id = p.a_id
WHERE
[...]
You can make use of user variables to generate row number and then filter based on it.
select p.title, a.title
from (
select
t.*,
#rn := if(
#id = id, #rn + 1,
if(
#id := id, 1, 1
)
) rn
from (
select *
from pages
order by id, a_id
) t cross join (select #rn := 0, #id:= -1) t2
) p left join articles a on a.id = p.a_id
where p.rn <= 3
order by p.id, p.a_id;
Demo
Related
I'm currently creating a small application where users can post a text which can be commented and the post can also be voted (+1 or -1).
This is my database:
Now I want to select all information of all posts with status = 1 plus two extra columns: One column containing the count of comments and one column containing the sum (I call it score) of all votes.
I currently use the following query, which correctly adds the count of the comments:
SELECT *, COUNT(comments.fk_commented_post) as comments
FROM posts
LEFT JOIN comments
ON posts.id_post = comments.fk_commented_post
AND comments.status = 1
WHERE posts.status = 1
GROUP BY posts.id_post
Then I tried to additionally add the sum of the votes, using the following query:
SELECT *, COUNT(comments.fk_commented_post) as comments, SUM(votes_posts.type) as score
FROM posts
LEFT JOIN comments
ON posts.id_post = comments.fk_commented_post
AND comments.status = 1
LEFT JOIN votes_posts
ON posts.id_post = votes_posts.fk_voted_post
WHERE posts.status = 1
GROUP BY posts.id_post
The result is no longer correct for either the votes or the comments. Somehow some of the values seem to be getting multiplied...
This is probably simpler using correlated subqueries:
select p.*,
(select count(*)
from comments c
where c.fk_commented_post = p.id_post and c.status = 1
) as num_comments,
(select sum(vp.type)
from votes_posts vp
where c.fk_voted_post = p.id_post
) as num_score
from posts p
where p.status = 1;
The problem with join is that the counts get messed up because the two other tables are not related to each tother -- so you get a Cartesian product.
You want to join comments counts and votes counts to the posts. So, aggregate to get the counts, then join.
select
p.*,
coalesce(c.cnt, 0) as comments,
coalesce(v.cnt, 0) as votes
from posts p
left join
(
select fk_commented_post as id_post, count(*) as cnt
from comments
where status = 1
group by fk_commented_post
) c on c.id_post = p.id_post
left join
(
select fk_voted_post as id_post, count(*) as cnt
from votes_posts
group by fk_voted_post
) v on v.id_post = p.id_post
where p.status = 1
order by p.id_post;
I am trying to obtain the row number (i.e. rank) for the following select statement (which includes a column JOIN) but without declaring a SET variable at the beginning.
Reason for this is because I am using a WordPress/MySQL plugin which can only emulate single statement code. The common hack of declaring a prior variable to 0 then incrementing is not recognized.
Is there another way to obtain the row number using the select & join below?
SELECT s.id
, s.item
, s.state
, c.job_count
FROM wp_state_tab s
LEFT
JOIN wp_custom_tab c
ON c.state_id = s.id
WHERE c.date = CURDATE()
ORDER
BY c.job_count DESC
Sample Data Output
MySQL version is 5.6.40-84.0-log
MySQL can be fiddly about variables -- good thing they are deprecated. With ORDER BY or GROUP BY, you often have to use a subquery:
SELECT (#rn := #rn + 1) as seqnum, sc.*
FROM (SELECT s.id, s.item, s.state, c.job_count
FROM wp_state_tab s LEFT JOIN
wp_custom_tab c
ON c.state_id = s.id
WHERE c.date = CURDATE()
ORDER BY c.job_count DESC
) sc CROSS JOIN
(SELECT #rn := 0) params;
You can use a subquery for the iteration of a newly defined row number without explicitly declaring variable as :
select #i := #i + 1 as rownum,
s.id, s.item, s.state, c.job_count
from wp_state_tab s
left join wp_custom_tab c
on c.state_id = s.id and c.date = CURDATE()
join (select #i:=0) t2
order by job_count desc;
I've got this query:
SELECT *, m.id AS mooringid
FROM mooring m JOIN customer c ON m.assignedTo = c.id
WHERE m.Number = :var OR (CONCAT(c.TitleName,' ',c.Surname) LIKE CONCAT('%', :var, '%')) OR m.MooringArea = :var
ORDER BY c.Surname limit 0,250
That is supposed to get elements assigned to a customer from another table, the only way I saw that I could do this is by "infusing" the customer details with the element, returning the elements but I want to limit the amount of customers returned but allow infinite elements, however, this limits the elements rather than the customer and that just doesn't work in my situation.
Is this possible? Am I missing something?
One way to tackle the issue is to do a subquery on customers and extract the number you need. Something like:
from (select c.*
from customers c
limit 100
) c
But, in your case, you have a lot of secondary filtering going on (with the join conditions and the where). Instead, add a customer counter to each row and use that for selecting a certain number of customers:
select t.*
from (SELECT *, m.id AS mooringid,
#rn := if(#cid = c.id, #rn + 1, 1) as rn,
#cid = c.id
FROM mooring m JOIN
customer c
ON m.assignedTo = c.id cross join
(select #rn := 0, #cid := -1) const
WHERE m.Number = :var OR
(CONCAT(c.TitleName,' ',c.Surname) LIKE CONCAT('%', :var, '%')) OR
m.MooringArea = :var
ORDER BY c.Surname
) t
order by c.SurName
where rn <= 10;
When I use the following query without LIMIT nested in a subquery
SELECT `c`.*,
GROUP_CONCAT(g.photo SEPARATOR "|") AS `photos_list`
FROM `contests` AS `c`
LEFT JOIN
(
SELECT `gallery`.`contest_id`,
`gallery`.`photo`
FROM `gallery`
) AS `g` ON c.id = g.contest_id
GROUP BY `c`.`id`
all works fine
id title photos_list
1 title1 50026c35632eb.jpg
2 title2 50026ac53567f.jpg|50026ac5ec82e.jpg|500e71557270f....
Bun when I add LIMIT, I get "photos_list" in only one row. Following query
SELECT `c`.*,
GROUP_CONCAT(g.photo SEPARATOR "|") AS `photos_list`
FROM `contests` AS `c`
LEFT JOIN
(
SELECT `gallery`.`contest_id`,
`gallery`.`photo`
FROM `gallery`
LIMIT 0, 2
) AS `g` ON c.id = g.contest_id
GROUP BY `c`.`id`
will return
id title photos_list
1 title1 NULL
2 title2 50026ac46ea05.jpg|50026ac53567f.jpg
Item with an id = 1 has to contain photos_list, but it doesn't. Noteworthy that LIMIT does work for item with an id = 2.
What should I do to get a correct result?
SELECT `c`.*,
GROUP_CONCAT(g.photo SEPARATOR "|") AS `photos_list`
FROM `contests` AS `c`
LEFT JOIN
(
SELECT `gallery`.`contest_id`,
`gallery`.`photo`
FROM `gallery`
) AS `g` ON c.id = g.contest_id
GROUP BY `c`.`id`
Change GROUP_CONCAT to this:
SUBSTRING_INDEX(GROUP_CONCAT(g.photo SEPARATOR "|"),'|',2) AS `photos_list`
You can do similar things with timestamps (e.g. AND photo_date > gsub.photo_date) or more complex criteria. The only caveat is that if there are several rows that all match the conditions (e.g. several photos have identical timestamps), all of them will be included. That's why I chose photo_id, which is assumably unique.
Insert it into your original query like so:
SELECT c.id, c.title,
GROUP_CONCAT(g.photo SEPARATOR "|") AS photos_list
FROM contests AS c
LEFT JOIN (
//put query from above here
) AS g
ON c.id = g.contest_id GROUP BY c.id
This works as well. However, without wrapping another SELECT clause around it, if there are no photos for a contest, the contest will not show up.
SELECT c.*, GROUP_CONCAT(g.photo SEPARATOR "|") AS photo_list
FROM
contests c
LEFT JOIN
(SELECT *, #num:= if(#contest = contest_id, #num + 1,1) as row_num,
#contest := contest_id as c_id
FROM gallery
ORDER BY contest_id) AS g
ON c.id = g.contest_id
WHERE g.row_num <= 2
GROUP BY c.id, c.title
SELECT c.*, ((
SELECT GROUP_CONCAT(temp.photo SEPARATOR "|")
FROM (SELECT photo FROM gallery g WHERE c.id = g.contest_id LIMIT 2) temp
)) AS photo_list
FROM contests c
Sorry for the incorrect answer. I'm not saying that the following solution is the optimum one but at least it works. BTW, in this new solution I've assumed that you gallery table has a primary key named id.
SELECT c.*, GROUP_CONCAT(g.photo SEPARATOR "|") AS photos_list
FROM contests AS c
LEFT JOIN (
SELECT
g_0.*
FROM (
SELECT
g_1.*
, ((SELECT COUNT(*) FROM gallery g_2 WHERE g_2.contest_id = g_1.contest_id AND g_2.id <= g_1.id)) AS i
FROM gallery g_1
) g_0
WHERE
g_0.i <= 2
) g ON (c.id = g.contest_id)
GROUP BY c.id
How do you decide which 2 of the possible set of photos for a particular contest should be returned? Is it meant to be a random thing? Or is it the 2 most recent photos, or the 2 highest rated photos, or some other criteria? Once you can set a condition for choosing the photos, the rest is straighforward. This query would get you the 2 photos with the highest photo_ids for each contest_id:
SELECT contest_id, photo, photo_id
FROM gallery gsub
WHERE (
SELECT COUNT(*) FROM gallery
WHERE contest_id=gsub.contest_id //for each category
AND photo_id > gsub.photo_id
) < 2 //if number of photo_ids > than this photo_id < 2, keep this photo
ORDER BY contest_id
You can do similar things with timestamps (e.g. AND photo_date > gsub.photo_date) or more complex criteria. The only caveat is that if there are several rows that all match the conditions (e.g. several photos have identical timestamps), all of them will be included. That's why I chose photo_id, which is assumably unique.
Insert it into your original query like so:
SELECT c.id, c.title,
GROUP_CONCAT(g.photo SEPARATOR "|") AS photos_list
FROM contests AS c
LEFT JOIN (
//put query from above here
) AS g
ON c.id = g.contest_id GROUP BY c.id
Im trying to select a table with multiple joins, one for the number of comments using COUNT and one to select the total vote value using SUM, the problem is that the two joins affect each other, instead of showing:
3 votes 2 comments
I get 3 * 2 = 6 votes and 2 * 3 comments
This is the query I'm using:
SELECT t.*, COUNT(c.id) as comments, COALESCE(SUM(v.vote), 0) as votes
FROM (topics t)
LEFT JOIN comments c ON c.topic_id = t.id
LEFT JOIN votes v ON v.topic_id = t.id
WHERE t.id = 9
What you're doing is an SQL antipattern that I call Goldberg Machine. Why make the problem so much harder by forcing it to be done in a single SQL query?
Here is how I would really solve this problem:
SELECT t.*, COUNT(c.id) as comments
FROM topics t
LEFT JOIN comments c ON c.topic_id = t.id
WHERE t.id = 9;
SELECT t.*, SUM(v.vote) as votes
FROM topics t
LEFT JOIN votes v ON v.topic_id = t.id
WHERE t.id = 9;
As you have found, combining these two into one query results in a Cartesian product. There may be clever and subtle ways to force it to give you the correct answer in one query, but what happens when you need a third statistic? It's much simpler to do it in two queries.
SELECT t.*, COUNT(c.id) as comments, COALESCE(SUM(v.vote), 0) as votes
FROM (topics t)
LEFT JOIN comments c ON c.topic_id = t.id
LEFT JOIN votes v ON v.topic_id = t.id
WHERE t.id = 9
GROUP BY t.id
or perhaps
SELECT `topics`.*,
(
SELECT COUNT(*)
FROM `comments`
WHERE `topic_id` = `topics`.`id`
) AS `num_comments`,
(
SELECT IFNULL(SUM(`vote`), 0)
FROM `votes`
WHERE `topic_id` = `topics`.`id`
) AS `vote_total`
FROM `topics`
WHERE `id` = 9
SELECT t.*, COUNT(DISTINCT c.id) as comments, COALESCE(SUM(v.vote), 0) as votes
FROM (topics t)
LEFT JOIN comments c ON c.topic_id = t.id
LEFT JOIN votes v ON v.topic_id = t.id
WHERE t.id = 9