MySQL trigger to update a column by reordering its values - mysql

Sorry if the title is miss-leading, I couldn't come up with a better one that is related to my issue.
I've been trying to solve this for a while now, and I couldn't find the solution.
I have a table categories:
+----+--------+----------+
| ID | Name | Position |
+----+--------+----------+
| 1 | Dogs | 4 |
| 2 | Cats | 3 |
| 3 | Birds | 10 |
| 4 | Others | 2 |
+----+--------+----------+
I need to keep the Position column in order, in a way not to miss an values as well, so the final table should look like:
+----+--------+----------+
| ID | Name | Position |
+----+--------+----------+
| 1 | Dogs | 3 |
| 2 | Cats | 2 |
| 3 | Birds | 4 |
| 4 | Others | 1 |
+----+--------+----------+
What I tried doing, is creating a trigger on UPDATE and on INSERT that would try to prevent this. The trigger I created ( same one before INSERT) :
DELIMITER //
CREATE TRIGGER sortPostions BEFORE UPDATE ON categories
FOR EACH ROW BEGIN
SET #max_pos = 0;
SET #min_pos = 0;
SET #max_ID = 0;
SET #min_ID = 0;
SELECT position, id INTO #max_pos,#max_ID FROM categories WHERE position = ( SELECT MAX(position) FROM categories);
SELECT position, id INTO #min_pos,#min_ID FROM categories WHERE position = ( SELECT MIN(position) FROM categories);
IF NEW.position >= #max_pos AND NEW.id != #max_ID THEN
SET NEW.position = #max_pos + 1;
END IF;
IF NEW.position <= #min_pos AND NEW.id != #min_ID THEN
SET NEW.position = #min_pos - 1;
END IF;
IF NEW.position < 0 THEN
SET NEW.position = 0;
END IF;
END//
DELIMITER ;
But unfortunately it's not working as intended. It's not fixing missing values and I think this is not a perfect solution.
I went ahead and created a procedure:
BEGIN
SET #n = 0;
UPDATE categories
SET position = #n:=#n+1
ORDER BY position ASC;
END
But I wasn't able to call this procedure from a trigger, as it seems that MySQL doesn't allow that. I get the following error:
#1442 - Can't update table 'categories' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
mysql -V output:
mysql Ver 14.14 Distrib 5.5.57, for debian-linux-gnu (x86_64) using readline 6.3
What's the perfect solution to solve this problem ?
Thanks a lot!

You can't do this in a trigger. MySQL does not allow you to do an INSERT/UPDATE/DELETE in a trigger (or in a procedure called by the trigger) against the same table for which the trigger was spawned.
The reason is that it can result in infinite loops (your update spawns the trigger, which updates the table, which spawns the trigger again, which updates the table again...). Also because it can create lock conflicts if more than one of these requests happens concurrently.
You should do this in application code if you must renumber the position of the rows. Do it with a separate statement after your initial query has completed.
Another option is don't worry about making the positions consecutive. Just make sure they are in the right order. Then when you query the table, generate row numbers on demand.
SELECT (#n:=#n+1) AS row_num, c.*
FROM (SELECT #n:=0 AS n) AS _init
CROSS JOIN categories AS c
ORDER BY c.position ASC;
+---------+----+--------+----------+
| row_num | id | name | position |
+---------+----+--------+----------+
| 1 | 4 | Others | 2 |
| 2 | 2 | Cats | 3 |
| 3 | 1 | Dogs | 4 |
| 4 | 3 | Birds | 10 |
+---------+----+--------+----------+
In MySQL 8.0, you'll be able to do this with more standard syntax using ROW_NUMBER().
SELECT ROW_NUMBER() OVER w AS row_num, c.*
FROM categories AS c
WINDOW w AS (ORDER BY c.position ASC);
Gives the same output as the query using the user-variable.

Related

MySQL Get All Children in a Column

I have a MySQL table that looks something like this:
----------------------
| ID | Name | Parent |
----------------------
| 1 | a | null |
| 2 | b | null |
| 3 | c | 1 |
| 4 | d | 3 |
| 5 | e | 2 |
| 6 | f | 2 |
----------------------
with an unknown number of possible depth to the parent/child relationship.
I want a query that will give me the following result:
-----------------
| ID | Children |
-----------------
| 1 | 3,4 | -- because 4 is a child of 3, and 3 is a child of 1, it should show in both
| 2 | 5,6 |
| 3 | 4 |
| 4 | null |
| 5 | null |
| 6 | null |
-----------------
Is it possible to get this kind of result in a query? I've been trying everything I know to try, and searching everywhere and have not found something that will give me this result.
Considering out table as employees with fields ID, Name, Parent.
Approach 1: When we know the depth of our hierarchy
We can simply join our table n time equal to our hierarchy and can use GROUP BY to get the desired results. Here it is 3 so
SELECT t1.ID AS lev1, GROUP_CONCAT(CONCAT_WS(',', t2.ID, t3.ID)) AS childs
FROM employees AS t1
LEFT JOIN employees AS t2 ON t2.Parent = t1.ID
LEFT JOIN employees AS t3 ON t3.Parent = t2.ID
GROUP BY t1.ID
Corresponding fiddle you can look here.
Approach 2: When we don't know the depth of our hierarchy
We'll create two procedures for that.
store_emp_childs - It will store parent and its child in a temporary table.
get_emp_child - It will create temp table, call store_emp_childs to generate resultset. Select(return) the data from temporary table and remove the temporaray table.
CREATE DEFINER=`root`#`localhost` PROCEDURE `get_emp_child`()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
CREATE TEMPORARY TABLE `tmp_emp_child` (
`emp_id` INT(11) NOT NULL,
`child_id` INT(11) NOT NULL,
PRIMARY KEY (`emp_id`, `child_id`)
);
CALL store_emp_childs(NULL, '');
SELECT e.ID, GROUP_CONCAT(ec.child_id) AS childs
FROM employees e
LEFT JOIN tmp_emp_child ec ON e.ID = ec.emp_id
GROUP BY e.ID;
DROP TEMPORARY TABLE tmp_emp_child;
END
CREATE DEFINER=`root`#`localhost` PROCEDURE `store_emp_childs`(
IN `int_parent` INT,
IN `old_parents` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
DECLARE finished, current_id INTEGER DEFAULT 0;
DEClARE cur CURSOR FOR SELECT id FROM employees WHERE IFNULL(Parent, 0) = IFNULL(int_parent, 0);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
OPEN cur;
curID: LOOP
FETCH cur INTO current_id;
IF finished = 1 THEN
LEAVE curID;
END IF;
INSERT INTO tmp_emp_child (emp_id, child_id)
SELECT id, current_id FROM employees WHERE FIND_IN_SET(id, old_parents) OR id = int_parent
ON DUPLICATE KEY UPDATE emp_id = emp_id;
CALL store_emp_childs(current_id, CONCAT(old_parents, ',', current_id));
END LOOP curID;
CLOSE cur;
END
Note:
We are recursively calling our store_emp_childs. It requires max_sp_recursion_depth parameter to be set more than 0. I recommend to make it 250. It will not work if records are more than this recursion depth. Will look further to improve this.
We are creating temporary table, so user should have rights to create that.

Renumber keys in compound unique constraint

I have a table that looks something like the following:
| id | sub_id | fk_id |
|----|--------|-------|
| 1 | 1 | 1 |
| 2 | 2 | 1 |
| 3 | 3 | 1 |
| 4 | 4 | 1 |
| 5 | 5 | 1 |
| 6 | 1 | 2 |
| 7 | 2 | 2 |
| 8 | 3 | 2 |
| 9 | 4 | 2 |
| 10 | 5 | 2 |
Within this table id is the primary key, and sub_id and fk_id make up a compound unique key, where fk_id is the primary key in another table.
I've found myself in the situation where I need to be able to remove rows within the table, but then renumber sub_id so that there aren't any gaps, e.g. remove (1, 1, 1) and all rows where fk_id=1 have their respective sub_id renumbered as 1, 2, 3, 4, etc.
I also need to be able to remove one or more rows at a time, then trigger the re-numbering (as I assume it's inefficient to try and renumber them multiple times when once will suffice). However, there's a maximum of 60 rows for each value of fk_id but there can be thousands of different values of fk_id.
How should I go about the re-numbering? I'm think some sort of INSERT ... SELECT query, but I can't get my head around how it should work.
You can renumber the rows for a given fk_id using this query:
select t_renum.*, count(t_lower.id) as new_sub_id
from mytable t_renum
join mytable t_lower
on t_lower.fk_id = t_renum.fk_id
and t_lower.id <= t_renum.id
where t_renum.fk_id = #renumber_fk_id
group by t_renum.id
The result can be joined with the original table for update like this:
update mytable t
join (
select t_renum.*, count(t_lower.id) as new_sub_id
from mytable t_renum
join mytable t_lower
on t_lower.fk_id = t_renum.fk_id
and t_lower.id <= t_renum.id
where t_renum.fk_id = #renumber_fk_id
group by t_renum.id
) t_renum using (id)
set t.sub_id = t_renum.new_sub_id
sqlfiddle
After digging around more, I discovered another answer that was remarkably simple and avoided the need for a new table which is recommended in many similar questions. I converted it to a stored procedure which suits my needs better:
DELIMITER //
CREATE PROCEDURE reindex (IN fk_key INT UNSIGNED)
BEGIN
SET #num := 0;
UPDATE example
SET sub_id = (#num := #num + 1)
WHERE fk_id = fk_key
ORDER BY id;
END //
DELIMITER ;

Comparing two tables in MySQL

I have a master table and a temp table that look something like:
things_temp
+----+--------+--------------+
| id | number | current_time |
+----+--------+--------------+
| 1 | 456 | 9/16/2013 |
| 2 | 123 | 9/16/2013 |
+----+--------+--------------+
things_master
+----+--------+--------------+-----+
| id | number | last_updated | old |
+----+--------+--------------+-----+
| 1 | 456 | 9/15/2013 | 0 |
| 2 | 234 | 9/15/2013 | 0 |
| 3 | 888 | 8/14/2012 | 1 |
+----+--------+--------------+-----+
I need to iterate through the things_temp table and, if there exists the same number in things_master AND old == 0, update the last_updated to the current_time.
Otherwise, if both conditions above are not satisfied, simply add the record from things_temp to things_master with last_updated as current_time and old = 0.
Now, I could easily get the count of things_temp and check each one individually. But there are something like 40,000 records in each table so I think that may be a bad idea.
I've been looking around and there are things like UNION ALL, LEFT JOIN, INNER JOIN that all seem like they may be a part of the solution, but I'm a bit lost.
Is there a better way to accomplish my task without iterating through each record of things_temp and searching through things_master?
You might be able to do this in one statement by abusing replace into, but it's probably clearer to do it in two steps. Other databases support merge which is designed for this sort of thing.
start transaction;
-- update any matching numbers with the data from thing_temp
update
things_master m
inner join
things_temp t
on m.number = t.number
set
m.last_updated = t.`current_time`
where
m.old = 0;
-- add any missing numbers
insert into
things_master (number, last_updated, old)
Select
number, `current_time`, 0
From
things_temp t
Where
not exists (
select
'x'
from
things_master m
where
t.number = m.number and
m.old = 0
);
commit transaction;

MySQL: Split comma separated list into multiple rows

I have an unnormalized table with a column containing a comma separated list that is a foreign key to another table:
+----------+-------------+ +--------------+-------+
| part_id | material | | material_id | name |
+----------+-------------+ +--------------+-------+
| 339 | 1.2mm;1.6mm | | 1 | 1.2mm |
| 970 | 1.6mm | | 2 | 1.6mm |
+----------+-------------+ +--------------+-------+
I want to read this data into a search engine that offers no procedural language.
So is there a way to either make a join on this column or run a query on this data that inserts appropriate entries into a new table?
The resulting data should look like this:
+---------+-------------+
| part_id | material_id |
+---------+-------------+
| 339 | 1 |
| 339 | 2 |
| 970 | 2 |
+---------+-------------+
I could think of a solution if the DBMS supported functions returning a table but MySQL apparently doesn't.
In MySQL this can be achieved as below
SELECT id, length FROM vehicles WHERE id IN ( 117, 148, 126)
+---------------+
| id | length |
+---------------+
| 117 | 25 |
| 126 | 8 |
| 148 | 10 |
+---------------+
SELECT id,vehicle_ids FROM load_plan_configs WHERE load_plan_configs.id =42
+---------------------+
| id | vehicle_ids |
+---------------------+
| 42 | 117, 148, 126 |
+---------------------+
Now to get the length of comma separated vehicle_ids use below query
Output
SELECT length
FROM vehicles, load_plan_configs
WHERE load_plan_configs.id = 42 AND FIND_IN_SET(
vehicles.id, load_plan_configs.vehicle_ids
)
+---------+
| length |
+---------+
| 25 |
| 8 |
| 10 |
+---------+
For more info visit http://amitbrothers.blogspot.in/2014/03/mysql-split-comma-separated-list-into.html
I've answered two similar questions in as many days but not had any responses so I guess people are put off by the use of the cursor but as it should be a one off process I personally dont think that matters.
As you stated MySQL doesnt support table return types yet so you have little option other than to loop the table and parse the material csv string and generate the appropriate rows for part and material.
The following posts may prove of interest:
split keywords for post php mysql
MySQL procedure to load data from staging table to other tables. Need to split up multivalue field in the process
Rgds
MySQL does not have temporary table reuse and functions do not return rows.
I can't find anything in Stack Overflow to convert string of csv integers into rows so I wrote my own in MySQL.
DELIMITER $$
DROP PROCEDURE IF EXISTS str_split $$
CREATE PROCEDURE str_split(IN str VARCHAR(4000),IN delim varchar(1))
begin
DECLARE delimIdx int default 0;
DECLARE charIdx int default 1;
DECLARE rest_str varchar(4000) default '';
DECLARE store_str varchar(4000) default '';
create TEMPORARY table IF NOT EXISTS ids as (select parent_item_id from list_field where 1=0);
truncate table ids;
set #rest_str = str;
set #delimIdx = LOCATE(delim,#rest_str);
set #charIdx = 1;
set #store_str = SUBSTRING(#rest_str,#charIdx,#delimIdx-1);
set #rest_str = SUBSTRING(#rest_str from #delimIdx+1);
if length(trim(#store_str)) = 0 then
set #store_str = #rest_str;
end if;
INSERT INTO ids
SELECT (#store_str + 0);
WHILE #delimIdx <> 0 DO
set #delimIdx = LOCATE(delim,#rest_str);
set #charIdx = 1;
set #store_str = SUBSTRING(#rest_str,#charIdx,#delimIdx-1);
set #rest_str = SUBSTRING(#rest_str from #delimIdx+1);
select #store_str;
if length(trim(#store_str)) = 0 then
set #store_str = #rest_str;
end if;
INSERT INTO ids(parent_item_id)
SELECT (#store_str + 0);
END WHILE;
select parent_item_id from ids;
end$$
DELIMITER ;
call str_split('1,2,10,13,14',',')
You will also need to cast to different types if you are not using ints.
You can also use REGEXP
SET #materialids=SELECT material FROM parttable where part_id=1;
SELECT * FROM material_id WHERE REGEXP CONCAT('^',#materialids,'$');
This will help if you want to get just one part. Not the whole table of course

Getting limited amount of records from hierarchical data

Let's say I have 3 tables (significant columns only)
Category (catId key, parentCatId)
Category_Hierarchy (catId key, parentTrail, catLevel)
Product (prodId key, catId, createdOn)
There's a reason for having a separate Category_Hierarchy table, because I'm using triggers on Category table that populate it, because MySql triggers work as they do and I can't populate columns on the same table inside triggers if I would like to use auto_increment values. For the sake of this problem this is irrelevant. These two tables are 1:1 anyway.
Category table could be:
+-------+-------------+
| catId | parentCatId |
+-------+-------------+
| 1 | NULL |
| 2 | 1 |
| 3 | 2 |
| 4 | 3 |
| 5 | 3 |
| 6 | 4 |
| ... | ... |
+-------+-------------+
Category_Hierarchy
+-------+-------------+----------+
| catId | parentTrail | catLevel |
+-------+-------------+----------+
| 1 | 1/ | 0 |
| 2 | 1/2/ | 1 |
| 3 | 1/2/3/ | 2 |
| 4 | 1/2/3/4/ | 3 |
| 5 | 1/2/3/5/ | 3 |
| 6 | 1/2/3/4/6/ | 4 |
| ... | ... | ... |
+-------+-------------+----------+
Product
+--------+-------+---------------------+
| prodId | catId | createdOn |
+--------+-------+---------------------+
| 1 | 4 | 2010-02-03 12:09:24 |
| 2 | 4 | 2010-02-03 12:09:29 |
| 3 | 3 | 2010-02-03 12:09:36 |
| 4 | 1 | 2010-02-03 12:09:39 |
| 5 | 3 | 2010-02-03 12:09:50 |
| ... | ... | ... |
+--------+-------+---------------------+
Category_Hierarchy makes it simple to get category subordinate trees like this:
select c.*
from Category c
join Category_Hierarchy h
on (h.catId = c.catId)
where h.parentTrail like '1/2/3/%'
Which would return complete subordinate tree of category 3 (that is below 2, that is below 1 which is root category) including subordinate tree root node. Excluding root node is just one more where condition.
The problem
I would like to write a stored procedure:
create procedure GetLatestProductsFromSubCategories(in catId int)
begin
/* return 10 latest products from each */
/* catId subcategory subordinate tree */
end;
This means if a certain category had 3 direct sub categories (with whatever number of nodes underneath) I would get 30 results (10 from each subordinate tree). If it had 5 sub categories I'd get 50 results.
What would be the best/fastest/most efficient way to do this? If possible I'd like to avoid cursors unless they'd work faster compared to any other solution as well as prepared statements, because this would be one of the most frequent calls to DB.
Edit
Since a picture tells 1000 words I'll try to better explain what I want using an image. Below image shows category tree. Each of these nodes can have an arbitrary number of products related to them. Products are not included in the picture.
So if I'd execute this call:
call GetLatestProductsFromSubCategories(1);
I'd like to effectively get 30 products:
10 latest products from the whole orange subtree
10 latest products from the whole blue subtree and
10 latest products from the whole green subtree
I don't want to get 10 latest products from each node under catId=1 node which would mean 320 products.
Final Solution
This solution has O(n) performance:
CREATE PROCEDURE foo(IN in_catId INT)
BEGIN
DECLARE done BOOLEAN DEFAULT FALSE;
DECLARE first_iteration BOOLEAN DEFAULT TRUE;
DECLARE current VARCHAR(255);
DECLARE categories CURSOR FOR
SELECT parentTrail
FROM category
JOIN category_hierarchy USING (catId)
WHERE parentCatId = in_catId;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = TRUE;
SET #query := '';
OPEN categories;
category_loop: LOOP
FETCH categories INTO current;
IF `done` THEN LEAVE category_loop; END IF;
IF first_iteration = TRUE THEN
SET first_iteration = FALSE;
ELSE
SET #query = CONCAT(#query, " UNION ALL ");
END IF;
SET #query = CONCAT(#query, "(SELECT product.* FROM product JOIN category_hierarchy USING (catId) WHERE parentTrail LIKE CONCAT('",current,"','%') ORDER BY createdOn DESC LIMIT 10)");
END LOOP category_loop;
CLOSE categories;
IF #query <> '' THEN
PREPARE stmt FROM #query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END
Edit
Due to the latest clarification, this solution was simply edited to simplify the categories cursor query.
Note: Make the VARCHAR on line 5 the appropriate size based on your parentTrail column.