error in loop of procedure using a cursor - mysql

I have this MySQL procedure where I used a cursor to:
CREATE DEFINER=`root`#`localhost` PROCEDURE `tax_to_salary`()
BEGIN
DECLARE basic_salary INTEGER;
DECLARE new_salary INTEGER;
DECLARE done INTEGER;
declare count INTEGER;
DECLARE counter INTEGER default 0;
DECLARE cur1 CURSOR FOR SELECT salary FROM employee;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
SELECT count(id) INTO count FROM employee;
SET #counter:=0;
OPEN cur1;
l1:LOOP
FETCH cur1 INTO basic_salary;
SET #counter:=#counter+1;
IF #counter>count THEN
leave l1;
end if;
IF basic_salary>2500 THEN
SET #new_salary := 500;
SET #basic_salary := #basic_salary - #new_salary;
else
SET #new_salary := 200;
SET #basic_salary := #basic_salary - #new_salary;
END IF;
SELECT emp_name, salary, basic_salary AS 'Salary after taxes' FROM employee;
END LOOP;
END
And I got this result:
But my procedure should remove 500 from all salaries over 2500 and remove 200 from salaries less than 2500.
I tried to put the final SELECT query inside the loop but I get 5 tabs and every tab contain the same of image below.

Schema
create table employee
( id int auto_increment primary key,
emp_name varchar(100) not null,
salary int not null
);
insert employee (emp_name,salary) values
('John',4400),
('Sarah',2700),
('Peter',2150),
('Ali',2650),
('Ashley',2650);
Note your language was greater than 2500 and also you said less than 2500. yet it has no condition for salary equaling 2500 exactly. So the below is one fix to that concept (otherwise there is no reduction).
Case when
best for many conditions, not that yours has it
select emp_name,salary,
CASE when salary>=2500 then salary-500
ELSE
salary-200
END as modified_salary
from employee;
+----------+--------+-----------------+
| emp_name | salary | modified_salary |
+----------+--------+-----------------+
| John | 4400 | 3900 |
| Sarah | 2700 | 2200 |
| Peter | 2150 | 1950 |
| Ali | 2650 | 2150 |
| Ashley | 2650 | 2150 |
+----------+--------+-----------------+
If
for simple conditions like yours
select emp_name,salary,
if(salary>=2500,salary-500,salary-200) as modified_salary
from employee;
+----------+--------+-----------------+
| emp_name | salary | modified_salary |
+----------+--------+-----------------+
| John | 4400 | 3900 |
| Sarah | 2700 | 2200 |
| Peter | 2150 | 1950 |
| Ali | 2650 | 2150 |
| Ashley | 2650 | 2150 |
+----------+--------+-----------------+
There is no reason to be using a row-by-row cursor the way you are. That is what people sometimes do just starting out with sql. Not only are they slow, often unbearably slow, but they keep you from harnessing the power of relations that make sql shine.
Said another way, you are trying to write procedural code and getting in the middle of it all by helping the sql engine figure it out with that mindset. It doesn't want it that way for optimization. You can, but you will slow it down horribly.
The reason you are getting multiple tabs as you say is that with your strategy, even if it worked well number-wise, each select statement returns a result set. And by going the dangerous cursor route, you returned five of them.

Related

Is there something like a FOR loop in MySql?

I have a table with matches information, and I need to return a row for each goal and each team. So for example:
+--------+-------+-------+-------+-------+
| Match | Team1 | goal1 | goal2 | Team2 |
+--------+-------+-------+-------+-------+
| 1 | Red | 1 | 0 | Blue |
+--------+-------+-------+-------+-------+
| 2 | Green | 2 | 1 | Black |
+--------+-------+-------+-------+-------+
I want to run a function for each row that returns a row for each goal for each team. So my function result would be:
+--------+-------+-------+
| Goal | Match | Team |
+--------+-------+-------+
| 1 | 1 | Red |
+--------+-------+-------+
| 2 | 2 | Green |
+--------+-------+-------+
| 3 | 2 | Green |
+--------+-------+-------+
| 4 | 2 | Black |
+--------+-------+-------+
My ultimate objective is that I need to have one row for each match/team/goal to fill in manually the Scorer and the minute. Since I hace over 40000 matches, copy pasting each row counting the amount of goals is a pain.
I would like to start with a goal table pre populated with as much information as I already have.
Create a table that contains numbers from 1 to the maximum number of possible goals, i.e.
CREATE TABLE numbers (
num INT PRIMARY KEY
);
INSERT INTO numbers VALUES (1), (2), (3), (4), (5), (6), ...
You can then join this table with your original table:
SELECT num AS Goal, `Match`, Team
FROM numbers
JOIN (
SELECT Team1 AS Team, goal1 AS goals, `Match`
FROM matches
UNION
SELECT Team2 AS Team, goal2 AS goals, `Match`
FROM matches
) ON num <= goals
While loop syntax example in MySQL:
delimiter //
CREATE procedure yourdatabase.while_example()
wholeblock:BEGIN
declare str VARCHAR(255) default '';
declare x INT default 0;
SET x = 1;
WHILE x <= 5 DO
SET str = CONCAT(str,x,',');
SET x = x + 1;
END WHILE;
select str;
END//
Which prints:
mysql> call while_example();
+------------+
| str |
+------------+
| 1,2,3,4,5, |
+------------+
FOR loop syntax example in MySQL:
delimiter //
CREATE procedure yourdatabase.for_loop_example()
wholeblock:BEGIN
DECLARE x INT;
DECLARE str VARCHAR(255);
SET x = -5;
SET str = '';
loop_label: LOOP
IF x > 0 THEN
LEAVE loop_label;
END IF;
SET str = CONCAT(str,x,',');
SET x = x + 1;
ITERATE loop_label;
END LOOP;
SELECT str;
END//
Which prints:
mysql> call for_loop_example();
+-------------------+
| str |
+-------------------+
| -5,-4,-3,-2,-1,0, |
+-------------------+
1 row in set (0.00 sec)
Tutorial: http://www.mysqltutorial.org/stored-procedures-loop.aspx

Group and make row become header

I have this data in my MySQL table fact
+------------+-------+--------+
| timestamp | code | unique |
+------------+-------+--------+
| 1416157200 | 7E001 | 100 |
| 1416157200 | 7E002 | 200 |
| 1416243600 | 7E001 | 100 |
| 1416243600 | 7E002 | 200 |
+------------+-------+--------+
I want to get this result
+-------+------------+------------+
| code | 2014-11-18 | 2014-11-17 |
+-------+------------+------------+
| 7E001 | 100 | 100 |
| 7E002 | 200 | 200 |
+-------+------------+------------+
I use this query select code, from_unixtime(timestamp, '%Y-%m-%d') as date, unique from fact; to produce this result and have no idea to aggregate this result became above desirable result.
+-------+------------+--------+
| code | date | unique |
+-------+------------+--------+
| 7E001 | 2014-11-17 | 100 |
| 7E002 | 2014-11-17 | 200 |
| 7E001 | 2014-11-18 | 100 |
| 7E002 | 2014-11-18 | 200 |
+-------+------------+--------+
Is it possible? And how to achieve that?
PS: Please help me editing the title to be more descriptive since I can't explain this problem in such short
Case based aggregation can be used
As the dates can be many, you need to use dynamic SQL
select code,
Max( case when date=1416157200 then unique end) as '2014-11-17' ,
Max( case when date=1416243600 t hen unique end) as '2014-11-18'
from fact
Group by date
Yes it is possible creating a dynamic query using that values as columns.
I use some code from article describing a way to get a pivot table in MySQL:
mysql> call get_fact_pivot_table();
+-------+------------+------------+
| code | 2014-11-16 | 2014-11-17 |
+-------+------------+------------+
| 7E001 | 100 | 100 |
| 7E002 | 200 | 200 |
+-------+------------+------------+
2 rows in set (0,06 sec)
The procedure get_fact_pivot_table contains a cursor to make a dynamic query, you can change it as you need:
DELIMITER $$
DROP PROCEDURE if exists get_fact_pivot_table$$
CREATE PROCEDURE `get_fact_pivot_table`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE p_sql text;
DECLARE p_col_date VARCHAR(20);
DECLARE p_col_value int;
DECLARE c_columns cursor FOR
select distinct from_unixtime(timestamp, '%Y-%m-%d') as `col_date` ,
`timestamp` as col_value
from fact;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;
SET p_sql = 'select f.code ';
OPEN c_columns;
read_loop: LOOP
FETCH c_columns INTO p_col_date, p_col_value;
IF done THEN
LEAVE read_loop;
END IF;
SET p_sql = concat(p_sql,
', (select c.`unique` from fact as c where
c.`timestamp` = ', p_col_value ,'
and c.code = f.code limit 1) as `',p_col_date,'` ');
END LOOP;
SET #SQL = concat(p_sql,' from fact as f group by f.code');
close c_columns;
PREPARE stmt1 FROM #SQL;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
END$$
delimiter ;

POSTGRES: problems on functions

So, Im trying to work on a simple book loans system and Im having problems on creating and using a function.
I have a Loans 'Table', Copies 'Table' and Available 'View'.
"Available View" looks like this:
book_id | available_copies
---------+------------------
BI6 | 1
wherein 'available_copies' column is
COUNT(copy_id) AS available_copies
FROM copies
WHERE copy_id NOT IN (SELECT copy_id FROM loans)
This is my "Copies Table"
copy_id | book_id | copy_no | copy_code
---------+---------+---------+-----------
CI8 | BI6 | 8 | CI
CI9 | BI6 | 9 | CI
CI7 | BI7 | 7 | CI
CI10 | BI7 | 10 | CI
and this is my "Loans Table"
loan_id | copy_id | user_id | borrow_date | due_date | loan_no | loan_code
---------+---------+---------+-------------+------------+---------+-----------
LI10 | CI10 | UI4 | 2013-05-21 | 2013-05-26 | 10 | LI
LI11 | CI8 | UI4 | 2013-05-21 | 2013-05-26 | 11 | LI
LI12 | CI7 | UI4 | 2013-05-22 | 2013-05-27 | 12 | LI
What i really wanted to do is.. if the available_copies is 0 (like in the "available view" above, BI7 is not in the Available View anymore because all copies where already borrowed) postgres will prompt something that you cannot borrow books in Loans anymore since the book is already out of copies.
Im kinda new to plpgsql. Please help. :(
I don't know what Pg version you has, but probably some older. I see lot of bugs in your example - so I don't believe it was accepted by postgres
CREATE OR REPLACE FUNCTION try(copyID TEXT)
RETURNS BOOLEAN AS $$
BEGIN
SELECT available_copies FROM available -- missing INTO ???
-- Undeclared variable "available_copies" and probably
-- collision with column named "available_copies"
IF available_copies > 0 THEN
INSERT INTO loans(copy_id) VALUES(copyID);
RETURN BOOLEAN; --- RETURN true or false, but BOOLEAN??
ELSE
RETURN BOOLEAN;
END IF;
END;
$$ LANGUAGE plpgsql;
Example of PL/SQL function:
CREATE OR REPLACE FUNCTION try(copyID TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN NOT EXISTS (SELECT l.copy_id FROM loans l WHERE l.copy_id = copyID);
END;
$$ LANGUAGE plpgsql;
It will RETURN TRUE if the record with copyID NOT EXISTS in loans;
Same function as SQL function:
CREATE OR REPLACE FUNCTION try(copyID TEXT)
RETURNS BOOLEAN AS $$
SELECT NOT EXISTS (SELECT l.copy_id FROM loans l WHERE l.copy_id = copyID)
$$ LANGUAGE SQL;

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)

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