MySQL - copy or update rows with a change within one table - mysql

I have a database table like this one:
group | detailsID | price
EK | 1 | 1.40
EK | 2 | 1.50
EK | 3 | 1.60
H | 1 | 2.40
H | 2 | 2.50
Now I want to copy the data from group "EK" to the group "H", so the prices for the detailsID's must be adjusted for the detailsIDs 1 and 2, and the entry for detailsID 3 must be inserted for group "H".
How can I do that with one or two MySQL query's?
Thanks!

We can try doing an INSERT INTO ... SELECT with ON DUPLICATE KEY UPDATE:
INSERT INTO yourTable (`group`, detailsID, price)
SELECT 'H', detailsID, price
FROM yourTable t
WHERE `group` = 'EK'
ON DUPLICATE KEY UPDATE price = t.price;
But this assumes that there exists a unique key on (group, detailsID). If this would not be possible, then this approach would not work.
As an alternative, I might do this in two steps. First, remove the H group records, then insert the updated H records you expect.
DELETE
FROM yourTable
WHERE `group` = 'H';
INSERT INTO yourTable (`group`, detailsID, price)
SELECT 'H', detailsID, price
FROM yourTable
WHERE `group` = 'EK';
I use the above approach because a single update can't handle your requirement, since new records also need to be inserted.
Note that you should avoid naming your columns and tables using reserved MySQL keywords such as GROUP.

You can try this as well, Following code implemented using stored procedures. Very simple not that difficult to understand. You may need to modify data type and optimize the code as per the requirement.
DELIMITER $$;
DROP PROCEDURE IF EXISTS update_H $$;
CREATE PROCEDURE update_H()
BEGIN
DECLARE finished INTEGER DEFAULT 0;
DECLARE `group_col` varchar(255) DEFAULT "";
DECLARE `detaildid_col` varchar(255) DEFAULT "";
DECLARE `price_col` varchar(255) DEFAULT "";
DECLARE H_FOUND INTEGER DEFAULT 0;
DECLARE pull_data CURSOR FOR select `group`, `detaildid`, `price` from test.newtab WHERE `group` = 'EK';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
OPEN pull_data;
traverse_data: LOOP
FETCH pull_data INTO group_col, detaildid_col, price_col;
IF finished = 1 THEN
LEAVE traverse_data;
END IF;
SET H_FOUND = (SELECT count(*) from test.newtab where `group` = 'H' AND `detaildid` = detaildid_col);
IF ( H_FOUND = 1 ) THEN
UPDATE test.newtab SET `price` = price_col where `group` = 'H' AND `detaildid` = detaildid_col;
ELSE
INSERT INTO test.newtab (`group`, `detaildid`, `price`) VALUES ('H', detaildid_col, price_col);
END IF;
END LOOP traverse_data;
CLOSE pull_data;
END $$;
DELIMITER ;
You can call this procedure by executing, call update_H();

Related

Update differents fields based on criterias in MySQL

I store in my DB the demands some users can do. The demands can have differents status (stored as events), such as in progress, finished, waiting, and so on (there's 30ish differents status). The demands have differents deadlines corresponding of differents steps of the treatment.
I need to "freeze" some deadlines of the demands, if their current status belongs to a list of pre-defined ones.
In example :
If a demand has the status "A", I have to "freeze" the deadline 2 to 5.
If the status is "B" or "C", I have to "freeze" the deadline 3 to 5.
If the status is "D", I have to "freeze" the deadline 4 and 5.
I plan to use an EVENT that runs every day, at 19:00 to update (add 1 day) the differents deadlines of the concerned demands.
Table structures :
Table demand
id | someDatas | deadline1 | deadline2 | deadline3 | deadline4 | deadline5
---+-----------+-----------+-----------+-----------+-----------+-----------
| | | | | |
Table status
id | name
---+-----
|
Table events
id | id_demand | someOthersDatas | id_status
---+-----------+-----------------+----------
| | |
I wrote a query to get the demands corresponding of a list of status :
SELECT dem.*, st.`name` as 'statusName'
FROM `status` st
INNER JOIN `events` eve
ON eve.id_status = st.id
INNER JOIN `demand` dem
ON eve.id_demand = dem.id
WHERE st.`name` IN ('A', 'B', 'C', 'D')
AND eve.id IN
(
SELECT MAX(even.id) ev
FROM `demand` de
INNER JOIN `events` even
ON even.id_demand = de.id
GROUP BY de.id
);
This query works perfectly and I can get the desired informations for my treatment, I have the id of the demands, its deadlines and the name of the current status.
I don't mind storing this result in a temporary table such as :
DROP TEMPORARY TABLE IF EXISTS pendingDemands;
CREATE TEMPORARY TABLE IF NOT EXISTS pendingDemands
SELECT /* the query shown above */
To make sure the day I want to add to the deadline is valid (= not a day off) I wrote a function that calculate the next valid day :
DELIMITER //
DROP FUNCTION IF EXISTS `get_next_valid_date`;
CREATE FUNCTION `get_next_valid_date`(MyDate DATETIME) RETURNS DATETIME
BEGIN
REPEAT
SET MyDate = (DATE_ADD(MyDate, INTERVAL 1 DAY));
SET #someCondition = (select isDayOff(MyDate));
UNTIL (#someCondition = 0) END REPEAT;
RETURN MyDate;
END//
This function works perfectly and I get the expected results, and isDayOff() don't need to be detailed.
My problem is that I don't know how to use them (the temporary table pendingDemands and the function get_next_valid_date) together to update the table demand, I'm not skilled enough in SQL to build such pretty UPDATE query.
Any direction I could take?
I finally found a work around based on this answer
I created a stored procedure in which I'm using a cursor storing the query I was using to feed the pendingDemands temporary table.
Then, I looped over that cursor and used a CASE WHEN statement to determine the values to modify :
DELIMITER $$
DROP PROCEDURE IF EXISTS `freezePendingDeadlines` $$
CREATE PROCEDURE `freezePendingDeadlines`()
BEGIN
-- from http://stackoverflow.com/questions/35858541/call-a-stored-procedure-from-the-declare-statement-when-using-cursors-in-mysql
-- declare the program variables where we'll hold the values we're sending into the procedure;
-- declare as many of them as there are input arguments to the second procedure,
-- with appropriate data types.
DECLARE p_id INT DEFAULT 0;
DECLARE pT2P DATETIME DEFAULT NULL;
DECLARE pT3P DATETIME DEFAULT NULL;
DECLARE pT4P DATETIME DEFAULT NULL;
DECLARE pT5P DATETIME DEFAULT NULL;
DECLARE pstatusName VARCHAR(255) DEFAULT NULL;
-- we need a boolean variable to tell us when the cursor is out of data
DECLARE done TINYINT DEFAULT FALSE;
-- declare a cursor to select the desired columns from the desired source table1
-- the input argument (which you might or might not need) is used in this example for row selection
DECLARE demandCursor
CURSOR FOR
SELECT p.id,
p.T2P,
p.T3P,
p.T4P,
p.T5P,
P.statusName
FROM
(
SELECT dem.*, st.`name` as 'statusName'
FROM `status` st
INNER JOIN `events` eve
ON eve.id_status = st.id
INNER JOIN `demand` dem
ON eve.id_demand = dem.id
WHERE st.`name` IN ('A', 'B', 'C', 'D')
AND eve.id IN
(
SELECT MAX(even.id) ev
FROM `demand` de
INNER JOIN `events` even
ON even.id_demand = de.id
GROUP BY de.id
)
) AS p;
-- a cursor that runs out of data throws an exception; we need to catch this.
-- when the NOT FOUND condition fires, "done" -- which defaults to FALSE -- will be set to true,
-- and since this is a CONTINUE handler, execution continues with the next statement.
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
DROP TEMPORARY TABLE IF EXISTS days_off;
CREATE TEMPORARY TABLE IF NOT EXISTS days_off
(
date_off VARCHAR(5)
);
INSERT INTO days_off VALUES('01-01'),
('05-01'),
('05-08'),
('07-14'),
('08-15'),
('11-01'),
('11-11'),
('12-25');
-- open the cursor
OPEN demandCursor;
my_loop: -- loops have to have an arbitrary label; it's used to leave the loop
LOOP
-- read the values from the next row that is available in the cursor
FETCH demandCursor INTO p_id, pT2P, pT3P, pT4P, pT5P, pstatusName;
IF done THEN -- this will be true when we are out of rows to read, so we go to the statement after END LOOP.
LEAVE my_loop;
ELSE
CASE pstatusName
WHEN 'A' THEN
SET pT2P=get_next_valid_date(pT2P);
SET pT3P=get_next_valid_date(pT3P);
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
WHEN 'B' THEN
SET pT3P=get_next_valid_date(pT3P);
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
WHEN 'C' THEN
SET pT3P=get_next_valid_date(pT3P);
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
WHEN 'D' THEN
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
END CASE;
UPDATE `demand`
SET T2P=pT2P,
T3P=pT3P,
T4P=pT4P,
T5P=pT5P
WHERE id=p_id;
END IF;
END LOOP;
CLOSE demandCursor;
DROP TEMPORARY TABLE IF EXISTS days_off;
END$$

Writing stored procedure which flags duplicate values in a comma separated field in MySQL

I have a database table like this sample:
ID THINGS HAS_DUPLICATES
1 AAA, BBB, AAA NULL
2 CCC, DDD NULL
I am trying to write a stored procedure to flag duplicate values in THINGS field.
After calling the procedure the table will become like this:
ID THINGS HAS_DUPLICATES
1 AAA, BBB, AAA YES
2 CCC, DDD NO
Please be informed that I am trying to resolve it using only SQL and without normalizing my database. I am also aware of other approaches like writing PHP code.
Schema:
DROP TABLE IF EXISTS evilThings; -- orig table with dupes
CREATE TABLE evilThings
( ID INT AUTO_INCREMENT PRIMARY KEY,
THINGS TEXT NOT NULL,
HAS_DUPLICATES INT NULL
);
INSERT evilThings(ID,THINGS) VALUES
(1,"'AAA, BBB, AAA'"),
(2,"'CCC, DDD'");
CREATE TABLE notEvilAssocTable
( ai INT AUTO_INCREMENT PRIMARY KEY, -- no shuffle on inserts
ID INT NOT NULL,
THING VARCHAR(100) NOT NULL,
UNIQUE KEY `unqK_id_thing` (ID,THING) -- no dupes, this is honorable
);
Stored Proc:
DROP PROCEDURE IF EXISTS splitEm;
DELIMITER $$
CREATE PROCEDURE splitEm()
BEGIN
DECLARE lv_ID,pos1,pos2,comma_pos INT;
DECLARE lv_THINGS TEXT;
DECLARE particle VARCHAR(100);
DECLARE strs_done INT DEFAULT FALSE; -- string search done
DECLARE done INT DEFAULT FALSE; -- cursor done
DECLARE cur111 CURSOR FOR SELECT ID,THINGS FROM evilThings ORDER BY ID;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- Please note in the above, CURSOR stuff MUST come LAST else "Error 1337: Variable or condition decl aft curs"
-- -------------------------------------------------------------------------------------------------------------------
TRUNCATE TABLE notEvilAssocTable;
OPEN cur111;
read_loop: LOOP
SET strs_done=FALSE;
FETCH cur111 INTO lv_ID,lv_THINGS;
IF done THEN
LEAVE read_loop;
END IF;
SET pos1=1,comma_pos=0;
WHILE !strs_done DO
SET pos2=LOCATE(',', lv_THINGS, comma_pos+1);
IF pos2=0 THEN
SET pos2=LOCATE("'", lv_THINGS, comma_pos+1);
IF pos2!=0 THEN
SET particle=SUBSTRING(lv_THINGS,comma_pos+1,pos2-comma_pos-1);
SET particle=REPLACE(particle,"'","");
SET particle=TRIM(particle);
INSERT IGNORE notEvilAssocTable (ID,THING) VALUES (lv_ID,particle);
END IF;
SET strs_done=1;
ELSE
SET particle=SUBSTRING(lv_THINGS,comma_pos+1,pos2-comma_pos-1);
SET particle=REPLACE(particle,"'","");
SET particle=TRIM(particle);
INSERT IGNORE notEvilAssocTable (ID,THING) VALUES (lv_ID,particle);
SET comma_pos=pos2;
END IF;
END WHILE;
END LOOP;
CLOSE cur111; -- close the cursor
END$$
DELIMITER ;
Test:
call splitEm();
See results of split:
select * from notEvilAssocTable;
Note that position 3, the InnoDB gap (from INSERT IGNORE). It is simply the innodb gap anomaly, an expected side effect like so many of InnoDB. In this case driven by the IGNORE part that creates a gap. No problem though. It forbids duplicates in our new table for split outs. It is common. It is there to protect you.
If you did not mean to have the single quote at the beginning and end of the string in the db, then change the routine accordingly.
Here is the answer to my question, assuming the data in THINGS field are separated by a bar '|'. Our original table will be myTABLE:
ID THINGS THINGSCount THINGSCountUnique HAS_DUPLICATES
1 AAA|BBB|AAA NULL NULL NULL
2 CCC|DDD NULL NULL NULL
Step 1. Check the maximum number of values separated by a bar '|' in THINGS field:
SELECT ROUND((CHAR_LENGTH(THINGS) - CHAR_LENGTH(REPLACE(THINGS,'|',''))) / CHAR_LENGTH('|')) + 1 FROM myTABLE;
Step 2. Assuming the answer from step 1 was 7, now use the following SQL to split the data in THINGS field into rows, there are many other approaches which you can Google to do the split:
CREATE TABLE myTABLE_temp
SELECT ID, SUBSTRING_INDEX(SUBSTRING_INDEX(myTABLE.THINGS, '|', n.n), '|', -1) THINGS
FROM myTABLE JOIN
( SELECT n FROM
( SELECT 1 AS N UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 ) a ) n
ON CHAR_LENGTH(THINGS) - CHAR_LENGTH(REPLACE(THINGS, '|', '')) >= n - 1
ORDER BY ID;
Our myTABLE_temp table will be something like:
ID THINGS
1 AAA
1 BBB
1 AAA
2 CCC
2 DDD
Step 3. Here we create two new tables to hold COUNT(THINGS) and COUNT(DISTINCT THINGS) as following:
# THINGSCount
CREATE TABLE myTABLE_temp_2
SELECT ID, COUNT(THINGS) AS THINGSCount FROM myTABLE_temp GROUP BY ID;
# Remember to ADD INDEX to ID field
UPDATE myTABLE A INNER JOIN myTABLE_temp_2 B ON(A.ID = B.ID) SET A.THINGSCount = B.THINGSCount;
# THINGSCountUnique
CREATE TABLE myTABLE_temp_3
SELECT ID, COUNT(THINGS) AS THINGSCountUnique FROM myTABLE_temp GROUP BY ID;
# Remember to ADD INDEX to ID field
UPDATE myTABLE A INNER JOIN myTABLE_temp_3 B ON(A.ID = B.ID) SET A.THINGSCountUnique = B.THINGSCountUnique;
Final Step: Flag duplicate values:
UPDATE myTABLE SET HAS_DUPLICATES = IF(THINGSCount>THINGSCountUnique, 'DUPLICATES', 'NO');

How can I simulate an array variable in MySQL?

It appears that MySQL doesn't have array variables. What should I use instead?
There seem to be two alternatives suggested: A set-type scalar and temporary tables. The question I linked to suggests the former. But is it good practice to use these instead of array variables? Alternatively, if I go with sets, what would be the set-based idiom equivalent to foreach?
Well, I've been using temporary tables instead of array variables. Not the greatest solution, but it works.
Note that you don't need to formally define their fields, just create them using a SELECT:
DROP TEMPORARY TABLE IF EXISTS my_temp_table;
CREATE TEMPORARY TABLE my_temp_table
SELECT first_name FROM people WHERE last_name = 'Smith';
(See also Create temporary table from select statement without using Create Table.)
You can achieve this in MySQL using WHILE loop:
SET #myArrayOfValue = '2,5,2,23,6,';
WHILE (LOCATE(',', #myArrayOfValue) > 0)
DO
SET #value = ELT(1, #myArrayOfValue);
SET #myArrayOfValue= SUBSTRING(#myArrayOfValue, LOCATE(',',#myArrayOfValue) + 1);
INSERT INTO `EXEMPLE` VALUES(#value, 'hello');
END WHILE;
EDIT:
Alternatively you can do it using UNION ALL:
INSERT INTO `EXEMPLE`
(
`value`, `message`
)
(
SELECT 2 AS `value`, 'hello' AS `message`
UNION ALL
SELECT 5 AS `value`, 'hello' AS `message`
UNION ALL
SELECT 2 AS `value`, 'hello' AS `message`
UNION ALL
...
);
Try using FIND_IN_SET() function of MySql
e.g.
SET #c = 'xxx,yyy,zzz';
SELECT * from countries
WHERE FIND_IN_SET(countryname,#c);
Note: You don't have to SET variable in StoredProcedure if you are passing parameter with CSV values.
Nowadays using a JSON array would be an obvious answer.
Since this is an old but still relevant question I produced a short example.
JSON functions are available since mySQL 5.7.x / MariaDB 10.2.3
I prefer this solution over ELT() because it's really more like an array and this 'array' can be reused in the code.
But be careful: It (JSON) is certainly much slower than using a temporary table. Its just more handy. imo.
Here is how to use a JSON array:
SET #myjson = '["gmail.com","mail.ru","arcor.de","gmx.de","t-online.de",
"web.de","googlemail.com","freenet.de","yahoo.de","gmx.net",
"me.com","bluewin.ch","hotmail.com","hotmail.de","live.de",
"icloud.com","hotmail.co.uk","yahoo.co.jp","yandex.ru"]';
SELECT JSON_LENGTH(#myjson);
-- result: 19
SELECT JSON_VALUE(#myjson, '$[0]');
-- result: gmail.com
And here a little example to show how it works in a function/procedure:
DELIMITER //
CREATE OR REPLACE FUNCTION example() RETURNS varchar(1000) DETERMINISTIC
BEGIN
DECLARE _result varchar(1000) DEFAULT '';
DECLARE _counter INT DEFAULT 0;
DECLARE _value varchar(50);
SET #myjson = '["gmail.com","mail.ru","arcor.de","gmx.de","t-online.de",
"web.de","googlemail.com","freenet.de","yahoo.de","gmx.net",
"me.com","bluewin.ch","hotmail.com","hotmail.de","live.de",
"icloud.com","hotmail.co.uk","yahoo.co.jp","yandex.ru"]';
WHILE _counter < JSON_LENGTH(#myjson) DO
-- do whatever, e.g. add-up strings...
SET _result = CONCAT(_result, _counter, '-', JSON_VALUE(#myjson, CONCAT('$[',_counter,']')), '#');
SET _counter = _counter + 1;
END WHILE;
RETURN _result;
END //
DELIMITER ;
SELECT example();
Dont know about the arrays, but there is a way to store comma-separated lists in normal VARCHAR column.
And when you need to find something in that list you can use the FIND_IN_SET() function.
I know that this is a bit of a late response, but I recently had to solve a similar problem and thought that this may be useful to others.
Background
Consider the table below called 'mytable':
The problem was to keep only latest 3 records and delete any older records whose systemid=1 (there could be many other records in the table with other systemid values)
It would be good if you could do this simply using the statement
DELETE FROM mytable WHERE id IN (SELECT id FROM `mytable` WHERE systemid=1 ORDER BY id DESC LIMIT 3)
However this is not yet supported in MySQL and if you try this then you will get an error like
...doesn't yet support 'LIMIT & IN/ALL/SOME subquery'
So a workaround is needed whereby an array of values is passed to the IN selector using variable. However, as variables need to be single values, I would need to simulate an array. The trick is to create the array as a comma separated list of values (string) and assign this to the variable as follows
SET #myvar = (SELECT GROUP_CONCAT(id SEPARATOR ',') AS myval FROM (SELECT * FROM `mytable` WHERE systemid=1 ORDER BY id DESC LIMIT 3 ) A GROUP BY A.systemid);
The result stored in #myvar is
5,6,7
Next, the FIND_IN_SET selector is used to select from the simulated array
SELECT * FROM mytable WHERE FIND_IN_SET(id,#myvar);
The combined final result is as follows:
SET #myvar = (SELECT GROUP_CONCAT(id SEPARATOR ',') AS myval FROM (SELECT * FROM `mytable` WHERE systemid=1 ORDER BY id DESC LIMIT 3 ) A GROUP BY A.systemid);
DELETE FROM mytable WHERE FIND_IN_SET(id,#myvar);
I am aware that this is a very specific case. However it can be modified to suit just about any other case where a variable needs to store an array of values.
I hope that this helps.
DELIMITER $$
CREATE DEFINER=`mysqldb`#`%` PROCEDURE `abc`()
BEGIN
BEGIN
set #value :='11,2,3,1,';
WHILE (LOCATE(',', #value) > 0) DO
SET #V_DESIGNATION = SUBSTRING(#value,1, LOCATE(',',#value)-1);
SET #value = SUBSTRING(#value, LOCATE(',',#value) + 1);
select #V_DESIGNATION;
END WHILE;
END;
END$$
DELIMITER ;
Maybe create a temporary memory table with columns (key, value) if you want associative arrays. Having a memory table is the closest thing to having arrays in mysql
Here’s how I did it.
First, I created a function that checks whether a Long/Integer/whatever value is in a list of values separated by commas:
CREATE DEFINER = 'root'#'localhost' FUNCTION `is_id_in_ids`(
`strIDs` VARCHAR(255),
`_id` BIGINT
)
RETURNS BIT(1)
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
DECLARE strLen INT DEFAULT 0;
DECLARE subStrLen INT DEFAULT 0;
DECLARE subs VARCHAR(255);
IF strIDs IS NULL THEN
SET strIDs = '';
END IF;
do_this:
LOOP
SET strLen = LENGTH(strIDs);
SET subs = SUBSTRING_INDEX(strIDs, ',', 1);
if ( CAST(subs AS UNSIGNED) = _id ) THEN
-- founded
return(1);
END IF;
SET subStrLen = LENGTH(SUBSTRING_INDEX(strIDs, ',', 1));
SET strIDs = MID(strIDs, subStrLen+2, strLen);
IF strIDs = NULL or trim(strIds) = '' THEN
LEAVE do_this;
END IF;
END LOOP do_this;
-- not founded
return(0);
END;
So now you can search for an ID in a comma-separated list of IDs, like this:
select `is_id_in_ids`('1001,1002,1003',1002);
And you can use this function inside a WHERE clause, like this:
SELECT * FROM table1 WHERE `is_id_in_ids`('1001,1002,1003',table1_id);
This was the only way I found to pass an "array" parameter to a PROCEDURE.
I'm surprised none of the answers mention ELT/FIELD.
ELT/FIELD works very similar to an array especially if you have static data.
FIND_IN_SET also works similar but doesn't have a built in complementary
function but it's easy enough to write one.
mysql> select elt(2,'AA','BB','CC');
+-----------------------+
| elt(2,'AA','BB','CC') |
+-----------------------+
| BB |
+-----------------------+
1 row in set (0.00 sec)
mysql> select field('BB','AA','BB','CC');
+----------------------------+
| field('BB','AA','BB','CC') |
+----------------------------+
| 2 |
+----------------------------+
1 row in set (0.00 sec)
mysql> select find_in_set('BB','AA,BB,CC');
+------------------------------+
| find_in_set('BB','AA,BB,CC') |
+------------------------------+
| 2 |
+------------------------------+
1 row in set (0.00 sec)
mysql> SELECT SUBSTRING_INDEX(SUBSTRING_INDEX('AA,BB,CC',',',2),',',-1);
+-----------------------------------------------------------+
| SUBSTRING_INDEX(SUBSTRING_INDEX('AA,BB,CC',',',2),',',-1) |
+-----------------------------------------------------------+
| BB |
+-----------------------------------------------------------+
1 row in set (0.01 sec)
Is an array variable really necessary?
I ask because I originally landed here wanting to add an array as a MySQL table variable. I was relatively new to database design and trying to think of how I'd do it in a typical programming language fashion.
But databases are different. I thought I wanted an array as a variable, but it turns out that's just not a common MySQL database practice.
Standard Practice
The alternative solution to arrays is to add an additional table, and then reference your original table with a foreign key.
As an example, let's imagine an application that keeps track of all the items every person in a household wants to buy at the store.
The commands for creating the table I originally envisioned would have looked something like this:
#doesn't work
CREATE TABLE Person(
name VARCHAR(50) PRIMARY KEY
buy_list ARRAY
);
I think I envisioned buy_list to be a comma-separated string of items or something like that.
But MySQL doesn't have an array type field, so I really needed something like this:
CREATE TABLE Person(
name VARCHAR(50) PRIMARY KEY
);
CREATE TABLE BuyList(
person VARCHAR(50),
item VARCHAR(50),
PRIMARY KEY (person, item),
CONSTRAINT fk_person FOREIGN KEY (person) REFERENCES Person(name)
);
Here we define a constraint named fk_person. It says that the 'person' field in BuyList is a foreign key. In other words, it's a primary key in another table, specifically the 'name' field in the Person table, which is what REFERENCES denotes.
We also defined the combination of person and item to be the primary key, but technically that's not necessary.
Finally, if you want to get all the items on a person's list, you can run this query:
SELECT item FROM BuyList WHERE person='John';
This gives you all the items on John's list. No arrays necessary!
This is my solution to use a variable containing a list of elements.
You can use it in simple queries (no need to use store procedures or create tables).
I found somewhere else on the site the trick to use the JSON_TABLE function (it works in mysql 8, I dunno of it works in other versions).
set #x = '1,2,3,4' ;
select c.NAME
from colors c
where
c.COD in (
select *
from json_table(
concat('[',#x,']'),
'$[*]' columns (id int path '$') ) t ) ;
Also, you may need to manage the case of one or more variables set to empty_string.
In this case I added another trick (the query does not return error even if x, y, or both x and y are empty strings):
set #x = '' ;
set #y = 'yellow' ;
select c.NAME
from colors
where
if(#y = '', 1 = 1, c.NAME = #y)
and if(#x = '', 1, c.COD) in (
select *
from json_table(
concat('[',if(#x = '', 1, #x),']'),
'$[*]' columns (id int path '$') ) t) ;
This works fine for list of values:
SET #myArrayOfValue = '2,5,2,23,6,';
WHILE (LOCATE(',', #myArrayOfValue) > 0)
DO
SET #value = ELT(1, #myArrayOfValue);
SET #STR = SUBSTRING(#myArrayOfValue, 1, LOCATE(',',#myArrayOfValue)-1);
SET #myArrayOfValue = SUBSTRING(#myArrayOfValue, LOCATE(',', #myArrayOfValue) + 1);
INSERT INTO `Demo` VALUES(#STR, 'hello');
END WHILE;
Both versions using sets didn't work for me (tested with MySQL 5.5). The function ELT() returns the whole set. Considering the WHILE statement is only avaible in PROCEDURE context i added it to my solution:
DROP PROCEDURE IF EXISTS __main__;
DELIMITER $
CREATE PROCEDURE __main__()
BEGIN
SET #myArrayOfValue = '2,5,2,23,6,';
WHILE (LOCATE(',', #myArrayOfValue) > 0)
DO
SET #value = LEFT(#myArrayOfValue, LOCATE(',',#myArrayOfValue) - 1);
SET #myArrayOfValue = SUBSTRING(#myArrayOfValue, LOCATE(',',#myArrayOfValue) + 1);
END WHILE;
END;
$
DELIMITER ;
CALL __main__;
To be honest, i don't think this is a good practice. Even if its realy necessary, this is barely readable and quite slow.
Isn't the point of arrays to be efficient? If you're just iterating through values, I think a cursor on a temporary (or permanent) table makes more sense than seeking commas, no? Also cleaner. Lookup "mysql DECLARE CURSOR".
For random access a temporary table with numerically indexed primary key. Unfortunately the fastest access you'll get is a hash table, not true random access.
Another way to see the same problem.
Hope helpfull
DELIMITER $$
CREATE PROCEDURE ARR(v_value VARCHAR(100))
BEGIN
DECLARE v_tam VARCHAR(100);
DECLARE v_pos VARCHAR(100);
CREATE TEMPORARY TABLE IF NOT EXISTS split (split VARCHAR(50));
SET v_tam = (SELECT (LENGTH(v_value) - LENGTH(REPLACE(v_value,',',''))));
SET v_pos = 1;
WHILE (v_tam >= v_pos)
DO
INSERT INTO split
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(v_value,',',v_pos),',', -1);
SET v_pos = v_pos + 1;
END WHILE;
SELECT * FROM split;
DROP TEMPORARY TABLE split;
END$$
CALL ARR('1006212,1006404,1003404,1006505,444,');
If we have one table like that
mysql> select * from user_mail;
+------------+-------+
| email | user |
+------------+-------+-
| email1#gmail | 1 |
| email2#gmail | 2 |
+------------+-------+--------+------------+
and the array table:
mysql> select * from user_mail_array;
+------------+-------+-------------+
| email | user | preferences |
+------------+-------+-------------+
| email1#gmail | 1 | 1 |
| email1#gmail | 1 | 2 |
| email1#gmail | 1 | 3 |
| email1#gmail | 1 | 4 |
| email2#gmail | 2 | 5 |
| email2#gmail | 2 | 6 |
We can select the rows of the second table as one array with CONCAT function:
mysql> SELECT t1.*, GROUP_CONCAT(t2.preferences) AS preferences
FROM user_mail t1,user_mail_array t2
where t1.email=t2.email and t1.user=t2.user
GROUP BY t1.email,t1.user;
+------------+-------+--------+------------+-------------+
| email | user | preferences |
+------------+-------+--------+------------+-------------+
|email1#gmail | 1 | 1,3,2,4 |
|email2#gmail | 2 | 5,6 |
+------------+-------+--------+------------+-------------+
In MYSQL version after 5.7.x, you can use JSON type to store an array. You can get value of an array by a key via MYSQL.
Inspired by the function ELT(index number, string1, string2, string3,…),I think the following example works as an array example:
set #i := 1;
while #i <= 3
do
insert into table(val) values (ELT(#i ,'val1','val2','val3'...));
set #i = #i + 1;
end while;
Hope it help.
Here is an example for MySQL for looping through a comma delimited string.
DECLARE v_delimited_string_access_index INT;
DECLARE v_delimited_string_access_value VARCHAR(255);
DECLARE v_can_still_find_values_in_delimited_string BOOLEAN;
SET v_can_still_find_values_in_delimited_string = true;
SET v_delimited_string_access_index = 0;
WHILE (v_can_still_find_values_in_delimited_string) DO
SET v_delimited_string_access_value = get_from_delimiter_split_string(in_array, ',', v_delimited_string_access_index); -- get value from string
SET v_delimited_string_access_index = v_delimited_string_access_index + 1;
IF (v_delimited_string_access_value = '') THEN
SET v_can_still_find_values_in_delimited_string = false; -- no value at this index, stop looping
ELSE
-- DO WHAT YOU WANT WITH v_delimited_string_access_value HERE
END IF;
END WHILE;
this uses the get_from_delimiter_split_string function defined here: https://stackoverflow.com/a/59666211/3068233
I Think I can improve on this answer. Try this:
The parameter 'Pranks' is a CSV. ie. '1,2,3,4.....etc'
CREATE PROCEDURE AddRanks(
IN Pranks TEXT
)
BEGIN
DECLARE VCounter INTEGER;
DECLARE VStringToAdd VARCHAR(50);
SET VCounter = 0;
START TRANSACTION;
REPEAT
SET VStringToAdd = (SELECT TRIM(SUBSTRING_INDEX(Pranks, ',', 1)));
SET Pranks = (SELECT RIGHT(Pranks, TRIM(LENGTH(Pranks) - LENGTH(SUBSTRING_INDEX(Pranks, ',', 1))-1)));
INSERT INTO tbl_rank_names(rank)
VALUES(VStringToAdd);
SET VCounter = VCounter + 1;
UNTIL (Pranks = '')
END REPEAT;
SELECT VCounter AS 'Records added';
COMMIT;
END;
This method makes the searched string of CSV values progressively shorter with each iteration of the loop, which I believe would be better for optimization.
I would try something like this for multiple collections. I'm a MySQL beginner. Sorry about the function names, couldn't decide on what names would be best.
delimiter //
drop procedure init_
//
create procedure init_()
begin
CREATE TEMPORARY TABLE if not exists
val_store(
realm varchar(30)
, id varchar(30)
, val varchar(255)
, primary key ( realm , id )
);
end;
//
drop function if exists get_
//
create function get_( p_realm varchar(30) , p_id varchar(30) )
returns varchar(255)
reads sql data
begin
declare ret_val varchar(255);
declare continue handler for 1146 set ret_val = null;
select val into ret_val from val_store where id = p_id;
return ret_val;
end;
//
drop procedure if exists set_
//
create procedure set_( p_realm varchar(30) , p_id varchar(30) , p_val varchar(255) )
begin
call init_();
insert into val_store (realm,id,val) values (p_realm , p_id , p_val) on duplicate key update val = p_val;
end;
//
drop procedure if exists remove_
//
create procedure remove_( p_realm varchar(30) , p_id varchar(30) )
begin
call init_();
delete from val_store where realm = p_realm and id = p_id;
end;
//
drop procedure if exists erase_
//
create procedure erase_( p_realm varchar(30) )
begin
call init_();
delete from val_store where realm = p_realm;
end;
//
call set_('my_array_table_name','my_key','my_value');
select get_('my_array_table_name','my_key');
Rather than Saving data as a array or in one row only you should be making diffrent rows for every value received. This will make it much simpler to understand rather than putting all together.
Have you tried using PHP's serialize()?
That allows you to store the contents of a variable's array in a string PHP understands and is safe for the database (assuming you've escaped it first).
$array = array(
1 => 'some data',
2 => 'some more'
);
//Assuming you're already connected to the database
$sql = sprintf("INSERT INTO `yourTable` (`rowID`, `rowContent`) VALUES (NULL, '%s')"
, serialize(mysql_real_escape_string($array, $dbConnection)));
mysql_query($sql, $dbConnection) or die(mysql_error());
You can also do the exact same without a numbered array
$array2 = array(
'something' => 'something else'
);
or
$array3 = array(
'somethingNew'
);

Select Inside Insert Statement - MySQL Cursors

I tried some, but I couldn't find the solution, somehow I managed to get this result.
Here is the query:
DELIMITER ##
CREATE PROCEDURE test1(start_date DATE,end_date DATE)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE a INT;
DECLARE present INT;
DECLARE total INT;
-- Declare the cursor
DECLARE id CURSOR
FOR
SELECT staff_id FROM ost_staff;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- Open the cursor
DROP TEMPORARY TABLE IF EXISTS reports;
CREATE TEMPORARY TABLE IF NOT EXISTS reports
(
staff_id INT(10),
present INT(10),
total INT(10)
);
OPEN id;
read_loop: LOOP
FETCH id INTO a;
IF done THEN
LEAVE read_loop;
END IF;
INSERT INTO reports(staff_id,present,total)
SELECT (COUNT(I.interval_start)) AS present, DATEDIFF(DATE_ADD(end_date,INTERVAL 1 DAY),start_date) AS total
FROM effort_frequency E
RIGHT OUTER JOIN time_intervals I ON I.interval_start = E.log_date
AND E.staffid=a AND E.log_date BETWEEN start_date AND end_date
LEFT OUTER JOIN ost_holidays H ON H.holiday_date = I.interval_start
WHERE DATE_FORMAT(I.interval_start,'%a') = 'Sun' OR H.holiday_date = I.interval_start OR E.total_effortspent IS NOT NULL;
-- Close the cursor
END LOOP;
CLOSE id;
END ##
I got the below result:
+----------+-----------------+
| staff_id | present | total |
+----------+---------+-------+
| (NULL) | 23 | 24 |
| (NULL) | 22 | 24 |
+----------+---------+-------+
I'm getting (NULL) for staff_id, How can I get the staff_id's there ?
I tried using declared variable 'a' in insert statement, but at that time I got only staff_id, I didn't get the other 2 fields, I can't get the staff_id from the select inside insert statement coz there is some problem.
Now what i need is I need to insert the staff_id from the variable 'a' into that temporary table.
note: I'm really new to this stored procedure, but somehow managed till here, Its good if I get some detail on how to use the Select inside Insert including the solution for this.
Try this -
SELECT a, (COUNT(I.interval_start)) AS present, DATEDIFF(DATE_ADD(end_date,INTERVAL 1 DAY),start_date) AS total
Your INSERT requires three fields, but your SELECT statement only selects two: present and total .
Try:
SELECT E.staffid, (COUNT(I.interval_start))
AS present, DATEDIFF(DATE_ADD(end_date,INTERVAL 1 DAY),start_date) AS total

How to query a MySql table to display the root and its subchild.

UserID UserName ParentID TopID
1 abc Null Null
2 edf 1 1
3 gef 1 1
4 huj 3 1
5 jdi 4 1
6 das 2 1
7 new Null Null
8 gka 7 7
TopID and ParentID is from the userID
I Want to get a user record and its child and subchild record. Here userid1 is the root and its child are userid2 and userid 3. So If the user id is 1 I have to display all the records from userid 1 to userid 6 since all are child and SUbchild of the root. Similarly for userid3 I have to display userid3 and its child Userid 4 and Child of Userid 4 Userid5
if the userid is 3
output should be
Userid Username
3 gef
4 huj
5 jdi
I will know the userid and the topID so how can I do the query to acheive the above result.
SELECT UserID, UserName FROM tbl_User WHERE ParentID=3 OR UserID=3 And TopID=1;
By the above query I am able to display userid 3 and userid 4 I am not able to display userid 5, Kind of struck in it. Need help. Thanks
It is technically possible to do recursive hierarchical queries in MySQL using stored procedures.
Here is one adapted to your scenario:
CREATE TABLE `user` (
`UserID` int(16) unsigned NOT NULL,
`UserName` varchar(32),
`ParentID` int(16) DEFAULT NULL,
`TopID` int(16) DEFAULT NULL,
PRIMARY KEY (`UserID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO user VALUES (1, 'abc', NULL, NULL), (2, 'edf', 1, 1), (3, 'gef', 1, 1),
(4, 'huj', 3, 1), (5, 'jdi', 4, 1), (6, 'das', 2, 1), (7, 'new', NULL, NULL),
(8, 'gka', 7, 7);
DELIMITER $$
DROP PROCEDURE IF EXISTS `Hierarchy` $$
CREATE PROCEDURE `Hierarchy` (IN GivenID INT, IN initial INT)
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE next_id INT;
-- CURSOR TO LOOP THROUGH RESULTS --
DECLARE cur1 CURSOR FOR SELECT UserID FROM user WHERE ParentID = GivenID;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
-- CREATE A TEMPORARY TABLE TO HOLD RESULTS --
IF initial=1 THEN
-- MAKE SURE TABLE DOESN'T CONTAIN OUTDATED INFO IF IT EXISTS (USUALLY ON ERROR) --
DROP TABLE IF EXISTS OUT_TEMP;
CREATE TEMPORARY TABLE OUT_TEMP (userID int, UserName varchar(32));
END IF;
-- ADD OURSELF TO THE TEMPORARY TABLE --
INSERT INTO OUT_TEMP SELECT UserID, UserName FROM user WHERE UserID = GivenID;
-- AND LOOP THROUGH THE CURSOR --
OPEN cur1;
read_loop: LOOP
FETCH cur1 INTO next_id;
-- NO ROWS FOUND, LEAVE LOOP --
IF done THEN
LEAVE read_loop;
END IF;
-- NEXT ROUND --
CALL Hierarchy(next_id, 0);
END LOOP;
CLOSE cur1;
-- THIS IS THE INITIAL CALL, LET'S GET THE RESULTS --
IF initial=1 THEN
SELECT * FROM OUT_TEMP;
-- CLEAN UP AFTER OURSELVES --
DROP TABLE OUT_TEMP;
END IF;
END $$
DELIMITER ;
CALL Hierarchy(3,1);
+--------+----------+
| userID | UserName |
+--------+----------+
| 3 | gef |
| 4 | huj |
| 5 | jdi |
+--------+----------+
3 rows in set (0.07 sec)
Query OK, 0 rows affected (0.07 sec)
CALL Hierarchy(1,1);
+--------+----------+
| userID | UserName |
+--------+----------+
| 1 | abc |
| 2 | edf |
| 6 | das |
| 3 | gef |
| 4 | huj |
| 5 | jdi |
+--------+----------+
6 rows in set (0.10 sec)
Query OK, 0 rows affected (0.10 sec)
Time to point out some caveats:
Since this is recursively calling a stored procedure, you need to increase the size of max_sp_recursion_depth, which has a max value of 255 (defaults to 0).
My results on a non-busy server with the limited test data (10 tuples of the user table) took 0.07-0.10 seconds to complete. The performance is such that it might be best to put the recursion in your application layer.
I didn't take advantage of your TopID column, so there might be a logic flaw. But the two test-cases gave me the expected results.
Disclaimer: This example was just to show that it can be done in MySQL, not that I endorse it in anyway. Stored Procedures, temporary tables and cursors are perhaps not the best way to do this problem.
Well not a pretty clean implementation but since you need only the children and sub-children, either of these might work:
Query1:
SELECT UserID, UserName
FROM tbl_user
WHERE ParentID = 3 OR UserID = 3
UNION
SELECT UserID, UserName
FROM tbl_user
WHERE ParentID IN (SELECT UserID
FROM tbl_user
WHERE ParentID = 3);
Query 2:
SELECT UserID, UserName
FROM tbl_user
WHERE UserID = 3
OR ParentID = 3
OR ParentID IN (SELECT UserID
FROM tbl_user
WHERE ParentID = 3);
EDIT 1: Alternatively, you may modify your table structure to make it more convenient to query all children of a particular category. Please follow this link to read more on storing hierarchical data in MySQL.
Also, you may think on storing your data hierarchically in a tree-like fashion that is very well explained in this article.
Please note that each method has its trade-offs with respect to retrieving desired results vs adding/removing categories but I'm sure you'll enjoy the reading.
This is one of the best articles I've seen for explaining the "Modified Preorder Tree Traversal" method of storing tree-like data in a SQL-style database.
http://www.sitepoint.com/hierarchical-data-database/
The MPTT stuff starts on page 2.
Essentially, you store a "Left" and a "Right" value for each node in the tree, in such a manner that to get all children of ParentA, you get the Left and Right for ParentA, then
SELECT *
FROM TableName
WHERE Left > ParentLeft
AND Right < ParentRight
To get all of the parents of the selected child (user_id = 3 in this example):
SELECT
#parent_id AS _user_id,
user_name,
(
SELECT #parent_id := parent_id
FROM users
WHERE user_id = _user_id
) AS parent
FROM (
-- initialize variables
SELECT
#parent_id := 3
) vars,
users u
WHERE #parent_id <> 0;
To get all of the children of a selected user_id
SELECT ui.user_id AS 'user_id', ui.user_name AS 'user_name', parent_id,
FROM
(
SELECT connect_by_parent(user_id) AS user_id
FROM (
SELECT
#start_user := 3,
#user_id := #start_user
) vars, users
WHERE #user_id IS NOT NULL
) uo
JOIN users ui ON ui.user_id = uo.user_id
This requires the following function
CREATE FUNCTION connect_by_parent(value INT) RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE _user_id INT;
DECLARE _parent_id INT;
DECLARE _next INT;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET #user_id = NULL;
SET _parent_id = #user_id;
SET _user_id = -1;
IF #user_id IS NULL THEN
RETURN NULL;
END IF;
LOOP
SELECT MIN(user_id)
INTO #user_id
FROM users
WHERE parent_id = _parent_id
AND user_id > _user_id;
IF #user_id IS NOT NULL OR _parent_id = #start_with THEN
RETURN #user_id;
END IF;
SELECT user_id, parent_id
INTO _user_id, _parent_id
FROM users
WHERE user_id = _parent_id;
END LOOP;
END
This example heavily uses session variables which many sql users may be unfamiliar with, so here's a link that may provide some insight: session variables