MYSQL - Check from Select "itself" possible? - mysql

I just started with mysql and I don't know if the way to make that check is the correct or I am going in the wrong direction.
I have a varchar named user_num in a table. and I need to check that when I do INSERT, the value of user_num_list have to be between [1, n] being "n" the quantiti of Objects that have the same Group has new Object.
I'm not English speaking and I'm sure it's a bit hard to understand, and for me to express myself, so there is some code:
create table player(
group varchar(15),
user_num int(15),
CONSTRAINT ck_player_user CHECK( user_num > 0 AND user_num < SELECT count(*) FROM player WHERE player.group=group)
)ENGINE=InnoDB;
I don't know if i can "SELECT" inside a CHECK statement and also i dont know how to express "player.group=group" meaning that group(new INSERT player_group) have to be same has player.group
Thank you.

You are out of luck.
First of all, MySQL does not validate the CHECK constraints before version 8.0.16. If you are using a MySQL version older than this one, MySQL will record the CHECK constraint and will silently ignore it
.
Second, in order to enforce this contraint you would need to use a subquery in it, and MySQL does not allow subqueries as part of the CHECK constraint. See 13.1.20.7 CHECK Constraints where it literally says:
Subqueries are not permitted.

The problem with subqueries in the CHECK condition is that it's practically impossible to implement. Consider what happens when you delete a row in this table. The count for the corresponding group will decrease, which might violate a CHECK constraint for another row. The system would need to reevaluate the checks for every row. If you now have one million rows in the table, one million queries need to be executed every time you INSERT, DELETE or UPDATE a row. If you would also permit cross table statements, all checks in the database need to be reevaluated on any change of data.
In theorie it would be possible to generate a "reverse check". The system would need to determine, which operations can possibly violate a check. In your case that would be deleting a row or updating a group. The "reverse check" could be
NOT EXISTS (
SELECT *
FROM player p
WHERE p.group = OLD.group
AND p.user_num > (SELECT COUNT(*) from player p WHERE p.group = OLD.group)
)
I think you would agree, that it would be hard to implement a system, that is able to generate a reverse check like this. Queries can be much more complicated than yours. And for some queries there might not even exist a reverse check. If you look at how long it has taken to implement the CHECK constraint - I wouldn't expect any subqueries to be permitted in the next decade.
So I would say - Your options are:
Implement your checks in triggers
Implement your checks in application code
Change the requirements
I'd prefer the last one.

Related

Leasing jobs (atomic update and get) from a MySQL database

I have a MySQL table that manages jobs that worker-clients can lease for processing. Apart from the columns that describe the job, the table has a unique primary key column id, a time-stamp-column lease, a boolean-column complete, and an int-column priority.
I'm trying to write a (set of) SQL statement(s) that will manage the leasing-process. My current plan is to find the first incomplete job that has a lease-date that is at least 8 hours in the past (no job should take more than one hour, so an incomplete lease that is that old probably means that the client died and the job needs to be restarted), set its lease-date to the current time-stamp, and return its info. All of this, of course, needs to happen atomically.
I found a neat trick here on SO and a variation of it in the discussion of the MySQL documentation (see post on 7-29-04 here) that uses user-defined variables to return the leased job from an UPDATE statement.
And, indeed, this works fine:
UPDATE jobs SET lease=NOW() WHERE TIMESTAMPDIFF(HOUR,lease,NOW())>=8 AND NOT complete AND #id:=id LIMIT 1;
SELECT * FROM jobs WHERE id=#id;
The problem comes in when I try to add priorities to the jobs and add ORDER BY priority into the UPDATE statement right before LIMIT. The UPDATE still works as expected, but the SELECT always returns the same row back (either the first or the last, but not the one that was actually updated). I'm a little confused by this, since LIMIT 1 should make sure that the first update that actually happens will terminate the UPDATE process, leaving #id set to the correct value of that updated row, no? For some reason it seems to keep evaluating the condition #id:=id for all rows anyways, even after it's done with its update (or maybe it evaluates it first for all rows before even figuring out which one to update, I don't know...).
To fix this, I tried rewriting the statement to make sure the variable really only gets set for the matching row:
UPDATE jobs SET lease=NOW(),#id:=id WHERE TIMESTAMPDIFF(HOUR,lease,NOW())>=8 AND NOT complete ORDER BY priority LIMIT 1;
But for some reason, this gives me the following error:
Error Code : 1064
You have an error in your SQL syntax; check the manual that corresponds
to your MySQL server version for the right syntax to use near
'#id:=id WHERE TIMESTAMPDIFF(HOUR,lease,NOW())>=8 AND NOT complete ORDER BY prior'
at line 1
So, it seems that I can't assign the variable in the SET-part of the UPDATE (although this was the way it was suggested in the SO-answer linked above).
Can this approach be salvaged somehow or is there a better one altogether?
PS: I'm using MySQL server v5.5.44-0+deb8u1
My solution with a little trick:
first: you must use a subselect so that UPDATE not nows thats the same table an
second: you must initialize the #id with "(SELECT #id:=0)" else if the found no row they returns the last set value. Here you can also specify if they return 0 or '' when no result is found.
UPDATE jobs SET lease=NOW() WHERE id =
( SELECT * FROM
( SELECT #id:=id FROM jobs,(SELECT #id:=0) AS tmp_id
WHERE TIMESTAMPDIFF(HOUR,lease,NOW())>=8
AND NOT complete ORDER BY priority LIMIT 1
) AS tmp
);
It is OK that you found a solution.
If this must be quite stable, I would go for a different solution. I would not use atomicity, but "commit"- like workflows. You should identify your worker-client with a unique key, either in it's own table or with a secure hash key. You add two fields to your jobs-table: worker and state. So if you look for a job for worker W345, you assign worker to that job.
First part would be
update jobs set worker='W345', state='planning', lease=now()
where TIMESTAMPDIFF(HOUR,lease,NOW())>=8
AND NOT complete
ORDER BY priority LIMIT 1;
Next part (could be even from different part of application)
select * from jobs where worker='W345' and state='planning';
get id and data, update:
update jobs set state='sending', lease=now() where id=...;
Maybe you even can commit the sending of the job, otherwise you guess that it started after sending.
update jobs set state='working', lease=now() where id = ...;
You find all jobs that are dead before being sent to worker by their state and some short minutes old lease. You can find out where the process got into trouble. You can find out which workers get most trouble, and so on.
Maybe the real details differ, but as long as you have some status column you should be quite flexible and find your solution.
I was able to fix things with the following hack:
UPDATE jobs SET lease=IF(#id:=id,NOW(),0) WHERE TIMESTAMPDIFF(HOUR,lease,NOW())>=8 AND NOT complete ORDER BY priority LIMIT 1;
Seems like it's simply not allowed to set a local variable within the SET section of UPDATE.
Note:Since the id column is an auto-increment primary key, it is never 0 or NULL. Thus, the assignment #id:=id inside the IF-statement should always evaluate to TRUE and therefore lease should be set correctly (correct me if I'm wrong on this, please!).
One thing to keep in mind:The variable #id by default is scoped to the MySQL connection (not any Java Statement-object, for example, or similar), so if one connection is to be used for multiple job-leases, one needs to ensure that the different UPDATE/SELECT-pairs never get interleaved. Or one could add an increasing number to the variable-name (#id1, #id2, #id3, ...) to guarantee correct results, but I don't know what performance (or memory-use) impact this will have on the MySQL-server. Or, the whole thing could be packaged up into a stored procedure and the variable declared as local.

What is the best way to prevent duplicate values in databases

What is the best way to prevent duplicate values in databases ?
I have a table called names that has only one column called name that is unique (declared as unique attribute).
What is the best way to insert a new name (x) ?
Way1: Should I make a select query for the name x first to check if exist or not. Then make another query to insert the name iff it is not exists in the table.
Way2: Make only one query to insert the name and ignore the error if name already exists.
The second way is the better way. Why run two queries when you can just run one?
When you declare the column as unique, you have told the database to do the extra work for ensure that this is true. You don't need to do anything else -- other than check the errors on the return.
Database constraint will definitely take care about uniqueness, but if you have logic where you need to use last inserted ID to other child table, then only I think you will require to perform manual check before insert, else just ignore exception if raise due to duplication.
The first way works. After the action you can be sure that the record exists (unless some other error occured) You do need a second query (or some another mechanism) to retrieve the actual tuple, either the existing one or a fresly inserted one.
The second way is terrible: the DBMS session is in error-state, {your current work has implicitely been rolled back, and your all cursors have been closed} So, you'll have to start your work allover again, hopefully without the duplicate.
The case you give is a simplified "upsert". Do a search for upsert and you will find answers to the more general question. Some databases, like mysql provide for
insert ignore for this simple case.
Otherwise for the simple case you mention you can use the second approach. For the more general upsert, it is surprisingly difficult to get it right. The issue is concurrent updates. In fact, I have not seen a satisfactory answer for general upserts. Some say to use "merge" but that is subject to concurrency issues.

MySql Triggers and performance

I have the following requirement. I have 4 MySQL databases and an application in which the user needs to get the count of number of records in tables of each of these databases. The issue is that count may change in every minute or second. So whenever the user mouse-hovering the particular UI area, I need to have a call to all these databases and get the count. I don’t think it is a best approach, as these tables contain millions of records and every time on mouse over, a dB call is going to all these databases.
Trigger is the one approach I found. Rather than we are pulling data from the database, I feel like whenever any insert/update/delete happening to these tables, a trigger will execute and that will increment/decrement the count in another table (which contain only the count of these tables). But I have read like triggers will affect database performance, but also read some situation trigger is the only solution.
So please guide me in my situation triggers are the solution? If it affects the database performance I don’t need that. Is there any other better approach for this problem?
Thanks
What I understood is you have 4 databases and n number of tables in each of them and when the user hovers over a particular area in your application the user should see the number of rows in that table.
I would suggest you to use count(*) to return the number of rows in each table in the database.Triggers are used to do something when a particular event like update,delete or insert occurs in a database.It's not a good idea to invoke triggers to react to user interactions like hovering.If you can tell me in which language you are designing the front end I can be more specific.
Example:
SELECT COUNT(*) FROM tablename where condition
OR
SELECT SQL_CALC_FOUND_ROWS * FROM tablename
WHERE condition
LIMIT 5;
SELECT FOUND_ROWS();
The second one is used when you want to limit the results but still return total number of rows found.Hope it helps.
Please don't use count(*). This is inefficient, possibly to the point of causing a table scan. If you can get to the information schema, this should return the result you need sub-second:
select table_rows from information_schema.tables where table_name = 'tablename'
If you can't for some reason, and your table has a primary key, try:
SELECT COUNT(field) FROM tablename
...where field is part of the primary key. This will be slower, especially on large tables, but still better than count(*).
Definitely don't use trigger.

Complex MySQL Delete Query

Current Structure
As you can see Path can be referenced by multiple Tables and multiple records within those tables.
Points can also be referenced by two different tables.
My Question
I would like to delete a PathType however this gets complicated as
a Path may be owned by more than one PathType so deleting the
Path without checking how many references there are to it is out
of the question.
Secondly, if this Path's only reference is the PathType I'm
trying to delete then I will want to delete this Path and any
records in PathPoints.
Lastly, if there are no other references on Point from any other records then this will also need to be deleted but only if its not used by any other object.
Attempts So Far
DELETE PathType1.*, Path.*, PathPoints.*, Point.* FROM PathType1,Path,PathPoints,Point WHERE PathType1.ID = 1 AND PathType1.PATH = Path.ID AND (SELECT COUNT(*) FROM PathType1 WHERE PathType1.PATH = Path.ID) < 1 AND (SELECT COUNT(*) FROM PathType2 WHERE PathType2.PATH = Path.ID) = 0
Obviously the above statement goes on but this isn't the right way about I don't think because if one fails then nothing is deleted...
I think that maybe it isn't possible to do what I'm attempting through one statement and I may have to iterate through each section and handle them based on the outcome. Not so efficient but I don't see any alternative at this time.
I hope this is clear. If you have any more questions or need any clarification then please do not hesitate to ask
First there is no way I would do this in a query like that even if the database allowed it which most will not. This is an unmaintanable mess.
The preferred method is to create a transaction, then delete from one table at a time starting with the bottommost child table. Then commit the transaction. And of course have error handling so the entire transaction is riolled back if one delete fails to maintain data integrity. If I intended to do this repeatedly, I would do it in a stored proc.

Setting a max record limit in access

I have a table that holds several hundred records but I with a special property. I only want the user to be able to select that property 50 times though. How can I set a limit and return an error when the limit is reached?
To add to #Kevin Ross, the mechanism "at the DB level" to enforce the rule is a CHECK constraint e.g.
ALTER TABLE tblFoo ADD
CONSTRAINT tblFoo_bar__50_limit
CHECK (NOT EXISTS (
SELECT T1.Bar
FROM tblFoo AS T1
WHERE T1.Bar = 'Thing_you_only_want_50_of'
GROUP
BY T1.Bar
HAVING COUNT(*) > 50
));
CHECK constraints have existed in Jet from version 4.0 (circa Access 2000) and still exists in ACE (e.g. Access 2010).
You need to create the CHECK constraint using SQL DDL while in ANSI-92 Query Mode. No, you can't create CHECK constraints using DAO or the Access UI but that doesn't mean they don't exist ;)
If for some reason you have philosophical objection to SQL DDL, you could do similar things with an additional 'sequence' column, row-level Validation Rules and a compound UNIQUE constraint, all of which can be created using DAO or the Access UI and have been available in Jet for more years than I can remember.
Here's a rough sketch of what that alternative approach could look like:
ALTER TABLE tblFoo ADD
Bar__sequence INTEGER;
ALTER TABLE tblFoo ADD
CONSTRAINT tblFoo_bar_sequence__values
CHECK (
(
Bar <> 'Thing_you_only_want_50_of'
AND Bar__sequence IS NULL
)
OR (
Bar = 'Thing_you_only_want_50_of'
AND Bar__sequence BETWEEN 1 AND 50
)
);
ALTER TABLE tblFoo ADD
CONSTRAINT tblFoo_bar__50_limit
UNIQUE (Bar__sequence);
In this case, the results of the above three SQL DDL statements can be achieved using the Table Designer in the Access UI i.e. add the column, amend the Table Validation Rule and add a unique index.
select top 50 * from ...
I believe what renick was trying to say was before your save operation you could do something like
SELECT Count(*)
FROM tblFoo
WHERE Bar=’Thing_you_only_want_50_of’
You would then check to see if this figure is greater than equal to or greater than 50, if it is then return an error and not then let the user save.
As far as I know there is no way to restrict that at the DB level however access 2010 does have more controls such as triggers etc. Not sure what version you are using but if you are on 2010 it might be worth checking out
The question is very unclear. It's not clear if "select" means "display limited to 50 items" or "allow user to create data records that are limited to 50 items". I won't address the first interpretation, since it doesn't seem to me to be relevant.
Assuming certain other things, such as a Jet/ACE back end (the only way the question makes sense to me), how you accomplish this depends on your version of Access.
up to and including Access 2007: you'll have to apply the limitation in the user interface of your application. EDIT: As #onedaywhen has pointed out in his answer, it's possible to use DDL in SQL 92 mode to add a CHECK CONSTRAINT that operates on the current record based on groups of other records. I did not know this was possible when I posted.
in Access 2010, you can avail yourself of the new table-level data macros (which work like triggers) and limit the user to the 50 selections.
I'm not providing details for either of these, as there's simply not enough information provided to do so.