better schema for a database - mysql

i have an application in which a user can lock a certain commodity and once he locks it,he has to pick it up in 2 hours. If he doesnt pick it up in 2 hours then that item is unlocked and user loses 1 locking chance. the user has 3 locking chances initially and if he loses all 3 in 2 months time then he is banned for 2 months. now i have prepared a schema for this but i feel its not optimum. the schema looks like this
user table
--locking_chances_left //initially 3
--first_chance_miss_date //date on which the user loses its first locking chance
--second_chance_miss_date //date on which the user loses its second locking chance
--banned // boolean field to indicate whether the user is banned
locked_items table
--item_no
--user_id
--locking_time
banned_users table
--user_id
--ban_date //date on which the user is banned i.e lost the last chance
now i have a event which is scheduled to run every minute to see if any item in locked_items table has been locked for more than 2 hours and if it finds any then it removes it from this table which unlocks the item and then decreases locking_chances_left by 1 from the users table. now i have to keep track of whether a user loses all his chances in a period of 2 months to ban him. so i have kept first_chance_miss_date to keep the date when his chances decrease from 3 to 2 and second_chance_miss_date to keep the date when his chances decrease from 2 to 1. i have an after update trigger on users table that checks when the value of locking_chances_left is changed and it updates the first_chance_miss_date and second_chance_miss_date accordingly. is there some better way without using these 2 fields for miss dates and just using one field.
thanks for bearing this

I'd probably do this with a "user_missed_date" table with user_id and missed_date as fields you can then
select user_id, count(*) as misses from user_missed_date where date>[last two months] group by user_id
Or use that as the basis for a subquery.
You would probably want indexes on both user_id, missed_date and missed_date,user_id

I don't think this is a better solution, but I'll throw it out there:
You could have a table of lock_events, instead of locked_items. Every time an item gets locked, it goes in the event table. If an item gets picked up, you could either delete it, or you could add an additional event saying it was picked up. If you select items that are older than 2 hours, you get a list of expired locked items.
This way you have a history of all the events in the system. It's simple to calculate chances_left and also simple to see if the user burnt all his chances in a 2 month period. You end up doing more CPU cycles here, but you also get a nice record of all the transactions on your site!

Related

Whats the best way to implement this use in my database?

I have a central database containing millions of IDs. And I have a group of users (50-100 users), all being able to request extraction of IDs from this big database.
Atm what I do is when a user sends a GET request, I SELECT 100 ids then update them with the flag USED and return the 100. The problem is, if I get too many requests at the same time, multiple users will receive the same ids (because I dont lock the db when doing select and then update)
If I lock the database my problem will be solved, but it will also be slower.
What other alternative I have?
Thanks!
Look ahead another step... What if a "user" gets 100 rows, then keels over dead. Do you have a way to release those 100 for someone else to work on?
You need an extra table to handle "check out" and "check in". Also, use that table to keep track of the "next" 100 to assign to a user.
When a user checks out the 100, a record of that is stored in the table, together with a timestamp and "who" checked them out. If they don't "check them back in within, say, an hour, then you assign that 100 to another user.
Back on something more mundane... How to pick 100. If there is an auto_increment id with no gaps, then use simple math to chunk up the list. If there are a lot of gaps, then use SELECT id FROM tbl WHERE id > $leftoff ORDER BY id LIMIT 100, 1 to get the end of the next 100.
If each user has their own key, you could pull from the millions of IDs starting from their key*10000. For example, user #9 would first get IDs #90000 to #90099, then #90100 to #90199 next time.
You could set the IDs as "Used" before they get sent back, so one user requesting IDs multiple times will never get duplicates. This needn't lock the database for other users.
If they don't request keys more than 100 times before the database can update, this should avoid collisions. You might need to add logic to allow users who request often not to run out, like by having a pool of IDs that can repopulate their supply, but that depends on particulars that aren't clear from the original question.

Storing count of records in SQL table

Lets say i have a table with posts, and each post has index of topic it belongs to. And i have a table with topics, with integer field, representing number of posts in this topic. When i create new post, i increase this value by 1, and then i delete post, i decrease value by 1.
I do it to not query database each time i need to count number of posts in certain topics.
But i heared that this approach may not be safe to use and actual number of posts in table may not match stored value.
Is there any ceratin info about how safe is it?
Without transactions, the primary issue is timing. Consider a delete and two users:
Time User 1 User 2
1 count = count - 1
2 update finishes How many posts?
3 delete post Count returned
4 delete finishes
Remember that actions such as updates and deletes take a finite amount of time -- even if they take effect all at once. Because of this, User 2 will get the wrong number of posts. This is a race condition; and it may or may not be an issue in your application.
Transactions fix this particular problem, by ensuring that resetting the count and deleting the post both take effect "at the same time".
A secondary issue is data quality. Your data consistency checks are outside the database. Someone can come directly into the database and say "Oh, these posts from user X should be removed". That user might then delete those posts en masse -- but "forget" or not know to change the associated values.
This can be a big issue. Triggers should solve this problem.

Table structure of active members for 4 years in sql

I am working on an achievement system and the users can unlock a badge when they have been active on the site for 4 years.
I tried to store each time a user in logged in but it's not really a good idea.
So my question is, how the table structure should be if I want to know if the user was active for 4 years?
It depends on what you mean by active, but if you mean that four years has elapsed since they first logged on, registered etc. then you just need a date field to store the account setup / logon etc. Then when they logon you can test to see if four years has past since that date and insert a badge accordingly.
You can save the last consecutive log in and the first log in. That's just two columns. Every time a user logs in, you can check if he logged in the day before. If yes, then update that column. If not then update the first log in because that's when the 4 year period will begin again. Let me know if that makes sense or if you need examples to help with understanding the thought process.
That is too generic a question to provide an full solution. By active for 4 years it sounds like you will need to track their actions. So perhaps some sort of transaction or history table that links their userid with a datetime plus actions performed.
Then you just have to define what "active" means such as having performed specific actions at least once per week, etc.
EDIT
First off I am a SQL Server developer, but have attempted to convert this to MySQL syntax for you.
~ = primay keys to ensure uniqueness & good performance
User table
~ UserID unique user id - could be an Identity, GUID or similar field
UserName unique user name
anything else you want to track such as First Name, Last Name, etc
UserAction table
~ UserID link to User table
~ ActionType number indicating what the user performed
1 = login
add in other types in the future if you ever want to track anything else
~ ActionDate datetime of the action
anything else that you may want to track such as the duration or end date (in this case that would be them logging out)
Database systems are designed to hold lots of data so you shouldn't have to worry too much about that unless space is a factor. You can always delete any data that is older than 4 or 5 years if you like.
Normally I would just use a CTE (common table expression) but apparently they won't be available until MySQL 8.0 [https://www.mysql.com/why-mysql/presentations/mysql-80-common-table-expressions/]
So instead here is another method. It assumes that you want a login within each given year. i.e. at least one login from 2017, one from 2016, 2015 & 2014. If instead you wanted at least one login going back 1 year from today and so on then we will need to modify this query.
-- this will return the number of years with at least one login, going as far back as 4 years
SELECT COUNT(1) AS NumberOfYearsWithLogin
FROM (
SELECT YEAR(CURDATE()) - YEAR(ActionDate) AS NumberOfYearsAgo
FROM UserAction
WHERE UserID = 123 -- put user of interest here
AND ActionType = 1 -- login
AND YEAR(CURDATE()) - YEAR(ActionDate) < 4 -- check the last 4 years
GROUP BY YEAR(CURDATE()) - YEAR(ActionDate) -- group by year number with today's year as number 0
) A
;

MySql Logic Optimization

Currently we have a ticket management system and like all ticketing systems it needs to assign cases in a round-robin manner to the agents. Also, at the same time the agent can apply their own filtering logic and work on their queue.
The problem,
The table with the tickets is very large now, spans over 10 million rows.
One ticket should never be assigned to two different users.
To solve the above problem, this is the flow we have,
Select query is fired with filter criteria and limit 0,1
The row returned by the above query is then selected based on id and locked for update.
Lastly we fire the update saying user X has picked the case.
While step 3 executes other user cannot get a lock on the same case, so they fire 3.a query may be multiple times to get the next available case.
As number of users increase this time in step 4 goes higher and higher.
We tried doing a select for update in query at step 4 itself, but it makes the entire query slow. Assuming this is because a huge number of rows in the select query.
Questions,
Is there a different approach we need to take altogether?
Would doing a select and update in a stored procedure ensure the same results as doing a select for update and then update?
P.S - I have asked the same question stackexchange.
The problem is that you are trying to use MySQL level locking to ensure that a ticket cannot be assigned to more than one person. This way there is no way to detect if a ticket is locked by a user.
I would implement an application level lock by adding 2 lock related fields to the tickets table: a timestamp when the lock was applied and a user id field telling you which user holds the lock. The lock related fields may be held in another table (shopping cart, for example can be used for this purpose).
When a user selects a ticket, then you try to update these lock fields with a conditional update statement:
update tickets
set lock_time=now(), lock_user=...
where ticket_id=... and lock_time is null
Values in place of ... are supplied by your application. lock_time is null criteria is there to make sure that if the ticket has already been selected by another user, then the later user does not override the lock. After the update statement check out the number of rows affected. If it is one, then the current user acquired the lock. If it is 0, then someone else locked the ticket.
If you have the locking data in another table, then place a unique restriction on the ticket id field in that table and use insert to acquire a lock. If the insert succeeds, then the lock is acquired. If it fails, then another user has locked the ticket.
The lock is usually held for a number of minutes, after that your application must release the lock (set locking fields to null or delete the locking record from the other table).

What's the correct way to protect against multiple sessions getting the same data?

Let's say I have a table called tickets which has 4 rows, each representing a ticket to a show (in this scenario these are the last 4 tickets available to this show).
3 users are attempting a purchase simultaneously and each want to buy 2 tickets and all press their "purchase" button at the same time.
Is it enough to handle the assignment of each set of 2 via a TRANSACTION or do I need to explicitly call LOCK TABLE on each assignment to protect against the possibility that 2 of the tickets will be assigned to two users.
The desire is for one of them to get nothing and be told that the system was mistaken in thinking there were available tickets.
I'm confused by the documentation which says that the LOCK will be implicitly released when I start a TRANSACTION, and was hoping to get some clarity on the correct way to handle this.
If you use a transaction, MySQL takes care of locking automatically. That's the whole point of transactions -- they totally prevent any kind of interference due to overlapping requests.
You could use "optimistic locking": When updating the ticket as sold, make sure you include the condition that the ticket is still available. Then check if the update failed (you get a count of rows updated, can be 1 or 0).
For example, instead of
UPDATE tickets SET sold_to = ? WHERE id = ?
do
UPDATE tickets SET sold_to = ? WHERE id = ? AND sold_to IS NULL
This way, the database will assure that you don't get conflicting updates. No need for explict locking (the normal transaction isolation will be sufficient).
If you have two tickets, you still need to wrap the two calls into a single transaction (and roll back if either of them failed.