MySQL - While in SELECT clause - mysql

Is it possible to call a while statement inside a SELECT clause in MySQL ?
Here is a example of what I want to do :
CREATE TABLE `item` (
`id` int,
`parentId` int,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
KEY `FK_parentId` (`parentId`),
CONSTRAINT `FK_parentId` FOREIGN KEY (`parentId`) REFERENCES `item` (`id`)
);
I would like to select the root of each item, i.e. the higher ancestor (the item that has no parentId). In my mind, I would do something like this :
select
`id` as 'ID',
while `parentId` is not null do `id` = `parentId` end while as 'Root ID'
from
`item`
Of course this can't work. What is the better way to achieve something like that ?
EDIT
Here a sample data :
id | parentId
1 | NULL
2 | 1
3 | 2
4 | 2
5 | 3
6 | NULL
7 | 6
8 | 7
9 | 7
And expected result :
ID | RootId
1 | NULL
2 | 1
3 | 1
4 | 1
5 | 1
6 | NULL
7 | 6
8 | 6
9 | 6
Thank you.

just use CASE
select
`id` as 'ID',
CASE `parentId` WHEN is not null THEN `parentId` END as 'Root ID'
from
`item`

Here is the procedure:
BEGIN
-- declare variables
DECLARE cursor_ID INT;
DECLARE cursor_PARENTID INT;
DECLARE done BOOLEAN DEFAULT FALSE;
-- declare cursor
DECLARE cursor_item CURSOR FOR SELECT id, parentId FROM item;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- create a temporary table
create TEMPORARY table IF NOT EXISTS temp_table as (select id, parentId from item);
truncate table temp_table;
OPEN cursor_item;
item_loop: LOOP
-- fetch row through cursor
FETCH cursor_item INTO cursor_ID, cursor_PARENTID;
IF done THEN
-- end loop if cursor is empty
LEAVE item_loop;
END IF;
-- insert into
insert into temp_table
select MAX(t.id) id, MIN(#pv := t.parentId) parentId
from (select * from item order by id desc) t
join (select #pv := cursor_ID) tmp
where t.id = #pv;
END LOOP;
-- close cursor
CLOSE cursor_item;
-- get the results
SELECT id id, parentid RootId from temp_table order by id ASC;
END
I created a temporary table and kept the results into it while running cursor. I couldn't think of a solution with just one query. I had to go for a cursor.
I took help from the following links:
How to do the Recursive SELECT query in MySQL?
How to create a MySQL hierarchical recursive query

Related

mysql trigger does not insert selected columns

CREATE DEFINER=`ir55`#`%` TRIGGER UpdateAverageBookRating
AFTER INSERT
ON Reviews FOR EACH ROW
BEGIN
DECLARE AverageRating double;
DECLARE lastbookid int;
SELECT BookID FROM Reviews WHERE ReviewID = LAST_INSERT_ID() INTO lastbookid;
SELECT AVG(Rating) FROM Reviews WHERE BookID = lastbookid INTO AverageRating;
UPDATE CentralCatelogue SET Rating = AverageRating WHERE BookID = lastbookid;
END
The above trigger does not update the Rating column in CentralCatelogue because the value of Rating and BookID are empty.
Not sure why, I tried to update the Rating with static values and it works, I believe there is something wrong with selecting the columns in 'Reviews' and assigning them into variables.
Appreciate your help.
Thanks
If your schema looks a bit like this
DROP TABLE REVIEWS,CENTRALCATALOGUE;
CREATE TABLE `reviews` (
`bookid` int(11) DEFAULT NULL,
`rating` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `centralcatalogue` (
`bookid` int(11) DEFAULT NULL,
`rating` double(10,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
DROP TRIGGER IF EXISTS T;
DELIMITER $$
CREATE TRIGGER `t` AFTER INSERT ON `reviews` FOR EACH ROW BEGIN
DECLARE AverageRating double(10,2);
if not exists (select 1 from centralcatalogue where bookid = new.bookid) then
insert into debug_table(msg) values (concat('inserting:',new.bookid,':',new.rating));
insert into centralcatalogue values(new.bookid,new.rating);
else
SELECT AVG(Rating) FROM Reviews WHERE BookID = new.bookid INTO AverageRating;
insert into debug_table(msg) values (concat('updating',new.bookid,':',averagerating));
UPDATE CentralCatalogue SET Rating = AverageRating WHERE BookID = new.bookid;
end if;
END $$
delimiter ;
Note 1) the test and insert to see if bookid exists in centralcatalogue 2) The debug_table to assist debugging 3) changed averagerating to double(10,2) to avoid rounding up
the insert
insert into reviews values(1,1),(1,2),(2,3);
produces
+--------+--------+
| bookid | rating |
+--------+--------+
| 1 | 1.50 |
| 2 | 3.00 |
+--------+--------+
2 rows in set (0.02 sec)
BTW I don't really like this trigger since it tests and calculates for each row. It might be better if you encapsulated the insert and update in a transaction.

soft delete on update in mysql with multiple schemas

I am in this situation:
Background
I have 2 database schemas called "prod" and "stg".
"prod" contains 2 tables called "parent" and "child"
"stg" only has the "parent" table
"parent" table defination is the same across "prod" and "stg" schemas.
In the case of deleting records, "parent" table is defined as soft delete (logically deletion, i.e. set delete_flg as "1") whereas the "child" table is true delete (physically remove the record)
Goal
I am trying to achieve the following goal:
when and only when both "prod"."parent" and "stg"."parent" are deleted (no matter physically or logically, or does not exist on one side) then automatically cascade a delete operation(physically remove) to the record in "prod"."child" table whose "SP_ID" matches the value in "parent".
For example, assuming I have
"prod"."parent"
+----+---------+--------+
| SP_ID | SP_NAME | DELETE_FLG |
+----+---------+--------+
| 1 | 1 | 1 |
+----+---------+--------+
"prod"."parent"
+----+---------+--------+
| SP_ID | SP_NAME | DELETE_FLG |
+----+---------+--------+
| 1 | 1 | 1 |
+----+---------+--------+
"stg"."parent"
+----+---------+--------+
| SP_ID | SP_NAME | DELETE_FLG |
+----+---------+--------+
| 1 | 1 | 0 |
+----+---------+--------+
"prod"."child"
+----+---------+
| SP_ID | JOB_KEY |
+----+---------+
| 1 | key |
+----+---------+
, if I execute a sql update "stg"."parent" set DELETE_FLG = 1 where SP_ID = 1, which logically delete the last "existing" record in "parent" table that has SP_ID 1, then the record in "prod"."child" will also be automatically phycially deleted by mysql.
Question
I have been thinking about making the SP_ID in the child table as a foreign key referencing the one in parent able (https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html)
however,
a) I don't know whether it is possible to reference multiple tables in differnet schemas, and
b) It seems mysql only support cascading same operation, i.e. delete on parent then delete the child OR update on parent then update the child. But in my case, I want a update on parent then delete the child.
Could somebody help me out here please?
Is this possible to be achieved in mysql? or I have to do this in application layer?
Table definition
CREATE TABLE `prod`.`parent` (
`SP_ID` varchar(20) NOT NULL COMMENT '',
`SP_NAME` varchar(100) NOT NULL COMMENT '',
`DELETE_FLG` tinyint(1) NOT NULL DEFAULT '0' COMMENT '',
PRIMARY KEY (`SP_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=''
CREATE TABLE `prod`.`child` (
`SP_ID` varchar(20) NOT NULL COMMENT '',
`JOB_KEY` varchar(11) NOT NULL,
PRIMARY KEY (`SP_ID`,`JOB_KEY`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=''
CREATE TABLE `stg`.`parent` (
`SP_ID` varchar(20) NOT NULL COMMENT '',
`SP_NAME` varchar(100) NOT NULL COMMENT '',
`DELETE_FLG` tinyint(1) NOT NULL DEFAULT '0' COMMENT '',
PRIMARY KEY (`SP_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=''
with the hint of using triggers, This is my solution which works:
Add 2 triggers(one is after update, on is after delete) to both prod and stg parent tables.
# after update trigger
CREATE DEFINER=`root`#`localhost` TRIGGER `stg`.`parent_AFTER_UPDATE` AFTER UPDATE ON `parent` FOR EACH ROW
BEGIN
IF (
select
count(*)
from(
select
*
from `prod`.`parent`
where `prod`.`parent`.id = old.id and `prod`.`parent`.delete_flg = 0
Union all
select
*
from `stg`.`parent`
where `stg`.`parent`.id = old.id and `stg`.`parent`.delete_flg = 0
) as a
) = 0 THEN
DELETE FROM `prod`.`child` WHERE `prod`.`child`.id = old.id;
END IF;
END
# after delete trigger
CREATE DEFINER=`root`#`localhost` TRIGGER `stg`.`parent_AFTER_DELETE` AFTER DELETE ON `parent` FOR EACH ROW
BEGIN
IF (
select
count(*)
from(
select
*
from `prod`.`parent`
where `prod`.`parent`.id = old.id and `prod`.`parent`.delete_flg = 0
Union all
select
*
from `stg`.`parent`
where `stg`.`parent`.id = old.id and `stg`.`parent`.delete_flg = 0
) as a
) = 0 THEN
DELETE FROM `prod`.`child` WHERE `prod`.`child`.id = old.id;
END IF;
END

MySql query to dynamically convert rows to columns with grouping by DATA_FORMAT

I have two tables in my MySQL 5.5 database:
CREATE TABLE `t_user` (
`USER_ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`USER_NAME` varchar(255) NOT NULL,
PRIMARY KEY (`USER_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
CREATE TABLE `t_user_activity_log` (
`USER_ID` int(10) unsigned NOT NULL,
`ACTIVITY_DATE` datetime NOT NULL,
`ACTIVITY_COUNT` int(10) unsigned NOT NULL,
PRIMARY KEY (`USER_ID`,`ACTIVITY_DATE`),
CONSTRAINT `FK_t_user` FOREIGN KEY (`USER_ID`) REFERENCES `t_user` (`USER_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
And I need to get result grouped by time period like:
----------------------------------------------
| Period | User1 | User2 | ***** | UserN |
----------------------------------------------
| 22.02.2012 | 12 | 12 | x | x |
----------------------------------------------
| 23.02.2012 | 7 | 3 | x | x |
----------------------------------------------
| 24.02.2012 | 0 | 0 | 0 | 0 |
----------------------------------------------
Period is changeable (HOUR, WEEK, MONTH, YEAR). It should be possible to limit query by FROM and TO dates and if no records for date is found - it should still be visible in result. Users should be selected from the list in().
Is it possible at all?
You can do something like that, obviously you would generate this query with your server language from the list of your actual users.
SELECT
t.ACTIVITY_DATE,
SUM(IF(t.USER_ID = 1, t.activity_count, 0)) as 'user1',
SUM(IF(t.USER_ID = 2, t.activity_count, 0)) as 'user2'
FROM (
SELECT t_user.USER_ID,
t_user.USER_NAME,
t_user_activity_log.ACTIVITY_DATE,
SUM(t_user_activity_log.ACTIVITY_COUNT) as activity_count
FROM t_user_activity_log
INNER JOIN t_user ON t_user_activity_log.USER_ID = t_user.USER_ID
GROUP BY t_user.USER_ID, t_user_activity_log.ACTIVITY_DATE
) as t
GROUP BY t.USER_ID, t.ACTIVITY_DATE
If you want to keep it in the database, you can create a dynamic query inside a procedure like this
DELIMITER $$
DROP PROCEDURE IF EXISTS `dynamic_query`$$
CREATE PROCEDURE `dynamic_query`()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE user_id INT;
DECLARE user_name VARCHAR(255);
#Cursor to fetch all the users
DECLARE cursor_user CURSOR FOR SELECT t_user.USER_ID, t_user.USER_NAME FROM t_user;
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET done = 1;
# start of the dynamic query
SET #query = CONCAT('SELECT t.ACTIVITY_DATE\n');
OPEN cursor_user;
# looping over the cursor
user_loop: LOOP
FETCH cursor_user INTO user_id, user_name;
# Leaving the loop inf no more records found
IF done = 1 THEN
LEAVE user_loop;
END IF;
#Concatenate the users in the dynamic query
SET #query = CONCAT(#query,',SUM(IF(t.USER_ID = ',user_id,', t.activity_count, 0)) as \'',user_name,'\'');
END LOOP;
CLOSE cursor_user;
#the rest of the dynamic query
SET #query = CONCAT(#query,' FROM (SELECT t_user.USER_ID, t_user.USER_NAME, t_user_activity_log.ACTIVITY_DATE, SUM(t_user_activity_log.ACTIVITY_COUNT) as activity_count FROM t_user_activity_log INNER JOIN t_user ON t_user_activity_log.USER_ID = t_user.USER_ID GROUP BY t_user.USER_ID, t_user_activity_log.ACTIVITY_DATE) as t GROUP BY t.USER_ID, t.ACTIVITY_DATE');
# Preparing and executing the dynamic query
PREPARE stmt FROM #query;
EXECUTE stmt;
# Cleaning up
DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;

Composite Primary Key and Auto_Increment [duplicate]

This question already has answers here:
mysql two column primary key with auto-increment
(4 answers)
Closed 9 years ago.
I'm trying to do the composite key with one of them auto incrementing, but when I try to enter a new row it just continue the sequential.
Here's the example of what happens:
Item_1 | Item_2
1 | 1
1 | 2
2 | 3
2 | 4
2 | 5
Here's the example of what I want:
Item_1 | Item_2
1 | 1
1 | 2
2 | 1
2 | 2
2 | 3
I create the table this way:
CREATE TABLE IF NOT EXISTS `usuarios` (
`cod_user` int(11) NOT NULL AUTO_INCREMENT,
`cod_user_emp` int(11) NOT NULL,
PRIMARY KEY (`cod_user`,`cod_user_emp`),
UNIQUE KEY `user` (`user`),
KEY `cod_user` (`cod_user`)
);
Edit
I resolved the problem doing a server sided php validation.
$result = $db->query("SELECT * FROM usuarios WHERE cod_user_emp=\"$emp\" ORDER BY cod_user DESC LIMIT 1");
while($row=$result->fetch_array()){
$cod2 = $row['cod_user']+1;
}
Remove that AUTO_INCREMENT column,
CREATE TABLE IF NOT EXISTS `usuarios`
(
`cod_user` int(11) NOT NULL,
`cod_user_emp` int(11) NOT NULL,
PRIMARY KEY (`cod_user`,`cod_user_emp`) -- <<== this is enough
);
And can create a Stored Procedure that increments Item_2 for every Item_1.
DELIMITER $$
CREATE PROCEDURE InsertRecord(IN ItemA INT)
BEGIN
SET #max_id = (
SELECT COALESCE(MAX(Item_2), 0) + 1
FROM TableName
WHERE Item_1 = ItemA
);
INSERT INTO tableName(Item_1, Item_2)
VALUES(ItemA, #max_id)
END $$
DELIMITER ;
and call it like this,
CALL InsertRecord(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