I have a SQL query in PHP that recalculates the average rating when the user cancels his vote:
$delete_vote = $conn->prepare("UPDATE table SET
votes = votes - 1,
total_value = total_value - :recall_vote,
average = (total_value * 10) / (total_votes * 10)
WHERE id=id");
The problem is with the recalculation of the average vote. Even if I set the default value for the column 'total_value' and for 'average' as zero, if the query in question has only one vote, and that vote is being recalled, the average value will be set to null instead of 0, as I wish it to be. One possible way of solving this would be to retrieve these two values from the database, check if they're null, and then have them changed to 0--but that's a bit of a hassle. So instead, I want to know if there's a simpler solution.
Another thing I've tried is to add a zero to the calculation, hoping that the null would convert to 0:
$vote_count = $conn->prepare("UPDATE table SET
total_votes = (0 + total_votes) + 1
That doesn't work either. Is there a simple solution to this?
Since you're using PHP, I'm assuming you're using MySQL with it. But I think every RDBMS has a similar function... In MySQL you can define a fallback value for when a value is NULL. If the value isn't NULL, it will return the original value:
IFNULL(something, 0)
Besides that, slightly offtopic maybe; I usually try to avoid denormalisation like you did by saving the average - which in most cases can be calculated when querying the database. But this depends on the situation.
I totally do not understand your where clause -- which would almost always evaluate to true.
The correct syntax to use is the ANSI standard COALESCE() or CASE which are available in almost all databases. You could write this as:
UPDATE table
SET votes = votes - 1,
total_value = total_value - :recall_vote,
average = COALESCE((total_value * 10) / NULLIF(total_votes * 10, 0), 0)
WHERE id = $id -- something other than `id`
I think I would be inclined to be explicit:
UPDATE table
SET votes = votes - 1,
total_value = total_value - :recall_vote,
average = (CASE WHEN total_votes > 0
THEN total_value / total_votes
ELSE 0
END)
WHERE id = $id -- something other than `id`
By the way, why are you multiplying by 10 in both the numerator and denominator? Seems unnecessary.
Related
I have a PHP file that looks for a random id from a MySQL DB, but when the table is big enough it gets slow.
ID row has gaps.
Original
$sql = "SELECT * FROM definiciones ORDER BY rand() LIMIT 1";
Idea
$random = mt_rand(0, 10000);
$sql = "SELECT * FROM definiciones WHERE id = (SELECT max(id) FROM definitiones WHERE id < $random)";
I know the exact amount of rows in the DB beforehand. Is it a good idea to replace the original query?
Is it a good idea to replace the original query?
Yes, but there's a simpler way of expressing this:
SELECT * FROM definiciones WHERE id >= ? ORDER BY id LIMIT 1
With ? set to a random number between 0 and the maximum ID in the table.
Now, an improvement: If there are any gaps in the values of id, the results from the previous method will be skewed somewhat. (For instance, if there are no rows with id < 100, then a row with id = 100 will be selected much more often than one with id = 101.) You can avoid this by using a separate column for randomization. First, you will need to add the column:
ALTER TABLE definiciones ADD COLUMN randomval FLOAT NOT NULL,
ADD KEY randomval (randomval);
UPDATE TABLE definiciones SET randomval = RAND();
Then, to select a fairly chosen random item:
SELECT * FROM definiciones WHERE randomval > ? LIMIT 1;
using a random value between 0 and 1 for the parameter.
There is a small chance that this will return no rows (if RAND() selects a value greater than the highest value in the table). If this happens, repeat the query.
You will need to set randomval = RAND() when inserting new rows into the table.
A user can input it's preferences to find other users.
Now based on that input, I'd like to get the top 10 best matches to the preferences.
What I thought is:
1) Create a select statement that resolves users preferences
if ($stmt = $mysqli->prepare("SELECT sex FROM ledenvoorkeuren WHERE userid = you"))
$stmt->bind_result($ownsex);
2) Create a select statement that checks all users except for yourself
if ($stmt = $mysqli->prepare("SELECT sex FROM ledenvoorkeuren WHERE userid <> you"))
$stmt->bind_result($othersex);
3) Match select statement 1 with select statement 2
while ($stmt->fetch()) {
$match = 0;
if ($ownsex == $othersex) {
$match = $match + 10;
}
// check next preference
4) Start with a variable with value 0, if preference matches -> variable + 10%
Problem is, I can do this for all members, but how can I then select the top 10???
I think I need to do this in the SQL statement, but I have no idea how...
Ofcourse this is one just one preference and a super simple version of my code, but you'll get the idea. There are like 15 preference settings.
// EDIT //
I would also like to see how much the match rating is on screen!
Well, it was a good question from the start so I upvoted it and then wasted about 1 hour to produce the following :)
Data
I have used a DB named test and table named t for our experiment here.
Below you can find a screenshot showing this table's structure (3 int columns, 1 char(1) column) and complete data
As you can see, everything is rather simple - we have a 4 columns, with id serving as primary key, and a few records (rows).
What we want to achieve
We want to be able to select a limited set of rows from this table based upon some complex criteria, involving comparison of several column's values against needed parameters.
Solution
I've decided to create a function for this. SQL statement follows:
use test;
drop function if exists calcMatch;
delimiter //
create function calcMatch (recordId int, neededQty int, neededSex char(1)) returns int
begin
declare selectedQty int;
declare selectedSex char(1);
declare matchValue int;
set matchValue = 0;
select qty, sex into selectedQty, selectedSex from t where id = recordId;
if selectedQty = neededQty then
set matchValue = matchValue + 10;
end if;
if selectedSex = neededSex then
set matchValue = matchValue + 10;
end if;
return matchValue;
end//
delimiter ;
Minor explanation
Function calculates how well one particular record matches the specified set of parameters, returning an int value as a result. The bigger the value - the better the match.
Function accepts 3 parameters:
recordId - id of the record for which we need to calculate the result(match value)
neededQty - needed quantity. if the record's qty matches it, the result will be increased
neededSex - needed sex value, if the record's sex matches it, the result will be increased
Function selects via id specified record from the table, initializes the resulting match value with 0, then makes a comparison of each required columns against needed value. In case of successful comparison the return value is increased by 10.
Live test
So, hopefully this solves your problem. Feel free to use this for your own project, add needed parameters to function and compare them against needed columns in your table.
Cheers!
Use the limit and offset in query:
SELECT sex FROM ledenvoorkeuren WHERE userid = you limit 10 offset 0
This will give the 10 users data of top most.
You can set a limit in your query like this:
SELECT sex FROM ledenvoorkeuren WHERE userid <> yourid AND sex <> yourpreferredsex limit 0, 10
Where the '0' is the offset, and the '10' your limit
More info here
you may try this
SELECT sex FROM ledenvoorkeuren WHERE userid = you limit 0, 10 order by YOUR_PREFERENCE
I have the following challenge:
a "Tasks" table:
tasksId int
listId int
taskOrder float
in case i want to move all the tasks from list 2 to list 3 i would do something like:
// pseodo code //
#lastTaskOrder = last task order in list 3
loop - {
UPDATE tasks SET taskOrder = #lastTaskOrder + 1, listId = 3 WHERE listId = 2;
#lastTaskOrder++
}
thus the taskOrder stays unique.
in case i want to move all the tasks from list 2 to the beginning of list 3 i would do something like:
// pseodo code //
#firstTaskOrder = first task order in list 3
#delta = #firstTaskOrder / #numberOfTasksToMove
UPDATE tasks SET taskOrder = #firstTaskOrder + #delta, listId = 3 WHERE listId = 2;
#firstTaskOrder = #firstTaskOrder + #delta
is it possible with mySQL + PDO + PHP?
Short answer: yes.
Longer answer involves some code. To update your list_ids and increment them based on the highest current value in the old list, I had to use a subquery with a window function:
UPDATE tasks
SET list_id = :toList,
task_order =
(SELECT MAX(task_order) from tasks where list_id = :toList)
+ t2.task_sort_order
FROM ( SELECT task_id,
row_number() OVER (PARTITION BY list_id order by task_order)
AS task_sort_order
FROM tasks ) t2
WHERE tasks.task_id = t2.task_id AND tasks.list_id = :fromList
Edit This is heavily edited from the first version. I've thrown away all the PHP in favor of just showing the SQL. I changed the column names because my version of Postgres was complaining about the camel-case names.
this proved to be easier than i thought it would be.
It took me 2 hours but i got it figured out:
SET #start=100;
SET #delta=1.5;
UPDATE tasks SET taskOrder = #start:= (#start+#delta), listId = 3
WHERE listId=2
ORDER BY taskOrder
I PDOed this query with the correct values
I have a file hosting site where I provide a point for every unique download to user.
Sample of my table
These points can be redeemed by user. So for example if a user redeems 100 points than what is the best query to reduce points available from each row till 100 points are reduced.
Thank You.
You should create two tables for this:
Table files
- id
- name
- size
Table points
- id
- file_id
(- user)
- points
Insert a new file:
INSERT INTO files (name, size) VALUES ('kat92a.jpg', 105544); // New file with ID 1
Now you can give points to a file, negative or positive:
INSERT INTO points (file_id, points) VALUES (1, 100); //Positive points
INSERT INTO points (file_id, points) VALUES (1, -10); //Negative points
And you can select the total number of points:
SELECT
files.name,
files.size,
(SELECT sum(points) FROM points WHERE file_id = 1) AS points
FROM files
WHERE id = 1
Alright, then, here's the SQL-dumb way I would do it. Hopefully an SQL guru will come around with a better solution. Note: This is pure pseudocode; write your own code based on this--it's not going to work out of the box.
$total_to_deduct = 100;
// Each time, get the row with the highest points
$top_points_query = "SELECT id, points FROM my_table ORDER BY points DESC LIMIT 1;"
do {
$result = do_query($top_points_query);
if($result) {
// I'm assuming you don't want to deduct more points from a row than it has
$num_to_deduct = min($result['points'], $total_to_deduct);
// Now deduct the points from the row we got earlier
$update_query = "UPDATE my_table SET points = points - $num_to_deduct
WHERE id = $result['id']";
if(do_query($update_query)) {
$total_to_deduct -= $num_to_deduct;
}
}
} while($total_to_deduct > 0); // If we still have points to deduct, do it again
Seems like you just need a simple update Statement and allows you to update the row and if it's more than 100 not update it.
update table set points = if( (points+<VALUE>) <= 100,points+<VALUE>,points) where id = <FILE ID>
This will check to see if the points is higher than 100, if it is then the update statement will just return no results. If the value is less than 100, then it will update the table and give you back the amount of rows that were updated.
Just add a column in your user table with the amount of redeemed points. Is that a viable solution for you?
Here is a pure SQL solution, but I warn you that (a) this is untested and (b) it's just a concept.
DECLARE curs CURSOR FOR
SELECT
id,
points,
FROM
points
WHERE
points > 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET remPoints = 0;
OPEN curs;
SET remPoints = 100; /* modify this value, probably in your app */
REPEAT
FETCH curs INTO cId, cPoints;
IF remPoints >= cPoints THEN
UPDATE points SET points = 0 WHERE id = cId;
ELSE
UPDATE points SET points = points - remPoints WHERE id = cId;
END IF;
SET remPoints = remPoints - cPoints;
UNTIL remPoints <= 0;
CLOSE curs;
I have a table y Which has two columns a and b
Entries are:
a b
1 2
1 3
1 4
0 5
0 2
0 4
I want to get 2,3,4 if I search column a for 1, and 5,2,4 if I search column a.
So, if I search A for something that is in A, (1) I get those rows, and if there are no entries A for given value, give me the 'Defaults' (a = '0')
Here is how I would know how to do it:
$r = mysql_query('SELECT `b` FROM `y` WHERE `a` = \'1\';');
//This gives desired results, 3 rows
$r = mysql_query('SELECT `b` FROM `y` WHERE `a` = \'2\';');
//This does not give desired results yet.
//Get the number of rows, and then get the 'defaults'
if(mysql_num_rows($r) === 0) $r = mysql_query('SELECT `b` FROM `y` WHERE `a` = 0;');
So, now that it's sufficiently explained, how do I do that in one query, and what about performance concerns?
The most used portion would be the third query, because there would only be values in a for a number IF you stray from the defaults.
I think I have it:
SELECT b FROM y where a=if(#value IN (select a from y group by a),#value,0);
It checks if #value exists in the table, if not, then it uses 0 as a default.
#value can be a php value too.
Hope it helps :)
You can try something like this. I'm not 100% sure it will work because count() is a aggregate function but its worth a shot.
SELECT b
FROM table1
WHERE a = (
SELECT
CASE count(b)
WHEN 0 THEN :default_value
ELSE :passed_value
END
FROM table1
WHERE a = :passed_value
)
What about
$rows = $db->fetchAll('select a, b FROM y WHERE a IN (2, 0) ORDER BY a DESC');
if(count($rows) > 0) {
$a = $rows[0]['a'];
$i = 0;
while($rows[$i]['a'] === $a) {
echo $rows[$i++]['b']."\n";
}
}
One query, but overhead if there are a lot of 'zero' values.
Depends if you care about the overhead...
I think Michal Kralik best answer in my opinion based on server performance. Doing subselects or stored procedures for such simple logic really is not worth it.
The only way I would improve on Michal's logic is if you are doing this query multiple times in one script. In this case I would query for the 0's first, and then run each individual query, then checking if there was any value.
Pseudo-code
// get the value for hte zero's
$zeros = $db->fetchAll('select a, b FROM y WHERE a = 0');
//checking for 1's
$ones = $db->fetchAll('select a, b FROM y WHERE a = 1');
if(empty($ones)) $ones = $zeros;
//checking for 2's
$twos = $db->fetchAll('select a, b FROM y WHERE a = 2');
if(empty($twos)) $twos = $zeros;
//checking for 3's
$threes = $db->fetchAll('select a, b FROM y WHERE a = 3');
if(empty($threes)) $threes = $zeros;
You can do all this in a single stored procedure with a single parameter.
I have to run out, but I'll try to write one up for you and add it here as soon as I get back from my errand.
I don't know why this was marked down - please educate me. It is a valid, tested stored procedure, and I answered the question. The OP didn't require that the answer be in php. ??
Here's a stored proc to do what you want that works in SQL Server. I'm not sure about MySQL.
create proc GetRealElseGetDefault (#key as int)
as
begin
-- Use this default if the correct data is not found
declare #default int
select #default = 0
-- See if the desired data exists, and if so, get it.
-- Otherwise, get defaults.
if exists (select * from TableY where a = #key)
select b from TableY where a = #key
else
select b from TableY where a = #default
end -- GetRealElseGetDefault
You would run this (in sql server) with
GetRealElseGetDefault 1
Based on a quick google search, exists is fast in MySQL. It would be especially fast is column A is indexed. If your table is large enough for you to be worried about performance, it is probably large enough to index.