Associated Name spiderweb - mysql

Say for instance I have the following entries in my table:
ID - 1
Name - Daryl
ID - 2
Name - Terry
ID - 3
Name - Dave
ID - 4
Name - Mitch
I eventually wish to search my table(s) for one specific name, but show all associated names. For instance,
Searching Daryl will return Terry, Dave & Daryl.
Searching Terry will return Dave, Daryl & Terry
Searching Mitch will only return Mitch.
The current table housing the names is as followed:
--
-- Table structure for table `members`
--
CREATE TABLE `members` (
`ID` int(255) NOT NULL,
`GuildID` int(255) NOT NULL,
`ToonName` varchar(255) NOT NULL,
`AddedOn` date NOT NULL,
`AddedByID` int(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `members`
--
INSERT INTO `members` (`ID`, `GuildID`, `ToonName`, `AddedOn`, `AddedByID`) VALUES
(1, 1, 'Daryl', '2020-01-17', 5),
(2, 1, 'Terry', '2020-01-17', 5),
(3, 1, 'Mitch', '2020-01-17', 5),
(4, 1, 'Dave', '2020-01-17', 5);
--
For Reference. GuildID will be a default search criteria based on the searchers login details. With a spiderweb like this, how would I go about creating another table (or another Column) to bring a combined search spiderweb structure based on the search criteria?
I was thinking something along the lines of:
CREATE TABLE `Associated`(
`ID` INT(255) NOT NULL,
`MainID` INT(255) NOT NULL,
`SecondaryID` INT(255) NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `Associated` (`ID`, `MainID`, `SecondaryID`) VALUES
(1, 1, 2) -- Daryl Associated With Terry
(2, 1, 4) -- Daryl Associated With Dave
But I feel this will make an over complicated value structure with alot of redundant inputs. Is there a more effective way to create a unified search?
The whole idea of operation is that each name is Individual. So certain Entries can be put under Daryl, Terry acting alone. But one search will bring together all associated Names by searching one name then pull together total entries based on the alisas?

You can try This
Select IFNULL(m.ToonName , members.ToonName) as ToonName
from members
LEFT JOIN Associated on Associated.MainID = members.ID
LEFT JOIN members as m on m.ID = Associated.SecondaryID
Where members.ToonName = "Mitch"
While you have entry for
"Mitch" in Associated table it will return you Daryl and when you dont have associated Id it will return the name from members table.
And If you will check this with "Daryl", it will give you Two results,
Select IFNULL(m.ToonName , members.ToonName) as ToonName
from members
LEFT JOIN Associated on Associated.MainID = members.ID
LEFT JOIN members as m on m.ID = Associated.SecondaryID
Where members.ToonName = "Daryl"
In case you want all the names in a single column you can use GROUP_CONCAT as #flash suggested in another answer.

You can directly get the data from the following SQL statement.
For Individual row
SELECT `members`.ToonName FROM `associated` JOIN `members` ON associated.SecondaryID = members.ID WHERE `associated`.MainID = (SELECT ID FROM `members` WHERE ToonName = 'Daryl');
# Output: **ToonName**
Terry,Dave
Grouping Row
// You can also group all rows by comma from following statement
SELECT GROUP_CONCAT(`members`.ToonName) FROM `associated` JOIN `members` ON associated.SecondaryID = members.ID WHERE `associated`.MainID = (SELECT ID FROM `members` WHERE ToonName = 'Daryl');
# Output: **ToonName**
Terry
Dave

Plan A
Add a column to each member. It is the number (or name) if the one group he/she belongs to. Terry, Dave & Daryl would get one value; Mitch would get a different value. Index the column for efficient lookup of related names.
Plan B
Implement a graph, like you suggested. Some tips: Get rid of id; instead have PRIMARY KEY(MainID, SecondaryID). The is an issue to resolve... This design implies a "directedness" of the relationships: Terry --> Dave, but not necessarily Dave --> Terry. If you want to force it to be reflexive, the force two rows to be inserted or insert the two IDs in an canonical order, but then check both directions.
Also, you need to "walk the graph". This is best done with a Recursive CTE. For that feature, you need MySQL 8.0 or MariaDB 10.2.
Plan C
Without the directedness of B, you run into more difficult issues. One is "cluster analysis". Another is messy paths and loops in the 'graph'. Let's avoid these.

Short Answer: Yes you need a second table Associated and it will not make completed structure.
Below is the query to get the required result
SELECT ID, ToonName,
(
SELECT GROUP_CONCAT(ToonName) FROM Associated
JOIN members child ON SecondaryID = child.ID
WHERE MainID = parent.ID
)
FROM members parent
You can also use join but I think, in this case sub query will be better.
NOTE : your tables need some optimization like remove ID field from Associated table, Add index etc.

Related

MYSQL ERROR CODE: 1288 - can't update with join statement

Thanks for past help.
While doing an update using a join, I am getting the 'Error Code: 1288. The target table _____ of the UPDATE is not updatable' and figure out why. I can update the table with a simple update statement (UPDATE sales.customerABC Set contractID = 'x';) but can't using a join like this:
UPDATE (
SELECT * #where '*' contains columns a.uniqueID and a.contractID
FROM sales.customerABC
WHERE contractID IS NULL
) as a
LEFT JOIN (
SELECT uniqueID, contractID
FROM sales.tblCustomers
WHERE contractID IS NOT NULL
) as b
ON a.uniqueID = b.uniqueID
SET a.contractID = b.contractID;
If changing that update statement a SELECT such as:
SELECT * FROM (
SELECT *
FROM opwSales.dealerFilesCTS
WHERE pcrsContractID IS NULL
) as a
LEFT JOIN (
SELECT uniqueID, pcrsContractID
FROM opwSales.dealerFileLoad
WHERE pcrsContractID IS NOT NULL
) as b
ON a."Unique ID" = b.uniqueID;
the result table would contain these columns:
a.uniqueID, a.contractID, b.uniqueID, b.contractID
59682204, NULL, NULL, NULL
a3e8e81d, NULL, NULL, NULL
cfd1dbf9, NULL, NULL, NULL
5ece009c, , 5ece009c, B123
5ece0d04, , 5ece0d04, B456
5ece7ab0, , 5ece7ab0, B789
cfd21d2a, NULL, NULL, NULL
cfd22701, NULL, NULL, NULL
cfd23032, NULL, NULL, NULL
I pretty much have all database privileges and can't find restrictions with the table reference data. Can't find much information online concerning the error code, either.
Thanks in advance guys.
You cannot update a sub-select because it's not a "real" table - MySQL cannot easily determine how the sub-select assignment maps back to the originating table.
Try:
UPDATE customerABC
JOIN tblCustomers USING (uniqueID)
SET customerABC.contractID = tblCustomers.contractID
WHERE customerABC.contractID IS NULL AND tblCustomers.contractID IS NOT NULL
Notes:
you can use a full JOIN instead of a LEFT JOIN, since you want uniqueID to exist and not be null in both tables. A LEFT JOIN would generate extra NULL rows from tblCustomers, only to have them shot down by the clause requirement that tblCustomers.contractID be not NULL. Since they allow more stringent restrictions on indexes, JOINs tend to be more efficient than LEFT JOINs.
since the field has the same name in both tables you can replace ON (a.field1 = b.field1) with the USING (field1) shortcut.
you obviously strongly want a covering index with (uniqueID, customerID) on both tables to maximize efficiency
this is so not going to work unless you have "real" tables for the update. The "tblCustomers" may be a view or a subselect, but customerABC may not. You might need a more complicated JOIN to pull out a complex WHERE which might be otherwise hidden inside a subselect, if the original 'SELECT * FROM customerABC' was indeed a more complex query than a straight SELECT. What this boils down to is, MySQL needs a strong unique key to know what it needs to update, and it must be in a single table. To reliably update more than one table I think you need two UPDATEs inside a properly write-locked transaction.

Group by query in sql

I have got 2 tables, Security and SecurityTransactions.
Security:
create table security(SecurityId int, SecurityName varchar(50));
insert into security values(1,'apple');
insert into security values(2,'google');
insert into security values(3,'ibm');
SecurityTable:
create table SecurityTransactions(SecurityId int, Buy_sell boolean, Quantity int);
insert into securitytransactions values ( 1 , false, 100 );
insert into securitytransactions values ( 1 , true, 20 );
insert into securitytransactions values ( 1 , false, 50 );
insert into securitytransactions values ( 2 , false, 120 );
I want to find out the security name and it's no of appearance in SecurityTransactions.
The answer is below:
SecurityName | Appearance
apple | 3
google | 1
I wrote the below sql query :
select S.SecurityName, count(t.securityID) as Appearance
from security S inner join securitytransactions t on S.SecurityId = t.SecurityId
group by t.SecurityId, S.SecurityName;
this query gave me the desired result, but it was still rejected by a person saying group by should have been s.securityName. why is it so ?
EDIT :
Which one do you this is correct and why ?
a. group by t.SecurityId, S.securityName
b. group by t.SecurityId
c. group by S.securityName
According to ANSI SQL, if you use a group by clause your select list may only contain items in the group by clause, single row transformations thereof, or aggregate expressions. MySQL is non-standard, and allows other columns too. In this case it happened to produce the right answer, as there's a 1:1 relationship between the SecuirtyId and SecurityName, but generally speaking, this is a bad practice that will make your code hard to understand at best, and unpredictable at worst.
EDIT:
To address the edited question - grouping by both SecuirtyId and SecurityName isn't technically wrong, it's just redundant. Since there's a 1:1 relationship between the two columns, adding the SecurityId column to the group by clause won't change the result, and will just confuse people reading the query.

List records in table A with only one relation in table B, and has a relation in table C

I have 3 tables:
ITEMS ITEM_FILES_MAP FILES
id id id
name item_id filename
in_trash file_id
FILES has a one to many relationship with ITEMS trough the ITEM_FILES_MAP table.
I need a select query that returns a list of files by the following critera:
Only return files related to items where in_trash = 1
Avoid files that are related to items where in_trash = 0
Example:
ITEMS
id name in_trash
1 Item A 0
2 Item B 0
3 Item C 1
4 Item D 1
FILES
id filename
1 File A
2 File B
3 File C
4 File D
5 File E
ITEM_FILES_MAP
id item_id file_id
1 1 2
2 1 3
3 2 1
4 3 2
5 3 4
6 4 3
7 4 4
Desired result:
Returns File D (id 4).
File B, C and D (id 2,3,4 in FILES table) is due to be returned, but because File B and C are related to items where in_trash = 0, they will not be listed.
Here is a sample dump if you want to test out solutions:
CREATE TABLE `files` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`filename` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `files` (`id`, `filename`)
VALUES
(1,'File A'),
(2,'File B'),
(3,'File C'),
(4,'File D'),
(5,'File E');
CREATE TABLE `item_files_map` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`item_id` int(11) DEFAULT NULL,
`file_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `item_files_map` (`id`, `item_id`, `file_id`)
VALUES
(1,1,2),
(2,1,3),
(3,2,1),
(4,3,2),
(5,3,4),
(6,4,3),
(7,4,4);
CREATE TABLE `items` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`in_trash` tinyint(1) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `items` (`id`, `name`, `in_trash`)
VALUES
(1,'Item A',0),
(2,'Item B',0),
(3,'Item C',1),
(4,'Item D',1);
Preparations
First, make sure you have an UNIQUE INDEX on fields item_id and file_id (in this order) on table item_files_map. No matter what query you run, if it includes this table the index will make the things fly instead or crawl. On some queries, however, an index having the fields in the opposite order would help more but for this task we need them in the presented order.
ALTER TABLE item_files_map
ADD UNIQUE INDEX item_file_id(`item_id`, `file_id`);
Also make sure you have an INDEX on items.in_trash.
ALTER TABLE items
ADD INDEX (`in_trash`);
For large tables it's possible that MySQL will ignore it if the ratio between 1 and 0 values is somewhere between 0.05 and 20 (if none of the values is used on less than 5% of the rows).
Probably the items having in_trash=1 are much fewer than those having in_trash=0 (or vice-versa) and this will convince MySQL to use the index for one of the instances of table items because the index removes a lot of rows from examination.
More, because the queries use only the fields PK and in_trash from this table, MySQL will use the index to get the information it needs and will not read the table data. And since the index is smaller than the table data, reading less bytes from the storage improves the execution speed.
The query, first attempt (following all the requirements)
A query that does what you need is:
# Query #1
SELECT DISTINCT f.id, f.filename
FROM items iit1
INNER JOIN item_files_map ifm1 ON iit1.id = ifm1.item_id
INNER JOIN files f ON f.id = ifm1.file_id
WHERE iit1.in_trash = 1
AND ifm1.file_id NOT IN (
SELECT ff.id
FROM files ff
INNER JOIN item_files_map ifm0 ON ff.id = ifm0.file_id
INNER JOIN items iit0 ON iit0.id = ifm0.item_id
WHERE iit0.in_trash = 0
);
Improving the query by slimming it
This query is not as good as can get and it can be improved if you are absolutely sure that table item_files_map does not contain orphan file_id values (i.e. values that cannot be found in column files.id). This should not happen on a well designed application and the database can help you avoid such situations by using FOREIGN KEY constraints (on InnoDB only).
Assuming this condition is met, we can remove table files from the inner query, making it simpler and faster:
# Query #2
SELECT DISTINCT f.id, f.filename
FROM items iit1
INNER JOIN item_files_map ifm1 ON iit1.id = ifm1.item_id
INNER JOIN files f ON f.id = ifm1.file_id
WHERE iit1.in_trash = 1
AND ifm1.file_id NOT IN (
SELECT ifm0.file_id
FROM item_files_map ifm0
INNER JOIN items iit0 ON iit0.id = ifm0.item_id
WHERE iit0.in_trash = 0
);
This query will produce the correct results.
The final query (ignore some of the requirements but produces correct results)
Another optimization can be done by selecting only the file.id and get rid of the filename for now, will run another query to get it:
# Query #3
SELECT DISTINCT ifm1.file_id
FROM items iit1
INNER JOIN item_files_map ifm1 ON iit1.id = ifm1.item_id
WHERE iit1.in_trash = 1
AND ifm1.file_id NOT IN (
SELECT ifm0.file_id
FROM item_files_map ifm0
INNER JOIN items iit0 ON iit0.id = ifm0.item_id
WHERE iit0.in_trash = 0
);
You can change the last JOIN to:
INNER JOIN items iit0 FORCE INDEX(PRIMARY) ON iit0.id = ifm0.item_id
to force MySQL use the PK for that join but I cannot tell if it will run faster. Maybe when the table becomes bigger.
This query doesn't select the filename (because it doesn't access the files table at all). It can be easily fetched (together with other fields from table files or with fields selected from other joined tables) using a query that runs like the wind because it uses the table's PK to get the rows it needs:
# Query #3-extra
SELECT *
FROM files
WHERE id IN (1, 2, 3)
Replace 1, 2, 3 with the list of file IDs returned by the previous query.
For big tables, these two queries could run faster than Query #2
Remark
As explained in the previous section, Query #2 and Query #3 assume there are no orphan file_id entries in the item_files_map table. If such orphan entries exist Query #3 can return invalid file_id values but they will be filtered out by Query #3-extra and the final result set returned by it will contain only valid results.
I did not test in mysql but you could do something like this :
SELECT filename FROM
(SELECT filename, sum(in_trash) AS s, count(*) AS c
FROM items, files, item_files_map
WHERE items.id = item_files_map.item_id AND files.id = item_files_map.file_id
GROUP BY filename) sub
WHERE s = c
The subquery computes for each filename the count of items referencing it and the count of items in trash. For your example it returns :
"D" 2 2
"B" 1 2
"C" 1 2
"A" 0 1
If these counts are the same then only in trash items reference.
EDIT: Following the suggestions of axiac, here is the query:
SELECT filename, files.id, sum(in_trash) AS s, count(*) AS c
FROM items, files, item_files_map
WHERE items.id = item_files_map.item_id AND files.id = item_files_map.file_id
GROUP BY files.id
HAVING s = c

field in subquery based on age of row instead of "group by"

I can't seem to get this query right. I have tables like this (simplified):
person: PersonID, ...other stuff...
contact: ContactID, PersonID, ContactDate, ContactTypeID, Description
I want to get a list of all the people who had a contact of a certain type (or types) but none of another type(s) that occurred later. An easy-to-understand example: Checking for records of gifts received without having sent a thank-you card afterward. There might have been other previous thank-you cards sent (pertaining to other gifts), but if the most recent occurrence of a Gift Received (we'll say that's ContactTypeID=12) was not followed by a Thank You Sent (ContactTypeID=11), the PersonID should be in the result set. Another example: A mailing list would be made up of everyone who has opted in (12) without having opted out (11) more recently.
My attempt at a query is this:
SELECT person.PersonID FROM person
INNER JOIN (SELECT PersonID,ContactTypeID,MAX(ContactDate) FROM contact
WHERE ContactTypeID IN (12,11) GROUP BY PersonID) AS seq
ON person.PersonID=seq.PersonID
WHERE seq.ContactTypeID IN (12)`
It seems that the ContactTypeID returned in the subquery is for the last record entered in the table, regardless of which record has the max date. But I can't figure out how to fix it. Sorry if this has been asked before (almost everything has!), but I don't know what terms to search for.
Wow. A system to check who has been good and sent thank yous. I think I would be in your list...
Anyway. Give this a go. The idea is to create two views: the first with personId and the time of the most recently received gift and the second with personId and the most recently sent thanks. Join them together using a left outer join to ensure that people who have never sent a thank you are included and then add in a comparison between the most recently received time and the most recent thanks time to find impolite people:
select g.personId,
g.mostRecentGiftReceivedTime,
t.mostRecentThankYouTime
from
(
select p.personId,
max(ContactDate) as mostRecentGiftReceivedTime
from person p inner join contact c on p.personId = c.personId
where c.ContactTypeId = 12
group by p.personId
) g
left outer join
(
select p.personId,
max(ContactDate) as mostRecentThankYouTime
from person p inner join contact c on p.personId = c.personId
where c.ContactTypeId = 11
group by p.personId
) t on g.personId = t.personId
where t.mostRecentThankYouTime is null
or t.mostRecentThankYouTime < g.mostRecentGiftReceivedTime;
Here is the test data I used:
create table person (PersonID int unsigned not null primary key);
create table contact (
ContactID int unsigned not null primary key,
PersonID int unsigned not null,
ContactDate datetime not null,
ContactTypeId int unsigned not null,
Description varchar(50) default null
);
insert into person values (1);
insert into person values (2);
insert into person values (3);
insert into person values (4);
insert into contact values (1,1,'2013-05-01',12,'Person 1 Got a present');
insert into contact values (2,1,'2013-05-03',11,'Person 1 said "Thanks"');
insert into contact values (3,1,'2013-05-05',12,'Person 1 got another present. Lucky person 1.');
insert into contact values (4,2,'2013-05-01',11,'Person 2 said "Thanks". Not sure what for.');
insert into contact values (5,2,'2013-05-08',12,'Person 2 got a present.');
insert into contact values (6,3,'2013-04-25',12,'Person 3 Got a present');
insert into contact values (7,3,'2013-04-30',11,'Person 3 said "Thanks"');
insert into contact values (8,3,'2013-05-02',12,'Person 3 got another present. Lucky person 3.');
insert into contact values (9,3,'2013-05-05',11,'Person 3 said "Thanks" again.');
insert into contact values (10,4,'2013-04-30',12,'Person 4 got his first present');

Performing joins

So this my first run into mysql databases,
I got a lot of help from my first question :
MYSQL - First Database Structure help Please
and built my database as pitchinnate recommended
I have :
Table structure for table club
Column Type Null Default
id int(11) No
clubname varchar(100) No
address longtext No
phone varchar(12) No
website varchar(255) No
email varchar(100) No
Table structure for table club_county
Column Type Null Default
club_id int(11) No
county_id int(11) No
Table structure for table county
Column Type Null Default
id int(11) No
state_id tinyint(4) No
name varchar(50) No
Table structure for table states
Column Type Null Default
id tinyint(4) No
longstate varchar(20) No
shortstate char(2) No
I set up foreign key relationships for everything above that looks that way.... states.id -> county.state_id for example
What I tried to run :
SELECT *
FROM club
JOIN states
JOIN county
ON county.state_id=states.id
JOIN club_county
ON club_county.club_id=club.id
club_county.county_id=county.id
This didn't work... I'm sure the reason is obvious to those of you who know what SHOULD be done.
What I'm trying to do is
get a listing of all clubs, with their associated state and county(ies)
You need to specify a JOIN condition for each of your joins. It should look something like the following:
SELECT *
FROM club
JOIN club_county ON club.id = club_county.club_id
JOIN county ON club_county.county_id = county.id
JOIN states ON county.state_id = state.id
Your version omitted an ON clause on the line that reads JOIN states.
One thing regarding your table names: It's advisable to stick to either singular or plural table names and not to mix them (notice you have club (singular) and states (plural) tables). This makes things easier to remember when you're developing and you're less likely to make mistakes.
EDIT:
If you want to limit which columns appear in your result, you just need to modify the SELECT clause. Instead if "SELECT *", you comma separate just the fields you want.
E.g.
SELECT club.id, club.name, county.name, states.name
FROM club
JOIN club_county ON club.id = club_county.club_id
JOIN county ON club_county.county_id = county.id
JOIN states ON county.state_id = state.id
The query you have written will not even execute as it has syntax error.
Please see this link for more details on JOINS:
13.2.8.2. JOIN Syntax
Also, `
SELECT *
FROM club a, county b, states c, club_county d
WHERE a.id = d.county_id
AND b.id = d.county_id
AND b.state_id = c.id
`
I hope this will help... If you still need help, please let us know...
Thanks...
Mr.777
So after you have modified the question, now the answer would be more like:
SELECT club.id,club.clubname,county.name,states.longstate,states.shortstate
FROM club,club_county,county,states
WHERE club.id=club_county.club_id
AND county.id=club_county.county_id
AND states.id = county.state_id
Please let me know if you need more help...
Thanks...
Mr.777