rails using sql variables in find_by_sql - mysql

I have this query:
SET #current_group = NULL;
SET #current_count = 0;
SELECT user_id, MIN( created_at ) as created_at, CASE WHEN #current_group = user_id THEN #current_count WHEN #current_group := user_id THEN #current_count := #current_count + 1 END AS c
FROM notifies
G ROUP BY user_id, c
ORDER BY id desc LIMIT 0 , 10
If i launch it it works
but if i put it in a find_by_sql method like:
Notify.find_by_sql("SET #current_group = NULL; SET #current_count = 0; SELECT user_id, MIN( created_at ) as created_at, CASE WHEN #current_group = user_id THEN #current_count WHEN #current_group := user_id THEN #current_count := #current_count + 1 END AS c FROM notifies GROUP BY user_id, c ORDER BY id desc LIMIT 0 , 10")
It returns this error:
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SET #current_count = 0; SELECT user_id, MIN( created_at ) as created_at, CASE WH' at line 1:
How can i do?
thanks

It's because find_by_sql only works with a single statement.
Setting variables happen in separate statements. set specifically is in the Database Administration section, number 12.4.4, and select is in Data Manipulation section, number 12.2.7. Consoles (usually) allow multiple statements, and keep the variables around, but ActiveRecord queries do not.
To allow multiple statements, I think you have to maintain a persistent connection with the database, which Rails doesn't do (edit: normally). But I'm not certain about that - if anyone else knows, I'd love a more definite reason.
Edit: actually, I have a solution for you. Try this:
items = YourModel.transaction do
YourModel.connection.execute("SET #current_group = NULL;")
YourModel.connection.execute("SET #current_count = 0;")
# this is returned, because it's the last line in the block
YourModel.find_by_sql(%Q|
SELECT user_id, MIN( created_at ) as created_at, CASE WHEN #current_group = user_id THEN #current_count WHEN #current_group := user_id THEN #current_count := #current_count + 1 END AS c
FROM notifies
GROUP BY user_id, c
ORDER BY id desc LIMIT 0 , 10
|)
end
All those run in a single transaction, and any variables / settings inside the transaction block will persist between queries. You're still bound to a single statement per query though. There might be a way to do it without an actual transaction wrapping the whole set, but I haven't looked for it - most likely you want one, or you now have a very specific thing to look for if you know you don't.
Enjoy!

Accordingly to the API it is this syntax:
Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
So you might want to try putting it into an array instead of using parenthesis.
Notify.find_by_sql ["SET #current_group = NULL; SET #current_count = 0; SELECT user_id, MIN( created_at ) as created_at, CASE WHEN #current_group = user_id THEN #current_count WHEN #current_group := user_id THEN #current_count := #current_count + 1 END AS c FROM notifies GROUP BY user_id, c ORDER BY id desc LIMIT 0 , 10"]

Related

how to calculate user ranking from 2 different tables

I have a users table with phase1 and phase2 columns that i need to calculate the users rank in each phase and store it in these fields.
the ranking is calculated based on a different table points where i have the points by phase for each user.
what i am trying to do is
sum all points for each user by phase and calculate his rank based on that
in case the user points are equal compare the sum of grade1 in case that is also equal compare the sum of grade2
update users table with his rank in each phase
here is how my new table look like with some demo data
sql fiddle demo
currently I use the below code to calculate the ranking from my old table where both rank and user info are in the same table
old sql fiddle demo
update users a
join (
select id,
(
select count(distinct total)
from users d
where c.total < d.total
) +1 rank
from users c
) b on a.id = b.id
set a.rank = b.rank
there are analytics function in oracle called as rank() and dense_rank() which can be useful to get your result.
As you are using mysql, I tried to convert those function in mysql equivalent.
You can get the desired result with following query which you can use to update users table. You may have to change it further if for the logic when there is tie on grades as well.
set #pk1 ='';
set #rn1 =1;
set #tot ='';
set #val =1;
SELECT id,
name,
phase,
phasetotal,
denseRank
FROM
(
SELECT id,
name,
phase,
phasetotal,
#rn1 := if(#pk1=phase, #rn1+#val,1) as denseRank,
#val := if(#pk1=phase, if(#tot=phasetotal, #val+1, 1),1) as value,
#pk1 := phase,
#tot := phasetotal
FROM
(
select users.id,users.name, points.phase, sum(points.points)
as phasetotal from users,points where users.id = points.userid
group by users.id, points.phase order by points.phase, phasetotal desc, points.grade1 desc, points.grade2 desc
) A
) B;
Here's the update query
set #pk1 ='';
set #rn1 =1;
set #tot ='';
set #val =1;
UPDATE users u join (
SELECT id,
name,
phase,
phasetotal,
denseRank
FROM
(
SELECT id,
name,
phase,
phasetotal,
#rn1 := if(#pk1=phase, #rn1+#val,1) as denseRank,
#val := if(#pk1=phase, if(#tot=phasetotal, #val+1, 1),1) as value,
#pk1 := phase,
#tot := phasetotal
FROM
(
select users.id,users.name, points.phase, sum(points.points)
as phasetotal from users,points where users.id = points.userid
group by users.id, points.phase order by points.phase, phasetotal desc, points.grade1 desc, points.grade2 desc
) A
) B ) C on u.id = C.id
SET u.phase1 = CASE WHEN C.phase = 1 and u.phase1 = 0 THEN C.denseRank ELSE u.phase1 END,
u.phase2 = CASE WHEN C.phase = 2 and u.phase2 = 0 THEN C.denseRank ELSE u.phase2 END;

Top 20 percent by id - MySQL

I am using a modified version of a query similiar to another question here:Convert SQL Server query to MySQL
Select *
from
(
SELECT tbl.*, #counter := #counter +1 counter
FROM (select #counter:=0) initvar, tbl
Where client_id = 55
ORDER BY ordcolumn
) X
where counter >= (80/100 * #counter);
ORDER BY ordcolumn
tbl.* contains the field 'client_id' and I am attempting to get the top 20% of the records for each client_id in a single statement. Right now if I feed it a single client_id in the where statement it gives me the correct results, however if I feed it multiple client_id's it simply takes the top 20% of the combined recordset instead of doing each client_id individually.
I'm aware of how to do this in most databases, but the logic in MySQL is eluding me. I get the feeling it involves some ranking and partitioning.
Sample data is pretty straight forward.
Client_id rate
1 1
1 2
1 3
(etc to rate = 100)
2 1
2 2
2 3
(etc to rate = 100)
Actual values aren't that clean, but it works.
As an added bonus...there is also a date field associated to these records and 1 to 100 exists for this client for multiple dates. I need to grab the top 20% of records for each client_id, year(date),month(date)
You need to do the enumeration for each client:
SELECT *
FROM (SELECT tbl.*, #counter := #counter +1 counter
(#rn := if(#c = client_id, #rn + 1,
if(#c := client_id, 1, 1)
)
)
FROM (select #c := -1, #rn := 0) initvar CROSS JOIN tbl
ORDER BY client_id, ordcolumn
) t cross join
(SELECT client_id, COUNT(*) as cnt
FROM tbl
GROUP BY client_id
) tt
where rn >= (80/100 * tt.cnt);
ORDER BY ordcolumn;
Using Gordon's answer as a starting point, I think this might be closer to what you need.
SELECT t.*
, (#counter := #counter+1) AS overallRow
, (#clientRow := if(#prevClient = t.client_id, #clientRow + 1,
if(#prevClient := t.client_id, 1, 1) -- This just updates #prevClient without creating an extra field, though it makes it a little harder to read
)
) AS clientRow
-- Alteratively (for everything done in clientRow)
, #clientRow := if(#prevClient = t.client_id, #clientRow + 1, 1) AS clientRow
, #prevClient := t.client_id AS extraField
-- This may be more reliable as well; I not sure if the order
-- of evaluation of IF(,,) is reliable enough to guarantee
-- no side effects in the non-"alternatively" clientRow calculation.
FROM tbl AS t
INNER JOIN (
SELECT client_id, COUNT(*) AS c
FROM tbl
GROUP BY client_id
) AS cc ON tbl.client_id = cc.client_id
INNER JOIN (select #prevClient := -1, #clientRow := 0) AS initvar ON 1 = 1
WHERE t.client_id = 55
HAVING clientRow * 5 < cc.c -- You can use a HAVING without a GROUP BY in MySQL
-- (note that clientRow is derived, so you cannot use it in the `WHERE`)
ORDER BY t.client_id, t.ordcolumn
;

SQL | insert into + select + variables with condition?

My task is inserting 3 random datas per ID from another table
and I got a mistake with syntax
set #num := 0, #type := '' ,#stat :='';
INSERT INTO random
as
(
SELECT
*
FROM (
select userID,userNAME, chaID, chaNAME,goal,gender,
#num := if(#type = userID, #num +1,1) as row_number,
#type := userID as dummy,
#stat as status
from userchar
order by userID
) as x where x.row_number <= 3)
I'm going to put this code in event scheduler to insert the new datas in daily
1064 - You have an error in your SQL syntax; check the manual that
corresponds to your MariaDB server version for the right syntax to use
near 'INSERT INTO random as ( SELECT * FROM ( select userID,userNAME,
chaID, c' at line 2
thank you so much for every suggestions.
I suspect the problem is trying to run multiple statements at the same time. You can fix this by initializing the variables in the query itself:
INSERT INTO random( . . . )
select u.*
from (select userID, userNAME, chaID, chaNAME, goal, gender,
(#num := if(#u = userID, #num +1,
if(#u := userId, 1, 1)
)
) as row_number,
userID as dummy,
#stat as status
from userchar u cross join
(select #u = '', #num := 0, #stat := '') params
order by userID, rand()
) u
where u.row_number <= 3;
There are several other issues:
When using insert, always list the columns. This is particularly important if you are learning SQL, so you learn good habits.
You should not assign a variable value in one expression and use it in another. MySQL (and MariaDB) do not guarantee the order of evaluation of expressions in a select, so the expressions can be evaluated in either order.
If you want random rows, then use rand(). There is a difference between "indeterminate" and "random".

How to get row number in mysql?

I'm making an tracking app, and i want to show the user on which place he/she is. For this I thaught of ordering by 'totalkm' desc and then get the row number. The problem is i don`t know how to do this, as i'm fairly new to the Database world.
I tried something like this:
WITH mytable AS {
SET #row_number = 0;
SELECT (#row_number := #row_number +1) AS num, user,totalkm
FROM profile ORDER BY totalkm DESC ; }
SELECT num
FROM mytable WHERE user = "bogdan9832";
But i get the error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'mytable AS {
SET #row_number = 0' at line 1
From what i understood, there is no support for WITH in mysql. Can someone show me an alternative?
You have to use following query for the same:-
First declare the variable as:-
SET #row_number = 0;
Then use this query:-
SELECT (#row_number:= #row_number + 1) AS ROW_NUMBER, OTHER_COLS
FROM YOUR_TABLE;
SELECT num, user, totalkm
FROM (
SELECT (
#row_number := #row_number +1
) AS num, user, totalkm
FROM profile
ORDER BY totalkm DESC
) AS a
WHERE user = "bogdan9832"
Using sub queries but only a single SQL statement:-
SELECT num
FROM
(
SELECT (#row_number := #row_number +1) AS num, user, totalkm
FROM
(
SELECT banner.*
FROM profile
ORDER BY totalkm DESC
) profile
CROSS JOIN (SELECT #row_number := 0) sub0
) sub0
WHERE user = "bogdan9832"

How to add a WHERE statement correctly to a complex MySQL query?

I have a quite complex query to get some data from the database, sort them and rank them accordingly.
Here is the SQL fiddle for it: SQL Fiddle
Now what I want to do is, to add a WHERE statement to this query, so only limited users will be selected (3 users above and 3 users below, the id = 8).
WHERE sort BETWEEN #userpos - 3 AND #userpos + 3
So it should look something like this, but with the first example:
SQL Fiddle
I have already tried to implement this WHERE statement to this query, but I couldn't figure it out where should I add, as I've always received error (that the column cannot be found).
Any suggestion and / or solution for my problem? Should I rewrite the whole query for this?
If I understand correctly, you can do this with a subquery:
SET #userid = 8
SELECT *
FROM (SELECT #pos := #pos + 1 AS sort, points, r.userid, s.active
FROM rank r JOIN
settings s
USING (userid) CROSS JOIN
(SELECT #pos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE userid = #userid;
Note that this eliminates a layer of subqueries that you have. Otherwise, it is quite similar to your query.
EDIT:
The above was based more on the SQL Fiddle than on the question. (Oops.)
To get three rows before and after a given row is possible and just a small tweak, using a trick. The trick is to define another variable with the user pos and then use that variable in the outer query:
SELECT *
FROM (SELECT #pos := #pos + 1 AS sort, points, r.userid, s.active,
if(userid = #userid, #userpos := #pos, 0)
FROM rank r JOIN
settings s
USING (userid) CROSS JOIN
(SELECT #pos := 0, #userpos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE `sort` between #userpos - 3 and #userpos + 3;
Note: MySQL does not guarantee the order of evaluation for variables in the select. The following is a bit safer in terms of order of execution:
SELECT *
FROM (SELECT (case when (#pos := #pos + 1) is NULL then NULL
when (case when (userid = #userid) then #userpos := #pos else 1 end) is null
then NULL
else #pos
end) AS sort, points, r.userid, s.active,
if(userid = #userid, #userpos := #pos)
FROM rank r JOIN
settings s
USING (userid) CROSS JOIN
(SELECT #pos := 0, #userpos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE `sort` between #userpos - 3 and #userpos + 3;
The weird case statements are to ensure statement executions. The is null is to ensure that the when clauses fail, so the assignments are made sequentially.