Scenario/background:
I am trying to create a table of "Tests". For purposes of this question my table will only have 5 columns defined as follows:
CREATE TABLE TestTable
(
Id UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL,
Name VARCHAR(75) NOT NULL,
DateRequested DATETIME NOT NULL DEFAULT GETDATE(),
TestYear AS YEAR(DateRequested) PERSISTED NOT NULL, -- computed column that shows the year the test was requested. I want to persist this column so I can index on it if need be.
TestNumber CHAR(4) NOT NULL DEFAULT '0000', -- need this to auto increment but also needs to reset for the first test of the year.
CONSTRAINT TestTablePK PRIMARY KEY(Id)
);
GO
My requirement is that I want the 'TestNumber' to "auto-increment" based on the year. For example:
GUID, Test 1 in Old Yr, 2013-01-01 05:00:00.000, 2013, 0001
GUID, Test 2 in Old Yr, 2013-12-25 11:00:00.000, 2013, 0002
GUID, Test 3 in Old Yr, 2013-12-26 09:00:00.000, 2013, 0003
...., ................, ......................., ...., N
GUID, Test N in Old Yr, 2013-12-31 09:00:00.000, 2013, N+1
GUID, Test 1 in New Yr, 2014-01-01 11:00:00.000, 2014, 0001 <-- reset to 1
I was thinking that it would be an auto-increment column but how would I reset it based on this being the first test of a new year? So my incorrect solution thus far has been an "instead of insert" trigger defined as follows:
CREATE TRIGGER InsteadOfInsertTrigger ON dbo.TestTable
INSTEAD OF INSERT AS
BEGIN
-- Get the year of the test request being inserted from the pseudo-insert table.
DECLARE #TestYear INT;
SET #TestYear = (SELECT YEAR(DateRequested) FROM inserted);
-- Grab the maximum TestNumber from TestTable based on the year
-- that we are inserting a record for.
DECLARE #MaxTestNumber INT;
SET #MaxTestNumber = (SELECT MAX(TestNumber) FROM dbo.TestTable WHERE TestYear = #TestYear);
-- If this is the first test of the year being inserted it is a special case
IF #MaxTestNumber IS NULL
BEGIN
SET #MaxTestNumber = 0;
END;
-- Here we take the MaxTestNumber, add 1 to it, and then pad it with
-- the appropriate number of zero's in front of it
DECLARE #TestNumber VARCHAR(4);
SET #TestNumber = (SELECT RIGHT('0000' + CAST((#MaxTestNumber + 1) AS VARCHAR(4)), 4));
INSERT INTO dbo.TestTable(Name, DateRequested, TestNumber)
SELECT Name, DateRequested, #TestNumber FROM inserted;
END;
GO
Now here is some DML showing the trigger in action:
INSERT INTO TestTable(Name, DateRequested)
VALUES('Some Test', '05-05-2013');
INSERT INTO TestTable(Name, DateRequested)
VALUES('Some Other Test', '12-25-2013');
INSERT INTO TestTable(Name, DateRequested)
VALUES('Blah Blah', '12-31-2013');
INSERT INTO TestTable(Name, DateRequested)
VALUES('Foo', '01-01-2014');
SELECT * FROM TestTable ORDER BY TestYear ASC, TestNumber ASC;
So as you can see my trigger works for single row inserts but a keen eye will be able to tell it will not work for multi-row inserts.
CREATE TABLE TempTestTable
(
Name VARCHAR(75) NOT NULL,
DateRequested DATETIME NOT NULL DEFAULT GETDATE()
);
GO
INSERT INTO TempTestTable(Name, DateRequested)
VALUES('Test1', '01-01-2012');
INSERT INTO TempTestTable(Name, DateRequested)
VALUES('Test2', '12-25-2012');
INSERT INTO TempTestTable(Name, DateRequested)
VALUES('Test3', '01-01-2013');
INSERT INTO TempTestTable(Name, DateRequested)
VALUES('Test4', '01-01-2014');
-- This doesnt work because it is a multi-row insert.
INSERT INTO TestTable(Name, DateRequested)
SELECT Name, DateRequested FROM TempTestTable;
My Question
I realize that I can probably handle this with stored procedures and force users to use stored procedures when updating the tables but I want to be extra careful and prevent sysadmins from being able to do direct inserts to the table with an incorrect 'TestNumber'.
So StackOverflow, my question is how can I achieve this? Would I use a cursor inside of my InsteadOfInsertTrigger? I am looking for alternatives.
Not the neatest thing I'll ever write, but seems to do the job:
CREATE TRIGGER InsteadOfInsertTrigger ON dbo.TestTable
INSTEAD OF INSERT AS
BEGIN
;With Years as (
select i.TestYear,
COALESCE(MAX(tt.TestNumber),0) as YMax
from inserted i left join TestTable tt
on i.TestYear = tt.TestYear
group by i.TestYear
), Numbered as (
select i.ID,i.Name,i.DateRequested,
RIGHT('000' + CONVERT(varchar(4),
ROW_NUMBER() OVER (PARTITION BY i.TestYear
ORDER BY i.DateRequested,i.Id) + YMax)
,4) as TestNumber
from inserted i
inner join
Years y
on
i.TestYear = y.TestYear
)
insert into TestTable (Id,Name,DateRequested,TestNumber)
select Id,Name,DateRequested,TestNumber from Numbered;
END;
The first CTE (Years) finds the highest used number for each year that's of interest. The second CTE (Numbered) then uses those values to offset a ROW_NUMBER() that's being assessed over all rows in inserted. I picked the ORDER BY columns for the ROW_NUMBER() so that it's as deterministic as possible.
(I was confused about one thing for a while, but it turns out that I can use TestYear from inserted rather than having to repeat the YEAR(DateRequested) formula)
Related
I have two tables, reviews and grade.
Reviews table has
id_review (primary key), id_lang, email, text etc.
Example
1, 2, email#email.com, test text
2, 2, email#email.com, test text
4, 2, email#email.com, test text
Grade table has
id_review (primary/foreign key), id_criterion, grade
1, 3, 5.00
1, 1, 4.00
2, 3, 3.00
2, 1, 5.00
4, 2, 3.00
I need to copy all the reviews with lang id 2, change the text and the lang id to 1 (this I can do manually).
But as the id_review changes with the copied reviews, I need to create new rows on the grade table, too. Is there a way to make sure that the foreign keys are matched with the copied reviews, too?
I tried to do it the old fashioned way with copy/paste on csv but as some reviews are removed from the reviews table and some reviews have differences in id_criterion count, it's very hard to do for a large table.
Or should I try to edit the table to allow the reviews table to have distinct values for id_lang with the same id_review?
You can create temporary tables (no foreign keys) out of the original ones, patch and validate the data until you satisfy and then insert back to the original tables.
I am not sure how you populate id_review, so I assume they are auto generated when you insert new rows.
create table reviews_temp_20211119 as
select r.id_review as old_id_review
, 0 as new_id_review
, row_number() over(order by r.id_review) as ref_patch_id
, r.id_lang
, r.email
, r.text
from reviews r
where id_lang = 2;
create table grades_temp_20211119 as
select g.id_review
, g.id_criterion
, g.gradate
, 0 as new_id_review
from grades g
where g.id_review in (select t.old_id_review from reviews_temp_20211119 t);
update reviews_temp_20211119
set id_lang = 1;
alter table reviews add column ref_patch_id bigint null;
-- insert back to original to get the auto generated id_review
-- if you use other strategies to populate the id_review, you can do update it directly to the temp table and review if all the data are correct before insert back to the original table
insert into reviews (id_lang, email, text, ref_patch_id)
select id_lang, email, text, ref_patch_id
from reviews_temp_20211119;
update reviews_temp_20211119 t
join reviews r on (r.ref_patch_id = t.ref_patch_id)
set t.new_id_review = r.id_review;
update grades_temp_20211119 g
join reviews_temp_20211119 t on (g.id_review = t.old_id_review)
set g.new_id_review = t.new_id_review);
insert into grades (id_review, id_criterion, grade)
select t.new_id_review
, t.id_criterion
, t.grade
from grades_temp_20211119 t;
By keeping the temporary tables, you have opportunity to review or rollback the change if something went wrong by looking back at the temporary tables.
For a repeatable process, I think a stored procedure with cursors is the way. Here's my version, it accept two parameters, the old idLang you wish to copy, and the new idLang:
CREATE PROCEDURE copyReviewWithNewLang(IN oldidLang INT, IN newidLang INT)
BEGIN
DECLARE c_idReview, c_maxIdReview INT;
DECLARE c_text, c_email VARCHAR;
DECLARE old_c_idreview INT DEFAULT 0;
-- first cursor gets all the review rows of the old language, ordered
DECLARE rev_cur CURSOR FOR SELECT idReview, email, text FROM reviews WHERE id_lang = oldidLang ORDER BY idReview ASC;
-- second cursor gets the highest idReview
DECLARE maxid_cur CURSOR FOR SELECT MAX(idReview) FROM reviews;
-- needed for ending the loop on end of retrieved data
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN rev_cur;
retrieving : LOOP
FETCH rev_cur INTO c_idReview, c_email, c_text;
-- ending the loop
IF done THEN
LEAVE retrieving;
END IF;
IF (old_c_idreview = 0) OR (old_c_idreview != c_idReview) THEN
OPEN maxid_cur;
FETCH maxid_cur INTO c_maxIdReview;
CLOSE maxid_cur;
SET c_maxIdReview = c_maxIdReview + 1
END IF;
-- copying the review row
INSERT INTO reviews (id_review, id_lang, email, text)
VALUES(c_maxIdReview, newidLang, c_email, c_text)
-- copying the grade rows
INSERT INTO grades (id_review, id_criterion, grade)
SELECT c_maxIdReview, id_criterion, grade FROM grades
WHERE id_review = c_idReview;
-- needed for checking if id changed
SET old_c_idreview = c_idReview;
END LOOP;
CLOSE rev_cur;
END;
Problem to insert running total in MySQL transactional database. need your help for solutions and opinion. Table structure of my table is,
create table `wtacct` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`ACCOUNT_NO` varchar(16),
`AMOUNT` float(16,2),
`BALANCE` float(16,2)
);
[Please note other fields have been removed to make it simple example]
I am doing Transaction as,
Dr 10 USD from account 1001 and
Cr 10 USD to account 2002
Insert query
INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE)
VALUES ('', 1001, -10, 100), ('', 2002, 10, 5000);
I want the Balance as,
BALANCE of Account no 1001 = Last transaction Balance of account 1001 - 10.
My solutions and limitations
Solution 1
In insert statement put sub query in balance field:
select balance from wtacct where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)
Limitation: Mysql does not support same table select query (wtacct) where inserting the data (wtacct).
Solution 2
Using insert into select statement
insert into wtacct select '' ID, 1001 ACCOUNT_NO, -10 AMOUNT, (BALANCE-10) BALANCE where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)
Limitation: For first transaction there is no record in wtacct for the account 1001 so select query will not return any record for first transaction.
Solution 3
Taking balance in variable and use it in insert statement.
select #balance1001 :=balance from wtacct
where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)
select #balance2002 :=balance from wtacct
where account_no=2002 and id in(select max(id) from wtacct where account_no=2002)
INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE)
VALUES ('', 1001, -10, #balance1001-10), ('', 2002, 10, #balance2002+10);
Limitation: there is a chance to be change the balance in time between select and insert query execution. also its costly, 3 query execution required.
Solution 4
Insert and then update Balance
INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE)
VALUES ('', 1001, -10, 0);
UPDATE wtacct set balance = (ifnull(Select balance from wtacct where account_no=1001 and id in(select max(id) from wtacct where id <last_insert_id() and account_no=1001),0) -10)
where id =last_insert_id() and account_no=1001
........
Limitation: query is costly. its required 4 (two insert and 2 update) query execution. note last_insert_id() is php function
Solution 5
Using a trigger on insert statement. In the trigger, the balance will be updated calculating last transaction value and insert amount.
Limitation: Trigger not support transaction behavior and may fail.
Please give your solution and opinion on the above solutions. Please note in the above example their may be some syntax error/error. Please ignore them.
A big limitation I didn't see listed is a potential race condition, where two rows are being inserted into the table at the same time. There's a chance that the two inserts will both get the current "balance" from the same previous row.
One question: do you also have a separate "current balance" table that keeps a single value of the current "balance" for each account? Or are you only relying on the "balance" from the previous transaction.
Personally, I would track the current balance on a separate "account balance" table. And I would use BEFORE INSERT/UPDATE triggers to maintain the value in that row, and use that to return the current balance for the account.
For example, I would define a trigger like this which gets fired when a row is inserted into `wtacct` table:
CREATE TRIGGER wtacct_bi
BEFORE INSERT ON wtacct
FOR EACH ROW
BEGIN
IF NEW.amount IS NULL THEN
SET NEW.amount = 0;
END IF
;
UPDATE acct a
SET a.balance = (#new_balance := a.balance + NEW.amount)
WHERE a.account_no = NEW.account_no
;
SET NEW.balance = #new_balance
;
END$$
The setup for that trigger...
CREATE TABLE acct
( account_no VARCHAR(16) NOT NULL PRIMARY KEY
, balance DECIMAL(20,2) NOT NULL DEFAULT 0
) ENGINE=InnoDB
;
CREATE TABLE wtacct
( id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT
, account_no VARCHAR(16) NOT NULL COMMENT 'FK ref acct.account_no'
, amount DECIMAL(20,2) NOT NULL
, balance DECIMAL(20,2) NOT NULL
, FOREIGN KEY FK_wtacct_acct (account_no) REFERENCES acct (account_no)
ON UPDATE CASCADE ON DELETE RESTRICT
) ENGINE=InnoDB
;
My reason for using a separate "current balance" table is that there is only one row for the given account_no, and that row retains the current balance of the account.
The UPDATE statement in the trigger should obtain an exclusive lock on the row being updated. And that exclusive lock prevents any other UPDATE statement from simultaneously updating the same row. The execution of the UPDATE statement will add the `amount` from the current transaction row being inserted to the current balance.
If we were using Oracle or PostgreSQL, we could use a RETURNING clause to get the value that was assigned to the \'balance\' column.
In MySQL we can do a wonky workaround, using a user-defined variable. The new value we are going to assign to the column is first assigned to the user_defined variable, and then that is assigned to the column.
And we can assign the value of the user-defined variable to the `balance` column of the row being inserted into `wtacct`.
The purpose of this approach is to make the retrieval and update of the current balance in a single statement, to avoid any race conditions.
The UPDATE statement locates the row, obtains an exclusive (X) lock on the row, retrieves the current balance (value from the \'balance\' column), calculates the new current balance, and assigns it back to the \'balance\' column. Then continues to hold the lock until the transaction completes.
Once the trigger completes, the INSERT statement (which initially fired the trigger) proceeds, attempting to insert the new row into `wtacct`. If that fails, then all of the changes made by the INSERT statement and execution of the trigger are rolled back, keeping everything consistent.
Once a COMMIT or ROLLBACK is issued by the session, the exclusive (X) lock held on the row(s) in `acct` are released, and other sessions can obtain locks on that row in `acct`.
I have done it using Store Procedure for MySql
CREATE DEFINER=`root`#`%` PROCEDURE `example_add`(IN dr Int, IN cr Int)
BEGIN
DECLARE LID int;
Declare Balance decimal(16,2);
INSERT INTO example (Debit,Credit)
VALUES (dr, cr);
SET LID = LAST_INSERT_ID();
SET Balance = (select SUM(Debit) - SUM(Credit) as Balance from example);
UPDATE Example SET Balance = Balance WHERE ID = LID;
END
Use it example_add(10,0) or example_add(0,15) then select and see the result.
I have a table on which id is a primary key column set with auto increment. It contains over 10,00 rows.
I need to get all primary keys that have been deleted.
like
1 xcgh fct
2 xxml fcy
5 ccvb fcc
6 tylu cvn
9 vvbh cvv
The result that i should get is
3
4
7
8
currently i count all records and then insert(1 to count) in another table and then i select id from that table that dosent exists in record table. But this method is very inefficient. Is there any direct query that i can use?
please specify for mysql.
See fiddle:
http://sqlfiddle.com/#!2/edf67/4/0
CREATE TABLE SomeTable (
id INT PRIMARY KEY
, mVal VARCHAR(32)
);
INSERT INTO SomeTable
VALUES (1, 'xcgh fct'),
(2, 'xxml fcy'),
(5, 'ccvb fcc'),
(6, 'tylu cvn'),
(9, 'vvbh cvv');
set #rank = (Select max(ID)+1 from sometable);
create table CompleteIDs as (Select #rank :=#rank-1 as Rank
from sometable st1, sometable st2
where #rank >1);
SELECT CompleteIDs.Rank
FROM CompleteIDs
LEFT JOIN someTable S1
on CompleteIDs.Rank = S1.ID
WHERE S1.ID is null
order by CompleteIDs.rank
There is one assumption here. That the number of records in someTable* the number of records in sometable is greater than the maximum ID in sometable. Otherwise this doesn't work.
You can try to create a temp table, fill it with e.g. 1,000 values, you can do it using any scripting language or try a procedure (This might be not-effective overall)
DELIMITER $$
CREATE PROCEDURE InsertRand(IN NumRows INT)
BEGIN
DECLARE i INT;
SET i = 1;
START TRANSACTION;
WHILE i <= NumRows DO
INSERT INTO rand VALUES (i);
SET i = i + 1;
END WHILE;
COMMIT;
END$$
DELIMITER ;
CALL InsertRand(5);
Then you just do query
SELECT id AS deleted_id FROM temporary_table
WHERE id NOT IN
(SELECT id FROM main_table)
Please note that it should be like every day action or something cause it's very memory inefficient
I have an auto increment column ID, and for some situation I wanted the other column to be equal to the primary key + 1 value
ID | other
1 | 2
2 | 3
3 | 4
4 | 123 (some situation, it is not always plus 1)
How can I achieve this?
Here's what I have tried
INSERT INTO table (`ID`,`other`) VALUES ('',(SELECT MAX(ID)+1 FROM table))
But that returns an error
You can't specify target table 'table' for update in FROM clause
Try Below query:
ALTER TABLE dbo.table ADD
Column AS ([ID]+1)
GO
It will definitely work
Using a normal AUTO_INCREMENT column as id, I cannot think of a way to do this in MySQL. Triggers, which otherwise would have been an option, don't work well with AUTO_INCREMENT columns.
The only way I see is to do two commands for an INSERT;
INSERT INTO bop (value) VALUES ('These values should be 1 and 2');
UPDATE bop SET other = id+1 WHERE id = LAST_INSERT_ID();
An SQLfiddle to test with.
The closest I'm getting to what you're looking for is to generate sequences separately from AUTO_INCREMENT using a function, and use that instead to generate the table id;
DELIMITER //
CREATE TABLE bop (
id INT UNIQUE,
other INT,
value VARCHAR(64)
)//
CREATE TABLE bop_seq ( seq INT ) // -- Sequence table
INSERT INTO bop_seq VALUES (1) // -- Start value
CREATE FUNCTION bop_nextval() RETURNS int
BEGIN
SET #tmp = (SELECT seq FROM bop_seq FOR UPDATE);
UPDATE bop_seq SET seq = seq + 1;
RETURN #tmp;
END//
CREATE TRIGGER bop_auto BEFORE INSERT ON bop
FOR EACH ROW
SET NEW.id = bop_nextval(), NEW.other=NEW.id + 1;
//
That'd let you do inserts and have it autonumber like you want. The FOR UPDATE should keep the sequence transaction safe, but I've not load tested so you may want to do that.
Another SQLfiddle.
I solved this by updating 2 times the DB..
I wanted to do +1 from 19 till ..
UPDATE `table` SET `id`=`id`+101 WHERE id <= 19
UPDATE `table` SET `id`=`id`-100 WHERE id <= 119 AND id >= 101
I have a table that contains computer login and logoff events. Each row is a separate event with a timestamp, machine name, login or logoff event code and other details. I need to create a SQL procedure that goes through this table and locates corresponding login and logoff event and insert new rows into another table that contain the machine name, login time, logout time and duration time.
So, should I use a cursor to do this or is there a better way to go about this? The database is pretty huge so efficiency is certainly a concern. Any suggested pseudo code would be great as well.
[edit : pulled from comment]
Source table:
History (
mc_id
, hs_opcode
, hs_time
)
Existing data interpretation:
Login_Event = unique mc_id, hs_opcode = 1, and hs_time is the timestamp
Logout_Event = unique mc_id, hs_opcode = 2, and hs_time is the timestamp
First, your query will be simpler (and faster) if you can order the data in such a way that you don't need a complex subquery to pair up the rows. Since MySQL doesn't support CTE to do this on-the-fly, you'll need to create a temporary table:
CREATE TABLE history_ordered (
seq INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
hs_id INT,
mc_id VARCHAR(255),
mc_loggedinuser VARCHAR(255),
hs_time DATETIME,
hs_opcode INT
);
Then, pull and sort from your original table into the new table:
INSERT INTO history_ordered (
hs_id, mc_id, mc_loggedinuser,
hs_time, hs_opcode)
SELECT
hs_id, mc_id, mc_loggedinuser,
hs_time, hs_opcode
FROM history ORDER BY mc_id, hs_time;
You can now use this query to correlate the data:
SELECT li.mc_id,
li.mc_loggedinuser,
li.hs_time as login_time,
lo.hs_time as logout_time
FROM history_ordered AS li
JOIN history_ordered AS lo
ON lo.seq = li.seq + 1
AND li.hs_opcode = 1;
For future inserts, you can use a trigger like below to keep your duration table updated automatically:
DELIMITER $$
CREATE TRIGGER `match_login` AFTER INSERT ON `history`
FOR EACH ROW
BEGIN
IF NEW.hs_opcode = 2 THEN
DECLARE _user VARCHAR(255);
DECLARE _login DATETIME;
SELECT mc_loggedinuser, hs_time FROM history
WHERE hs_time = (
SELECT MAX(hs_time) FROM history
WHERE hs_opcode = 1
AND mc_id = NEW.mc_id
) INTO _user, _login;
INSERT INTO login_duration
SET machine = NEW.mc_id,
logout = NEW.hs_time,
user = _user,
login = _login;
END IF;
END$$
DELIMITER ;
CREATE TABLE dummy (fields you'll select data into, + additional fields as needed)
INSERT INTO dummy (columns from your source)
SELECT * FROM <all the tables where you need data for your target data set>
UPDATE dummy SET col1 = CASE WHEN this = this THEN that, etc
INSERT INTO targetTable
SELECT all columns FROM dummy
Without any code that you're working on.. it'll be hard to see if this approach will be any useful.. There may be some instances when you really need to loop through things.. and some instances when this approach can be used instead..
[EDIT: based on poster's comment]
Can you try executing this and see if you get the desired results?
INSERT INTO <your_target_table_here_with_the_three_columns_required>
SELECT li.mc_id, li.hs_time AS login_time, lo.hs_time AS logout_time
FROM
history AS li
INNER JOIN history AS lo
ON li.mc_id = lo.mc_id
AND li.hs_opcode = 1
AND lo.hs_opcode = 2
AND lo.hs_time = (
SELECT min(hs_time) AS hs_time
FROM history
WHERE hs_time > li.hs_time
AND mc_id = li.mc_id
)