Related
We added two columns to our main MySQL table (named objects), a column for quantity and a column for id of the units. If the quantity and/or units are unknown, those columns are set to NULL.
We wanted to join this table with the unittype table to display the name of the units (pounds, pages, grams, whatever).
When I run the SELECT inside MYSQL's command line client, it returns the expected the results. When my PHP page runs the query, the first row shows values from the wrong side of the join.
Example:
The tables being joined are objects and unittypes with these definitions
mysql> show create table objects;
CREATE TABLE `objects` (
`objId` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`description` varchar(250) DEFAULT NULL,
`quantity` int DEFAULT NULL,
`unitid` int DEFAULT NULL,
`create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modify_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`objId`),
KEY `unitid` (`unitid`),
CONSTRAINT `objects_ibfk_1` FOREIGN KEY (`unitid`) REFERENCES `unittypes` (`unitId`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4735 DEFAULT CHARSET=utf8
and:
mysql> show create table unittypes;
CREATE TABLE `unittypes` (
`unitId` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`description` varchar(50) DEFAULT NULL,
`create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`modify_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`unitId`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
Sample rows:
Unittypes:
+--------+------+--------------+-
| unitId | name | description |
+--------+------+--------------+-
| 1 | Each | Each |
Objects:
+-------+---------------------------------------+-------------+----------+--------+
| objId | NAME | description | quantity | unitid |
+-------+---------------------------------------+-------------+----------+--------+
| 1018 | UNKNOWN cables | NULL | NULL | NULL |
| 3466 | UNKNOWN replies | NULL | NULL | NULL |
+-------+---------------------------------------+-------------+----------+--------+
| 722 | Soundgarden-Loudest Love | NULL | 1 | 1 |
| 4703 | Soundgarden-Live FROM The Artists Den | NULL | 1 | 1 |
+-------+---------------------------------------+-------------+----------+--------+
Sample join query:
SELECT o.objid, o.name, o.description,
o.quantity, u.name, u.description, o.create_date, o.modify_date
FROM objects AS o
LEFT JOIN unittypes u USING(unitid)
WHERE o.NAME LIKE '%Soundgarden-L%'
In MYSql command line client it returns:
+-------+---------------------------------------+-------------+----------+------+-------------+---------------------+---------------------+
| objid | name | description | quantity | name | description | create_date | modify_date |
+-------+---------------------------------------+-------------+----------+------+-------------+---------------------+---------------------+
| 722 | Soundgarden-Loudest Love | NULL | 1 | Each | Each | 2017-04-01 05:44:56 | 2020-01-15 08:59:45 |
| 4703 | Soundgarden-Live From The Artists Den | NULL | 1 | Each | Each | 2019-09-17 21:52:26 | 2020-01-15 08:59:45 |
+-------+---------------------------------------+-------------+----------+------+-------------+---------------------+---------------------+
In PHP page, it returns (PHP page returned the expected results without a join:
objid name description quantity create_date modify_date
722 Each Each 1 2017-04-01 05:44:56 2020-01-15 08:59:45
4703 Soundgarden-Live From The Artists Den NULL 1 Each Each 2019-09-17 21:52:26 2020-01-15 08:59:45
Result set has 3 rows.
Example when unittypes doesn't have a matching row:
In command line client:
SELECT o.objid, o.name, o.description,
o.quantity, u.name, u.description, o.create_date, o.modify_date
FROM objects AS o
LEFT JOIN unittypes u USING(unitid)
WHERE o.NAME LIKE '%unknown %';
result:
+-------+--------------------------------------------------+-------------+----------+------+-------------+---------------------+---------------------+
| objid | name | description | quantity | name | description | create_date | modify_date |
+-------+--------------------------------------------------+-------------+----------+------+-------------+---------------------+---------------------+
| 1018 | Unknown cables | NULL | NULL | NULL | NULL | 2017-06-27 02:22:12 | 2017-06-27 02:22:12 |
| 3466 | Unknown replies | NULL | NULL | NULL | NULL | 2018-11-05 01:45:17 | 2018-11-05 01:45:17 |
|
In PHP page:
objid name description quantity create_date modify_date
1018 NULL NULL NULL 2017-06-27 02:22:12 2017-06-27 02:22:12
3466 Unknown replies NULL NULL NULL NULL 2018-11-05 01:45:17 2018-11-05 01:45:17
I get the same results using the join syntax
left join unittypes u on o.unitid = u.unitid
PHP code that performs query:
if ($result = $GLOBALS['DB']->query($selObj)) {
if ($row = $result->fetch_assoc()) {
$row_cnt = $result->num_rows;
echo '<table><tr>';
foreach(array_keys($row) as $heading) echo "<th>$heading</th>";
echo '</tr>';
/* process result set */
do {
echo '<tr>';
foreach($row as $item) echo '<td>'.($item==NULL?'NULL':$item).'</td>';
echo '</tr>';
} while ($row = $result->fetch_row()); // get next result set
echo '</table>';
printf("Result set has %d row%s.<br><br>", $row_cnt,($row_cnt==1?"":"s"));
$result->close();
} else echo 'Empty result set<br><br>';
} else echo $DB->error;
I would expect the same query to behave the same way in both examples.
I would have guessed that the command-line and the PHP simply hand off a string to mySQL to process, but there is obviously some difference I'm not seeing.
Is there something weird about how the PHP passes the query in; does MYsql not handle joins through that interface properly; is the code wrong (it works without a join)?
(I also asked this at https://dba.stackexchange.com/q/258940/200179)
You have two columns called name. The second will overwrite the first.
You should alias them to have distinct names. select `o`.`name` as `o_name` ...
In your case, the reason why only the first row is affected is because you use fetch_assoc to get that, but then fetch_row to get the others (and fetch_row works because it ignores column names entirely)
I'm trying to normalize a table which a previous developer designed to have a column containing pipe-separated IDs which link to other rows in the same table.
Customers Table
id | aliases (VARCHAR)
----------------------------
1 | |4|58|76
2 |
3 |
4 | |1|58|76
... |
58 | |1|4|76
... |
76 | |1|4|58
So customer 1, 4, 58 and 76 are all "aliases" of each other. Customer 2 and 3 have no aliases, so the field contains an empty string.
I want to do away with the entire "alias" system, and normalise the data so I can map those other customers all to the one record. So I want related table data for customer 1, 4, 58, and 76 all to be mapped just to customer 1.
I figured I would populate a new table which later I can then join and perform updates on other tables.
Join Table
id | customer_id | alias_id
-------------------------------
1 | 1 | 4
2 | 1 | 58
3 | 1 | 76
How can I get the data from that first table, into the above format? If this is going to be an absolute nightmare in pure SQL, I will just write a PHP script which attempts to do this work and insert the data.
When I started to answer this question, I thought it would be quick and easy because I'd done something very similar once in SQL Server, but proving out the concept in translation burgeoned into this full solution.
One caveat that wasn't clear from your question is whether you have a condition for declaring the primary id vs the alias id. For instance, this solution will allow 1 to have an alias of 4 as well as 4 to have an alias of 1, which is consistent with the provided data in your simplified example question.
To setup the data for this example, I used this structure:
CREATE TABLE notnormal_customers (
id INT NOT NULL PRIMARY KEY,
aliases VARCHAR(10)
);
INSERT INTO notnormal_customers (id,aliases)
VALUES
(1,'|4|58|76'),
(2,''),
(3,''),
(4,'|1|58|76'),
(58,'|1|4|76'),
(76,'|1|4|58');
First, in order to represent the one-to-many relationship for one-customer to many-aliases, I created this table:
CREATE TABLE customer_aliases (
primary_id INT NOT NULL,
alias_id INT NOT NULL,
FOREIGN KEY (primary_id) REFERENCES notnormal_customers(id),
FOREIGN KEY (alias_id) REFERENCES notnormal_customers(id),
/* clustered primary key prevents duplicates */
PRIMARY KEY (primary_id,alias_id)
)
Most importantly, we'll use a custom SPLIT_STR function:
CREATE FUNCTION SPLIT_STR(
x VARCHAR(255),
delim VARCHAR(12),
pos INT
)
RETURNS VARCHAR(255)
RETURN REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos),
LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1),
delim, '');
Then we'll create a stored procedure to do all the work. Code is annotated with comments to source references.
DELIMITER $$
CREATE PROCEDURE normalize_customers()
BEGIN
DECLARE cust_id INT DEFAULT 0;
DECLARE al_id INT UNSIGNED DEFAULT 0;
DECLARE alias_str VARCHAR(10) DEFAULT '';
/* set the value of the string delimiter */
DECLARE string_delim CHAR(1) DEFAULT '|';
DECLARE count_aliases INT DEFAULT 0;
DECLARE i INT DEFAULT 1;
/*
use cursor to iterate through all customer records
http://burnignorance.com/mysql-tips/how-to-loop-through-a-result-set-in-mysql-strored-procedure/
*/
DECLARE done INT DEFAULT 0;
DECLARE cur CURSOR FOR
SELECT `id`, `aliases`
FROM `notnormal_customers`;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
/*
Fetch one record from CURSOR and set to customer id and alias string.
If not found then `done` will be set to 1 by continue handler.
*/
FETCH cur INTO cust_id, alias_str;
IF done THEN
/* If done set to 1 then exit the loop, else continue. */
LEAVE read_loop;
END IF;
/* skip to next record if no aliases */
IF alias_str = '' THEN
ITERATE read_loop;
END IF;
/*
get number of aliases
https://pisceansheart.wordpress.com/2008/04/15/count-occurrence-of-character-in-a-string-using-mysql/
*/
SET count_aliases = LENGTH(alias_str) - LENGTH(REPLACE(alias_str, string_delim, ''));
/* strip off the first pipe to make it compatible with our SPLIT_STR function */
SET alias_str = SUBSTR(alias_str, 2);
/*
iterate and get each alias from custom split string function
https://stackoverflow.com/questions/18304857/split-delimited-string-value-into-rows
*/
WHILE i <= count_aliases DO
/* get the next alias id */
SET al_id = CAST(SPLIT_STR(alias_str, string_delim, i) AS UNSIGNED);
/* REPLACE existing values instead of insert to prevent errors on primary key */
REPLACE INTO customer_aliases (primary_id,alias_id) VALUES (cust_id,al_id);
SET i = i+1;
END WHILE;
SET i = 1;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
Finally you can simply run it by calling:
CALL normalize_customers();
Then you can check the data in console:
mysql> select * from customer_aliases;
+------------+----------+
| primary_id | alias_id |
+------------+----------+
| 4 | 1 |
| 58 | 1 |
| 76 | 1 |
| 1 | 4 |
| 58 | 4 |
| 76 | 4 |
| 1 | 58 |
| 4 | 58 |
| 76 | 58 |
| 1 | 76 |
| 4 | 76 |
| 58 | 76 |
+------------+----------+
12 rows in set (0.00 sec)
Update 2 (One-Query-Solution)
Assuming that the aliases list is always sorted, you can achieve the result with only one query:
CREATE TABLE aliases (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id INT UNSIGNED NOT NULL,
alias_id INT UNSIGNED NOT NULL
) AS
SELECT NULL AS id, c1.id AS customer_id, c2.id AS alias_id
FROM customers c1
JOIN customers c2
ON c2.aliases LIKE CONCAT('|', c1.id , '|%') -- c1.id is the first alias of c2.id
WHERE c1.id < (SUBSTRING(c1.aliases,2)+0) -- c1.id is smaller than the first alias of c2.id
It will also be much faster, if the aliases column is indexed, so the JOIN will be supported by a range search.
sqlfiddle
Original answer
If you replace the pipes with commas, you can use the FIND_IN_SET function.
I would first create a temporary table (does not need to be technicaly temporary) to store comma separated alias lists:
CREATE TABLE tmp (`id` int, `aliases` varchar(50));
INSERT INTO tmp(`id`, `aliases`)
SELECT id, REPLACE(aliases, '|', ',') AS aliases
FROM customers;
Then populate your normalized table using FIND_IN_SET in the JOINs ON clause:
CREATE TABLE aliases (`id` int, `customer_id` int, `alias_id` int) AS
SELECT t.id as customer_id, c.id AS alias_id
FROM tmp t
JOIN customers c ON find_in_set(c.id, t.aliases);
If needed - delete duplicates with higher customer_id (only keep lowest):
DELETE FROM aliases
WHERE customer_id IN (SELECT * FROM(
SELECT DISTINCT a1.customer_id
FROM aliases a1
JOIN aliases a2
ON a2.customer_id = a1.alias_id
AND a1.customer_id = a2.alias_id
AND a1.customer_id > a1.alias_id
)derived);
If needed - create AUTO_INCREMENT id:
ALTER TABLE aliases ADD column id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
The aliases table will now look like that:
| id | customer_id | alias_id |
|----|-------------|----------|
| 1 | 1 | 4 |
| 2 | 1 | 58 |
| 3 | 1 | 76 |
sqlfiddle
Don't forget to define proper indexes.
Update 1
You can skip creating a temporary table and populate the aliases table using LIKE instead of FIND_IN_SET:
CREATE TABLE aliases (`customer_id` int, `alias_id` int) AS
SELECT c2.id as customer_id, c1.id AS alias_id
FROM customers c1
JOIN customers c2
ON CONCAT(c1.aliases, '|') LIKE CONCAT('%|', c2.id , '|%');
sqlfiddle
Using a table of integers (0-9) - although you can achieve the same the thing with (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3...etc.)...
SELECT DISTINCT id old_id /* the technique below inevitably creates duplicates. */
/* DISTINCT discards them. */
, SUBSTRING_INDEX(
SUBSTRING_INDEX(SUBSTR(aliases,2),'|',i+1) /* isolate text between */
,'|',-1) x /* each pipe and the next */
FROM customers
, ints /* do this for the first 10 pipes in each string */
ORDER
BY id,x+0 /* implicit CASTING */
+--------+------+
| old_id | x |
+--------+------+
| 1 | 4 |
| 1 | 58 |
| 1 | 76 |
| 2 | NULL |
| 3 | NULL |
| 4 | 1 |
| 4 | 58 |
| 4 | 76 |
| 58 | 1 |
| 58 | 4 |
| 58 | 76 |
| 76 | 1 |
| 76 | 4 |
| 76 | 58 |
+--------+------+
(Edit: In line comments added)
Database example:
| country | animal | size | x_id* |
|---------+--------+--------+-------|
| 777 | 1001 | small | 1 |
| 777 | 2002 | medium | 2 |
| 777 | 7007 | medium | 3 |
| 777 | 7007 | large | 4 |
| 42 | 1001 | small | 1 |
| 42 | 2002 | medium | 2 |
| 42 | 7007 | large | 4 |
I need to generate the x_id continuously based on entries in (animal, size) and if x_id for the combination x_id exist use it again.
Currently i use the following PHP script for this action, but on a large db table it is very slow.
query("UPDATE myTable SET x_id = -1");
$i = $j;
$c = array();
$q = query("
SELECT animal, size
FROM myTable
WHERE x_id = -1
GROUP BY animal, size");
while($r = fetch_array($q)) {
$hk = $r['animal'] . '-' . $r['size'];
if( !isset( $c[$hk] ) ) $c[$hk] = $i++;
query("
UPDATE myTable
SET x_id = {$c[$hk]}
WHERE animal = '".$r['animal']."'
AND size = '".$r['size']."'
AND x_id = -1");
}
Is there a way to convert the PHP script to one or two mysql commands?
edit:
CREATE TABLE `myTable` (
`country` int(10) unsigned NOT NULL DEFAULT '1', -- country
`animal` int(3) NOT NULL,
`size` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`lang_id` tinyint(4) NOT NULL DEFAULT '1',
`x_id` int(10) NOT NULL,
KEY `country` (`country`),
KEY `x_id` (`x_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
UPDATE myTable m
JOIN (
SELECT animal, size, #newid := #newid + 1 AS x_id
FROM myTable a
CROSS JOIN (SELECT #newid := 0) b
WHERE x_id = -1
GROUP BY animal, size
) t ON m.animal = t.animal AND m.size = t.size
SET m.x_id = t.x_id
;
http://sqlfiddle.com/#!9/5525ba/1
The group by in the subquery is not needed. It generates useless overhead. If it's fast enough, leave it like this, otherwise we can use distinct+another subquery instead.
User variables are awkward but should do the trick,tested on my machine
CREATE TABLE t
( animal VARCHAR(20),
size VARCHAR(20),
x_id INT);
INSERT INTO T(animal,size) VALUES('crocodile','small'),
('elephant','medium'),
('giraffe','medium'),
('giraffe','large'),
('crocodile','small'),
('elephant','medium'),
('giraffe','large');
UPDATE t RIGHT JOIN
(SELECT animal,size,
MIN(CASE WHEN #var:=CONCAT(animal,size) THEN #id ELSE #id:=#id+1 END)id
FROM t,
(SELECT #var:=CONCAT(animal,size) FROM t)x ,
(SELECT #id:=0)y
GROUP BY animal,size)q
ON t.animal=q.animal AND t.size=q.size
SET x_id=q.id
Results
"animal" "size" "x_id"
"crocodile" "small" "1"
"elephant" "medium" "2"
"giraffe" "medium" "3"
"giraffe" "large" "4"
"crocodile" "small" "1"
"elephant" "medium" "2"
"giraffe" "large" "4"
You want these indexes added for (a lot) faster access
ALTER TABLE `yourtable` ADD INDEX `as_idx` (`animal`,`size`);
ALTER TABLE `yourtable` ADD INDEX `id_idx` (`x_id`);
This is a conceptual. Worm it into your world if useful.
Schema
create table AnimalSize
( id int auto_increment primary key,
animal varchar(100) not null,
size varchar(100) not null,
unique key(animal,size) -- this is critical, no dupes
);
create table CountryAnimalSize
( id int auto_increment primary key,
country varchar(100) not null,
animal varchar(100) not null,
size varchar(100) not null,
xid int not null -- USE THE id achieved thru use of AnimalSize table
);
Some queries
-- truncate table animalsize; -- clobber and reset auto_increment back to 1
insert ignore AnimalSize(animal,size) values ('snake','small'); -- id=1
select last_insert_id(); -- 1
insert ignore AnimalSize(animal,size) values ('snake','small'); -- no real insert but creates id GAP (ie blows slot 2)
select last_insert_id(); -- 1
insert ignore AnimalSize(animal,size) values ('snake','small'); -- no real insert but creates id GAP (ie blows slot 3)
select last_insert_id(); -- 1
insert ignore AnimalSize(animal,size) values ('frog','medium'); -- id=4
select last_insert_id(); -- 4
insert ignore AnimalSize(animal,size) values ('snake','small'); -- no real insert but creates id GAP (ie blows slot 3)
select last_insert_id(); -- 4
Note: insert ignore says do it, and ignore the fact that it may die. In our case, it would fail due to unique key (which is fine). In general, do not use insert ignore unless you know what you are doing.
It is often thought of in connection with an insert on duplicate key update (IODKU) call. Or should I say thought about, as in, How can I solve this current predicament. But, that (IODKU) would be a stretch in this case. Yet, keep both in your toolchest for solutions.
After insert ignore fires off, you know, one way or the other, that the row is there.
Forgetting the INNODB GAP aspect, what the above suggests is that if the row already exists prior to insert ignore, that
You cannot rely on last_insert_id() for the id
So after firing off insert ignore, go and fetch the id that you know has to be there. Use that in subsequent calls against CountryAnimalSize
continue along this line of reasoning for your CountryAnimalSize table inserts where the row may or may not already be there.
There is no reason to formalize the solution here because, as you say, these aren't even your tables anyway in the Question.
Also, back to INNODB GAP. Google that. Figure out whether or not you can live with gaps created.
Most people have bigger fish to fry that keeping id's tight and gapless.
Other people (read: OCD) are so consumed by the perceived gap problem that they blow days on it.
So, these are general comments meant to help a broader audience, than to answer your question, which, as you say, isn't even your schema.
You can use x_id as this:
CONCAT(`animal`, '_', `size`) AS `x_id`
And then compare it with x_id, so that you will get something like:
+---------+-----------+--------+------------------+
| country | animal | size | x_id* |
+---------+-----------+--------+------------------+
| africa | crocodile | small | crocodile_small |
| africa | elephant | medium | elephant_medium |
| africa | giraffe | medium | giraffe_medium |
| africa | giraffe | large | giraffe_large |
| europe | crocodile | small | crocodile_small |
| europe | elephant | medium | elephant_medium |
| europe | giraffe | large | giraffe_large |
+---------+-----------+--------+------------------+
As I see, you are already using MyISAM engine type, You can just define both country and x_id field as PRIMARY KEY (jointly) and you can set the AUTO_INCREMENT for x_id field. Now MySQL will do the rest for you! BINGO!
Here is the SQL Fiddle for you!
CREATE TABLE `myTable` (
`country` int(10) unsigned NOT NULL DEFAULT '1', -- country
`animal` int(4) NOT NULL,
`size` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`lang_id` tinyint(4) NOT NULL DEFAULT '1',
`x_id` int(10) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (country,x_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `myTable` (`country`, `animal`, `size`) VALUES
(777, 1001, 'small'),
(777, 2002, 'medium'),
(777, 7007, 'medium'),
(777, 7007, 'large'),
(42, 1001, 'small'),
(42, 2002, 'medium'),
(42, 7007, 'large')
The result will be like this:
| country | animal | size |lang_id | x_id |
|---------+--------+--------+--------+-------|
| 777 | 1001 | small | 1 | 1 |
| 777 | 2002 | medium | 1 | 2 |
| 777 | 7007 | medium | 1 | 3 |
| 777 | 7007 | large | 1 | 4 |
| 42 | 1001 | small | 1 | 1 |
| 42 | 2002 | medium | 1 | 2 |
| 42 | 7007 | large | 1 | 4 |
NOTE: This will only work for MyISAM and BDB tables, for other engine types you will get error saying "Incorrect table definition; there can be only one auto column and it must be defined as a key!" See this answer for more on this : https://stackoverflow.com/a/5416667/5645769.
I have a 'users' table with 100 entries, each having an empty 'first_name' column. I wish to update each of these with names from another table. They do not need to correspond, they can be random, I just need data from one table into the other. I have found other people asking similar questions, but they all seem to have corresponding columns, like "username" being the same in either table and can get it working using a JOIN ON. As there are no corresponding columns I cannot do this.
I currently have tried the following which does not work:
UPDATE users
SET first_name =
(
SELECT `user_firstname`
FROM past_users
WHERE `active` = '1' LIMIT 100
)
This gives the error:
Subquery returns more than 1 row
The only way it works is using LIMIT 1, which updates each entry with the same data. I want them each to be unique.
Ok, maybe this concept. The below is just an illustration. Uses random, and limit 1.
Schema
create table user
( userId int auto_increment primary key,
firstName varchar(50) not null
-- etc
);
create table prevUser
( userId int auto_increment primary key,
firstName varchar(50) not null,
active int not null
);
-- truncate table user;
-- truncate table prevuser;
insert user(firstName) values (''),(''),(''),(''),(''),(''),(''),(''),('');
insert prevUser(firstName,active) values
('user1prev',0),('snickers bar',1),('Stanley',1),('user4prev',0),('zinc',1),
('pluto',1),('us7545rev',0),('uffallfev',0),('user4prev',0),('tuna',1),
('Monty Python',1),('us4 tprev',0),('mouse',1),('user4prev',0),('Sir Robin',1),
('lizard',1),('Knights that says, Nee!',0),('mayo',1),('656user4prev',0),('kiwi',1);
Query (similar to yours)
UPDATE user
SET firstName =
(
SELECT firstName
FROM prevUser
WHERE `active` = '1'
order by rand()
limit 1
)
Results
select * from user;
+--------+--------------+
| userId | firstName |
+--------+--------------+
| 1 | snickers bar |
| 2 | tuna |
| 3 | mouse |
| 4 | Sir Robin |
| 5 | mouse |
| 6 | mayo |
| 7 | lizard |
| 8 | snickers bar |
| 9 | pluto |
+--------+--------------+
You need something like this:
UPDATE users
JOIN past_users ON past_users.user_id = users.id AND past_users.`active` = '1'
SET users.first_name = past_users.user_firstname
I have unlimited level menu structure in MySQL, where parent and children elements are connected with column p_id:
+----+------+------+----------------+
| id | p_id | sort | name_en |
+----+------+------+----------------+
| 1 | 0 | 1 | menu-1 |
| 2 | 0 | 2 | menu-2 |
| 3 | 0 | 6 | menu-3 |
| 4 | 2 | 3 | sub-menu-2-1 |
| 5 | 2 | 4 | sub-menu-2-2 |
| 6 | 5 | 5 | sub-menu-2-2-1 |
+----+------+------+----------------|
What is best practice for deleting root menu element with sub-menu elements?
I can achieve it with PHP recursion, but it will cause some number of queries.
So I need to find out if is there any possible way to do it with one MySQL query.
Since you have name_en, cannot you use that one to delete the rows? For example,
DELETE FROM `table` WHERE `id` = 2 OR `name_en` LIKE 'sub-menu-2-%'
New method:
You can use a Foreign Key with a constraint. I created your table and called it treelist,
CREATE TABLE `treelist` (
`item_id` int(10) unsigned NOT NULL auto_increment,
`parent_id` int(10) unsigned default NULL,
`name_en` varchar(40) NOT NULL,
PRIMARY KEY (`item_id`),
KEY `FK_parent_id` (`parent_id`),
CONSTRAINT `FK_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `treelist` (`item_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
I then added some test data, the ones you had in your question,
INSERT INTO `treelist` (`item_id`, `parent_id`, `name_en`) VALUES (NULL, NULL, 'Menu 1'), (NULL, NULL, 'Menu 2'), (NULL, NULL, 'Menu 3'), (NULL, 2, 'Sub Menu 2-1'), (NULL, 2, 'Sub Menu 2-2'), (NULL, 5, 'Sub Menu 2-2-1');
Now, when you delete a row, for example
DELETE FROM `treelist` WHERE `item_id` = 2
It will delete all children, grand-children etc as well. Afterwards the table looks like,
+----+------+----------------+
| id | p_id | name_en |
+----+------+----------------+
| 1 | NULL | Menu 1 |
| 3 | NULL | Menu 3 |
+----+------+----------------+
To delete just a row and it's direct children (not grandchildren):
DELETE FROM tablename where id = 1 or p_id = 1;
UPDATE 1:
If you have freedom to add a column to the table, you could add root_id and easily do the delete based on that. This will not ruin any current queries in your system and will only take a one-time run of a simple script to add the initial data.
DELETE FROM tablename where id = 1 or root_id = 1;
UPDATE 2:
A pretty awesome option is that foreign keys to the same table are allowed. So you can add a foreign key to p_id that references id, use on delete cascade and when you delete the root, all descendants will be removed also. I created a test table and this worked beautifully for me. It may also be beneficial to add on update cascade. Remember that p_id as well as id need to be unsigned for this to work.
ALTER TABLE tablename ADD CONSTRAINT fk_tablename_id FOREIGN KEY (p_id) references tablename(id) ON DELETE CASCADE;
Just category id or parent id in first parameter:
function delete_parent_and_child_subchild($category_id,$all_cate=array())
{
if(!is_array($category_id))
{
$this->db->where('parent_id', $category_id);
$all_cate[]=$category_id;
}
else
{
$this->db->where_in('parent_id', $category_id);
}
$get_categories= $this->db->get('newcategory');
if($get_categories->num_rows()>0)
{
$categories_vales=$get_categories->result();
$new_subcat = array();
foreach($categories_vales as $cate_val)
{
$category_id=$cate_val->category_id;
array_push($new_subcat,$category_id);
}
$all_cate = array_merge($all_cate,$new_subcat);
if(count($new_subcat)>0)
{
$this->delete_parent_and_child_subchild($new_subcat,$all_cate);
}
}
$this->db->where_in('category_id', $all_cate)->delete('newcategory');
return true;
}