SQL - Setting a maximum number when incrementing a value by 1 - mysql

I want increment difficulty by 1 but the value of difficulty should not go past 3. How do I do that?
This is my query
$query=mysqli_query($con,"UPDATE ticket
SET status = 'open',
difficulty = difficulty + 1
WHERE ticketid = '$_POST[ticketid]'")

Schema
create table ticket
( ticketid int auto_increment primary key,
status varchar(20) not null,
difficulty int not null
);
insert ticket(status,difficulty) values (0,0),(0,0); -- 2 rows
run this query a bunch of times:
update ticket
set status ='open',
difficulty=least(difficulty+1,3)
where ticketid=2;
Now look at data
select * from ticket;
+----------+--------+------------+
| ticketid | status | difficulty |
+----------+--------+------------+
| 1 | 0 | 0 |
| 2 | open | 3 |
+----------+--------+------------+
See Mysql Comparison functions

Try this:
$ticket_id = mysqli_real_escape_string($con, $_POST['ticketid']);
$query=mysqli_query($con,"UPDATE ticket
SET status = 'open',
difficulty = least(difficulty + 1,3)
WHERE ticketid = '$ticket_id'")
Note that I added ticket_id escaping to fix the SQL injection.

You can also use a if statement:
CREATE TABLE IF NOT EXISTS `ticket` (
`ticketid` int(11) NOT NULL AUTO_INCREMENT,
`status` varchar(20) NOT NULL,
`difficulty` int(11) NOT NULL,
PRIMARY KEY (`ticketid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;
INSERT INTO ticket (difficulty, status) VALUES (0,0);
UPDATE
ticket
SET
`difficulty` = IF(`difficulty` < 3,`difficulty` + 1, 3)
WHERE
`ticketid`=1;
For more information have a look at https://www.ask-sheldon.com/conditions/.

Related

Get id of records updated with ON DUPLICATE KEY UPDATE

I want to know if there is a way to get the ID of records updated with ON DUPLICATE KEY UDATE.
For example, I have the users table with the following schema:
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL,
`username` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx-users-email` (`email`)
);
and insert some users:
INSERT INTO users (email, username) VALUES ("pioz#example.org", "pioz"),("luke#example.org", "luke"),("mike#example.org", "mike");
the result is:
+----+------------------+----------+
| id | email | username |
+----+------------------+----------+
| 1 | pioz#example.org | pioz |
| 2 | luke#example.org | luke |
| 3 | mike#example.org | mike |
+----+------------------+----------+
Now I want to know if, with a query like the following one, is possible to get the ID of the updated records:
INSERT INTO users (email, username) VALUES ("luke#example.org", "luke2"),("mike#example.org", "mike2") ON DUPLICATE KEY UPDATE username=VALUES(username);
In this example ID 2 and 3.
It seems that the only solution is to used a stored procedure. Here is an example for one row, which could be expanded.
See dbFiddle link below for schema and testing.
CREATE PROCEDURE add_update_user(IN e_mail VARCHAR(25), IN user_name VARCHAR(25) )
BEGIN
DECLARE maxB4 INT DEFAULT 0;
DECLARE current INT DEFAULT 0;
SELECT MAX(ID) INTO maxB4 FROM users;
INSERT INTO users (email, username) VALUES
(e_mail, user_name)
ON DUPLICATE KEY UPDATE username=VALUES(username);
SELECT ID INTO current FROM users WHERE email =e_mail;
SELECT CASE WHEN maxB4 < current THEN CONCAT('New user with ID ', current, ' created')
ELSE CONCAT('User with ID ', current, ' updated') END Report;
/*SELECT CASE WHEn maxB4 < current THEN 1 ELSE 0 END;*/
END
call add_update_user('jake#example.com','Jake');
| Report |
| :------------------------- |
| New user with ID 6 created |
call add_update_user('jake#example.com','Jason');
| Report |
| :--------------------- |
| User with ID 6 updated |
db<>fiddle here
Plan A: Use the technique in the ref manual -- see LAST_INSERT_ID()
Plan B: Get rid of id and make email the PRIMARY KEY

How to use helper functions to create a stored procedure that updates two fields mySQL

So I have a few tables on mySQL:
CREATE TABLE IF NOT EXISTS `salarygrade` (
`GRADE` INT(11) NOT NULL,
`HOURLYRATE` FLOAT NOT NULL,
PRIMARY KEY (`GRADE`));
===========================================================================
CREATE TABLE IF NOT EXISTS `staffongrade` (
`STAFFNO` INT(11) NOT NULL,
`GRADE` INT(11) NOT NULL,
`STARTDATE` DATE NULL DEFAULT NULL,
`FINISHDATE` DATE NULL DEFAULT NULL,
INDEX `STAFFONGRADE_FK` (`STAFFNO` ASC),
INDEX `STAFFONGRADE2_FK` (`GRADE` ASC),
PRIMARY KEY (`GRADE`, `STAFFNO`),
CONSTRAINT `FK_STAFFONG_STAFFONGR_SALARYGR`
FOREIGN KEY (`GRADE`)
REFERENCES `salarygrade` (`GRADE`),
CONSTRAINT `FK_STAFFONG_STAFFONGR_STAFF`
FOREIGN KEY (`STAFFNO`)
REFERENCES `staff` (`STAFFNO`));
===========================================================================
CREATE TABLE IF NOT EXISTS `campaign` (
`CAMPAIGN_NO` INT(11) NOT NULL,
`TITLE` VARCHAR(30) NOT NULL,
`CUSTOMER_ID` INT(11) NOT NULL,
`THEME` VARCHAR(40) NULL DEFAULT NULL,
`CAMPAIGNSTARTDATE` DATE NULL DEFAULT NULL,
`CAMPAIGNFINISHDATE` DATE NULL DEFAULT NULL,
`ESTIMATEDCOST` INT(11) NULL DEFAULT NULL,
`ACTUALCOST` FLOAT NULL DEFAULT NULL,
PRIMARY KEY (`CAMPAIGN_NO`),
INDEX `OWNS_FK` (`CUSTOMER_ID` ASC),
CONSTRAINT `FK_CAMPAIGN_OWNS_CUSTOMER`
FOREIGN KEY (`CUSTOMER_ID`)
REFERENCES `customer` (`CUSTOMER_ID`)
ON DELETE RESTRICT
ON UPDATE RESTRICT);
===========================================================================
CREATE TABLE IF NOT EXISTS `workson` (
`STAFFNO` INT(11) NOT NULL,
`CAMPAIGN_NO` INT(11) NOT NULL,
`WDATE` DATE NOT NULL,
`HOUR` FLOAT NULL DEFAULT NULL,
PRIMARY KEY (`STAFFNO`, `CAMPAIGN_NO`, `WDATE`),
INDEX `WORKSON_FK` (`STAFFNO` ASC),
INDEX `FK_WORKSON_WORKSON2_CAMPAIGN_idx` (`CAMPAIGN_NO` ASC),
CONSTRAINT `FK_WORKSON_WORKSON2_CAMPAIGN`
FOREIGN KEY (`CAMPAIGN_NO`)
REFERENCES `campaign` (`CAMPAIGN_NO`)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
CONSTRAINT `FK_WORKSON_WORKSON_STAFF`
FOREIGN KEY (`STAFFNO`)
REFERENCES `staff` (`STAFFNO`));
And I want to create a stored procedure called sp_finish_campaign (in c_title varchar(30)) that takes a title of a campaign and finishes the campaign by updating the CAMPAIGNFINISHDATE to the current date and ACTUALCOST to the cost of the campaign, which is calculated from the number of hours different staff put into it on different dates, and the salary grade (this changes based on staffID and the timeframe based on the STARTDATE and FINISHDATE of the staffongrade table.
To calculate the ACTUALCOST, I created a helper function:
DELIMITER //
CREATE FUNCTION rate_on_date(staff_id int, given_date date)
RETURNS int
DETERMINISTIC
BEGIN
DECLARE salaryGrade int;
SET salaryGrade = (select grade from staffongrade
where staffno = staff_id AND (given_date BETWEEN STARTDATE AND FINISHDATE));
RETURN salaryGrade;
END //
DELIMITER ;
Which returns the pay grade based on the staff_id and given_date parameters I give it:
select rate_on_date(1, "2018-02-02") as Grade_On_Date;
For the parameters of this I assume I would have to get it from the workson table which looks like this:
I have tried using a select statement to get the paygrade:
select hourlyrate as 'grade' from salarygrade
where rate_on_date(1, "2018-02-02") = grade;
To calculate ACTUALCOST I assume I would have to do a calculation by multiplying HOUR column with the grade costs, and use the WDATE and STAFFNO columns in the workson table as parameters for my stored procedure that will calculate and update the CAMPAIGNFINISHDATE and ACTUALCOST of the campaign by inputting the campaign title into it.
But how would I go about doing this?
I'm just confused as to how to go about creating this procedure, and also confused about how to properly use these helper functions in my stored procedure.
I feel like this question is quite long but I don't really know what to ask or what direction I should take to solve this problem.
You don't really need a function. mysql can do multi-table updates (see https://dev.mysql.com/doc/refman/8.0/en/update.html) in your case it could look like this
update campaign c
join
(select c.campaign_no,
sum(hour * hourlyrate) cost
from campaign c
join workson w on w.campaign_no = c.campaign_no
join staffongrade s on s .staffno = w.staffno and w.wdate between s.startdate and s.finishdate
join salarygrade g on g.grade = s.grade
group by c.campaign_no
) s
on s.campaign_no = c.campaign_no
set actualcost = s.cost
where c.campaign_no = 1
;
Where the sub query does the needful
if you simplify your data this should be easy to prove;
drop table if exists salarygrade,campaign,workson,staffongrade;
CREATE TABLE `salarygrade`
( GRADE INT NOT NULL,
hOURLYRATE decimal(10,2) NOT NULL
);
insert into salarygrade values(1,10),(2,20);
cREATE TABLE IF NOT EXISTS `staffongrade` (
`STAFFNO` INT(11) NOT NULL,
`GRADE` INT(11) NOT NULL,
`STARTDATE` DATE NULL DEFAULT NULL,
`FINISHDATE` DATE NULL DEFAULT NULL
);
insert into staffongrade values
(1,1,'2019-01-01','2019-06-30'),(1,2,'2019-06-01','2019-12-31'),(2,1,'2019-01-01','2019-01-31');
CREATE TABLE IF NOT EXISTS `campaign` (
`CAMPAIGN_NO` INT(11) NOT NULL,
`CAMPAIGNSTARTDATE` DATE NULL DEFAULT NULL,
`CAMPAIGNFINISHDATE` DATE NULL DEFAULT NULL,
`ESTIMATEDCOST` INT(11) NULL DEFAULT NULL,
`ACTUALCOST` FLOAT NULL DEFAULT NULL
);
insert into campaign values (1,'2019-01-01','2019-12-31',null,null);
CREATE TABLE IF NOT EXISTS `workson` (
`STAFFNO` INT(11) NOT NULL,
`CAMPAIGN_NO` INT(11) NOT NULL,
`WDATE` DATE NOT NULL,
`HOUR` FLOAT NULL DEFAULT NULL
);
insert into workson values
(1,1,'2019-01-01',1),(1,1,'2019-12-01',1),(2,1,'2019-01-01',1);
select * from campaign;
+-------------+-------------------+--------------------+---------------+------------+
| CAMPAIGN_NO | CAMPAIGNSTARTDATE | CAMPAIGNFINISHDATE | ESTIMATEDCOST | ACTUALCOST |
+-------------+-------------------+--------------------+---------------+------------+
| 1 | 2019-01-01 | 2019-12-31 | NULL | 40 |
+-------------+-------------------+--------------------+---------------+------------+
1 row in set (0.00 sec)
Got to dash so I'll leave you to drop the update into a procedure.
IF staffongrade has NULL for finishdate then a bit of data cleansing is required. for simplicity I would create a temporary table to fill in gaps and change the update statement to use the projectfinishdate (if that's not known then substitute a suitable future date). This code would be inserted in your procedure prior to the update
so
insert into staffongrade values
(1,1,'2019-01-01',null),(1,2,'2019-07-01',null),(2,1,'2019-01-01',null);
drop temporary table if exists staffongradetemp;
create temporary table staffongradetemp like staffongrade;
insert into staffongradetemp
select s.STAFFNO,s.GRADE,s.STARTDATE,
case when s.FINISHDATE is not null then s.finishdate
else date_sub((select s1.startdate
from staffongrade s1
where s1.STAFFNO = s.STAFFNO and s1.startdate > s.STARTDATE
order by startdate limit 1), interval 1 day)
end
from staffongrade s
;
select * from staffongradetemp;
+---------+-------+------------+------------+
| STAFFNO | GRADE | STARTDATE | FINISHDATE |
+---------+-------+------------+------------+
| 1 | 1 | 2019-01-01 | 2019-06-30 |
| 1 | 2 | 2019-07-01 | NULL |
| 2 | 1 | 2019-01-01 | NULL |
+---------+-------+------------+------------+
3 rows in set (0.00 sec)
Which leaves all the last finshdates as null which we can trap in the update statement using coalesce
update campaign c
join
(select c.campaign_no,
sum(hour * hourlyrate) cost
from campaign c
join workson w on w.campaign_no = c.campaign_no
join **staffongradetemp s** on s .staffno = w.staffno and w.wdate between s.startdate and **coalesce(s.finishdate,c.CAMPAIGNFINISHDATE)**
join salarygrade g on g.grade = s.grade
where c.campaign_no = 1
group by c.campaign_no
) s
on s.campaign_no = c.campaign_no
set actualcost = s.cost
where 1 = 1;

How to generate a dynamic sequence table in MySQL?

I'm trying to generate a sequence table in MySQL, so that I can get unique ids from last_insert_id.
The problem is that I need multiple sequences dynamically.
At the first, I created a table:
CREATE TABLE `sequence` (
`label` char(30) CHARACTER SET latin1 NOT NULL,
`id` mediumint(9) NOT NULL DEFAULT '0',
PRIMARY KEY (`label`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
And then tried to get the number, using example from http://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_last-insert-id
UPDATE sequence SET id = LAST_INSERT_ID(id + 1) WHERE label = 'test';
SELECT LAST_INSERT_ID();
After a while I realized that I also need to generate rows for new labels safely.
So I changed this schema into:
CREATE TABLE `sequence` (
`label` char(30) CHARACTER SET latin1 NOT NULL,
`id` mediumint(9) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`label`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
And I simply gave up using WHERE clause to update its id.
INSERT INTO sequence (label) values ( ? )
SELECT LAST_INSERT_ID()
Is this a proper way? I want to know if there is a better solution.
The MyISAM engine will do it for you -
Table definition:
CREATE TABLE `sequence` (
`label` char(30) CHARACTER SET latin1 NOT NULL,
`id` mediumint(9) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`label`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Populate table:
INSERT INTO sequence VALUES ('a', NULL); -- add some 'a' labels
INSERT INTO sequence VALUES ('a', NULL);
INSERT INTO sequence VALUES ('a', NULL);
INSERT INTO sequence VALUES ('b', NULL); -- add another labels 'b'
INSERT INTO sequence VALUES ('b', NULL);
INSERT INTO sequence VALUES ('a', NULL); -- add some 'a' labels
INSERT INTO sequence VALUES ('a', NULL);
Show result:
SELECT * FROM sequence;
+-------+----+
| label | id |
+-------+----+
| a | 1 |
| a | 2 |
| a | 3 |
| a | 4 |
| a | 5 |
| a | 6 |
| b | 1 |
| b | 2 |
+-------+----+

Two primary keys & auto increment

I have a MySQL table with two fields as primary key (ID & Account), ID has AUTO_INCREMENT.
This results in the following MySQL table:
ID | Account
------------------
1 | 1
2 | 1
3 | 2
4 | 3
However, I expected the following result (restart AUTO_INCREMENT for each Account):
ID | Account
------------------
1 | 1
2 | 1
1 | 2
1 | 3
What is wrong in my configuration? How can I fix this?
Thanks!
Functionality you're describing is possible only with MyISAM engine. You need to specify the CREATE TABLE statement like this:
CREATE TABLE your_table (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
account_id INT UNSIGNED NOT NULL,
PRIMARY KEY(account_id, id)
) ENGINE = MyISAM;
If you use an innoDB engine, you can use a trigger like this:
CREATE TRIGGER `your_table_before_ins_trig` BEFORE INSERT ON `your_table`
FOR EACH ROW
begin
declare next_id int unsigned default 1;
-- get the next ID for your Account Number
select max(ID) + 1 into next_id from your_table where Account = new.Account;
-- if there is no Account number yet, set the ID to 1 by default
IF next_id IS NULL THEN SET next_id = 1; END IF;
set new.ID= next_id;
end#
Note ! your delimiter column is # in the sql statement above !
This solution works for a table like yours if you create it without any auto_increment functionality like this:
CREATE TABLE IF NOT EXISTS `your_table` (
`ID` int(11) NOT NULL,
`Account` int(11) NOT NULL,
PRIMARY KEY (`ID`,`Account`)
);
Now you can insert your values like this:
INSERT INTO your_table (`Account`) VALUES (1);
INSERT INTO your_table (`Account`, `ID`) VALUES (1, 5);
INSERT INTO your_table (`Account`) VALUES (2);
INSERT INTO your_table (`Account`, `ID`) VALUES (3, 10205);
It will result in this:
ID | Account
------------------
1 | 1
2 | 1
1 | 2
1 | 3

How to optimise that query?

I've a vote system which is designed like this:
CREATE TABLE `vote` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`weight` int(11) NOT NULL,
`submited_date` datetime NOT NULL,
`resource_type` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2963832 DEFAULT CHARSET=latin1;
CREATE TABLE `article_preselection_vote` (
`id` int(11) NOT NULL,
`article_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `IDX_9B145DEA62922701` (`article_id`),
KEY `IDX_9B145DEAA76ED395` (`user_id`),
CONSTRAINT `article_preselection_vote_ibfk_4` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`),
CONSTRAINT `article_preselection_vote_ibfk_5` FOREIGN KEY (`id`) REFERENCES `vote` (`id`) ON DELETE CASCADE,
CONSTRAINT `article_preselection_vote_ibfk_6` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
v.weight can be +1 or -1, I need, given a bunch of articles ID, to get the sum of each positive vote (+1) and the sum of negative vote (-1) per articles id.
Then my result should be
article_id | vote_up | vote_down
-----------|---------|----------
1 | 36 | 20
-----------|---------|----------
68 | 12 | 56
-----------|---------|----------
25 | 90 | 12
-----------|---------|----------
I can get that result by doing the following request, but it's quite heavy and slow on 2,000,000 votes.
SELECT apv.article_id, COALESCE(SUM(up),0) as up, COALESCE(SUM(down),0) as down
FROM article_preselection_vote apv
LEFT JOIN(
SELECT id, weight up FROM vote WHERE weight > 0 AND vote.resource_type = 'article') v1 ON apv.id = v1.id
LEFT JOIN(
SELECT id, weight down FROM vote WHERE weight < 0 AND vote.resource_type = 'article') v2 ON apv.id = v2.id
WHERE apv.article_id IN (11702,11703,11704,11632,11652,11658)
GROUP BY apv.article_id
Any ideas?
Thanks in advance.
Subselects, IN (...) and GROUP BY in one query are killers.
You should redesign to have a more traditional solution:
Have a table with the votes article_id, votes_up, votes_down, vote_date, ...
Update (cron) the summary fields in your article table votes_up, votes_down, ... with one UPDATE.
That way, you can better handle the row/table locks and have fast queries
You can try a single join:
SELECT
apv.article_id,
SUM(COALESCE(weight, 0) > 0) AS up,
SUM(COALESCE(weight, 0) < 0) AS down
FROM article_preselection_vote apv
LEFT JOIN vote
ON apv.id = vote.id
AND vote.resource_type = 'article'
WHERE apv.article_id IN (11702, 11703, 11704, 11632, 11652, 11658)
GROUP BY apv.article_id
If you need to calculate this often it might be worthwhile to denormalize your database and store a cached copy of the results.
Instead of weighting the votes, why don't you just create two tables, one for up votes and one for down votes? The only thing it will complicate is vote combination, which will still be a simple sum of the counts of two different queries.
in a nut shell do something like this:
select * from article where article_id in (1,2,3);
+------------+-----------+---------------+-----------------+
| article_id | title | up_vote_count | down_vote_count |
+------------+-----------+---------------+-----------------+
| 1 | article 1 | 2 | 3 |
| 2 | article 2 | 2 | 1 |
| 3 | article 3 | 1 | 1 |
+------------+-----------+---------------+-----------------+
3 rows in set (0.00 sec)
drop table if exists article;
create table article
(
article_id int unsigned not null auto_increment primary key,
title varchar(255) not null,
up_vote_count int unsigned not null default 0,
down_vote_count int unsigned not null default 0
)
engine = innodb;
drop table if exists article_vote;
create table article_vote
(
article_id int unsigned not null,
user_id int unsigned not null,
score tinyint not null default 0,
primary key (article_id, user_id)
)
engine=innodb;
delimiter #
create trigger article_vote_after_ins_trig after insert on article_vote
for each row
begin
if new.score < 0 then
update article set down_vote_count = down_vote_count + 1 where article_id = new.article_id;
else
update article set up_vote_count = up_vote_count + 1 where article_id = new.article_id;
end if;
end#
delimiter ;
insert into article (title) values ('article 1'),('article 2'), ('article 3');
insert into article_vote (article_id, user_id, score) values
(1,1,-1),(1,2,-1),(1,3,-1),(1,4,1),(1,5,1),
(2,1,1),(2,2,1),(2,3,-1),
(3,1,1),(3,5,-1);
select * from article where article_id in (1,2,3);