Different results for the same query but inside a function - mysql

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

Related

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;

select one row multiple time when using IN()

I have this query :
select
name
from
provinces
WHERE
province_id IN(1,3,2,1)
ORDER BY FIELD(province_id, 1,3,2,1)
the Number of values in IN() are dynamic
How can I get all rows even duplicates ( in this example -> 1 ) with given ORDER BY ?
the result should be like this :
name1
name3
name2
name1
plus I shouldn't use UNION ALL :
select * from provinces WHERE province_id=1
UNION ALL
select * from provinces WHERE province_id=3
UNION ALL
select * from provinces WHERE province_id=2
UNION ALL
select * from provinces WHERE province_id=1
You need a helper table here. On SQL Server that can be something like:
SELECT name
FROM (Values (1),(3),(2),(1)) As list (id) --< List of values to join to as a table
INNER JOIN provinces ON province_id = list.id
Update: In MySQL Split Comma Separated String Into Temp Table can be used to split string parameter into a helper table.
To get the same row more than once you need to join in another table. I suggest to create, only once(!), a helper table. This table will just contain a series of natural numbers (1, 2, 3, 4, ... etc). Such a table can be useful for many other purposes.
Here is the script to create it:
create table seq (num int);
insert into seq values (1),(2),(3),(4),(5),(6),(7),(8);
insert into seq select num+8 from seq;
insert into seq select num+16 from seq;
insert into seq select num+32 from seq;
insert into seq select num+64 from seq;
/* continue doubling the number of records until you feel you have enough */
For the task at hand it is not necessary to add many records, as you only need to make sure you never have more repetitions in your in condition than in the above seq table. I guess 128 will be good enough, but feel free to double the number of records a few times more.
Once you have the above, you can write queries like this:
select province_id,
name,
#pos := instr(#in2 := insert(#in2, #pos+1, 1, '#'),
concat(',',province_id,',')) ord
from (select #in := '0,1,2,3,1,0', #in2 := #in, #pos := 10000) init
inner join provinces
on find_in_set(province_id, #in)
inner join seq
on num <= length(replace(#in, concat(',',province_id,','),
concat(',+',province_id,',')))-length(#in)
order by ord asc
Output for the sample data and sample in list:
| province_id | name | ord |
|-------------|--------|-----|
| 1 | name 1 | 2 |
| 2 | name 2 | 4 |
| 3 | name 3 | 6 |
| 1 | name 1 | 8 |
SQL Fiddle
How it works
You need to put the list of values in the assignment to the variable #in. For it to work, every valid id must be wrapped between commas, so that is why there is a dummy zero at the start and the end.
By joining in the seq table the result set can grow. The number of records joined in from seq for a particular provinces record is equal to the number of occurrences of the corresponding province_id in the list #in.
There is no out-of-the-box function to count the number of such occurrences, so the expression at the right of num <= may look a bit complex. But it just adds a character for every match in #in and checks how much the length grows by that action. That growth is the number of occurrences.
In the select clause the position of the province_id in the #in list is returned and used to order the result set, so it corresponds to the order in the #in list. In fact, the position is taken with reference to #in2, which is a copy of #in, but is allowed to change:
While this #pos is being calculated, the number at the previous found #pos in #in2 is destroyed with a # character, so the same province_id cannot be found again at the same position.
Its unclear exactly what you are wanting, but here's why its not working the way you want. The IN keyword is shorthand for creating a statement like ....Where province_id = 1 OR province_id = 2 OR province_id = 3 OR province_id = 1. Since province_id = 1 is evaluated as true at the beginning of that statement, it doesn't matter that it is included again later, it is already true. This has no bearing on whether the result returns a duplicate.

How to loop through results in an sproc?

I'm learning about stored procedures in mysql (5.5), and have hit a bit of a mental block here about what can be done using sprocs.
The base data looks like this:
select * from fruit;
name | variety | price | quantity
---------------------------------
Pear Comice - 15 - 2
Pear Barlett - 20 - 3
Pear Anjou - 20 - 3
Apple Red - 10 - 7
etc
How do I get the combined monetary value of ALL types of a fruit, say, all Pear types?
I got as far as making this sproc which will get the value of a single variety of a fruit.
DROP PROCEDURE IF EXISTS getStockValue;
DELIMITER // CREATE PROCEDURE `getStockValue`(
IN variety varchar(20),
IN vat BOOLEAN,
OUT tot DECIMAL(8,2)
)
BEGIN
DECLARE nett_value INT;
SELECT (quantity*price) INTO nett_value from fruit where variety = variety;
IF vat = 1 THEN
SELECT (nett_value*20/100)+(nett_value) INTO tot;
ELSE
SELECT nett_value INTO tot;
END IF;
SELECT tot;
END;// DELIMITER ;
CALL getStockValue('Comice',1,#tot);
So from my base data you see that without VAT it should come back with the total 150, and with VAT 180.
Do I have another sproc which loops through a result set somehow?
What is the best way to tackle this so that this computation stays on the database server?
Is this where a cursor would be used?
I've read an awful lot about when to/not to use sprocs, but I have an interview with a company that have warned me they rely heavily on them already.
EDIT - in order to clarify my overall question.
How do I get from where I am:
CALL getStockValue('Comice',1,#tot);
// gives 36
(in hindsight should be renamed getStockValueByVariety())
To where I want to be:
CALL getStockValueByName('Pear',1,#tot);
// gives 180 - because it gets ALL Pear types, not just the variety Comice
FINALLY - twigged, I was missing a GROUP BY ...
SELECT SUM(price*quantity) as tot
FROM fruit
WHERE name = 'Pear'
GROUP BY name;
use a CASE statement and just return the value from the stored procedure.
SELECT
CASE vat
WHEN 1 THEN (((quantity*price)*20/100) + (quantity * price))
ELSE (quantity*price)
END AS nett_value
FROM fruit;
If you want the sum total of all of a particular variety, then SUM it
SELECT
SUM(CASE vat
WHEN 1 THEN (((quantity*price)*20/100) + (quantity * price))
ELSE (quantity*price)
END) AS tot
FROM fruit
WHERE variety = #variety
GROUP BY
name

Replace mysql user defined variable

I have a query which works great given that the result is only one number, but now I need to allow for multiple rows to be returned and the query cannot handle that because it uses a user define variable... here is original procedure
CREATE DEFINER=`root`#`%` PROCEDURE `MapRank`(pTankID bigint,pMapID int, pColor int(2))
BEGIN
SET #RankNumber:=0;
select RankNumber
from
(select
TankID,
#RankNumber:=#RankNumber+1 as RankNumber,
MapID,
Color
from MAPDATA WHERE MapID = pMapID order by Rank DESC, TotalPP DESC) Query1 where TankID = pTankID AND COLOR = pColor ;
END
this returns a single number, essentially counting the number of records down it is, giving me the "row" location.
now I need to change it to give me all rows with out the where for mapid and color, so that I can see all ranks for all mapid/color combo
this is what I have that currently does not work
SET #RankNumber:=0;
select
RankNumber,MapID,COlor
from
(select
TankID,
#RankNumber:=#RankNumber + 1 as RankNumber,
MapID,
Color
from
MAPDATA
order by TotalPP DESC) Query1
where
TankID = 18209 ORDER BY RankNumber
the yielding query result looks as such:
1062 3 1
3544 3 0
6717 17 1
6752 17 3
7453 3 2
7860 17 0
7984 17 2
9220 3 3
if I run manually lets say, map id 3 and color 3 which says rank number is 9220 with the FIRST query I get this
6022
I need this to be able to be done possibly from multiple MySQL connections so ideally done without use of a temporary variable since its possible another person may come in and use that... any help would be great.
After digging and messing more I have found the solution to be to set the variable back to zero from within the outer select.. and since user defined variable are connection level and I utilize pooling we should never have an issue.
SET #RankNumber:=0;
select
RankNumber,MapID,COlor, #RankNumber:=0
from
(select
TankID,
#RankNumber:=#RankNumber + 1 as RankNumber,
MapID,
Color
from
MAPDATA
order by MapID, Rank DESC, TotalPP DESC ) Query1
where
TankID = pTankID ORDER BY RankNumber;

mysql scalar function with for loop

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