problem with updating table in mysql procedures - mysql

I am creating a procedure that updates any invoice with unpaid status for more than 30 days to 'OVERDUE'. However with my current code whenever I am calling this procedure even the invoice which does not have UNPAID status gets its status updated to OVERDUE.
CREATE procedure sync_invoice()
begin
declare dDate date;
declare stat varchar(20);
declare d_finished int default 0;
declare d_array cursor for
select DATEISSUED, STATUS from invoice;
declare continue handler for not found set d_finished = 1;
open d_array;
repeat
fetch d_array into dDate, stat;
if (datediff(current_date(), dDate)> 30 )then
update invoice
set STATUS = 'OVERDUE'
where stat = 'UNPAID';
end if;
until d_finished
end repeat;
close d_array;
-- code
end
//
here is the invoice table
CREATE TABLE IF NOT EXISTS `invoice` (
`INVOICENO` INT(11) NOT NULL AUTO_INCREMENT,
`CAMPAIGN_NO` INT(11) NOT NULL,
`DATEISSUED` DATE NULL DEFAULT NULL,
`DATEPAID` DATE NULL DEFAULT NULL,
`BALANCEOWING` INT(11) NULL DEFAULT NULL,
`STATUS` VARCHAR(20) NULL DEFAULT NULL,
PRIMARY KEY (`INVOICENO`, `CAMPAIGN_NO`),
INDEX `FK_INVOICE_SENDS2_CAMPAIGN_idx` (`CAMPAIGN_NO` ASC),
CONSTRAINT `FK_INVOICE_SENDS2_CAMPAIGN`
FOREIGN KEY (`CAMPAIGN_NO`)
REFERENCES `campaign` (`CAMPAIGN_NO`)
ON DELETE RESTRICT
ON UPDATE RESTRICT)
AUTO_INCREMENT = 6;

Ok so there are a few wrong things here:
CREATE procedure sync_invoice()
begin
declare dDate date;
declare stat varchar(20);
declare d_finished int default 0;
declare d_array cursor for
select DATEISSUED, STATUS from invoice;
declare continue handler for not found set d_finished = 1;
open d_array;
repeat
-- The loop will be executed for each entry into invoice
fetch d_array into dDate, stat;
-- For each, if it's old enough, regardless of the status...
if (datediff(current_date(), dDate)> 30 )then
-- This SQL request, which is called over the whole invoice table is executed
update invoice
set STATUS = 'OVERDUE' -- Effectively setting the status to overdue
where stat = 'UNPAID'; -- for each entry, so far that the one inspected is UNPAID
end if;
until d_finished
end repeat;
close d_array;
-- code
end
So basically, with this code, you'll always set every invoice to OVERDUE so long that there is a single, 30+ days old UNPAID invoice in the table.
Some way around this include replacing the whole content of the procedure by the single appropriate UPDATE call:
Update invoice Set STATUS = 'OVERDUE' Where datediff(current_date(), DATEISSUED) > 30 And STATUS = 'UNPAID';
Edit: To clarify, you could 'fix' the whole thing by replacing 'stat' at line where stat = 'UNPAID'; by 'STATUS', but you would still be executing way too many instructions without any reason to, effectively iterating through your whole invoice table as many time as you have 30+ days old invoices.
But in practice, SQL is a quite powerful language in which you might not need to loop yourself for tasks working with a single entry in a table. Statements such as UPDATE ... WHERE ... do it for you. c:
Loops and cursor, then, become useful when you have to do process that need to consider multiple entry at once.

Related

Boolean variable always assuming 1 in select...into - MySQL

I'm doing a stored procedure to update a table and that table has a boolean named: "Finished", as a field. This field informs us if a game is finished. In my problem it makes sense to be able to set something as finished before the expiration date so, because of that, I'm checking if the row to update has the "Finished" field as true or if the expiration date has passed.
SET #isFinished=0;
SELECT Finished INTO #isFinished FROM game WHERE ID = gameID;
-- gameID comes as a parameter
-- date comes as a parameter as well
IF DATEDIFF(STR_TO_DATE(date, "%Y-%m-%d") , CURDATE()) < 0 OR #isFinished<>0 THEN
select CONCAT("Game can't be updated because it's already finished. Days missing:",DATEDIFF( STR_TO_DATE(date, "%Y-%m-%d"), CURDATE() )," and #finished=", #isFinished, ", game=",gameID)
into #msg;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = #msg;
END IF;
The problem is that when I try to update an unfinished game it is getting into the if and throwing the error message:
"sqlMessage: 'Game can\'t be updated because it\'s already finished. Days missing:337 and #finished=1, game=2'".
quick note: The variable #isFinished is never used but in this block of code.
I've always assured that the value of "Finished" was 0 before I tested it and yet it keeps selecting it as 1 and, because of that, getting into the if.
I thought it could be from the select...into so I tried it out of the stored procedure (literally copy paste, just changed the "gameID" to the actual ID that I'm using) and it worked perfectly.
SELECT Finished INTO #isFinished FROM game WHERE ID = 2;
SELECT #isFinished
After this, I don't know what more can I check. If anyone could help I'd be thankful.
Isolated test:
create database test;
use test;
create table Tournament(
ID int(10) not null unique auto_increment,
Name varchar(250) not null,
Start_Date date not null,
End_Date date not null,
Primary key(ID)
);
create table Game(
ID int(10) not null unique auto_increment,
Tournament_ID int(10),
Date date,
Finished boolean,
Foreign Key(Tournament_ID) references Tournament(ID) ON DELETE CASCADE,
Primary Key(ID)
);
INSERT INTO Tournament VALUES(NULL, "tournament1", str_to_date("2020-06-01","%Y-%m-%d"), str_to_date("2020-07-01","%Y-%m-%d"));
INSERT INTO Game VALUES(NULL, 1, str_to_date("2020-06-02","%Y-%m-%d"), 0);
DELIMITER $$
DROP PROCEDURE IF EXISTS `UpdateGame`$$
CREATE PROCEDURE `UpdateGame`(IN gameID int, date varchar(10), finished boolean)
BEGIN
SET #isFinished=0;
SELECT Finished INTO #isFinished FROM game WHERE ID = gameID;
IF DATEDIFF(STR_TO_DATE(date, "%Y-%m-%d") , CURDATE()) < 0 OR #isFinished<>0 THEN
select CONCAT("Game can't be updated because it's already finished. Days missing:",DATEDIFF( STR_TO_DATE(date, "%Y-%m-%d"), CURDATE() )," and #finished=", #isFinished, ", game=",gameID)
into #msg;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = #msg;
END IF;
UPDATE game SET Date=STR_TO_DATE(date, "%Y-%m-%d"), Finished=finished WHERE ID=gameID;
END$$
call UpdateGame(1,"2020-06-03",1);
SELECT * FROM game;
SELECT Finished INTO #isFinished FROM game WHERE ID = 1;
SELECT #isFinished;
You have IN param Finished so inside your stored procedure, when querying the table, in fact you querying the IN param.
So you need to delete/rename the Finished IN param or write your internal select query as:
SELECT `game`.`Finished`
INTO #isFinished
FROM game
WHERE ID = gameID;
P.S. People usually have some kind of prefixes on input params to distinguish them visually inside the stored procedure from tables, columns of local variables.

MySQL innodb Procedure only active once(call serveral times) in a Transaction

Firstly,I made a MYSQL Procedure to increase a number[+1 every time](there is no TRANSACTION in it), and I called the Procedure n(n>1) times in a Spring Transaction and got a same number, and the number +1 finally(+n expected)
Secondly, I added TRANSACTION in Procedure and commit atfer +1, and got the same result as above;
Thirdly, I added #Transaction (rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) on the Method A(A calls Procedure), and call Method A serveral times in Method B, which is annotationed by #Transactional, then I got the same result as above;
Anyone help? can you give me a way to handle it?
Plus:
the table in MySQL
CREATE TABLE `SEQUENCE` (
`ID` bigint(10) NOT NULL ,
`COUNT` int(11) NOT NULL ,
`CUR_DATE` date NOT NULL ,
`READ_ME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`ID`)
);
the Procedure in MySQL
CREATE DEFINER="root"#"%" PROCEDURE "SEQUENCE_PROCEDURE"(IN _id bigint)
BEGIN
UPDATE `SEQUENCE` SET `COUNT`=-1,CUR_DATE=now() where `ID`=_id and TIMESTAMPDIFF(DAY,CUR_DATE,now())>0 and _id=1;
UPDATE `SEQUENCE` SET `COUNT`=-1,CUR_DATE=now() where `ID`=_id and TIMESTAMPDIFF(MONTH,CUR_DATE,now())>0 and _id=2;
UPDATE `SEQUENCE` SET `COUNT`=`COUNT`+1 where `ID`=_id;
SELECT * FROM `SEQUENCE` where `ID`=_id;
END
the SQL in mybatis
<select id="getSequence" parameterType="java.lang.Long" resultMap="baseResult" statementType="CALLABLE">
{call SEQUENCE_PROCEDURE(#{id,jdbcType=BIGINT,mode=IN})}
</select>
the Test in project
#Test
#Transactional
public void testSequence() {
System.out.println(sequenceService.getId(2L));
System.out.println(sequenceService.getId(2L));
System.out.println(sequenceService.getId(2L));
}
where
public String getId(Long id) {
Sequence sequence = sequenceMapper.getSequence(id);
String temp='000000000000'+sequence.getCount();
return temp.substring(temp.length-12);
}
the result of test
000000000000 000000000000 000000000000
expected result
000000000000 000000000001 000000000002
add START TRANSACTION and COMMIT in Procedure do not work!
In your procedure the lines 1 and 2 have specific queries to id 1 and 2 (why?) and double comparison to _id variable. Also the third query runs on another table COUNT, may be is an error.
I changed the procedure assuming the record already exists, and change the third query to SEQUENCE table:
CREATE PROCEDURE SEQUENCE_PROCEDURE(IN _id bigint)
BEGIN
UPDATE `SEQUENCE` SET `COUNT`=-1,CUR_DATE=now()
WHERE `ID`=_id and TIMESTAMPDIFF(DAY,CUR_DATE,now())>0;
UPDATE `SEQUENCE` SET `COUNT`=`COUNT`+1 where `ID`=_id;
SELECT * FROM `SEQUENCE` where `ID`=_id;
END

mysql optimize stored procedure insert

This is my first stored procedure so I am not sure I am doing this correctly. I have tried to optimize this as much as I can but still end up with a query timeout at 10 minutes of running. I really need this to scale even higher than what I am working with currently. Any assistance would be great.
I have a decent sized data set (108K rows) and one of the fields contains a comma delimited list (I wish the engineers hadn't done this). I need to break apart that field so each entry is on it's own row AND all other fields are assigned to that row as well. I have developed a stored procedure that loops through the table row by row then breaks apart the field and inserts it into a second table.
Here is the code I have used:
DROP TABLE IF EXISTS dwh_inventory.nas_share_temp;
CREATE TABLE dwh_inventory.nas_share_temp (
share_id int(11) NOT NULL,
fileShareId int(11) NOT NULL,
storageId int(11) NOT NULL,
identifier varchar(1024) NOT NULL,
name varchar(255) NOT NULL,
protocol enum('CIFS','NFS') NOT NULL,
ipInterfaces VARCHAR(100) NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;
DROP PROCEDURE IF EXISTS dwh_inventory.share_step;
DELIMITER $$
CREATE PROCEDURE dwh_inventory.share_step()
BEGIN
DECLARE n INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
DECLARE strLen INT DEFAULT 0;
DECLARE SubStrLen INT DEFAULT 0;
DECLARE ip VARCHAR(20);
SET autocommit = 0;
SELECT COUNT(*) FROM dwh_inventory.nas_share INTO n;
SET i=0;
WHILE i<n DO
SELECT id, fileShareId, storageId, identifier, name, protocol, ipInterfaces
INTO #share_id, #fileShareId, #storageId, #identifier, #name, #protocol, #ipInterfaces
FROM dwh_inventory.nas_share LIMIT i,1;
IF #ipInterfaces IS NULL THEN
SET #ipInterfaces = '';
END IF;
do_this:
LOOP
SET strLen = CHAR_LENGTH(#ipInterfaces);
SET ip = SUBSTRING_INDEX(#ipInterfaces, ',', 1);
INSERT INTO dwh_inventory.nas_share_temp
(share_id, fileShareId, storageId, identifier,name,protocol,ipInterfaces)
VALUES (#share_id,
#fileShareId,
#storageId,
#identifier,
#name,
#protocol,
ip
);
SET SubStrLen = CHAR_LENGTH(SUBSTRING_INDEX(#ipInterfaces, ',', 1)) + 2;
SET #ipInterfaces = MID(#ipInterfaces, SubStrLen, strLen);
IF #ipInterfaces = '' THEN
LEAVE do_this;
END IF;
END LOOP do_this;
COMMIT;
SET i = i + 1;
END WHILE;
SET autocommit = 1;
END;
$$
DELIMITER ;
CALL dwh_inventory.share_step();
Example of the data:
id,fileShareId,storageId,identifier,name,protocol,ipInterfaces
1325548,1128971,33309,/vol/vol0/:NFS,/vol/vol0/,NFS,"10.66.213.118,10.68.208.76"
1325549,1128991,33309,/vol/vol0/:NFS,/vol/vol0/,NFS,"10.66.213.119,10.68.208.77"
1325550,1128992,33325,/vol/aggr2_64_hs2032/EPS_ROOT/:NFS,/vol/aggr2_64_hs2032/EPS_ROOT/,NFS,10.17.124.10
1325551,1128993,33325,/vol/aggr2_64_hs2032/GCO_Report/:NFS,/vol/aggr2_64_hs2032/GCO_Report/,NFS,10.17.124.10
1325552,1128995,33325,/vol/aggr2_64_hs2032/PI/:NFS,/vol/aggr2_64_hs2032/PI/,NFS,10.17.124.10
1325553,1128996,33325,/vol/aggr2_64_hs2032/a/:NFS,/vol/aggr2_64_hs2032/a/,NFS,10.17.124.10
1325554,1128997,33325,/vol/aggr1_64_sapserv/:NFS,/vol/aggr1_64_sapserv/,NFS,147.204.2.13
1325555,1128999,33325,/vol/aggr2_64_hs2032/:NFS,/vol/aggr2_64_hs2032/,NFS,10.17.124.10
1325556,1129001,33325,/vol/aggr2_64_hs2032/central/:NFS,/vol/aggr2_64_hs2032/central/,NFS,10.17.124.10
1325557,1129004,33325,/vol/nsvfm0079b_E5V/db_clients/:NFS,/vol/nsvfm0079b_E5V/db_clients/,NFS,"10.21.188.161,10.70.151.93"
1325558,1129006,33325,/vol/aggr2_64_hs2032/istrans/:NFS,/vol/aggr2_64_hs2032/istrans/,NFS,10.17.124.10
1325559,1129008,33325,/vol/nsvfm0017_DEWDFGLD00603/:NFS,/vol/nsvfm0017_DEWDFGLD00603/,NFS,"10.21.188.115,10.70.151.138"
1325560,1129009,33325,/vol/nsvfm0017_vol0/:NFS,/vol/nsvfm0017_vol0/,NFS,"10.21.188.115,10.70.151.138"
1325561,1129011,33325,/vol/nsvfm0017a_ls2278/:NFS,/vol/nsvfm0017a_ls2278/,NFS,"10.21.188.115,10.70.151.138"
1325562,1129015,33325,/vol/nsvfm0051passive_vol0/:NFS,/vol/nsvfm0051passive_vol0/,NFS,10.17.144.249
1325563,1129017,33325,/vol/nsvfm0053_vol0/:NFS,/vol/nsvfm0053_vol0/,NFS,"10.21.189.251,10.70.151.109"
InnoDB tables must have a PRIMARY KEY.
LIMIT i,1 will get slower and slower as you go through the table -- it hast to skip over i rows before finding the one you need.
Don't try to split comma-separated text in SQL; use a real language (PHP / Perl / etc). Or, as Lew suggests, write out that column, then use LOAD DATA to bring it into another table.
LIMIT should be preceded by an ORDER BY.

Obtaining the amount of time spent above a threshold value from time series data

Given a table like this:
`sensor` int(11)
`reading` decimal(5,2)
`timestamp` datetime
that is representing temperature data and logging an entry whenever a value changes, how would I go about finding the amount of time recorded above a given value?
So there may be a bunch of readings from, say, 16 up to 30, the requirement would be to find the amount of time spent above 16.
Two solutions
I propose two solutions:
one query, but I don't know if it is very efficient with a lot of data, because of the subquery;
one function.
Schema
I tested the query and the function with this table:
CREATE TABLE `temperature` (
`sensor` int(11) NOT NULL,
`timestamp` datetime NOT NULL,
`reading` decimal(5, 2) NOT NULL,
PRIMARY KEY (`sensor`,`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
And some data:
1;2014-09-18 17:00:00;15.0
1;2014-09-18 18:00:00;16.0
1;2014-09-18 19:00:00;15.0
Solution 1: a query
SELECT SUM(elapsed_time)
FROM (
SELECT
(UNIX_TIMESTAMP((
SELECT MIN(t2.timestamp)
FROM temperature t2
WHERE t1.sensor = t2.sensor AND t1.timestamp < t2.timestamp
)) - UNIX_TIMESTAMP(t1.timestamp))
AS elapsed_time
FROM temperature t1
WHERE t1.sensor = 1 AND t1.reading >= 16.0
) a;
Solution 2: a function
The function returns the amount of time above a value in seconds for a sensor.
It initializes the amount of time to 0. It then read all readings of the desired sensor. If the temperature is above your requirement, it adds the amount of time to go until the next reading to the sum. At last, it returns the amount of time above your requirement for the sensor.
DROP FUNCTION IF EXISTS getTotalTimeAbove;
DELIMITER $$
CREATE FUNCTION getTotalTimeAbove(sensor_id INTEGER, above DECIMAL(5, 2))
RETURNS INTEGER
BEGIN
DECLARE sum INTEGER DEFAULT 0;
DECLARE curr_time DATETIME;
DECLARE next_time DATETIME;
DECLARE curr_reading INTEGER;
DECLARE next_reading INTEGER;
DECLARE done INT DEFAULT FALSE;
DECLARE cur CURSOR FOR
SELECT `timestamp`, `reading`
FROM `temperature`
WHERE `sensor` = sensor_id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO next_time, next_reading;
IF done THEN
LEAVE read_loop;
END IF;
IF (curr_reading >= above) THEN
SET sum = sum + (UNIX_TIMESTAMP(next_time) - UNIX_TIMESTAMP(curr_time));
END IF;
SET curr_time = next_time;
SET curr_reading = next_reading;
END LOOP;
CLOSE cur;
RETURN sum;
END
$$
DELIMITER ;
The query and its result:
> SELECT getTotalTimeAbove(1, 16.0);
3600
Bonus
You can have the total amount of recorded time for a sensor with this query:
SELECT UNIX_TIMESTAMP(MAX(`timestamp`)) - UNIX_TIMESTAMP(MIN(`timestamp`))
FROM `temperature`
WHERE `sensor` = 1

Find node level in a tree

I have a tree (nested categories) stored as follows:
CREATE TABLE `category` (
`category_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`category_name` varchar(100) NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`category_id`),
UNIQUE KEY `category_name_UNIQUE` (`category_name`,`parent_id`),
KEY `fk_category_category1` (`parent_id`,`category_id`),
CONSTRAINT `fk_category_category1` FOREIGN KEY (`parent_id`) REFERENCES `category` (`category_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci
I need to feed my client-side language (PHP) with node information (child+parent) so it can build the tree in memory. I can tweak my PHP code but I think the operation would be way simpler if I could just retrieve the rows in such an order that all parents come before their children. I could do that if I knew the level for each node:
SELECT category_id, category_name, parent_id
FROM category
ORDER BY level -- No `level` column so far :(
Can you think of a way (view, stored routine or whatever...) to calculate the node level? I guess it's okay if it's not real-time and I need to recalculate it on node modification.
First update: progress so far
I've written these triggers based on feedback by Amarghosh:
DROP TRIGGER IF EXISTS `category_before_insert`;
DELIMITER //
CREATE TRIGGER `category_before_insert` BEFORE INSERT ON `category` FOR EACH ROW BEGIN
IF NEW.parent_id IS NULL THEN
SET #parent_level = 0;
ELSE
SELECT level INTO #parent_level
FROM category
WHERE category_id = NEW.parent_id;
END IF;
SET NEW.level = #parent_level+1;
END//
DELIMITER ;
DROP TRIGGER IF EXISTS `category_before_update`;
DELIMITER //
CREATE TRIGGER `category_before_update` BEFORE UPDATE ON `category` FOR EACH ROW BEGIN
IF NEW.parent_id IS NULL THEN
SET #parent_level = 0;
ELSE
SELECT level INTO #parent_level
FROM category
WHERE category_id = NEW.parent_id;
END IF;
SET NEW.level = #parent_level+1;
END//
DELIMITER ;
It seems to work fine for insertions and modifications. But it doesn't work for deletions: MySQL Server does not launch triggers when the rows are updated from ON UPDATE CASCADE foreign keys.
The first obvious idea is to write a new trigger for deletion; however, a trigger on table categories is not allowed to modify other rows on this same table:
DROP TRIGGER IF EXISTS `category_after_delete`;
DELIMITER //
CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
/*
* Raises an error, see below
*/
UPDATE category SET parent_id=NULL
WHERE parent_id = OLD.category_id;
END//
DELIMITER ;
Error:
Grid editing error: SQL Error (1442):
Can't update table 'category' in
stored function/trigger because it is
already used by statement which
invoked this stored function/trigger.
Second update: working solution (unless proved wrong)
My first attempt was pretty sensible but I found a problem I could not manage to solve: when you launch a series of operations from a trigger, MySQL will not allow to alter other lines from the same table. Since node deletions require to adjust the level of all descendants, I had hit a wall.
In the end, I changed the approach using code from here: rather than correcting individual levels when a node change, I have code to calculate all levels and I trigger it on every edit. Since it's a slow calculation and fetching data requires a very complex query, I cache it into a table. In my case, it's an acceptable solution since editions should be rare.
1. New table for cached levels:
CREATE TABLE `category_level` (
`category_id` int(10) NOT NULL,
`parent_id` int(10) DEFAULT NULL, -- Not really necesary
`level` int(10) NOT NULL,
PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci
2. Helper function to calculate levels
If I really got a grasp on how it works, it doesn't really return anything useful by itself. Instead, it stores stuff in session variables.
CREATE FUNCTION `category_connect_by_parent_eq_prior_id`(`value` INT) RETURNS int(10)
READS SQL DATA
BEGIN
DECLARE _id INT;
DECLARE _parent INT;
DECLARE _next INT;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET #category_id = NULL;
SET _parent = #category_id;
SET _id = -1;
IF #category_id IS NULL THEN
RETURN NULL;
END IF;
LOOP
SELECT MIN(category_id)
INTO #category_id
FROM category
WHERE COALESCE(parent_id, 0) = _parent
AND category_id > _id;
IF #category_id IS NOT NULL OR _parent = #start_with THEN
SET #level = #level + 1;
RETURN #category_id;
END IF;
SET #level := #level - 1;
SELECT category_id, COALESCE(parent_id, 0)
INTO _id, _parent
FROM category
WHERE category_id = _parent;
END LOOP;
END
3. Procedure to launch the recalculation process
It basically encapsulates the complex query that retrieves the levels aided by the helper function.
CREATE PROCEDURE `update_category_level`()
SQL SECURITY INVOKER
BEGIN
DELETE FROM category_level;
INSERT INTO category_level (category_id, parent_id, level)
SELECT hi.category_id, parent_id, level
FROM (
SELECT category_connect_by_parent_eq_prior_id(category_id) AS category_id, #level AS level
FROM (
SELECT #start_with := 0,
#category_id := #start_with,
#level := 0
) vars, category
WHERE #category_id IS NOT NULL
) ho
JOIN category hi ON hi.category_id = ho.category_id;
END
4. Triggers to keep cache table up-to-date
CREATE TRIGGER `category_after_insert` AFTER INSERT ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
CREATE TRIGGER `category_after_update` AFTER UPDATE ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
5. Known issues
It's pretty suboptimal if nodes are altered frequently.
MySQL does not allow transactions or table locking in triggers and procedures. You must take care of these details where you edit nodes.
There's an excellent series of articles here on Hierarchical Queries in MySQL that includes how to identify level, leaf nodes, loops in the hierarchy, etc.
If there won't be any cycles (if it'd always be a tree and not a graph), you can have a level field that is set to zero (top most) by default and a stored procedure that updates the level to (parent's level + 1) whenever you update the parent_id.
CREATE TRIGGER setLevelBeforeInsert BEFORE INSERT ON category
FOR EACH ROW
BEGIN
IF NEW.parent_id IS NOT NULL THEN
SELECT level INTO #pLevel FROM category WHERE id = NEW.parent_id;
SET NEW.level = #pLevel + 1;
ELSE
SET NEW.level = 0;
END IF;
END;
No level column so far :(
Hmm * shrug *
I'd just made this level field manually.
Say, like Materialized path, with just one update after insert and without all these fancy triggers.
A field which is going to be like 000000100000210000022 for the 3-rd level for example
so it can build the tree in memory.
if you going to get whole table into PHP, I see no problem here. A little recursive function can give you your nested arrays tree.
I can tweak my PHP code but I think the operation would be way simpler
well, well.
The code you've got so far doesn't seem to me "way simple" :)