A multitude of the same id in an WHERE id IN () statement - mysql

I have a simple query that increases the value of a field by 1.
Now I used to loop over all id's and fire a query for each of them, but now that things are getting a bit resource heavy I wanted to optimize this. Normally I would just do
UPDATE table SET field = field + 1 WHERE id IN (all the ids here)
but now I have the problem that there are id's that occur twice (or more, I can't know that on forehand).
Is there a way to have the query run twice for id 4 if the query looks like this:
UPDATE table SET field = field + 1 WHERE id IN (1, 2, 3, 4, 4, 5)
Thanks,
lordstyx
Edit: sorry for not being clear enough.
The id here is an auto inc field, so it are all unique ID's. the id's that have to be updated are indirectly comming from users, so I can't predict which id is going to occur how often.
If there are the ID's (1, 2, 3, 4, 4, 5) I need the field of row with id 4 to be incremented with 2, and all the rest with 1.

If (1, 2, 3, 4, 4, 5) comes from a SELECT id ... query, then you can do something like this:
UPDATE yourTable
JOIN
( SELECT id
, COUNT(id) AS counter
....
GROUP BY id
) AS data
ON yourTable.id = data.id
SET yourTable.field = yourTable.field + data.counter
;
Since the input comes from users, perhaps you can manipulate it a bit. Change (1, 2, 3, 4, 4, 5) to (1), (2), (3), (4), (4), (5).
Then (having created a temporary table):
CREATE TABLE tempUpdate
( id INT )
;
Do the following procedure:
add the values in the temporary table,
run the update and
delete the values.
Code:
INSERT INTO TempUpdate
VALUES (1), (2), (3), (4), (4), (5)
;
UPDATE yourTable
JOIN
( SELECT id
, COUNT(id) AS counter
FROM TempUpdate
GROUP BY id
) AS data
ON yourTable.id = data.id
SET yourTable.field = yourTable.field + data.counter
;
DELETE FROM TempUpdate
;

No. But you could perform something like
UPDATE table
SET field = field + (LENGTH(',1,2,3,4,4,5,') - LENGTH(REPLACE(',1,2,3,4,4,5,', CONCAT(',', id, ','), ''))) / LENGTH(CONCAT(',', id, ','))
WHERE id IN (1, 2, 3, 4, 4, 5)
if you need row with id = 4 specifically to be incremented twice

Here is solution you wanted, but I'm not sure this is what you need.
Let's say that your talbe is called test. You want to increase id. I've added a field idwas to easily show what was the id before the query:
CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL auto_increment,
`idwas` int(8) unsigned default NULL,
PRIMARY KEY (`id`)
) ;
Let's fill it with data:
truncate table test;
insert into test(id) VALUES(1),(3),(15);
update test set idwas = id;
Now let's say that you have user input 1,3,5,3, so:
id 1 should be increased by 1
id 3 should be increased by 2
id 5 is missing, nothing to increase.
row with id 15 should not be changed because not in user input
We'll put the user input in a variable to be easier to use it:
SET #userInput = '1,3,5,3';
then do the magic:
SET #helperTable = CONCAT(
'SELECT us.id, count(us.id) as i FROM ',
'(SELECT ',REPLACE(#userInput, ',',' AS `id` UNION ALL SELECT '),
') AS us GROUP BY us.id');
SET #stmtText = CONCAT(
' UPDATE ',
'(',#helperTable,') AS h INNER JOIN test as t ON t.id = h.id',
' SET t.id = t.id + h.i');
PREPARE stmt FROM #stmtText;
EXECUTE stmt;
And this is the result:
mysql> SELECT * FROM test;
+----+-------+
| id | idwas |
+----+-------+
| 2 | 1 |
| 5 | 3 |
| 15 | 15 |
+----+-------+
3 rows in set (0.00 sec)

If it's reasonable, you could try doing a combination of what you had before and what you have now.
In whatever is creating this list, separate it into (depending on the language's constructs) some type of array. Follow this by sorting it,finding how many multiples of each there are, and doing whatever else you need to to get the following: an array with (increment-number => list of ids), so you do one query for each increment amount. Thus, your example becomes
UPDATE table SET field = field + 1 WHERE id IN (1, 2, 3, 5)
UPDATE table SET field = field + 2 WHERE id IN (4)
In php, for example, I would take the array, sort the array, use the content of the array as the keys for another array of the form (id => count), and then fold that over into the (count => list of ids) array.
It's not that efficient, but is definitely better than one query per id. It's also probably better than using iteration and string manipulation in SQL. Unless you're forced to use SQL to do everything (which it sounds like you're not), I wouldn't use it to do everything, when it's overly awkward to do so.

You could use the following:
create temporary table temp1 (id integer);
insert into temp1 (id) values (1),(2),(3),(4),(4),(5);
update your_table set your_field = your_field + (select count(*) from temp1 where id = your_table.id)
This solution requires you to format the id list like (1),(2),(3),(4),(4),(5) but I don't think that is a problem, right?
This worked on my test database, hope it works for you too!
Regards,
Arthur

Related

Adapt result of one row to other rows

How can I can write the query this way, that the result of the CASE WHEN - statement is adapted to every row. So that in every row the result will be 5. Thank you very much!
CREATE TABLE DATA
(`Person` CHAR(4),
`Apples` INT(1),
`Tomatoes` INT(1),
`Result` INT(1)
);
INSERT INTO DATA
(Person, Apples, Tomatoes)
VALUES ('Mark' , 1, 2),
('Sepp', 2, 3),
('Carl', 3, 1);
UPDATE DATA
SET `Result` = CASE WHEN (`Person` = 'Sepp') THEN (`Apples` + `Tomatoes`) END;
Table of result as it should be
SQL fiddle demonstration
If you want all rows to get the value from Sepps row you can do it using a subquery.
The "normal" way would be to do this:
UPDATE DATA
SET Result = (SELECT Apples + Tomatoes FROM DATA WHERE Person = 'Sepp')
But this will most likely give you an error with MySQL (can't specify target table for update) and a workaround is to introduce another level in the query which forces a temporary table to be used, like this:
UPDATE DATA
SET Result = (
SELECT Value FROM (
SELECT Apples + Tomatoes AS Value
FROM DATA WHERE Person = 'Sepp'
) t
);

Update multiple rows in table with values from a temporary table

I'm trying to write a database migration script to add a column to a table that contains existing data, and then populate that column with appropriate data.
I'm doing the migration in a few steps. I've created a temporary table that contains a single column with ids like this:
new_column
==========
1000
1001
1002
1003
...
I now want to update my existing table so that each row in the temporary table above is used to update each row in my existing table. The existing table looks like this:
old_column_1 | old_column_2 | new_column
========================================
1 | 100 | null
2 | 101 | null
3 | 102 | null
...
I've tried a few variations of this sort of update -
select min(t.new_column)
from temp t
where t.new_column not in (select new_column from existing_table);
But I can't seem to get the syntax right...
Your problem is more complicated than you think. There's nothing reliable to join on. So, either you write a stored procedure which uses a cursor to loop through both tables and updating the existing table row by row (which can quickly become a performance nightmare, therefore I wouldn't recommend it) or you use this a little complicated query:
CREATE TABLE temp
(id int auto_increment primary key, `new_column` int)
;
INSERT INTO temp
(`new_column`)
VALUES
(1000),
(1001),
(1002),
(1003)
;
CREATE TABLE existing
(`old_column_1` int, `old_column_2` int, `new_column` varchar(4))
;
INSERT INTO existing
(`old_column_1`, `old_column_2`, `new_column`)
VALUES
(1, 100, NULL),
(2, 101, NULL),
(3, 102, NULL)
;
update
existing e
inner join (
select * from (
select
t.*
from temp t
)t
inner join
(
select
e.old_column_1, e.old_column_2,
#rownum := #rownum + 1 as rn
from existing e
, (select #rownum:=0) vars
)e on t.id = e.rn
) sq on sq.old_column_1 = e.old_column_1 and sq.old_column_2 = e.old_column_2
set e.new_column = sq.new_column;
see it working live in an sqlfiddle
I added an auto_increment column in your temporary table. Either you do it this way, or you simulate a rownumber like I did here:
select
e.old_column_1, e.old_column_2,
#rownum := #rownum + 1 as rn
from existing e
, (select #rownum:=0) vars
If you want to influence which row gets which row number, you can use ORDER BY whatever_column ASC|DESC in there.
So, what the query basically does, is, to create a row number in your existing table and join it via this column and the auto_increment column in the temporary table. Then I join this subquery again to the existing table, so that we can easily copy the column from temporary table to existing table.

mysql insert with value equal to primary key + 1

I have an auto increment column ID, and for some situation I wanted the other column to be equal to the primary key + 1 value
ID | other
1 | 2
2 | 3
3 | 4
4 | 123 (some situation, it is not always plus 1)
How can I achieve this?
Here's what I have tried
INSERT INTO table (`ID`,`other`) VALUES ('',(SELECT MAX(ID)+1 FROM table))
But that returns an error
You can't specify target table 'table' for update in FROM clause
Try Below query:
ALTER TABLE dbo.table ADD
Column AS ([ID]+1)
GO
It will definitely work
Using a normal AUTO_INCREMENT column as id, I cannot think of a way to do this in MySQL. Triggers, which otherwise would have been an option, don't work well with AUTO_INCREMENT columns.
The only way I see is to do two commands for an INSERT;
INSERT INTO bop (value) VALUES ('These values should be 1 and 2');
UPDATE bop SET other = id+1 WHERE id = LAST_INSERT_ID();
An SQLfiddle to test with.
The closest I'm getting to what you're looking for is to generate sequences separately from AUTO_INCREMENT using a function, and use that instead to generate the table id;
DELIMITER //
CREATE TABLE bop (
id INT UNIQUE,
other INT,
value VARCHAR(64)
)//
CREATE TABLE bop_seq ( seq INT ) // -- Sequence table
INSERT INTO bop_seq VALUES (1) // -- Start value
CREATE FUNCTION bop_nextval() RETURNS int
BEGIN
SET #tmp = (SELECT seq FROM bop_seq FOR UPDATE);
UPDATE bop_seq SET seq = seq + 1;
RETURN #tmp;
END//
CREATE TRIGGER bop_auto BEFORE INSERT ON bop
FOR EACH ROW
SET NEW.id = bop_nextval(), NEW.other=NEW.id + 1;
//
That'd let you do inserts and have it autonumber like you want. The FOR UPDATE should keep the sequence transaction safe, but I've not load tested so you may want to do that.
Another SQLfiddle.
I solved this by updating 2 times the DB..
I wanted to do +1 from 19 till ..
UPDATE `table` SET `id`=`id`+101 WHERE id <= 19
UPDATE `table` SET `id`=`id`-100 WHERE id <= 119 AND id >= 101

Recursive mysql select?

I saw this answer and i hope he is incorrect, just like someone was incorrect telling primary keys are on a column and I can't set it on multiple columns.
Here is my table
create table Users(id INT primary key AUTO_INCREMENT,
parent INT,
name TEXT NOT NULL,
FOREIGN KEY(parent)
REFERENCES Users(id)
);
+----+--------+---------+
| id | parent | name |
+----+--------+---------+
| 1 | NULL | root |
| 2 | 1 | one |
| 3 | 1 | 1down |
| 4 | 2 | one_a |
| 5 | 4 | one_a_b |
+----+--------+---------+
I'd like to select user id 2 and recurse so I get all its direct and indirect child (so id 4 and 5).
How do I write it in such a way this will work? I seen recursion in postgresql and sqlserver.
CREATE DEFINER = 'root'#'localhost'
PROCEDURE test.GetHierarchyUsers(IN StartKey INT)
BEGIN
-- prepare a hierarchy level variable
SET #hierlevel := 00000;
-- prepare a variable for total rows so we know when no more rows found
SET #lastRowCount := 0;
-- pre-drop temp table
DROP TABLE IF EXISTS MyHierarchy;
-- now, create it as the first level you want...
-- ie: a specific top level of all "no parent" entries
-- or parameterize the function and ask for a specific "ID".
-- add extra column as flag for next set of ID's to load into this.
CREATE TABLE MyHierarchy AS
SELECT U.ID
, U.Parent
, U.`name`
, 00 AS IDHierLevel
, 00 AS AlreadyProcessed
FROM
Users U
WHERE
U.ID = StartKey;
-- how many rows are we starting with at this tier level
-- START the cycle, only IF we found rows...
SET #lastRowCount := FOUND_ROWS();
-- we need to have a "key" for updates to be applied against,
-- otherwise our UPDATE statement will nag about an unsafe update command
CREATE INDEX MyHier_Idx1 ON MyHierarchy (IDHierLevel);
-- NOW, keep cycling through until we get no more records
WHILE #lastRowCount > 0
DO
UPDATE MyHierarchy
SET
AlreadyProcessed = 1
WHERE
IDHierLevel = #hierLevel;
-- NOW, load in all entries found from full-set NOT already processed
INSERT INTO MyHierarchy
SELECT DISTINCT U.ID
, U.Parent
, U.`name`
, #hierLevel + 1 AS IDHierLevel
, 0 AS AlreadyProcessed
FROM
MyHierarchy mh
JOIN Users U
ON mh.Parent = U.ID
WHERE
mh.IDHierLevel = #hierLevel;
-- preserve latest count of records accounted for from above query
-- now, how many acrual rows DID we insert from the select query
SET #lastRowCount := ROW_COUNT();
-- only mark the LOWER level we just joined against as processed,
-- and NOT the new records we just inserted
UPDATE MyHierarchy
SET
AlreadyProcessed = 1
WHERE
IDHierLevel = #hierLevel;
-- now, update the hierarchy level
SET #hierLevel := #hierLevel + 1;
END WHILE;
-- return the final set now
SELECT *
FROM
MyHierarchy;
-- and we can clean-up after the query of data has been selected / returned.
-- drop table if exists MyHierarchy;
END
It might appear cumbersome, but to use this, do
call GetHierarchyUsers( 5 );
(or whatever key ID you want to find UP the hierarchical tree for).
The premise is to start with the one KEY you are working with. Then, use that as a basis to join to the users table AGAIN, but based on the first entry's PARENT ID. Once found, update the temp table as to not try and join for that key again on the next cycle. Then keep going until no more "parent" ID keys can be found.
This will return the entire hierarchy of records up to the parent no matter how deep the nesting. However, if you only want the FINAL parent, you can use the #hierlevel variable to return only the latest one in the file added, or ORDER BY and LIMIT 1
I know there is probably better and more efficient answer above but this snippet gives a slightly different approach and provides both - ancestors and children.
The idea is to constantly insert relative rowIds into temporary table, then fetch a row to look for it's relatives, rinse repeat until all rows are processed. Query can be probably optimized to use only 1 temporary table.
Here is a working sqlfiddle example.
CREATE TABLE Users
(`id` int, `parent` int,`name` VARCHAR(10))//
INSERT INTO Users
(`id`, `parent`, `name`)
VALUES
(1, NULL, 'root'),
(2, 1, 'one'),
(3, 1, '1down'),
(4, 2, 'one_a'),
(5, 4, 'one_a_b')//
CREATE PROCEDURE getAncestors (in ParRowId int)
BEGIN
DECLARE tmp_parentId int;
CREATE TEMPORARY TABLE tmp (parentId INT NOT NULL);
CREATE TEMPORARY TABLE results (parentId INT NOT NULL);
INSERT INTO tmp SELECT ParRowId;
WHILE (SELECT COUNT(*) FROM tmp) > 0 DO
SET tmp_parentId = (SELECT MIN(parentId) FROM tmp);
DELETE FROM tmp WHERE parentId = tmp_parentId;
INSERT INTO results SELECT parent FROM Users WHERE id = tmp_parentId AND parent IS NOT NULL;
INSERT INTO tmp SELECT parent FROM Users WHERE id = tmp_parentId AND parent IS NOT NULL;
END WHILE;
SELECT * FROM Users WHERE id IN (SELECT * FROM results);
END//
CREATE PROCEDURE getChildren (in ParRowId int)
BEGIN
DECLARE tmp_childId int;
CREATE TEMPORARY TABLE tmp (childId INT NOT NULL);
CREATE TEMPORARY TABLE results (childId INT NOT NULL);
INSERT INTO tmp SELECT ParRowId;
WHILE (SELECT COUNT(*) FROM tmp) > 0 DO
SET tmp_childId = (SELECT MIN(childId) FROM tmp);
DELETE FROM tmp WHERE childId = tmp_childId;
INSERT INTO results SELECT id FROM Users WHERE parent = tmp_childId;
INSERT INTO tmp SELECT id FROM Users WHERE parent = tmp_childId;
END WHILE;
SELECT * FROM Users WHERE id IN (SELECT * FROM results);
END//
Usage:
CALL getChildren(2);
-- returns
id parent name
4 2 one_a
5 4 one_a_b
CALL getAncestors(5);
-- returns
id parent name
1 (null) root
2 1 one
4 2 one_a

Connect By Prior Equivalent for MySQL

All,
I have three fields in a table that define a parent child relationship present in a MySQL database version 5.0 . The table name is tb_Tree and it has the following data:
Table Name: tb_Tree
Id | ParentId | Name
--------------------
1 | 0 | Fruits
2 | 0 | Vegetables
3 | 1 | Apple
4 | 1 | Orange
5 | 2 | Cabbage
6 | 2 | Eggplant
How do I write a Query to get all the children if a ParentId is specified. Note that the table entries given are just sample data and they can have many more rows. Oracle has a "CONNECT BY PRIOR" clause, but I didn't find anything similar for MySQL. Can anyone please advise?
Thanks
MySQL doesn't support recursive queries so you have to do it the hard way:
Select the rows where ParentID = X where X is your root.
Collect the Id values from (1).
Repeat (1) for each Id from (2).
Keep recursing by hand until you find all the leaf nodes.
If you know a maximum depth then you can join your table to itself (using LEFT OUTER JOINs) out to the maximum possible depth and then clean up the NULLs.
You could also change your tree representation to nested sets.
Might be late post.
With MySQL8 you can achieve it with recursive clause. Here is the example.
with recursive cte (id, name, parent_id) as (
select id,
name,
parent_id
from products
where parent_id = 19
union all
select p.id,
p.name,
p.parent_id
from products p
inner join cte
on p.parent_id = cte.id
)
select * from cte;
For more help find another thread, Hope It will help someone.
You Can also look into this interesting blog, which demonstrate how can we get similar results in mysql
http://explainextended.com/2009/03/17/hierarchical-queries-in-mysql/
This is an old thread, but since I got the question in another forum I thought I'd add it here. For this case, I created a stored procedure that is hard-coded to handle the specific case. This do, of course have some drawbacks since not all users can create stored procedures at will, but nevertheless.
Consider the following table with nodes and children:
CREATE TABLE nodes (
parent INT,
child INT
);
INSERT INTO nodes VALUES
( 5, 2), ( 5, 3),
(18, 11), (18, 7),
(17, 9), (17, 8),
(26, 13), (26, 1), (26,12),
(15, 10), (15, 5),
(38, 15), (38, 17), (38, 6),
(NULL, 38), (NULL, 26), (NULL, 18);
With this table, the following stored procedure will compute a result set consisting of all the decedents of the node provided:
delimiter $$
CREATE PROCEDURE find_parts(seed INT)
BEGIN
-- Temporary storage
DROP TABLE IF EXISTS _result;
CREATE TEMPORARY TABLE _result (node INT PRIMARY KEY);
-- Seeding
INSERT INTO _result VALUES (seed);
-- Iteration
DROP TABLE IF EXISTS _tmp;
CREATE TEMPORARY TABLE _tmp LIKE _result;
REPEAT
TRUNCATE TABLE _tmp;
INSERT INTO _tmp SELECT child AS node
FROM _result JOIN nodes ON node = parent;
INSERT IGNORE INTO _result SELECT node FROM _tmp;
UNTIL ROW_COUNT() = 0
END REPEAT;
DROP TABLE _tmp;
SELECT * FROM _result;
END $$
delimiter ;
The below select lists all plants and their parentid up to 4-level (and of course you can extend the level):
select id, name, parentid
,(select parentid from tb_tree where id=t.parentid) parentid2
,(select parentid from tb_tree where id=(select parentid from tb_tree where id=t.parentid)) parentid3
,(select parentid from tb_tree where id=(select parentid from tb_tree where id=(select parentid from tb_tree where id=t.parentid))) parentid4
from tb_tree t
and then you can use this query to get the final result. for example, you can get all children of "Fruits" by the below sql:
select id ,name from (
select id, name, parentid
,(select parentid from tb_tree where id=t.parentid) parentid2
,(select parentid from tb_tree where id=(select parentid from tb_tree where id=t.parentid)) parentid3
,(select parentid from tb_tree where id=(select parentid from tb_tree where id=(select parentid from tb_tree where id=t.parentid))) parentid4
from tb_tree t) tt
where ifnull(parentid4,0)=1 or ifnull(parentid3,0)=1 or ifnull(parentid2,0)=1 or ifnull(parentid,0)=1
The below stored procedure order a table that has rows with back reference to the previous one. Notice on the first step I copy rows into temp table - those rows match some condition. In my case those are rows that belong to the same linear (road that is used in GPS navigation). Business domain is not important. Just in my case I am sorting segments that belong to the same road
DROP PROCEDURE IF EXISTS orderLocations;
DELIMITER //
CREATE PROCEDURE orderLocations(_full_linear_code VARCHAR(11))
BEGIN
DECLARE _code VARCHAR(11);
DECLARE _id INT(4);
DECLARE _count INT(4);
DECLARE _pos INT(4);
DROP TEMPORARY TABLE IF EXISTS temp_sort;
CREATE TEMPORARY TABLE temp_sort (
id INT(4) PRIMARY KEY,
pos INT(4),
code VARCHAR(11),
prev_code VARCHAR(11)
);
-- copy all records to sort into temp table - this way sorting would go all in memory
INSERT INTO temp_sort SELECT
id, -- this is primary key of original table
NULL, -- this is position that still to be calculated
full_tmc_code, -- this is a column that references sorted by
negative_offset -- this is a reference to the previous record (will be blank for the first)
FROM tmc_file_location
WHERE linear_full_tmc_code = _full_linear_code;
-- this is how many records we have to sort / update position
SELECT count(*)
FROM temp_sort
INTO _count;
-- first position index
SET _pos = 1;
-- pick first record that has no prior record
SELECT
code,
id
FROM temp_sort l
WHERE prev_code IS NULL
INTO _code, _id;
-- update position of the first record
UPDATE temp_sort
SET pos = _pos
WHERE id = _id;
-- all other go by chain link
WHILE (_pos < _count) DO
SET _pos = _pos +1;
SELECT
code,
id
FROM temp_sort
WHERE prev_code = _code
INTO _code, _id;
UPDATE temp_sort
SET pos = _pos
WHERE id = _id;
END WHILE;
-- join two tables and return position along with all other fields
SELECT
t.pos,
l.*
FROM tmc_file_location l, temp_sort t
WHERE t.id = l.id
ORDER BY t.pos;
END;