Calculation in Nested Stored Procedure - mysql

I'm using a data sample looking like that :
| Parent | Child | Order | Quantity |
|:-----------|:--------:| -----:|---------:|
| Meal | Element 1| 10 |1 |
| Meal | Element 2| 20 |1 |
| Element 1 | Recipe 1 | 10 |0.2 |
| Element 1 | Recipe 2 | 20 |0.5 |
| Recipe 1 | Recipe 3 | 10 |0.1 |
| Recipe 3 | Raw Mat1 | 10 |1 |
| Element 2 | Recipe 4 | 10 |0.6 |
| Element 2 | Recipe 5 | 20 |0.3 |
| Recipe 4 | Recipe 6 | 10 |1.2 |
| Recipe 6 | Raw Mat2 | 10 |1.5 |
I know the unit weight of each Recipe and Raw material but I need to calculate the weight of Element1 and 2 as well as Meal parent, which depends from the calculated weight of both elements.
My result should look like that :
| Material | Order full | Quantity | U.Weight |
|:-----------|:------------:| --------:|---------:|
| Meal | / | 1 | TOBECALC | --0.2*1+0.5*1 + 0.6*1+0.3*1
| Element 1 | /10 | 1 | TOBECALC | --0.2*1+0.5*1
| Recipe 1 | /10/10 | 0.2 | 1 |
| Recipe 3 | /10/10/10 | 0.02 | 1 |
| Raw Mat1 | /10/10/10/10 | 0.02 | 1 |
| Recipe 2 | /10/20 | 0.5 | 1 |
| Element 2 | /20 | 1 | TOBECALC | --0.6*1+0.3*1
| Recipe 4 | /20/10 | 0.6 | 1 |
| Recipe 6 | /20/10/10 | 0.72 | 1 |
| Raw Mat2 | /20/10/10/10 | 1.08 | 1 |
| Recipe 5 | /20/20 | 0.3 | 1 |
Here is what I did so far :
A procedure PROC_INCO calling PROC_DETAILED_INCO calling PROC_UNIT_INCO that inserts lines in the final table. PROC_DETAILED_INCO is recursively called for each child and this is where I implemented the weight calculation.
DELIMITER $$
CREATE PROCEDURE PROC_UNIT_INCO
(IN ID_ARTICLE INTEGER,
IN ID_ROOT_EXE INTEGER,
IN T_ORDER_FULL VARCHAR(100),
IN QUANTITY FLOAT,
OUT U_WEIGHT FLOAT)
BEGIN
DECLARE WEIGHT FLOAT;
SELECT F_WEIGHT INTO WEIGHT FROM MATERIAL WHERE I_ID = ID_ARTICLE;
IF T_ORDER_FULL IS NULL THEN
SET T_ORDER_FULL = '/';
END IF;
IF TYPEART IN ('RawMat','Recipe') THEN
SET WEIGHT = WEIGHT;
SET U_WEIGHT = WEIGHT * QUANTITY;
ELSE
SET WEIGHT = 0;
SET U_WEIGHT = 0;
END IF;
INSERT INTO FINAL_RESULT (T_CODE, TT_ORDER_FULL, F_WEIGHT, F_QUANTITY, I_ID_ROOT_EXE)
VALUES(CODEART, T_ORDER_FULL, WEIGHT, QUANTITY, ID_ROOT_EXE);
END$$
CREATE PROCEDURE PROC_DETAILED_INCO
(IN ID_ARTICLE INTEGER,
IN ID_ROOT_EXE INTEGER,
IN T_ORDER_FULL VARCHAR(100),
IN QUANTITY FLOAT,
OUT CHILD_WEIGHT FLOAT)
BEGIN
DECLARE IS_DONE INT DEFAULT FALSE;
DECLARE ID_CHILD INTEGER;
DECLARE TEMP_ORDER INTEGER;
DECLARE NEW_ORDER_FULL VARCHAR(100);
DECLARE TYPE INTEGER;
DECLARE CHILD_CURSOR CURSOR FOR
SELECT A.CHILD, A.ORDER, QUANTITY * A.QUANTITY AS QUANTITY
FROM BOM A
INNER JOIN MATERIAL B ON A.CHILD = B.CODE
WHERE A.PARENT = ID_ARTICLE
ORDER BY A.ORDER;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_DONE = TRUE;
CALL PROC_UNIT_INCO (ID_ARTICLE, ID_ROOT_EXE, T_ORDER_FULL, QUANTITY, #U_WEIGHT);
SET #FIN_WEIGHT:= 0;
SELECT #U_WEIGHT INTO CHILD_WEIGHT;
OPEN FILS_CURSOR;
get_list: LOOP
FETCH FILS_CURSOR INTO ID_CHILD, TEMP_ORDER, QUANTITY, PERTE;
IF IS_DONE THEN
LEAVE get_list;
END IF;
SET NEW_ORDER_FULL = CONCAT(COALESCE(T_ORDER_FULL,''),'/',lpad(cast(TEMP_ORDER AS char(200)),5,'0'));
CALL PROC_DETAILED_INCO(ID_CHILD, ID_ROOT_EXE, NEW_ORDER_FULL, QUANTITY, #CHILD_WEIGHT);
--this is where i want to calculate my weight by summing my children's weight
SET #FIN_WEIGHT := #FIN_WEIGHT + #CHILD_WEIGHT;
END LOOP get_list;
IF TYPE NOT IN ('RawMat','Recipe') THEN
UPDATE FINAL_RESULT SET F_WEIGHT = #FIN_WEIGHT WHERE TT_ORDER_FULL = T_ORDER_FULL;
END IF;
CLOSE FILS_CURSOR;
END$$
CREATE PROCEDURE PROC_INCO
(IN CODE_ART VARCHAR(50))
BEGIN
DECLARE ID_ARTICLE INTEGER;
SELECT I_ID INTO ID_ARTICLE FROM MATERIAL WHERE T_CODE = CODE_ART;
DELETE FROM DETAILED_INCO WHERE I_ID_ROOT_EXE = ID_ARTICLE;
CALL PROC_DETAILED_INCO (ID_ARTICLE, ID_ARTICLE, NULL, 1, #CHILD_WEIGHT);
select
*
from final_result
order by tt_order_full;
END$$
DELIMITER ;
CALL PROC_INCO ('Meal');
Apologies if there is any mistakes, I simplified the procedure for better readability.
The weight calculation is not working as I want it... I do not really understand the calculated weight that I obtain but it seems that it resets each time the procedure enters in the loop and then fetches the latest child weight at the parent level.
Thanks in advance for your help on this,
Nico

Related

using a user variable in where clause without subquery

I have the following scenario:
+-----------+
| my_column |
+-----------+
| A |
| B |
| C |
| D |
| E |
+-----------+
I have simplified my_function bellow for this example;
DROP FUNCTION IF EXISTS my_function;
CREATE FUNCTION my_function(
phrase VARCHAR(255),
column_value VARCHAR(255)
)
RETURNS FLOAT(20,10)
READS SQL DATA
SQL SECURITY INVOKER
BEGIN
IF(column_value = 'A') THEN RETURN 1.0000000000;
ELSEIF(column_value = 'B') THEN RETURN 0.7500000000;
ELSEIF(column_value = 'C') THEN RETURN 0.7500000000;
ELSEIF(column_value = 'D') THEN RETURN 0.5000000000;
ELSEIF(column_value = 'E') THEN RETURN 0.0000000000;
END IF;
END;
Here is my main stored procedure:
DROP PROCEDURE IF EXISTS my_procedure;
CREATE PROCEDURE my_procedure(
IN phrase VARCHAR(255)
)
READS SQL DATA
SQL SECURITY INVOKER
BEGIN
SET #phrase = phrase;
SET #query = "
SELECT
my_column,
#score_var := my_function(?,my_column) as score,
#score_var
FROM my_table
ORDER BY score DESC;
";
PREPARE stmt FROM #query;
EXECUTE stmt USING #phrase;
DEALLOCATE PREPARE stmt;
END;
Now if I call my_procedure
call my_procedure('anything');
The result is:
+-----------+--------------+------------+
| my_column | score | #score_var |
+-----------+--------------+------------+
| A | 1.0000000000 | 1 |
| B | 0.7500000000 | 0.75 |
| C | 0.7500000000 | 0.75 |
| D | 0.5000000000 | 0.5 |
| E | 0.0000000000 | 0 |
+-----------+--------------+------------+
But if I add WHERE #score_var > 0.5 inside of the query in my_procedure, the result is:
+-----------+--------------+------------+
| my_column | score | #score_var |
+-----------+--------------+------------+
| A | 1.0000000000 | 1 |
| C | 0.7500000000 | 0.75 |
| E | 0.0000000000 | 0 |
+-----------+--------------+------------+
Expected result ´> 0.5´:
+-----------+--------------+------------+
| my_column | score | #score_var |
+-----------+--------------+------------+
| A | 1.0000000000 | 1 |
| B | 0.7500000000 | 0.75 |
| C | 0.7500000000 | 0.75 |
+-----------+--------------+------------+
I have seen some answers that use a subquery, but my question is: can (in this case) I not use a subquery?
Alternative approaches are also welcome.
When you read and write a user variable in the same statement, the behavior is documented as "undocumented". In other words the result is unpredictable unlesss you read and understand the source code of your MySQL version.
However - I think you are complicating things here unnecessarily. I don't see a reason to use a prepared statement, neither to use u user variable. Your procedure body could be just:
SELECT
my_column,
my_function(phrase, my_column) as score,
FROM my_table
HAVING score > 0.5
ORDER BY score DESC
Also your function could be written with less code duplicatin:
RETURN
CASE column_value
WHEN 'A' THEN 1.0000000000
WHEN 'B' THEN 0.7500000000
WHEN 'C' THEN 0.7500000000
WHEN 'D' THEN 0.5000000000
WHEN 'E' THEN 0.0000000000
END

SQL query - Fetch data from one column separated by comma and display it by row

I have a problem with fetching data separated by a comma. I want the
Here is my problem
Table
ID | TDNO | PREVIOUS_TD |
1 | 14 | 13,12,11 |
2 | 23 | 45,12 |
3 | 32 | 89 |
4 | 55 | NEW |
I want to have a result like this. Example when the user will choose 14 in TD the result should be like this:
ID | TD |
1 | 14 |
2 | 13 |
3 | 12 |
4 | 11 |
And when the user will choose 32 in TD the result should be like this:
ID | TD |
1 | 32 |
2 | 89 |
when the user will select 23 the result should be like this:
ID | TD |
1 | 23 |
2 | 45 |
3 | 12 |
how to achieve this?
You might try a stored procedure or function in your version of SQL. This is MySql pseudo code and could be very buggy. Some SQL flavors do not support returning tables:
create function returnCommaSepList (IN myId INT)
begin
--
-- is mtId in the source table?
SET #previousTD = (
select PREVIOUS_TD
from TheTable
where ID = myId
)
--
-- if the result is NULL then id was not in the table, return
if #previousTD IS NULL then return
--
-- create a temporary table
create table #temp (
id INT primary key autoincrement,
td int
)
--
-- add myId to the temp table
insert into #temp (td) values(myId)
--
-- prepare to do the string handling. Step through
-- #previousTD looking for commas
SET #startPos = 0
SET #commaPos = LOCATE(',', #previousTD, #startPos)
--
-- #commaPos will be NULL if the string is NULL
if #commaPos IS NULL then return
--
-- #commaPos will be 0 if there are no commas in the string
if #commaPos = 0 then
SET #previousTD = TRIM(#previousTD)
--
-- if #previousTD is empty then return
if LENGTH(#previousTD) = 0 then return
--
-- #previousTD has something in it that is not a comma.
-- try to insert it and return
insert into #temp (td) values(#previousTD)
select * from #temp order bu id
return
endif
--
-- should have a #previousTD with at least 1 comma
while #commaPos > 0
begin
SET #item = substring(#previousTD, #startPos, #commaPos)
insert into #temp (td) values(TRIM(#item))
SET #startPos = #commaPos + 1
SET #commaPos = LOCATE(',', #previousTD, #startPos)
end
select * from #temp order bu id
end
In order to make your database, you need to create a new table which has Id of Td and tdNos and have relationships with this. for example:
Table TdNos
ID | TDNO | PREVIOUS_TD |
1 | 14 | 13,12,11 |
2 | 23 | 45,12 |
3 | 32 | 89 |
4 | 55 | NEW |
Table TdNoHistory
TdID|Priority| PREVIOUS_TD |
1 | 1 | 13 |
1 | 2 | 12 |
1 | 3 | 11 |
2 | 1 | 45 |
2 | 2 | 12 |
3 | 1 | 89 |
Which for the second table the combination of TdId and Priority are the primary key and it has a relation with table TdNos through TdId column

How to delete rows inserted unintentionally with a given column value

I am a beginner with MySQL. I made a stored procedure to insert 1,000 random names from a table. It has 3 fields with num, course_name and grade. num is foreign key--as this was for a test purpose, I just kept incremented the num only. So I didn't mark it as a PRIMARY KEY/AUTO_INCREMENT. I called the procedure, and it inserted 1,000 random names in the table. Unknowingly, I called the procedure again, and stopped it after some time. Then the table got 500 more entries after that previous 1000 entries. I wanted to delete the rows that created after the second procedure call.
Below is my statements in a stored procedure: (course_name and grade_details are additional tables with course names and grades.)
DELIMITER //
CREATE PROCEDURE course_grade(IN name_entries int)
BEGIN
DECLARE i int DEFAULT 0;
DECLARE course varchar(20);
DECLARE crs_grade char(1);
gradeloop : LOOP
SELECT name INTO course FROM course_name ORDER BY rand() LIMIT 1;
SELECT grade INTO crs_grade FROM grade_details ORDER BY rand() LIMIT 1;
INSERT INTO tbl_grade(fk_int_roll_no,vchr_course,vchr_grade)
VALUES (i+1,course,crs_grade);
SET i = i + 1;
IF (i=name_entries)
THEN LEAVE gradeloop;
END IF;
END LOOP gradeloop;
SELECT COUNT(*) FROM tbl_grade;
END //
DELIMITER ;
And my table is like :
+----------------+-------------+------------+
| fk_int_roll_no | vchr_course | vchr_grade |
+----------------+-------------+------------+
| 1 | AE | A |
| 2 | MECH | B |
| 3 | EC | A |
| . | .... | . |
| . | .... | . |
| 1000 | IT | E |
| 1 | MARINE | F |
| 2 | BIOTECH | F |
| . | .... | . |
| . | .... | . |
| . | .... | . |
| . | .... | . |
| 500 | RM | A |
+----------------+-------------+------------+
Wanted to delete the last 1 to 500 rows made by mistake!

Returning values from two tables based on a third "key" table, then multiplying values and summing products

I have three tables:
Value v
+-------------------------------+
| Owner | Location | Value |
+-------------------------------+
|Bob | A1 | 0.25 |
|Bob | B4 | 0.10 |
|Dale | Z3 | 0.50 |
|Dale | A1 | 0.25 |
|Rick | B4 | 0.10 |
|Rick | Z3 | 0.50 |
+-------------------------------+
PurchasePercentage p
+-------------------------------+
| Buyer | Location | Percentage |
+-------------------------------+
|Bill | A1 | 0.10 |
|Bill | B4 | 0.20 |
|Kyle | Z3 | 0.30 |
|Kyle | A1 | 0.50 |
|Jan | B4 | 0.15 |
|Jan | Z3 | 0.25 |
+-------------------------------+
Buyout b
+------------------+
| Owner | Buyer |
+------------------+
|Bob | Bill |
|Bob | Kyle |
|Dale | Jan |
|Dale | Bill |
|Rick | Kyle |
|Rick | Jan |
+------------------+
What I'm looking for is a fourth table:
PossibleBuyouts
+--------------------------------+
| Owner | Buyer | BuyoutCost |
+--------------------------------+
based on the transactions laid out in the Buyout table
where BuyoutCost is the SUM of v.Value * p.Percentage
for each distinct Location
the Buyer and Owner have in common.
So these examples would return a PossibleBuyouts table of
+--------------------------------+
| Owner | Buyer | BuyoutCost |
+--------------------------------+
|Bob | Bill | 0.045 |
|Bob | Kyle | 0.125 |
|Dale | Jan | 0.125 |
|Dale | Bill | 0.025 |
|Rick | Kyle | 0.150 |
|Rick | Jan | 0.140 |
+--------------------------------+
Laying out the first row as an example, the math works out like this:
-Bob and Bill both have Locations A1 and B4 in common
-Bob's ownership value for A1 is 0.25 and Bill wants 0.10 percent
-the cost for A1 would be (0.25*0.10) = 0.025
-Bob's ownership value for B4 is 0.10 and Bill wants 0.20 percent
-the cost for B4 would be (0.10*0.20) = 0.020
-Sum(0.025 + 0.020) = the BuyoutCost of 0.045.
If you could help me out, I'm looking for the most efficient way to do this - whether it's multiple queries, one query with a bunch of subqueries, whatever should take the least amount of time.
There are ~1500 different owners, ~1000 different buyers and ~500 different locations, so the number of potential combinations leads to looooooong query times. What do you think will be the fastest way to do this? Everything is indexed and the data pared down to the smallest size I can get it to.
I recommend that you create a procedure that uses a cursor to accomplish your goal. Iterating over one of the three tables, it should be a pretty straightforward process.
MySQL Cursors: http://dev.mysql.com/doc/refman/5.0/en/cursors.html
Edit: The following stored MySQL script creates and calls a stored procedure that correctly reproduces the results you're looking for:
DELIMITER $$
DROP PROCEDURE IF EXISTS `computeBuyouts` $$
CREATE PROCEDURE `computeBuyouts` ()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE o, b VARCHAR(32);
DECLARE bc FLOAT;
DECLARE cur CURSOR FOR SELECT Owner,Buyer FROM Buyout;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO o, b;
IF done THEN
LEAVE read_loop;
END IF;
SET bc = 0;
-- compute the sum of products of each value/percentage pair for all locations
SELECT SUM(v.Value * pp.Percentage) INTO bc FROM Value v LEFT JOIN (PurchasePercentage pp) ON (v.Location = pp.Location) where v.Owner = o and pp.Buyer = b group by v.Owner;
INSERT INTO PossibleBuyouts VALUES (o, b, bc);
END LOOP;
CLOSE cur;
END $$
DELIMITER ;
CALL computeBuyouts();
Hope that helps. At the table sizes you mentioned, this should complete extremely quickly.

MySql: ORDER BY parent and child

I have a table like:
+------+---------+-
| id | parent |
+------+---------+
| 2043 | NULL |
| 2044 | 2043 |
| 2045 | 2043 |
| 2049 | 2043 |
| 2047 | NULL |
| 2048 | 2047 |
| 2049 | 2047 |
+------+---------+
which shows a simple, 2-level "parent-child"-corelation. How can I ORDER BY an SELECT-statement to get the order like in the list above, which means: 1st parent, childs of 1st parent, 2nd parent, childs of 2nd parent and so on (if I have that, I can add the ORDER BYs for the children... I hope). Is it possible withoug adding a sort-field?
Including sorting children by id:
ORDER BY COALESCE(parent, id), parent IS NOT NULL, id
SQL Fiddle example
Explanation:
COALESCE(parent, id): First sort by (effectively grouping together) the parent's id.
parent IS NOT NULL: Put the parent row on top of the group
id: Finally sort all the children (same parent, and parent is not null)
If your table uses 0 instead of null to indicate an entry with no parent:
id | parent
-------------
1233 | 0
1234 | 1233
1235 | 0
1236 | 1233
1237 | 1235
Use greatest instead of coalesce and check the value does not equal 0:
ORDER BY GREATEST(parent, id), parent != 0, id
The solution above didn't work for me, my table used 0 instead of NULL.
I found this other solution: you create a column with the concatened parent id and child id in your query and you can sort the result by it .
SELECT CONCAT(IF(parent = 0,'',CONCAT('/',parent)),'/',id) AS gen_order
FROM table
ORDER BY gen_order
This question still shows as one of the first search results. So I would like to share a my solution and hope it will help more people out. This will also work when you have a table with many levels of parent and child relations. Although it is quite a slow solution. The top level has NULL as parent.
+---------+---------+
| id | parent |
+---------+---------+
| 1 | NULL |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
+---------+---------+
In my approach I will use a procedure that will recursively call itself and keep prepending the path with the parent of the requested id until it reaches the NULL parent.
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `PATH`(IN `input` INT, OUT `output` VARCHAR(128))
BEGIN
DECLARE _id INT;
DECLARE _parent INT;
DECLARE _path VARCHAR(128);
SET `max_sp_recursion_depth` = 50;
SELECT `id`, `parent`
INTO _id, _parent
FROM `database`.`table`
WHERE `table`.`id` = `input`;
IF _parent IS NULL THEN
SET _path = _id;
ELSE
CALL `PATH`(_parent, _path);
SELECT CONCAT(_path, '-', _id) INTO _path;
END IF;
SELECT _path INTO `output`;
END $$
DELIMITER ;
To use the results in an ORDER BY clause you will need a FUNCTION too that wraps the results of the PROCEDURE.
DELIMITER $$
CREATE DEFINER=`root`#`localhost` FUNCTION `GETPATH`(`input` INT) RETURNS VARCHAR(128)
BEGIN
CALL `PATH`(`input`, #path);
RETURN #path;
END $$
DELIMITER ;
Now we can use the recursive path to sort the order of the table. On a table with 10000 rows it takes just over a second on my workstation.
SELECT `id`, `parent`, GETPATH(`id`) `path` FROM `database`.`table` ORDER BY `GETPATH`(`id`);
Example output:
+---------+---------+---------------+
| id | parent | path |
+---------+---------+---------------+
| 1 | NULL | 1 |
| 10 | 1 | 1-10 |
| 300 | 10 | 1-10-300 |
| 301 | 300 | 1-10-300-301 |
| 302 | 300 | 1-10-300-302 |
+---------+---------+---------------+
5 rows in set (1,39 sec)