MySQL - Get row number post select & join - mysql

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;

Related

SQL Select Statement off by one variable but can't find issue

I am testing out how to use a count-variable to have a column in my SELECT output be dedicated to simply listing the row number. So if I had 3 rows outputed in the query, then they would be numbered {1,2,3} in their "rank" column as I have written....
Furthermore, I am wanting to select only one row, where that row's "rank" column is 1. The following SQL statement works (making sure I SET #rownum=0 everytime) however, it doesn't give me the appropriate output. So looking for rank=1 I get the row where rank is actually 2. If I look for rank=2 then I get where its really 3. and so on. If I then search for rank=0, I DO NOT get where the rank is 1 Surprisingly. So I'm lost
SET #rownum=0;
SELECT #rownum := #rownum + 1 AS rank,
r.id, c.fname, r.pickupCity, r.pickupState
FROM request r
INNER JOIN client c ON r.client = c.id
INNER JOIN pickup p ON r.id = p.request
INNER JOIN driver d ON d.id = p.driver
WHERE date = '2018-04-18' AND d.id = 1
HAVING rank = 1;
You are missing a really important point: SQL tables represent unordered sets. You should expect no correspondence between ids and row numbers without an order by. Period. And this is even more notable in queries that use joins.
I would suggest using an order by in a subquery, like this:
SELECT x.*, #rownum := #rownum + 1 AS rank
FROM (SELECT r.id, c.fname, r.pickupCity, r.pickupState
FROM request r JOIN
client c
ON r.client = c.id
pickup p
ON r.id = p.request
driver d
ON d.id = p.driver
WHERE date = '2018-04-18' AND d.id = 1
ORDER BY r.id
) x CROSS JOIN
(SELECT #rownum := 0) params
HAVING rank = 1;
That said, another way to write the query would use limit and offset:
SELECT r.id, c.fname, r.pickupCity, r.pickupState
FROM request r JOIN
client c
ON r.client = c.id
pickup p
ON r.id = p.request
driver d
ON d.id = p.driver
WHERE date = '2018-04-18' AND d.id = 1
ORDER BY r.id
LIMIT 1 OFFSET 0;
You can change the OFFSET value to get rows other than the first.

MYSQL how to limit results from join

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

Refer to outer column in inner query Where clause

I have two tables Employee and Departament in relation that is shown bellow. For each departament I want to get the fifth best paid employee.
How the query should look like if we have for example MySQL?
I trying to do something like this but it doesn’t see d.id column in inner select.
select d.name, e.id from Employee e
join Departament d on d.id = e.dep_id
where e.id = (
select s.eid from (
select ee.id as eid, #rowid:=#rowid+1 as rowid from Employee ee, (SELECT #rowid:=0) as init
where ee.dep_id = d.id
order by ee.salary desc
) s
where s.rowid = 5
)
Is it possible to do it in generic way not dependent to any database semantics?
How to do it using MySQL and what is the best way to do it?
SELECT X.NAME, X.SALARY AS SAL_5 FROM
(
SELECT DEPT.NAME,EMP.SALARY, RANK() OVER (PARTITION BY NAME ORDER BY SALARY DESC) RN
FROM EMP, DEPT
WHERE EMP.DEPT_ID=DEPT.DEPT_ID
) X
WHERE X.RN=5;
Just do the row_id calculation in the from clause:
select d.name, e.id
from (select ee.*,
(#rowid := if(#d = dept_id, #rowid + 1,
if(#d := dept_id, 1, 1)
)
) as rowid
from Employee ee cross join
(SELECT #rowid := 0, #d := NULL) as init
order by ee.dept_id, ee.salary desc
) e join
Departament d
on d.id = e.dept_id
where e.rowid = 5;
And, yes, there is an ANSI standard way of doing this. In fact, I can readily think of two approaches. But MySQL supports neither window functions not fetch first 1 row only.

Limit amount of results from one table, but allow infinite from another

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;

Rank not being determined properly

I'm using this query:
SELECT A.place_idx,A.place_id,B.TOTAL_CNT,(#r := #r + 1) AS rank FROM CUSTOM_LIST
AS A
INNER JOIN
(SELECT #r := 0)
AS C
INNER JOIN
(SELECT place_id,COUNT(place_id) AS TOTAL_CNT from COUNT_TABLE GROUP BY place_id)
AS B ON B.place_id=A.place_id order by B.TOTAL_CNT desc;
Which gives this result:
But I want this result:
How do I need to modify my query? What am I doing wrong?
SELECT *,(#r := #r + 1) AS rank FROM
(
SELECT A.place_idx,A.place_id,B.TOTAL_CNT FROM CUSTOM_LIST
AS A
INNER JOIN
(SELECT place_id,COUNT(place_id) AS TOTAL_CNT from COUNT_TABLE GROUP BY place_id)
AS B ON B.place_id=A.place_id order by B.TOTAL_CNT desc
) AS T, (SELECT #r := 0) AS tt
Your C.rank is getting calculated as they are processed, not after they are sorted. There is really no need for this data, anyways. Since you're sorting the rows by your metric, you know the first row is the first rank, etc. You can handle it on the programming side of things after you pull it out.
Alternatively, you can put what you have in an inner select, then do the rank after.