I'm having a problem with a cursor fetch loop in a mysql stored procedure. My stored procedure runs a reordering process which works just fine until the last record of the sort where the order numbering skips a single digit. For example, if I have 10 records and the sort ordering procedure starts at 1, all digits from 1 to 10 should be shown in the resulting records. However, my stored procedure skips the last count, 10 in the case above, and renumbers the final record 11, so the count goes from 9 to 11. This is the case regardless of the number of records involved.
The procedure's logic is fairly simple:
I have a table that holds product type records, with a sort_order column that is used to reorder the records based on usage during a regular batch cycle.
CREATE TABLE `PRODUCT_TYPE` (
`PRODUCT_TYPE_ID` int(11) NOT NULL AUTO_INCREMENT,
`PRODUCT_TYPE_NAME` varchar(45) NOT NULL,
`PRODUCT_CATEGORY_ID` int(11) DEFAULT NULL,
`LIFESPAN_MONTHS` int(11) DEFAULT NULL,
`USER_ID` int(11) DEFAULT NULL,
`UPDATED_BY` int(11) DEFAULT NULL,
`UPDATED_DATE` datetime DEFAULT NULL,
`CREATED_DATE` datetime DEFAULT NULL,
`CREATED_BY` int(11) DEFAULT NULL,
`REVIEWED` bit(1) NOT NULL DEFAULT b'0',
`SORT_ORDER` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`PRODUCT_TYPE_ID`),
KEY `fk_PRODUCT_TYPE_PRODUCT_CATEGORY1_idx` (`PRODUCT_CATEGORY_ID`),
KEY `fk_PRODUCT_TYPE_USERS1_idx` (`USER_ID`),
KEY `fk_PRODUCT_TYPE_USERS2_idx` (`UPDATED_BY`),
KEY `fk_PRODUCT_TYPE_USERS3_idx` (`CREATED_BY`),
CONSTRAINT `fk_PRODUCT_TYPE_PRODUCT_CATEGORY1` FOREIGN KEY (`PRODUCT_CATEGORY_ID`) REFERENCES `PRODUCT_CATEGORY` (`PRODUCT_CATEGORY_ID`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `fk_PRODUCT_TYPE_USERS1` FOREIGN KEY (`USER_ID`) REFERENCES `USERS` (`USER_ID`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `fk_PRODUCT_TYPE_USERS2` FOREIGN KEY (`UPDATED_BY`) REFERENCES `USERS` (`USER_ID`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `fk_PRODUCT_TYPE_USERS3` FOREIGN KEY (`CREATED_BY`) REFERENCES `USERS` (`USER_ID`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=latin1;
I run the following stored procedure on a nightly basis to reorder the product type records based on the number of references to each type using the sort_order column to record the order.
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `REORDER_MANUFACTURERS`()
BEGIN
DECLARE DONE BOOL;
DECLARE MID INT;
DECLARE MNAME VARCHAR(255);
DECLARE USES INT;
DECLARE SORT_ORDER_COUNTER INT;
DECLARE CUR CURSOR FOR SELECT M.MANUFACTURER_ID, M.MANUFACTURER_NAME, COUNT(U.UNIT_ID) AS USES
FROM MANUFACTURERS M LEFT JOIN mydb.UNITS U ON M.MANUFACTURER_ID = U.MANUFACTURER_ID
GROUP BY M.MANUFACTURER_ID, M.MANUFACTURER_NAME
ORDER BY USES DESC, MANUFACTURER_NAME;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET DONE = TRUE;
SET SORT_ORDER_COUNTER = 0;
OPEN CUR;
READ_LOOP: LOOP
FETCH CUR INTO MID, MNAME, USES;
UPDATE MANUFACTURERS SET SORT_ORDER = SORT_ORDER_COUNTER WHERE MANUFACTURER_ID = MID;
IF DONE THEN
LEAVE READ_LOOP;
END IF;
SET SORT_ORDER_COUNTER = SORT_ORDER_COUNTER + 1;
END LOOP;
CLOSE CUR;
END
For the life of me, I can't find a problem with this logic that would cause the count to skip a beat. Any help would be appreciated.
Change:
FETCH CUR INTO MID, MNAME, USES;
UPDATE MANUFACTURERS SET SORT_ORDER = SORT_ORDER_COUNTER
WHERE MANUFACTURER_ID = MID;
IF DONE THEN
LEAVE READ_LOOP;
END IF;
To:
FETCH CUR INTO MID, MNAME, USES;
IF DONE THEN
LEAVE READ_LOOP;
END IF;
UPDATE MANUFACTURERS SET SORT_ORDER = SORT_ORDER_COUNTER
WHERE MANUFACTURER_ID = MID;
It is because:
If you FETCH past the last row in the result set, the values of the target fields or variables are indeterminate and the NOTFOUND attribute returns TRUE.
Refer to: (This is on Oracle cursors, but applicable to others as well):
Oracle: Fetch Statement
Related
For class I am to make a stored procedure named add_parts that will be used to add records to the parts_service table. It has 2 input parameters that match the part_id and parts_qty field/data types from that table. I have to also declare a variable that will hold onto the service_id value from another table services and set it to MAX using a select into. Here is the code I have:
/*Set DB context and drop the procedure if it exists (2 lines)*/
USE cf;
DROP PROCEDURE IF EXISTS add_parts;
/*Delimiter statement (1 line)*/
DELIMITER //
/*Create procedure statement & 2 int parameters.*/
CREATE PROCEDURE add_parts (
IN part_id_param INT,
IN parts_qty_param INT
)
BEGIN
/*Declare the internal int variable (1 line)*/
DECLARE service_id_var INT;
/*Declare sql_error variable, Declare continue handler, set sql_error variable (3 lines)*/
DECLARE sql_error TINYINT DEFAULT FALSE;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
SET sql_error = TRUE;
/*Set the internal int variable */
/*From the appropriate table (3 lines)*/
SELECT MAX(service_id) INTO service_id_var FROM service;
START TRANSACTION;
/*Insert into parts_service values statement */
INSERT INTO parts_service VALUES(DEFAULT, part_id, part_qty, service_id_var);
/* If/else conditional; if sql_error variable is false, commit the transaction and select the appropriate message*/
/* else, rollback the transaction and select the appropriate message */
IF sql_error = FALSE THEN
COMMIT;
SELECT 'Record was added!' AS Message;
ELSE
ROLLBACK;
SELECT 'The part id you entered does not exist' AS Message;
END IF;
END//
CALL add_parts(15,7);
The CALL in the last line should return "Record was added!" but instead returns "The part id you entered does not exist". How do I tell if I am doing this correctly?
CREATE TABLE parts_service (
parts_service_id int(11) NOT NULL AUTO_INCREMENT,
part_id int(11) NOT NULL,
service_id int(11) NOT NULL,
parts_qty int(11) DEFAULT NULL,
PRIMARY KEY (parts_service_id),
KEY ps_fk_parts (part_id),
KEY ps_fk_service (service_id),
CONSTRAINT ps_fk_parts FOREIGN KEY (part_id) REFERENCES parts (part_id),
CONSTRAINT ps_fk_service FOREIGN KEY (service_id) REFERENCES service (service_id)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Aha, these two are out of order:
service_id int(11) NOT NULL,
parts_qty int(11) DEFAULT NULL,
The query (with constraint on the service table) must be failing when trying to insert the parts_qty to the service_id.
Switch your insert to this:
INSERT INTO parts_service VALUES(DEFAULT, part_id, service_id_var, part_qty);
Alternatively, and this is probably better, you could explicitly set the columns your values are to be placed into:
INSERT INTO parts_service
(`parts_service_id`, `part_id`, `parts_qty`, `service_id`)
VALUES
(DEFAULT, part_id, part_qty, service_id_var);
Challenge:
Create a method to set "auto_increment" values for tables in a non-sequential way.
The goal is to override the "auto_increment" mechanism and allow the function "LAST_INSERT_ID()" to continue working as expected (returning an INT), so that no changes are needed in software side.
My Solution
The method I found is based on an auxiliary table (unique_id), that stores values available to be assigned. Values are then selected randomly, and removed from the tables as used. When the table gets empty, a new set of ID's is created.
This example is working as expected, but with one problem.
Tables for the demo:
CREATE TABLE `unique_id` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
AUTO_INCREMENT=100;
CREATE TABLE `test_unique_id` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1;
Defined a stored procedure and a function:
DELIMITER $$
DROP PROCEDURE IF EXISTS `UNIQUE_ID_REFILL`$$
CREATE PROCEDURE UNIQUE_ID_REFILL()
BEGIN
DECLARE a INT Default 0 ;
simple_loop: LOOP
SET a=a+1;
INSERT INTO unique_id (id) values(null);
IF a=100 THEN
LEAVE simple_loop;
END IF;
END LOOP simple_loop;
END $$
DROP FUNCTION IF EXISTS `UNIQUE_ID_GET`$$
CREATE FUNCTION UNIQUE_ID_GET()
RETURNS INT(11)
MODIFIES SQL DATA
BEGIN
DECLARE new_id INT(11);
DECLARE unique_id_count INT(11);
SET new_id = 0;
SELECT COUNT(*) INTO unique_id_count FROM unique_id;
IF unique_id_count=0 THEN
CALL UNIQUE_ID_REFILL();
END IF;
SELECT id INTO new_id FROM unique_id ORDER BY RAND() LIMIT 1;
DELETE FROM unique_id WHERE id = new_id;
RETURN new_id;
END $$
Created a Trigger on the destination table (test_unique_id):
CREATE TRIGGER test_unique_id__unique_id BEFORE INSERT ON test_unique_id
FOR EACH ROW
SET NEW.id = UNIQUE_ID_GET();
The solution is getting the random ID's as expected:
INSERT INTO test_unique_id(name) VALUES ('A'),('B'),('C');
Creates the rows:
id name
154 'A'
129 'B'
173 'C'
The Problem
The main problem is that LAST_INSERT_ID() stops working... and the software side is broken:
SELECT LAST_INSERT_ID();
0
Any ideas on how to solve this problem? or any other different approach to the challenge?
Thank you very much.
I have a MySQL table, where one of the fields contains a textual description (~5-200 words).
For example Reviews:
Rev_id Place_id Stars Category Text
1 12 3 Food Nice food but a bad dirty place.
2 31 4 Sport Not bad, they have everything.
3 55 1 Bar Poor place,bad audience.
I'd like to make some word count analysis, such as general word frequency count (how many times each of the words has appeared) or top-K words per Category.
In the example:
word count
bad 3
place 2
...
Is there a way to do it solely in MySQL without involving programming languages?
My logic for this question is: extract all words and count them!
So, create a table like your stored data:
CREATE TABLE `tbltest` (
`Rev_id` int(11) NOT NULL AUTO_INCREMENT,
`place_id` int(11) DEFAULT NULL,
`Stars` int(11) DEFAULT NULL,
`Category` varchar(45) DEFAULT NULL,
`Text` varchar(255) DEFAULT NULL,
PRIMARY KEY (`Rev_id`),
UNIQUE KEY `id_UNIQUE` (`Rev_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
And creating a table for words:
CREATE TABLE `counting` (
`word` varchar(45) NOT NULL,
`counts` int(11) DEFAULT NULL,
PRIMARY KEY (`word`),
UNIQUE KEY `word_UNIQUE` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Now, create the MySQL Stored Procedure for splitting sentences and counting words:
drop procedure if exists sentence_words;
delimiter #
create procedure sentence_words(IN Cat VARCHAR(45))
begin
declare w_max int unsigned default 1;
declare w_counter int unsigned default 0;
declare done int unsigned default 0;
declare sentence varchar(255) default null;
declare cur cursor for select `text` from `tbltest` where `Category` = Cat;
declare continue handler for not found set done=1;
set done=0;
open cur;
myloop: loop
fetch cur into sentence;
if done = 1 then leave myloop; end if;
-- refine sentence!
set sentence = replace(replace(replace(replace(
sentence
,'.',' '),'!',' '),',',' '),';',' ');
set sentence = replace(trim(sentence),' ',' ');
set w_max = length(sentence)-length(replace(sentence,' ',''))+1;
start transaction;
while w_counter < w_max do
insert into `counting`(counts,word) values
(1, substring_index( substring_index(
sentence,' ',w_counter+1) ,' ',-1)
)
ON DUPLICATE KEY UPDATE counts=counts+1;
set w_counter=w_counter+1;
end while;
commit;
end loop;
close cur;
end #
delimiter ;
Finally, you can call the procedure and find words and counts in counting table. If you need each category word counts separated, remember to truncate or backup counting table before calling procedure for each Category.
truncate `counting`;
call sentence_words('Bar');
select * from `counting` order by counts desc; -- ? where length(word)>2
-- words | counts --
'audience', '1'
'bad', '1'
'place', '1'
'Poor', '1'
I'm trying to write a mySQL procedure with a cursor to calculate a fare. I pass in the stationid's then I figure out what zone they are in. The fare is a set value of $1 and an additional $0.20 for each zone traveled in.
The code I have so far runs but there is a problem with the cursor not fetching the values into the variables.
Any help would be greatly appreciated. Thanks
Tables:
DROP DATABASE IF EXISTS luasSystem;
CREATE DATABASE luasSystem;
USE luasSystem;
CREATE TABLE IF NOT EXISTS line
(
line_id INT NOT NULL AUTO_INCREMENT,
Line_colour CHAR(10) NOT NULL,
PRIMARY KEY (line_id)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS zone
(
zone_id INT NOT NULL AUTO_INCREMENT,
zone_name VARCHAR(20) NOT NULL,
line INT NOT NULL,
PRIMARY KEY (zone_id),
FOREIGN KEY (line) REFERENCES line(line_id) ON UPDATE CASCADE ON DELETE RESTRICT
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS station
(
station_id INT NOT NULL AUTO_INCREMENT,
station_name CHAR(20) NOT NULL,
service CHAR(20),
line INT NOT NULL,
zone INT NOT NULL,
PRIMARY KEY (station_id),
FOREIGN KEY (line) REFERENCES line(line_id) ON UPDATE CASCADE ON DELETE RESTRICT,
FOREIGN KEY (zone) REFERENCES zone(zone_id) ON UPDATE CASCADE ON DELETE RESTRICT
) ENGINE=InnoDB;
Stored Procedure:
DROP PROCEDURE IF EXISTS calculateFare;
DELIMITER //
CREATE PROCEDURE calculateFare
(
IN stationid1 INT, IN stationid2 INT
)
BEGIN
DECLARE zoneNum1 INT;
DECLARE zoneNum2 INT;
DECLARE num INT;
DECLARE fare DOUBLE;
DECLARE tableEnd BOOLEAN;
DECLARE zoneCur CURSOR FOR
SELECT zone, zone FROM station
WHERE station_name = stationid1 AND station_name = stationid2;
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET tableEnd = TRUE;
OPEN zoneCur;
the_loop: LOOP
FETCH zoneCur
INTO zoneNum1, zoneNum2;
IF tableEnd THEN
CLOSE zoneCur;
LEAVE the_loop;
END IF;
SET fare = 1;
SET num = 0;
IF zoneNum1 < zoneNum2 THEN
SET num = zoneNum2 - zoneNum1;
ELSEIF zoneNum1 > zoneNum2 THEN
SET num = zoneNum1 - zoneNum2;
END IF;
SET fare = (num * 0.20) + 1;
SELECT fare;
END LOOP the_loop;
END //
DELIMITER ;
CAll calculateFare(3,5);
Wouldn't this be easier of you used decimal values instead of integers? Suggest you look at : http://dev.mysql.com/doc/refman/5.0/en/fixed-point-types.html
Within a BPM web application, I have a field for an invoice # on a particular page but I need for it to be auto generated every time a user attaches an invoice and views that page. That number must be unique and preferably auto-incremented. A value for the invoice # field can be displayed by querying from a table from an external MYSQL database. So every time a user lands on that particular page, a SELECT query statement can be fired.
On MYSQL end, how would I set this up? So basically, I would like to setup a query for that invoice # field where it will for run a query for example,
SELECT invoice_num FROM invoice_generator
and every time this query runs, it would return the next incremented number.
You can use mysql trigger concept here....
I have added one example here...
It will be very usefull for u (see this link also :http://www.freemindsystems.com/mysql-triggers-a-practical-example/)
CREATE TABLE `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`price` int(20) NOT NULL DEFAULT '0',
`other` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `products_name_idx` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `freetags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tag` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `freetagged_objects` (
`tag_id` int(20) NOT NULL DEFAULT '0',
`object_id` int(20) NOT NULL DEFAULT '0',
`tagged_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`module` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`tag_id`, `object_id`),
KEY `freetagged_objects_tag_id_object_id_idx` (`tag_id`, `object_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT_PRODUCTS_TAGS
DELIMITER ||
DROP TRIGGER IF EXISTS insert_products_tags;
||
DELIMITER ##
CREATE TRIGGER insert_products_tags AFTER INSERT ON products
FOR EACH ROW
BEGIN
DECLARE current_id integer;
DECLARE tag_id integer;
DECLARE next integer;
DECLARE tag_field varchar(255);
DECLARE next_sep integer;
DECLARE current_tag varchar(255);
DECLARE right_tag varchar(255);
-- We use the field other as comma-separated tag_field
SET tag_field = NEW.other;
-- Check for empty tags
IF (CHAR_LENGTH(tag_field) <> 0) THEN
-- Loop until no more ocurrencies
set next = 1;
WHILE next = 1 DO
-- Find possition of the next ","
SELECT INSTR(tag_field, ',') INTO next_sep;
IF (next_sep > 0) THEN
SELECT SUBSTR(tag_field, 1, next_sep - 1) INTO current_tag;
SELECT SUBSTR(tag_field, next_sep + 1, CHAR_LENGTH(tag_field)) INTO right_tag;
set tag_field = right_tag;
ELSE
set next = 0;
set current_tag = tag_field;
END IF;
-- Drop spaces between comas
SELECT TRIM(current_tag) INTO current_tag;
-- Insert the tag if not already present
IF (NOT EXISTS (SELECT tag FROM freetags WHERE tag = current_tag)) THEN
-- Insert the tag
INSERT INTO freetags (tag) values (current_tag);
SELECT LAST_INSERT_ID() INTO tag_id;
ELSE
-- Or get the id
SELECT id FROM freetags WHERE tag = current_tag INTO tag_id;
END IF;
-- Link the object tagged with the tag
INSERT INTO freetagged_objects
(tag_id, object_id, module)
values
(tag_id, NEW.id, 'products');
END WHILE;
END IF;
END;
##
Now If you execute an insert on products table:
INSERT INTO PRODUCTS
(name, price, other)
values
("product1", 2, "tag1, tag2,tag3 , tag 4");