InnoDB and count: Are helper tables the way to go? - mysql

Assume I've got an users table with 1M users on MySQL/InnoDB:
users
userId (Primary Key, Int)
status (Int)
more data
If I would want to have an exact count of the amount of users with status = 1 (denoting an activate account), what would be the way to go for big tables, I was thinking along the lines of:
usercounts
status
count
And then run an TRIGGER AFTER INSERT on users that updates the appropiate columns in usercounts
Would this be the best way to go?
ps. An extra small question: Since you also need an TRIGGER AFTER UPDATE on users for when status changes, is there a syntax available that:
Covers both the TRIGGER AFTER INSERT and TRIGGER AFTER UPDATE on status?
Increments the count by one if a count already is present, else inserts a new (status, count = 0) pair?

Would this be the best way to go?
Best (opinion-based) or not but it's definitely a possible way to go.
is there a syntax available that: covers both the TRIGGER AFTER INSERT and TRIGGER AFTER UPDATE on status?
No. There isn't a compound trigger syntax in MySQL. You'll have to create separate triggers.
is there a syntax available that: increments the count by one if a count already is present, else inserts a new (status, count = 0) pair?
Yes. You can use ON DUPLICATE KEY clause in INSERT statement. Make sure that status is a PK in usercounts table.
Now if users can be deleted even if only for maintenance purposes you also need to cover it with AFTER DELETE trigger.
That being said your triggers might look something like
CREATE TRIGGER tg_ai_users
AFTER INSERT ON users
FOR EACH ROW
INSERT INTO usercounts (status, cnt)
VALUES (NEW.status, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;
CREATE TRIGGER tg_ad_users
AFTER DELETE ON users
FOR EACH ROW
UPDATE usercounts
SET cnt = cnt - 1
WHERE status = OLD.status;
DELIMITER $$
CREATE TRIGGER tg_au_users
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
IF NOT NEW.status <=> OLD.status THEN -- proceed ONLY if status has been changed
UPDATE usercounts
SET cnt = cnt - 1
WHERE status = OLD.status;
INSERT INTO usercounts (status, cnt) VALUES (NEW.status, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;
END IF;
END$$
DELIMITER ;
To initially populate usercounts table use
INSERT INTO usercounts (status, cnt)
SELECT status, COUNT(*)
FROM users
GROUP BY status
Here is SQLFiddle demo

I think there are simpler options available to you.
Just add an index to the field you'd like to count on.
ALTER TABLE users ADD KEY (status);
Now a select should be very fast.
SELECT COUNT(*) FROM users WHERE status = 1

Related

Is there a way to get the number of comments for each user and update it in the number_of_comments column automatically?

I have two tables in MySQL like this
Users -> user_id , user_name , number_of_comments
Comments -> comment_id , comment , user_id
Is there a way to get the number of comments for each user and update it in the number_of_comments column automatically?
Not recommended, but solves nevertheless. For learning purposes only.
CREATE TRIGGER tr_ai_update_n_of_comments
AFTER INSERT ON comments
FOR EACH ROW
UPDATE users
SET number_of_comments = ( SELECT COUNT(*)
FROM comments
WHERE comments.user_id = NEW.user_id )
WHERE user_id = NEW.user_id;
If the rows in comments may be updated (with user_id value changing) and/or deleted then create similar AFTER DELETE and AFTER UPDATE triggers.
PS. I strongly recommend you to remove users.number_of_comments column at all and calculate actual comments amount value by according query when needed.
If you agree that the value may be approximate (slightly different from the exact one), then you can use an incremental trigger.
CREATE TRIGGER tr_ai_update_n_of_comments
AFTER INSERT ON comments
FOR EACH ROW
UPDATE users
SET number_of_comments = number_of_comments + 1
WHERE user_id = NEW.user_id;
But just in case, provide for the creation of a service stored procedure (or event) that will periodically recalculate the accumulated value.

Create an auto_increment column based on a group in MySQL

I'm quite new to SQL and databases.
I'm trying to make a preference table of an user.
The fields will be user_id, pref_no, prg_code.
Now if I create the table making pref_no auto_increment then it will automatically increase irrespective of the user_id.
So, my question is - Is there a way to define the table in such a way that it will be auto_incremented taking user_id into account or I have to explicitly find the last pref_no that has occurred for an user and then increment it by 1 before insertion?
Any help is appreciated. Thanks in advance.
Following what Mjh and Fahmina suggested, we can create a procedure for the insertion.
DELIMITER //
CREATE PROCEDURE test(IN u_id INT(7), p_code INT(5))
BEGIN
SELECT #pno:= MAX(pref_no) FROM temp_choice WHERE user_id = u_id;
IF #pno IS NULL THEN
SET #pno = 1;
ELSE
SET #pno = #pno + 1;
END IF;
INSERT INTO temp_choice VALUES (u_id, #pno, p_code);
END //
DELIMITER ;
Now we can easily insert data by using
CALL test(1234567, 10101);
To manage user's preference, you don't need user_id to be auto_incremented in this table, but pref_no has to be.
user_id will just be a refence (or foreign key in sql) to your user table (where user_id should be auto_incremented).
And to request preference for a given user your request would be :
SELECT * FROM [user table] INNER JOIN [pref table] ON ([user table].user_id = [pref table].user_id) WHERE [user table].user_id = ?
(replace '?' by the user_id you want)

Mysql create or update lock

I have a stored procedure which simulate a create or update.
Here is my Algorithm:
SELECT Id INT rId FROM MY_TABLE WHERE UNIQUE_FIELD = XXX LIMIT 1;
IF (rId is not null) THEN
UPDATE ELSE INSERT
The problem is that i got duplicates. How can i prevent theses duplicates? I can't add an UNIQUE INDEX because some fields can be NULL.
Thank you.
EDIT: I'm using InnoDB. Does row lock can helpme ? Locking the whole table is not acceptable for performance reason.
Use a transaction along with the FOR UPDATE clause in SELECT. This will just lock that one row, and should not block transactions that use other rows of the table.
START TRANSACTION;
SELECT id as rId FROM my_table
WHERE unique_field = XXX LIMIT 1
FOR UPDATE;
If (rId IS NOT NULL) THEN
UPDATE ...
ELSE
INSERT ...
END;
COMMIT;

Add a row if there are less than 5, otherwise replace the lowest number in MySQL

I'm pretty stuck on a Mysql query.
I have a table with three columns;
user_id | person_id | score.
The table is going to be used to store top 5 highscores for each person.
I need at query that checks if there is less than five rows for a specific person.
Is there is less, insert new row. But if there is five rows I have to replace the lowest score with the new one.
It is for a webservice written in PHP and the data about the new score is posted to the method as params.
Been stuck for some hours now — is it even possible to make this happen in one query ?
You can use stored procedure in mysql. I dont know the names of the tables but if you look closer you will understand how it works.
DELIMITER $$
DROP PROCEDURE IF EXISTS test $$
CREATE PROCEDURE test( IN testparam VARCHAR(22) )
BEGIN
DECLARE count INT(11);
SET count = (SELECT COUNT(*) FROM persons );
IF count < 5 THEN
insert into table_needed_for_insert values(testparam);
ELSE
update table_needed_for_insert where score=(select min(score) from table_needed_for_insert);
END IF;
select * from table_needed_for_insert
END $$
DELIMITER;
And how to execute this thing CALL test(1); 1 is the parameter, you can create as many as you need.
And from php you can call directly as like
$result = mysql_query("call test(".$param.")");
And here you can check a tutorial on mysql stored procedures:
http://www.mysqltutorial.org/mysql-stored-procedure-tutorial.aspx
It might be possible if you have a unique key which identifies the lowest score. Then you could use the
INSERT ... ON DUPLICATE KEY construct. But you would have to install a trigger which keeps explicit track of the lowest score.
I would propose this scenario (I have not tried it, it is just an idea):
as I understand, you only need 5 ids. you can run a subqueries like these
SELECT MAX(id) AS last_id FROM table
SELECT MIN(score), id AS lowest_id FROM table
then
insert or replace into table (id, ...) values ( MIN(last_id+1, lowest_id), ... )
there are possible mistakes and also only one subquery is possible, but I hope you get the main idea
The simplest way imo is to insert data,
INSERT INTO top_scores (user_id, person_id, score_id) VALUES (1,2,3)
then delete inappropriate rows
DELETE top_scores FROM top_scores
INNER JOIN
(SELECT * FROM top_scores WHERE person_id = 2 ORDER BY score ASC LIMIT 5, 1000000) AS inappropriate_rows
USING (user_id, person_id, score)

Mysql: Toggle value of int field under two conditions for UPDATE query

What I'm looking to do is insert a record, then deactivate previous records with the same ID because they will no longer be in use. However, I'm looking to do this in the simplest way possible. Deleting the record really isn't an option.
Attempted order of operations:
Insert with active inUse value inUse = 1
Update the following records for the same ID that are no longer in use: inUse = 0
My first thought was to run this query:
UPDATE page_tags
SET inUse = IF(inUse = 1, 0, 1)
WHERE page_id = 23678459
AND tag_id NOT IN (10, 4);
The only problem with this query is that if it's run again, it will toggle all of those deactivated values back to 1. I need all of the tags for the specific ID to only toggle back if they are being targeted by the WHERE statement.
Sounds like a job for trigger. Something like will perhaps do (pseudocode)?
UPDATE for handling reuse of previuos tags:
Do your insert/update:
INSERT INTO ... ON DUPLICATE KEY UPDATE
Then use two triggers, one for inserts and one for updates.
CREATE TRIGGER tr_inuse_insert BEFORE INSERT ON page_tags
FOR EACH ROW BEGIN
UPDATE page_tags SET inuse=0 WHERE page_id = NEW.page_id;
END;
CREATE TRIGGER tr_inuse_update BEFORE UPDATE ON page_tags
FOR EACH ROW BEGIN
UPDATE page_tags SET inuse=0 WHERE page_id = NEW.page_id;
END;
#John P has a decent answer, however his answer requires the use of triggers. That to me seems to be more than needed to solve the problem at hand. The current working solution is:
Create an Unique Index on page_tags.page_id and page_tags.tag_id.
Update all rows where the *page_id = 234234*:
UPDATE page_tags SET inUse = 0 WHERE page_id = 234234
Insert tags:
INSERT INTO page_tags (page_id, tag_id, inUse) VALUES (234234, 49343, 1) ON DUPLICATE KEY UPDATE inUse = 1