mysql scalar function with for loop - mysql

i have the table 'points_level' with :
id | description | level
--------------------------
1 | noob | 0
2 | rookie | 50
3 | The boss | 100
i need to build a function that return the id of the level giving points as integer for example :
select calc_level(12)
result: 1
select calc_level(90)
result: 2
select calc_level(300)
result: 3
select calc_level(100)
result: 3
select calc_level(-50)
result: 1
i need to add some level in the points_level table (in the near future) so i shuldn't calculate the results via some simple "CASE WHEN" i think about a "CASE WHEN" in a for loop, i found some example for SSQL but nothing for MYSQL and i don't know the MYSQL SYNTAX for create this kind of function :(
somebody can help me ?
Thanks

The algorithm is relatively simple - you need the id where the points supplied is greater than or equal to the level:
SELECT id
FROM points_level
WHERE
(<points_input> >= level)
-- For negative input, return the lowest level
OR (<points_input> < 0 AND level <= 0)
ORDER BY level DESC LIMIT 1
http://sqlfiddle.com/#!2/07601/4
Review the MySQL CREATE FUNCTION syntax for the proper way to wrap it in a function (if you feel you really need that).
CREATE FUNCTION calc_level (in_points INT)
RETURNS INT
BEGIN
DECLARE id_out INT;
SET id_out = (
SELECT id
FROM points_level
WHERE
in_points >= level
OR (in_points < 0 AND level <= 0)
ORDER BY level DESC LIMIT 1
);
RETURN id_out;
END

Related

Why does RAND sometimes produce multiple results in a MySQL call?

I have table agent which includes 13 agent IDs and agent names.
agent_id|agent_name|
--------|----------|
1|Jack |
2|Jill |
3|Jo |
...
When I run this query...
select agent_name from agent where agent_id = (FLOOR( 1 + RAND( ) * 13 ))
...MySQL sometimes returns 0 names and sometimes returns many names. Since (FLOOR( 1 + RAND( ) * 13 )) on its own always seems to return a single, non-zero integer, I would expect to get a single name back, but this is not the case. On testing the above query, the following number of names are returned on each execution;
Execution | Total Names Returned
1 | 3
2 | 2
3 | 1
4 | 0
5 | 1
6 | 1
7 | 0
8 | 0
9 | 1
10 | 4
Clearly, when one runs...
select agent_name from agent where agent_id = 3
...the same, single, name is returned each time.
I see that the docs explain that
RAND() in a WHERE clause is evaluated for every row (when selecting from one table) or combination of rows (when selecting from a multiple-table join). Thus, for optimizer purposes, RAND() is not a constant value and cannot be used for index optimizations
I am not sure why this would mean that a single call would return many rows.
If I add LIMIT 1
select * from agent a where agent_id = (FLOOR( 1 + RAND() * 13 )) limit 1
...then the query sometimes returns NULL.
Questions
Why does RAND return different numbers of records?
What is the proper way to return a single random agent?
As the documentation says, it is evaluated for each row. It means, the following:
MySQL gets all rows
For each row, executes RAND() and if RAND() == id, return the row
For example
Get all rows
ID = 1, RAND() = 3, no return
ID = 2, RAND() = 2, return
ID = 3, RAND() = 4, no return
ID = 4, RAND() = 4, return
In this case, you have two results.
If you want to get random agent, maybe better approach would be
SELECT * FROM `agent` ORDER BY RAND() LIMIT 1
I assume that your first query is sometimes returning multiple records because RAND() is being evaluted for each record in the query. If you just want a single random agent, then use:
SELECT agent_name
FROM agent
ORDER BY RAND()
LIMIT 1;
To further explain your current observations, the following expression is being evaluated once per each agent record:
(FLOOR( 1 + RAND( ) * 13 ))
Imagine that for each record, the above happens, by chance, to equal the agent_id. If so, then your first query would return all 13 records. By the way, the documentation link you cited basically says this.

MySQL: select random individual from available to populate new table

I am trying to automate the production of a roster based on leave dates and working preferences. I have generated some data to work with and I now have two tables - one with a list of individuals and their preferences for working on particular days of the week(e.g. some prefer to work on a Tuesday, others only every other Wednesday, etc), and another with leave dates for individuals. That looks like this, where firstpref and secondpref represent weekdays with Mon = 1, Sun = 7 and firstprefclw represents a marker for which week of a 2 week pattern someone prefers (0 = no pref, 1 = wk 1 preferred, 2 = wk2 preferred)
initials | firstpref | firstprefclw | secondpref | secondprefclw
KP | 3 | 0 | 1 | 0
BD | 2 | 1 | 1 | 0
LW | 3 | 0 | 4 | 1
Then there is a table leave_entries which basically has the initials, a start date, and an end date for each leave request.
Finally, there is a pre-calculated clwdates table which contains a marker (a 1 or 2) for each day in one of its columns as to what week of the roster pattern it is.
I have run this query:
SELECT #tdate, DATE_FORMAT(#tdate,'%W') AS whatDay, GROUP_CONCAT(t1.initials separator ',') AS available
FROM people AS t1
WHERE ((t1.firstpref = (DAYOFWEEK(#tdate))-1
AND (t1.firstprefclw = 0 OR (t1.firstprefclw = (SELECT c_dates.clw from clwdates AS c_dates LIMIT i,1))))
OR (t1.secondpref = (DAYOFWEEK(#tdate))-1
AND (t1.secondprefclw = 0 OR (t1.secondprefclw = (SELECT c_dates.clw from clwdates AS c_dates LIMIT i,1)))
OR ((DAYOFWEEK(#tdate))-1 IN (0,5,6))
AND t1.initials NOT IN (SELECT initials FROM leave_entries WHERE #tdate BETWEEN leave_entries.start_date and leave_entries.end_date)
);
My output from that is a list of dates with initials of the pattern:
2018-01-03;Wednesday;KP,LW,TH
My desired output is
2018-01-03;Wednesday;KP
Where the initials of the person have been randomly selected from the list of available people generated by the first set of SELECTs.
I have seen a SO post where a suggestion of how to do this has been made involving SUBSTRING_INDEX (How to select Random Sub string,which seperated by coma(",") From a string), however I note the comment that CSV is not the way to go, and since I have a table which is not CSV, I am wondering:
How can I randomly select an individual's initials from the available ones and create a table which is basically date ; random_person?
So I figured out how to do it.
The first select (outlined above) forms the heart of a PROCEDURE called ROWPERROW() and generates a table called available_people
This is probably filthy MySQL code, but it works:
SET #tdate = 0
DROP TABLE IF EXISTS on_call;
CREATE TABLE working(tdate DATE, whatDay VARCHAR(20), selected VARCHAR(255));
DELIMITER //
DROP PROCEDURE IF EXISTS ROWPERROW2;
CREATE PROCEDURE ROWPERROW2()
BEGIN
DECLARE n INT DEFAULT 0;
DECLARE kk INT DEFAULT 0;
SET n=90; -- or however many days the roster is going to run for
SET kk=0;
WHILE kk<n DO
SET #tdate = (SELECT c_dates.fulldate from clwdates AS c_dates LIMIT kk,1);
INSERT INTO working
SELECT #tdate, DATE_FORMAT(#tdate,'%W') AS whatDay, t1.available
FROM available_people AS t1 -- this is the table created by the first query above
WHERE tdate = #tdate ORDER BY RAND() LIMIT 1;
SET kk = kk + 1;
END WHILE;
end;
//
DELIMITER ;
CALL ROWPERROW2();
SELECT * from working;

Different results for the same query but inside a function

I have a table with Pontuation(Pontuacao) and an unique number for Accomodation(Estadia) and i want to calculate the average pontuation of each accomodation.
This is the table:
Estadia | Pontuacao
-------------------
5 | 5
-------------------
5 | 5
So i made this funcion:
delimiter $$
create function mediapontuacao(estadia int)
returns float
begin
declare media float;
select sum(Pontuacao)/count(*) into media
from EstadiaUtilizador
where Estadia = estadia;
return media;
end $$
If i do this
select mediapontuacao(5); //calculate average pontuation of the accomodation which number is 5
This query gives me the value of 3.965.
But if i do this
select sum(Pontuacao)/count(*)
from EstadiaUtilizador
where Estadia = 5;
In other words calculate average pontuation of the accomodation which number is 5, the exact same thing the function i wrote should do and this query gives me the value of 5.00 which is the correct answer.
I am puzzled why i get different values when it should give the same value, i think.
The problem is here:
where Estadia = estadia
which is the same as, say,
where 1 = 1
Your parameter and column should have different names, so the DBMS knows what you are talking about.
You must use GROUP BYclause
select
Estadia,
sum(Pontuacao) / count(*) as mediapontuacao
from
EstadiaUtilizador
group by
Estadia
having Estadia = 5

MySQL variable in WHERE clause

I need a query that find the recommended TV shows for an user, based on the TV Shows he is following.
Do to this I have the following tables:
the table Progress that contains wich show the user is following and the percentage of seen episodes (to solve this problem we can assume I have only one user in the database)
the table Suggested that contains _id1,_id2 and value (value is the strength of the connections between the show with id=_id1 and the show with id=_id2: the more value is great, the more the shows have something in common).
Note that in this table applies the commutative property, so the strength of the connection between id1 and _id2 is the same of _id1 and _id2. Moreover there aren't two rows such as ROW1._id1=ROW2._id2 AND ROW1._id2 = ROW2._id1
the table ShowCache that contains the details about a TV Show, such as name etc..
The following query is what I'm trying to do, but the result is an empty set:
SET #a = 0; //In other tests this line seem to be necessary
SELECT `ShowCache`.*,
(SUM(value) * (Progress.progress)) as priority
FROM `Suggested`,`ShowCache`, Progress
WHERE
((_id2 = Progress.id AND _id1 NOT IN (SELECT id FROM Progress) AND #a:=_id1)//There is a best way to set a variable here?
OR
(_id1 = Progress.id AND _id2 NOT IN (SELECT id FROM Progress) AND #a:=_id2))
AND `ShowCache`._id = #a //I think that the query fails here
GROUP BY `ShowCache`._id
ORDER BY priority DESC
LIMIT 0,20
I know the problem is related to the usage of variables, but I can't solve it. Any help is really appreciated.
PS: the main problem is that (because of the commutative propriety), without variables I need two queries, wich takes about 3 secs to begin executed (the query is more complex than the above). I'm really trying to make a single query to do this task
PPS: I tied also with an XOR operation, that results in an infinite loop?!?!? Here's the WHERE clause I tried:
((_id2=Progress.id AND #a:=_id1) XOR (_id1=Progress.id AND #a:=_id2)) AND `ShowCache`._id = #a
EDIT:
I come up with this WHERE conditions without using any variable:
(_id2 = Progress.id OR _id1 = Progress.id)
AND `ShowCache`._id = IF(_id2 = Progress.id, _id1,_id2)
AND `ShowCache`._id NOT IN (SELECT id FROM Progress)
It works, but it is very slow.
Your attempt to use xor is clever. If you want to get the nonmatching value you want to use bitwise XOR which is ^
Progress.id ^_id1 ^ _id2
3 ^ 2 ^ 3 = 2
2 ^ 2 ^ 3 = 3
You can use this trick to setup a join and really simplify your query (eliminate the OR's and NOT IN's and do it in one query without variables.)
select users.name as username, showcache.name as show_name,
sum(progress * value) as priority from users
inner join progress on users.id = progress.user_id
inner join suggested on progress.show_id in (suggested.id_1, suggested.id_2)
inner join showcache on showcache.id =
(suggested.id_1 ^ suggested.id_2 ^ progress.show_id)
where showcache.id not in
(select show_id from progress where user_id = users.id)
group by showcache.id
order by priority desc;
I also setup a fiddle to demonstrate it:
http://sqlfiddle.com/#!2/2dcd8/24
To break it down. I created a users table with a single user (but the solution will work with multiple users.)
The select and join to progress is straightforward. The join to suggested uses IN as an alternative to writing it with OR
The join to showcache is where the bitwise XOR happens. One of the id's links up to the progress.show_id and we want to use the other one.
It does include a not in to exclude shows already watched from the results. I could have changed it to not exists? but it seems clearer this way.
You're setting #a's value twice within the where clause, meaning that the query is actually boiling down to:
...
WHERE ... AND `ShowCache`._id = _id2
MySQL evalutes variable assignments in a first-encountered order, so you should leave #a constant until the END of the clause, then assign a new value, e.g
mysql> set #a=5;
mysql> select #a, #a+1, #a*5, #a := #a + 1, #a;
+------+------+------+--------------+------+
| #a | #a+1 | #a*5 | #a := #a + 1 | #a |
+------+------+------+--------------+------+
| 0 | 1 | 0 | 1 | 1 |
| 1 | 2 | 5 | 2 | 2 |
| 2 | 3 | 10 | 3 | 3 |
+------+------+------+--------------+------+
Note that #a's value in the first 3 columns remains constant, UNTIL mysql reaches the #a := #a +1, after which #a has a new value
So perhaps your query should be
set #a = 0;
select #temp := #a, ..., #a := _id2
where
((_id2 = Progress.id AND _id1 NOT IN (SELECT id FROM Progress) AND #temp =_id1)
...
etc...

Given a date find the previous &/or current and next x# Dates in MySQL non-linear

I have a table code_prices that looks something like this:
CODE | DATE | PRICE
ABC | 25-7-2011 | 2.81
ABC | 23-7-2011 | 2.52
ABC | 22-7-2011 | 2.53
ABC | 21-7-2011 | 2.54
ABC | 20-7-2011 | 2.58
ABC | 17-7-2011 | 2.42
ABC | 16-7-2011 | 2.38
The problem with the data set is there are gaps in the dates, so I may want to look for the price of item ABC on the 18th however there is no entry because the item wasnt sold on this date. So I would like to return the most recent hisotrical entry for the price.
Say if I query on the date 19-7-2011, I would like to return the entry on the 17th then the next 10 avalaible entries.
If however I query for the price of ABC on the 20th, I would want to return the price on the 20th and the next 10 prices after that...
What is the most efficient way to go about this either in SQL statement or using a stored proc.
I can think of just writing a stored proc which takes the date as a param and then querying for all rows where DATE >= QUERY-DATE ordering by the date and then selecting the 11 items (via limit). Then basically I need to see if that set contains the current date, if it does then return, otherwise I will need to return the 10 most recent entires out of those 11 and also do another query on the table to return the previous entry by getting the max date where date < QUERY-DATE. I am thinking there might be a better way, however I'm not an expert with SQL (clearly)...
Thanks!
This is for one specific code:
SELECT code, `date`, price
FROM code_prices
WHERE code = #inputCode
AND `date` >=
( SELECT MAX(`date`)
FROM code_prices
WHERE code = #inputCode
AND `date` <= #inputDate
)
ORDER BY `date`
LIMIT 11
For ABC and 19-7-2011, the above will you give the row for 17-7-2011 and the 10 subsequent rows (20-7-2011, 21-7-2011, etc)
I'm not entirely clear on what you want to achieve, but I'll have a go anyway. This searches for the ID of the row that contains a date less than or equal to your specified date. It then uses that ID to return all rows with an ID greater than or equal to that value. It assumes that you have a column other than the date column on which the rows can be ordered. This is because you said that the dates are non-linear - I assume that you must have some other way of ordering the rows.
SELECT id, code, dt, price
FROM code_prices
WHERE id >= (
SELECT id
FROM code_prices
WHERE dt <= '2011-07-24'
ORDER BY dt DESC
LIMIT 1 )
ORDER BY id
LIMIT 11;
Alternative with code condition - thanks to #ypercube for highlighting that ;-)
SELECT id, code, dt, price
FROM code_prices
WHERE code = 'ABC'
AND id >= (
SELECT id
FROM code_prices
WHERE dt <= '2011-07-23'
AND code = 'ABC'
ORDER BY dt DESC
LIMIT 1 )
ORDER BY id
LIMIT 11;