Prevent table from being updated while updating und deleting rows - mysql

I'm bulding a matchmaking system for a little game. My table which saves all players searching for opponents looks like this:
lobby
----------
userID
points
range
opponentID
Every 5 seconds the script tries to find an opponent within a certain range of points.
UPDATE lobby SET
opponentID = {myUserID}
WHERE points >= {myPoints} - {myRange} AND points <= {myPoints} + {myRange}
AND opponentID = 0 AND userID != {myUserID}
ORDER BY ABS(points - {myPoints}) ASC
LIMIT 1
If there are affected rows I remove the current player from the lobby and get the opponents ID:
DELETE FROM lobby
WHERE userID = {myUserID}
SELECT userID FROM lobby
WHERE opponentID = {myUserID}
Now my fear is that someone assigns the current user between the UPDATE and DELETE. How can I prevent this? Can I combine the UPDATE and DELETE?

Related

SELECT last row grouped by account and assigned number

I have a table "Log"
My game server inserts a record into this table when someone login the server, then inserts a second record when they logout.
What I want to do is create a query to count the number of people logged in.
the main data that gets inserted to the table "Log"
When they Login:
[Type] = 0
[Player1] = Their account ID
[Value2] = a random number which matches the logout row when they logout
[Value3] = 0
When they Logout:
[Type] = 1
[Player1] = Their account ID
[Value2] = a random number which matches the login row when they logout
[Value3] = some random number
Is there a way I can count the last "Player1" of each account and check if "Type" = 0 which means that account is logged in then echo the result.
The result I'm looking for would pull the last record of every account an count them.
Note: everytime an account logs in and out it inserts them 2 records so if 1 account logs in 20 times there would be 40 records in "Log"
One way to do it is to count all rows with type 0 for which there doesn't exist any type 1 row with the same player and a later date:
select count(*) as number_of_logged_in
from log l
where Type = 0 -- 0 meaning log on event
-- and [Value3] = 0 -- maybe this should be included
and not exists (
select 1 from log
where Player1 = l.Player1
and type = 1 -- 1 meaning log out event
and date > l.date
-- and [Value2] = l.[Value2] -- maybe this should be included
);
I found your problem statement a bit confusing as you say you want to count the number of people that are logged in, but then you say I want to count the last of each [Player!] where [Type] is 1 which seems to be the opposite. It's also not clear to me why the random number would be important - if the last recorded type for a user is 0 then they should be considered as logged in, or?
Sample SQL Fiddle with some demo data
I am assuming you want list of the all the logged in players names,so you can try using the ROW_NUMBER() to get what you want,
;WITH CTE AS(
SELECT
Player1 AS LoggedInPlayer,
ROW_NUMBER() OVER (PARTITION BY Player1 ORDER BY datecolumn Asc) As LoggedValue
FROM
yourtable
)
SELECT
*
FROM
CTE
WHERE
LoggedValue = 1
If you know, that all logins and logouts are stored in Log without gaps, you can simply count them and if there's a difference you know, that the player is currently logged in.
SELECT logins.player1, logouts.cnt - logins.cnt
FROM
(select player1, count(*) as cnt from Log where type = 0 group by player1) as logins
LEFT OUTER JOIN
(select player1, count(*) as cnt from Log where type = 1 group by player1) as logouts
ON (logins.player1 = logouts.player1)
WHERE logins.cnt > logouts.cnt or logouts.player1 is null
You need the left outer join, if the player logged in one time and never logged out. Sorry, if you encounter syntax issues. I just wrote this without testing and usually work on a Teradata System and the SQL Dialect there. But as the SQL given here is plain Ansi, it should work on any database.

Prevent simultaneous database updates in MySQL without locking the table

I have a multiplayer card game. Each game is played by 4 players. Each player is asigned a seat (1, 2, 3, 4), players click to join the game and when all 4 seats are filled the game can begin.
A game entry in the DB looks like:
id | status
13 | 3
The status is the number of filled seats. It is incremented every time a new player joins. Now I have a problem. Sometimes 2 players send a request simultaneously and they are both added to the same seat. In the example above, two requests are received and suddenly there are 5 players in the game, with 2 of them assigned to seat 4.
Obviously before I update, I check that there aren't already 4 players.
Something like:
if($row['status'] < 4){
mysql_query("update games set status=status+1 where id=13");
}
Logically, this should prevent the status ever being more than 4, but if there are lots of players joining games, there's often a situation where status becomes 5 because multiple players join simultaneously.
I suppose I could prevent this by using table lock but I don't want to lock the whole table for the purpose of updating just one row (there could be 50 games in progress at the same time).
There's gotta be a reliable way to resolve this, right?
You could incorporate the constraint into the update statement itself, so that it is regulated in the DB:
update games set status = coalesce(status, 0) + 1
where id=13
and coalesce(status, 0) < 4
You could also use "select for update."
Something like:
sql:
begin(); // Need to be in a transaction.
select status from $YOUR_TABLE where $SOMETHING for update
code:
if(status<4)
Set new status in database.
commit();
Select for update lock the row(Not the database, only the single row which match the where).

Relational Database Logic

I'm fairly new to php / mysql programming and I'm having a hard time figuring out the logic for a relational database that I'm trying to build. Here's the problem:
I have different leaders who will be in charge of a store anytime between 9am and 9pm.
A customer who has visited the store can rate their experience on a scale of 1 to 5.
I'm building a site that will allow me to store the shifts that a leader worked as seen below.
When I hit submit, the site would take the data leaderName:"George", shiftTimeArray: 11am, 1pm, 6pm (from the example in the picture) and the shiftDate and send them to an SQL database.
Later, I want to be able to get the average score for a person by sending a query to mysql, retrieving all of the scores that that leader received and averaging them together. I know the code to build the forms and to perform the search. However, I'm having a hard time coming up with the logic for the tables that will relate the data. Currently, I have a mysql table called responses that contains the following fields,
leader_id
shift_date // contains the date that the leader worked
shift_time // contains the time that the leader worked
visit_date // contains the date that the survey/score was given
visit_time // contains the time that the survey/score was given
score // contains the actual score of the survey (1-5)
I enter the shifts that the leader works at the beginning of the week and then enter the survey scores in as they come in during the week.
So Here's the Question: What mysql tables and fields should I create to relate this data so that I can query a leader's name and get the average score from all of their surveys?
You want tables like:
Leader (leader_id, name, etc)
Shift (leader_id, shift_date, shift_time)
SurveyResult (visit_date, visit_time, score)
Note: omitted the surrogate primary keys for Shift and SurveyResult that I would probably include.
To query you join shifts and surveys group on leader and taking the average then jon that back to leader for a name.
The query might be something like (but I haven;t actually built it in MySQL to verify syntax)
SELECT name
,AverageScore
FROM Leader a
INNER JOIN (
SELECT leader_id
, AVG(score) AverageScore
FROM Shift
INNER JOIN
SurveyResult ON shift_date = visit_date
AND shift_time = visit_time --depends on how you are recording time what this really needs to be
GROUP BY leader ID
) b ON a.leader_id = b.leader_id
I would do the following structure:
leaders
id
name
leaders_timetabke (can be multiple per leader)
id,
leader_id
shift_datetime (I assume it stores date and hour here, minutes and seconds are always 0
survey_scores
id,
visit_datetime
score
SELECT l.id, l.name, AVG(s.score) FROM leaders l
INNER JOIN leaders_timetable lt ON lt.leader_id = l.id
INNER JOIN survey_scores s ON lt.shift_datetime=DATE_FORMAT('Y-m-d H:00:00', s.visit_datetime)
GROUP BY l.id
DATE_FORMAT here helps to cut hours and minutes from visit_datetime so that it could be matched against shift_datetime. This is MYSQL function, so if you use something else you'll need to use different function
Say you have a 'leader' who has 5 survey rows with scores 1, 2, 3, 4 and 5.
if you select all surveys from this leader, sum the survey scores and divide them by 5 (the total amount of surveys that this leader has). You will have the average, in this case 3.
(1 + 2 + 3 + 4 + 5) / 5 = 3
You wouldn't need to create any more tables or fields, you have what you need.

Get entry with max value in MySQL

I've got a MySQL database with lots of entris of highscores for a game. I would like to get the "personal best" entry with the max value of score.
I found a solution that I thought worked, until I got more names in my database, then it returnes completely different results.
My code so far:
SELECT name, score, date, version, mode, custom
FROM highscore
WHERE score =
(SELECT MAX(score)
FROM highscore
WHERE name = 'jonte' && gamename = 'game1')
For a lot of values, this actually returns the correct value as such:
JONTE 240 2014-04-28 02:52:33 1 0 2053
It worked fine with a few hundred entries, some with different names. But when I added new entries and swapped name to 'gabbes', for the new names I instead get a list of multiple entries. I don't see the logic here as the entries in the database seem quite identical with some differences in data.
JONTE 176 2014-04-28 11:03:46 1 0 63
GABBES 176 2014-04-28 11:09:12 1 0 3087
The above has two entires, but sometimes it may also return 10-20 entries in a row too.
Any help?
If you want the high score for each person (i.e. personal best) you can do this...
SELECT name, max(score)
FROM highscore
WHERE gamename = 'game1'
GROUP BY name
Alternatively, you can do this...
SELECT name, score, date, version, mode, custom
FROM highscore h1
WHERE score =
(SELECT MAX(score)
FROM highscore h2
WHERE name = h1.name && gamename = 'game1')
NOTE: In your SQL, your subclause is missing the name = h1.name predicate.
Note however, that this second option will give multiple rows for the same person if they recorded the same high score multiple times.
The multiple entries are returned because multiple entries have the same high score. You can add LIMIT 1 to get only a single entry. You can choose which entry to return with the ORDER BY clause.

Recordings mark for deletion after X weeks and X count based on a business

I am having troubles writing (what I think is) a complex query. I have a recordings table that holds all active recordings on my site. I wish to keep recordings for a given X number of weeks before they are marked "marked for deletion". Also I want to only store X number of recordings for a particular "recs_bus_id" or business. Each business is only allowed to have X number of recordings for a account before marked for deletion and the timestamp for recs_mark_deletion_time is also updated
Here is my problem, I don't know how to do this without doing a insane amount of queries
RECORDINGS
recs_id | recs_insert_time | recs_title | recs_bus_id | recs_mark_deletion_time | recs_mark_deletion
BUSINESSES
bus_id | bus_rec_save_weeks | bus_max_rec_save_count
bus_max_rec_save_count is the total number of recordings that business is allowed save
Right now I am doing this for get the recordings to mark for deletion
SELECT * FROM recordings JOIN businesses ON Recording.recs_bus_id = Business.bus_id AND Recording.recs_insert_time < NOW() - INTERVAL Business.bus_rec_save_weeks WEEK
I then just loop through the resultset and delete mark each for deletion
I do not know how to get the resultset of the records that are outside of the top "bus_max_rec_save_count" based on a "bus_id" to mark for deletion.
You should be using update to update the flag:
update recordings JOIN
businesses
ON Recording.recs_bus_id = Business.bus_id AND
Recording.recs_insert_time < NOW() - INTERVAL Business.bus_rec_save_weeks WEEK
set recordins.MarkForDeletion = 1;