MySQL Update Table from Another Table Recursively - mysql

I have two tables, table1 and table2, where table1 is updated to fill in missing (null) values based on matching fields in table2 to create a more complete table1.
I have tried numerous queries such as
UPDATE table1 INNER JOIN table2...SET...
and
UPDATE table1 SET ... (SELECT)...
However my results are always incomplete. Note that I have a much larger dataset in both tables in terms of both columns and rows). I just used this as simpler example.
Rules:
1) The `keyword` from table2 looks for a match in `keyword` in table1 and must accept partial matches.
2) No values can be overwritten in table1 (update null values only)
3) The lookup order is per run_order in table2.
Specific example:
Table1:
|-----|-------------------------------|----------|-----|---------|-------|
|t1_id|keyword |category |month|age |skill |
|-----|-------------------------------|----------|-----|---------|-------|
| 1 |childrens-crafts-christmas |kids | | | |
| 2 |kids-costumes | | |tween | |
| 3 |diy-color-printable |printable | | |easy |
| 4 |toddler-halloween-costume-page | | | | |
|-----|-------------------------------|----------|-----|---------|-------|
Table2:
|-----|---------|---------|----------|-----|----------|-------|
|t2_id|run_order|keyword |category |month|age |skill |
|-----|---------|---------|----------|-----|----------|-------|
| 1 | 1 |children | | |4-11 yrs | |
| 2 | 2 |printable| | | |easy |
| 3 | 3 |costume |halloween | 10 |0-12 years| |
| 4 | 4 |toddler | | |1-3 years | |
| 5 | 5 |halloween|holiday | 10 | | |
|-----|---------|---------|----------|-----|----------|-------|
Result:
|-----|-------------------------------|----------|-----|---------|-------|
|t1_id|keyword |category |month|age |skill |
|-----|-------------------------------|----------|-----|---------|-------|
| 1 |childrens-crafts-christmas |kids | |4-11 yrs | |
| 2 |kids-costumes |halloween | 10 |tween | |
| 3 |diy-color-printable printable |printable | | |easy |
| 4 |toddler-halloween-costume-page |holiday | 10 |1-3 years| |
|-----|-------------------------------|----------|-----|---------|-------|
MySQL for schema and table data:
DROP TABLE IF EXISTS table1;
DROP TABLE IF EXISTS table2;
CREATE TABLE `table1` (
`t1_id` INT NOT NULL AUTO_INCREMENT,
`keyword` VARCHAR(200) NULL,
`category` VARCHAR(45) NULL,
`month` VARCHAR(45) NULL,
`age` VARCHAR(45) NULL,
`skill` VARCHAR(45) NULL,
PRIMARY KEY (`t1_id`));
CREATE TABLE `table2` (
`t2_id` INT NOT NULL AUTO_INCREMENT,
`run_order` INT NULL,
`keyword` VARCHAR(200) NULL,
`category` VARCHAR(45) NULL,
`month` INT NULL,
`age` VARCHAR(45) NULL,
`skill` VARCHAR(45) NULL,
PRIMARY KEY (`t2_id`));
INSERT INTO `table1` (`keyword`, `category`) VALUES ('childrens-crafts-christmas', 'kids');
INSERT INTO `table1` (`keyword`, `age`) VALUES ('kids-costumes', 'tween');
INSERT INTO `table1` (`keyword`, `category`, `skill`) VALUES ('diy-color-printable', 'printable', 'easy');
INSERT INTO `table1` (`keyword`) VALUES ('toddler-halloween-costume-page');
INSERT INTO `table2` (`run_order`, `keyword`, `age`) VALUES (1, 'children', '4-11 yrs');
INSERT INTO `table2` (`run_order`, `keyword`, `skill`) VALUES (2, 'printable', 'easy');
INSERT INTO `table2` (`run_order`, `keyword`, `category`, `month`, `age`) VALUES (3, 'costume', 'halloween', 10, '0-12 years');
INSERT INTO `table2` (`run_order`, `keyword`, `age`) VALUES (4, 'toddler', '1-3 years');
INSERT INTO `table2` (`run_order`, `keyword`, `category`, `month`) VALUES (5, 'halloween', 'holiday', 10);

You want to update empty values in table1 with the value in the corresponding column of the first matching record in table2, run_order wise. A typical solution is to use a combination of correlated subqueries to find the matching records in table2 and keep only the one with lowest run_order.
Here is a query that will update null categories with this logic:
update table1 t1
set category = (
select category
from table2 t2
where t2.run_order = (
select min(t22.run_order)
from table2 t22
where
t1.keyword like concat('%', t22.keyword, '%')
and t22.category is not null
)
)
where t1.category is null
This assumes that run_order is unique in table2 (which seems relevant in your use case).
You can extend the logic for more columns with coalesce(). Here is the solution for columns category and month:
update table1 t1
set
category = coalesce(
t1.category,
(
select category
from table2 t2
where t2.run_order = (
select min(t22.run_order)
from table2 t22
where
t1.keyword like concat('%', t22.keyword, '%')
and t22.category is not null
)
)
),
month = coalesce(
t1.month,
(
select month
from table2 t2
where t2.run_order = (
select min(t22.run_order)
from table2 t22
where
t1.keyword like concat('%', t22.keyword, '%')
and t22.month is not null
)
)
)
where t1.category is null or t1.month is null
Demo on DB Fiddle

You can use a join between the 2 tables using a like expression and the if nul() function to ensure you don't overwrite non null values.
UPDATE table1 t1
INNER JOIN table2 t2 ON t1.keyword like concat("%",t2.keyword,"%")
SET
t1.category = ifnull(t1.category,t2.category),
t1.age = ifnull(t1.age,t2.age),
t1.skill = ifnull(t1.skill,t2.skill);

Related

MySql: how to get the desired result

I've a table like this:
CREATE TABLE `base_build_floor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`build_no` varchar(64) NOT NULL,
`build_name` varchar(64) DEFAULT NULL,
`floor_no` varchar(64) DEFAULT NULL,
`floor_name` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
)
and insert some data:
INSERT INTO `base_build_floor` VALUES ('41', 'BUILD40210011', 'A', null, null);
INSERT INTO `base_build_floor` VALUES ('42', 'BUILD40210012', 'B', null, null);
INSERT INTO `base_build_floor` VALUES ('43', 'BUILD40210013', 'C', null, null);
INSERT INTO `base_build_floor` VALUES ('44', 'BUILD40210013', 'C', 'FLOOR40210002', 'C1');
INSERT INTO `base_build_floor` VALUES ('45', 'BUILD40210013', 'C', 'FLOOR40210003', 'C2');
INSERT INTO `base_build_floor` VALUES ('46', 'BUILD40210012', 'B', 'FLOOR40210004', 'B1');
the table is about a build-floor table, first you should make a building, then, a building can has no or some floors. the A building has no floor, the B building has one floor named B1, the C building has two floors named C1 and C2, I want to get the result as below:
41 BUILD40210011 A null null
44 BUILD40210013 C FLOOR40210002 C1
45 BUILD40210013 C FLOOR40210003 C2
46 BUILD40210012 B FLOOR40210004 B1
it means that, if a building has no floors, then get it, while if a building has any one floor, the building itself should not be got, so how to write the mysql?I've tried to use Subquery but doesn't work
I've try like this :
SELECT
b.*
FROM
base_build_floor b
WHERE
b.floor_no IS NOT NULL
OR (
b.floor_no IS NULL
AND b.build_no NOT IN (
SELECT
GROUP_CONCAT(nostr)
FROM
(
SELECT
concat("'", f.build_no, "'") as nostr
FROM
base_build_floor f
WHERE
f.floor_no IS NOT NULL
GROUP BY
f.build_no
) t
)
)
but I get all the data
With NOT EXISTS:
select t.* from base_build_floor t
where t.floor_no is not null
or not exists (
select 1 from base_build_floor
where build_no = t.build_no and floor_no is not null
)
See the demo.
Results:
| id | build_no | build_name | floor_no | floor_name |
| --- | ------------- | ---------- | ------------- | ---------- |
| 41 | BUILD40210011 | A | | |
| 44 | BUILD40210013 | C | FLOOR40210002 | C1 |
| 45 | BUILD40210013 | C | FLOOR40210003 | C2 |
| 46 | BUILD40210012 | B | FLOOR40210004 | B1 |
This query would be much simpler if you had normalized tables. Ideally, you would have a buildings table with building id, no, and name, and a floors table with building id, floor no, and floor name. Then you could just join the two tables. Since that's not the case, we can basically extract the building and floor sub-tables from the main one and join them like this:
SELECT
b.build_no,
b.build_name,
f.floor_no,
f.floor_name
FROM
(SELECT DISTINCT build_no, build_name
FROM base_build_floor) b
LEFT OUTER JOIN
(SELECT *
FROM base_build_floor
WHERE floor_no IS NOT NULL) f ON b.build_no = f.build_no

Get Users available based on validation of status, id, and time spread across 3 tables

Lets say I want to pull a list of users(From TableA) who:
are currently within Status = 2
and
currently not in TableB,
OR
are in TableB.UserId AND (TableB.AppId > 1year) by comparing the dates between TableC.id's CreatedDate and with the CurrentDate.
Current Schema Setup...
TableA (UserID)
--------------------------------------
id | fName | lName | Status | CreatedDate
1 | John | Doe | 2 | 2017-03-02 06:31:15.482
2 | Marry | Jane | 2 | 2017-05-03 16:43:56.937
3 | William | Thompson | 4 | 2017-06-15 13:12:32.219
4 | Timothy | Limmons | 2 | 2017-09-27 01:52:42.842
TableB
--------------------------------------
id | AppID | UserID | CreatedDate
1 | 2 | 1 | 2019-04-16 23:21:56.099
2 | 3 | 4 | 2019-08-03 04:32:18.472
TableC (AppID)
--------------------------------------
id | Title | CreatedDate
1 | ToDo List | 2017-03-09 22:45:12.907
2 | Magic Marshmellows | 2018-11-14 07:01:04.050
3 | Project Falcon | 2019-07-23 14:22:44.837
The info above should pull users from TableA with the id's of 1 and 2.
Marry has not been paired with an App, and is therefor available
John is paired with the App Magic Marshmellows, but the project began over 1 year ago and is therefor available
The following info should NOT pull users with the id's of 3 and 4.
William is a status of 4 (not 2) and is therefor NOT available.
Timothy is paired with the App Project Falcon, and this app began within a year from the current DateTime (12/15/2019)... and is therefor NOT available
I need something like...
Select *
FROM
[TableA] a
WHERE
a.Status = 2
IF
TableB.UserID NOT CONTAINS a.id
ELSE IF
TableB.UserID = a.id
AND WHERE
TableB.AppID = TableC.id
AND WHERE
TableC.CreatedDate is less than 1 year old from Current Date
I'm just not sure how to go about using the right syntax for this. Any help would be appreciated.
P.S. If there is a better title for this complicated question, please let me know.
IN MYSQL you would do a query like this.
CREATE TABLE UserID
(`id` int, `fName` varchar(7), `lName` varchar(8), `Status` int, `CreatedDate` Date)
;
INSERT INTO UserID
(`id`, `fName`, `lName`, `Status`, `CreatedDate`)
VALUES
(1, 'John', 'Doe', 1, '2017-03-02 06:31:15.482'),
(2, 'Marry', 'Jane', 2, '2017-05-03 16:43:56.937'),
(3, 'William', 'Thompson', 4, '2017-06-15 13:12:32.219'),
(4, 'Timothy', 'Limmons', 2, '2017-09-27 01:52:42.842')
;
✓
✓
CREATE TABLE TableB
(`id` int, `AppID` int, `UserID` int, `CreatedDate` Date)
;
INSERT INTO TableB
(`id`, `AppID`, `UserID`, `CreatedDate`)
VALUES
(1, 2, 1, '2019-04-16 23:21:56.099'),
(2, 3, 4, '2019-08-03 04:32:18.472')
;
✓
✓
CREATE TABLE APPID
(`id` int, `Title` varchar(18), `CreatedDate` Date)
;
INSERT INTO APPID
(`id`, `Title`, `CreatedDate`)
VALUES
(1, 'ToDo List', '2017-03-09 22:45:12.907'),
(2, 'Magic Marshmellows', '2018-11-14 07:01:04.050'),
(3, 'Project Falcon', '2019-07-23 14:22:44.837')
;
✓
✓
SELECT u.*
From UserID u LEFT JOIN TableB b ON u.id = b.UserID
LEFT JOIN APPID a ON b.APPID = a.id
WHERE Status = 2
AND (u.id NOT IN (SELECT UserID FROM TableB)
OR (u.id IN (SELECT UserID FROM TableB) AND a.CreatedDate > NOW() - INTERVAL 1 YEAR));
id | fName | lName | Status | CreatedDate
-: | :------ | :------ | -----: | :----------
4 | Timothy | Limmons | 2 | 2017-09-27
2 | Marry | Jane | 2 | 2017-05-03
SELECT * FROM APPID WHERE `CreatedDate` > NOW() - INTERVAL 1 YEAR
id | Title | CreatedDate
-: | :------------- | :----------
3 | Project Falcon | 2019-07-23
db<>fiddle here
SELECT
a.*
FROM
TableA AS a
LEFT JOIN (
SELECT
b.UserID
FROM
TableB AS b
INNER JOIN TableC AS c ON (
b.AppID = c.id
)
WHERE
c.CreatedDate >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
) AS s ON (
a.id = s.UserID
)
WHERE
a.Status = 2
AND s.UserID IS NULL
OR
SELECT
a.*
FROM
TableA AS a
LEFT JOIN TableB AS b ON (
b.UserID = a.id
)
LEFT JOIN TableC AS c ON (
c.id = b.AppID
)
WHERE
a.Status = 2
AND (
b.UserID IS NULL
OR c.CreatedDate < DATE_SUB(NOW(), INTERVAL 1 YEAR)
)
OR
SELECT
a.*
FROM
TableA AS a
WHERE
a.Status = 2
AND NOT EXISTS (
SELECT
*
FROM
TableB AS b
INNER JOIN TableC AS c ON (
b.AppID = c.id
)
WHERE
b.UserID = a.id
AND c.CreatedDate >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
)
=>
id fName lName Status CreatedDate
1 John Doe 2 2017-03-02
2 Marry Jane 2 2017-05-03
CREATE TABLE IF NOT EXISTS `TableA` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fName` varchar(7) DEFAULT NULL,
`lName` varchar(8) DEFAULT NULL,
`Status` int(11) DEFAULT NULL,
`CreatedDate` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `Status` (`Status`,`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
DELETE FROM `TableA`;
INSERT INTO `TableA` (`id`, `fName`, `lName`, `Status`, `CreatedDate`) VALUES
(1, 'John', 'Doe', 2, '2017-03-02'),
(2, 'Marry', 'Jane', 2, '2017-05-03'),
(3, 'William', 'Thompson', 4, '2017-06-15'),
(4, 'Timothy', 'Limmons', 2, '2017-09-27');
CREATE TABLE IF NOT EXISTS `TableB` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`AppID` int(11) DEFAULT NULL,
`UserID` int(11) DEFAULT NULL,
`CreatedDate` date DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `AppID` (`AppID`),
KEY `UserID` (`UserID`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
DELETE FROM `TableB`;
INSERT INTO `TableB` (`id`, `AppID`, `UserID`, `CreatedDate`) VALUES
(1, 2, 1, '2019-04-16'),
(2, 3, 4, '2019-08-03');
CREATE TABLE IF NOT EXISTS `TableC` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`Title` varchar(18) DEFAULT NULL,
`CreatedDate` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `CreatedDate` (`CreatedDate`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
DELETE FROM `TableC`;
INSERT INTO `TableC` (`id`, `Title`, `CreatedDate`) VALUES
(1, 'ToDo List', '2017-03-09 22:45:12'),
(2, 'Magic Marshmellows', '2018-11-14 07:01:04'),
(3, 'Project Falcon', '2019-07-23 14:22:44');
UPDATED: WHERE condition fixed

Creating summary VIEW from fields from multiple tables

I am trying to write a select query for creating a view in MySQL. Each row in the view should display weekly summary (sum, avg) for user values collected from multiple tables. The tables are similar to each-other but not identical. The view should include rows also in case other table doesn't have a values for that week. Something like this:
| week_year | sum1 | avg1 | sum2 | user_id |
| --------- | ---- | ---- | ---- | ------- |
| 201840 | | | 3 | 1 |
| 201844 | 45 | 55 | | 1 |
| 201845 | 55 | 65 | | 1 |
| 201849 | 65 | 75 | | 1 |
| 201849 | 75 | 85 | 3 | 2 |
The tables (simplified) are as follows:
CREATE TABLE IF NOT EXISTS `t1` (
`user_id` INT NOT NULL AUTO_INCREMENT,
`date` DATE NOT NULL,
`value1` int(3) NOT NULL,
`value2` int(3) NOT NULL,
PRIMARY KEY (`user_id`,`date`)
) DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `t2` (
`id` INT NOT NULL AUTO_INCREMENT,
`date` DATE NOT NULL,
`value3` int(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `t3` (
`t3_id` INT NOT NULL,
`user_id` INT NOT NULL
) DEFAULT CHARSET=utf8;
My current solution doesn't seem reasonable and I am not sure how it would perform in case of thousands of rows:
select ifnull(yearweek(q1.date1), yearweek(q1.date2)) as week_year,
sum(value1) as sum1,
avg(value2) as avg1,
sum(value3) as sum2,
q1.user_id
from (select t2.date as date2,
t1.date as date1,
ifnull(t3.user_id, t1.user_id) as user_id,
t1.value1,
t1.value2,
t2.value3
from t2
join t3 on t3.t3_id=t2.id
left join t1 on yearweek(t1.date) = yearweek(t2.date) and t1.user_id = t3.user_id
union
select t2.date as date2,
t1.date as date1,
ifnull(t3.user_id, t1.user_id) as user_id,
t1.value1,
t1.value2,
t2.value3
from t2
join t3 on t3.t3_id=t2.id
right join t1 on yearweek(t1.date) = yearweek(t2.date) and t1.user_id = t3.user_id) as q1
group by week_year, user_id;
DB Fiddle
Is the current solution okay performance wise or are there better options? In case of in the future third (or fourth) table is added, how would I manage the query? Should I consider creating a separate table, that is updated with triggers?
Thanks in advance.
Another way you can do it is to union all the data and then group it. You'll have to perf test to see which is better:
SELECT
yearweek(date),
SUM(value1) as sum1,
AVG(value2) as avg1,
SUM(value3) as sum2
FROM
(
SELECT user_id, date, value1, value2, CAST(null as INT) as value3 FROM t1
UNION ALL
SELECT user_id, date, null, null, value3 FROM t2 INNER JOIN t3 ON t2.id = t3.t3_id
)
GROUP BY
user_id,
yearweek(date)
Hopefully mysql won't take issue with casting null to an int..

Log affected rows into another table in MySQL

Given the table:
CREATE TABLE `records` (
`id_type` varchar(50) NOT NULL,
`old_id` INT,
`new_id` INT,
) ENGINE=InnoDB;
And the data:
id_type | old_id | new_id
USER | 11 | NULL
USER | 15 | NULL
USER | 56 | NULL
USER | NULL | 500
USER | NULL | 523
USER | NULL | 800
I want to perform a query that will return:
id_type | old_id | new_id
USER | 11 | 500
USER | 15 | 523
USER | 56 | 800
Create table records_old
(
id_type varchar(20) primary key,
old_id int not null
);
Create table records_new
(
id_type varchar(20),
new_id int not null
);
insert into records_old(id_type,old_id) values ('USER1',11);
insert into records_old(id_type,old_id) values ('USER2',12);
insert into records_old(id_type,old_id) values ('USER3',13);
insert into records_new(id_type,new_id) values ('USER1',500);
insert into records_new(id_type,new_id) values ('USER2',600);
insert into records_new(id_type,new_id) values ('USER3',700);
select * from records_old;
select * from records_new;
select a.id_type,a.old_id,b.new_id from records_old a
inner join records_new b
where a.id_type=b.id_type;
SET #old_row_number = 0;
SET #new_row_number = 0;
SELECT OldData.id_type, OldData.old_id, NewData.new_id
FROM (SELECT id_type, old_id, (#old_row_number:=#old_row_number + 1) AS OldRowNumber
FROM `records`
WHERE old_id IS NOT NULL) OldData
JOIN (SELECT id_type, new_id, (#new_row_number:=#new_row_number + 1) AS NewRowNumber
FROM `records`
WHERE new_id IS NOT NULL) NewData ON NewData.NewRowNumber = OldData.OldRowNumber
Filter with id is not null and separate as two sub-queries and add a row number for each row then join will help in your case.
Working Demo

MySQL join, empty rows in junction table

I have three tables I'd like to join in a way that produces all records from one table and any matching records or NULL from another table. It is imperative that all records from the first table be returned. I thought I had done this before but I can't remember when or where and MySQL just isn't playing ball.
SELECT VERSION();
5.0.51a-3ubuntu5.7
/* Some table that may or may not be needed, included to give context */
CREATE TABLE `t1` (
`a` int(4) NOT NULL,
`a_name` varchar(10) NOT NULL,
PRIMARY KEY (`a`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `t2` ( /* "One table" */
`b` int(2) NOT NULL,
`b_name` varchar(10) NOT NULL,
PRIMARY KEY (`b`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `t3` ( /* "Another table" */
`a` int(4) NOT NULL,
`b` int(2) NOT NULL,
PRIMARY KEY (`a`,`b`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO t1 VALUES (1, '1-one'),(2, '1-two'),(3, '1-three');
INSERT INTO t2 VALUES (1, '2-one'),(2, '2-two'),(3, '2-three'),
(4, '2-four'),(5, '2-five');
INSERT INTO t3 VALUES (1,1),(1,2),(1,3),(1,4),(2,2),(2,5);
t3 is a junction table for t1 and t2. The result set I'm looking for should look like this for any a=n:
n=1
b | b_name | a
-------------------
1 | 2-one | 1
2 | 2-two | 1
3 | 2-three | 1
4 | 2-four | 1
5 | 2-five | NULL
n=2
b | b_name | a
-------------------
1 | 2-one | NULL
2 | 2-two | 2
3 | 2-three | NULL
4 | 2-four | NULL
5 | 2-five | 2
n=7
b | b_name | a
-------------------
1 | 2-one | NULL
2 | 2-two | NULL
3 | 2-three | NULL
4 | 2-four | NULL
5 | 2-five | NULL
The value of a in the result set actually isn't important as long as it unambiguously reflects the presence or absence of records in t3 (does that make sense?).
The query
SELECT t2.b, t2.b_name, a
FROM t2
LEFT /* OUTER */ JOIN t3 ON t3.b = t2.b
WHERE (
a = 2
OR
a IS NULL
);
returns
b | b_name | a
-------------------
2 | 2-two | 2
5 | 2-five | 2
Can this be done?
SELECT t2.b, t2.b_name, MAX(IF(a=2, a, NULL)) AS a
FROM t2
LEFT OUTER JOIN t3
ON t3.b = t2.b
GROUP by t2.b
ORDER BY b ASC;
Something like this? If not, can you give an example of the output you'd like to get.
select * from t1
left join t3 using(a)
left join t2 using(b)