Minimizing repeated joins in MySQL to horizontally constructed table - mysql

I have a table which contains contacts (simplified here):
create table contacts
(
id_c char(36) not null primary key,
first_name varchar(255) not null,
last_name varchar(255) not null,
user_id1_c char(36) not null,
user_id2_c char(36) not null,
user_id3_c char(36),
user_id4_c char(36),
user_id5_c char(36),
user_id6_c char(36),
user_id7_c char(36) null,
user_id8_c char(36) null,
user_id9_c char(36) null,
user_id10_c char(36) null,
user_id11_c char(36) null,
user_id12_c char(36) null,
user_id13_c char(36) null,
user_id14_c char(36),
user_id15_c char(36) null,
user_id16_c char(36) null
)
engine = MyISAM
charset = utf8;
and a related table containing user information:
create table users (
id char(36) not null primary key,
first_name varchar(255),
last_name varchar(255)
)
For a report, I would want to be able to get the names of the associated users, and in MySQL this is a JOIN statement.
SELECT contacts.*,
CONCAT_WS(' ', u1.first_name, u1.last_name) AS `some_user`,
CONCAT_WS(' ', u2.first_name, u2.last_name) AS `some_other_user`,
FROM contacts
LEFT JOIN users u1 ON u1.id = user_id1_c
LEFT JOIN users u2 ON u2.id = user_id2_c
...
In the query, I would be able to get the names no problem (and before you go rushing to the comments with criticism for the design, this is the structure I got, so building a more abstract middleman user table isn't really within my pay grade). I recognize that appending 16 joins onto a query is slow even if it is well indexed. So my question becomes the following:
Is there a more direct way to address this problem? I have considered doing a LEFT JOIN users WHERE users.id IN (user_id1_c,...), but I cannot organize a GROUP BY or a distinct such that I retrieve the names matched to the column, just that a user maps to some column in a contact record.
Am I doomed to repeated LEFT JOINs, or is there a better way. If it matters, I am currently in version MySQL5.7, though I think we might be upgrading our company to version 8 sometime soon.

SELECT
c.*,
MAX(CASE WHEN u.id = c.user_id1 THEN u.whole_name END) AS user_name_1,
MAX(CASE WHEN u.id = c.user_id2 THEN u.whole_name END) AS user_name_2,
MAX(CASE WHEN u.id = c.user_id3 THEN u.whole_name END) AS user_name_3,
...
FROM
contacts
LEFT JOIN
(
SELECT
id,
CONCAT_WS(' ', first_name, last_name) AS whole_name
FROM
users
)
AS u
ON u.id IN (c.user_id1, c.user_id2, c.user_id3, ...)
GROUP BY
c.id
Really you should group by everything that you select from the contacts table, but MySQL 5.7 is lax and allows you to group by only the unique identififer.

Related

Tweets implementation MySQL and queries

I am implementing a simple follow/followers system in MySQL. So far I have three tables that look like:
CREATE TABLE IF NOT EXISTS `User` (
`user_id` INT AUTO_INCREMENT PRIMARY KEY,
`username` varchar(40) NOT NULL ,
`pswd` varchar(255) NOT NULL,,
`email` varchar(255) NOT NULL ,
`first_name` varchar(40) NOT NULL ,
`last_name` varchar(40) NOT NULL,
CONSTRAINT uc_username_email UNIQUE (username , email)
);
-- Using a middle table for users to follow others on a many-to-many base
CREATE TABLE Following (
follower_id INT(6) NOT NULL,
following_id INT(6) NOT NULL,
KEY (`follower_id`),
KEY (`following_id`)
)
CREATE TABLE IF NOT EXISTS `Tweet` (
`tweet_id` INT AUTO_INCREMENT PRIMARY KEY,
`text` varchar(280) NOT NULL ,
-- I chose varchar vs TEXT as the latter is not stored in the database server’s memory.
-- By querying text data MySQL has to read from it from the disk, much slower in comparison with VARCHAR.
`publication_date` DATETIME NOT NULL,,
`username` varchar(40),
FOREIGN KEY (`username`) REFERENCES `user`(`username`)
ON DELETE CASCADE
);
Lets say I want to write a query that returns the 10 latest tweets by users followed by the user with username "Tom". What is the best way to writhe that query and return results with username, first name, last name, text and publication date.
Also if one minute later I want to query again 10 latest tweets and assuming someone Tom follows tweets during that minute, how do I query the database to not select tweets that have already shown in the first query?
To answer your first question:
SELECT u1.username, u1.first_name, u1.last_name, t.text, t.publication_date
FROM Tweet t
JOIN User u1 ON t.username = u1.username
JOIN Following f ON f.following_id = u1.user_id
JOIN User u2 ON u2.user_id = f.follower_id
WHERE u2.username = 'Tom'
ORDER BY t.publication_date DESC
LIMIT 10
For the second part, simply take the tweet_id from the first row of the first query (so the latest tweet_id value) and use it in the WHERE clause for the next query i.e.
WHERE u2.username = 'Tom'
AND t.tweet_id > <value from previous query>
To get latest 10 tweets for Tom:
select flg.username, flg.first_name, flg.last_name, t.tweet_id, t.text, t.publication_date
from user flr
inner join following f on f.follower_id = flr.user_id
inner join user flg on flg.user_id = f.following_id
inner join tweet t on t.username = flg.username
where flr.username = 'Tom'
order by tweet_id desc
limit 10
To get the next 10 tweets, pass in the max tweet_id, and apply an additional condition in the where clause:
where flr.username = 'Tom'
and t.tweet_id > <previous_max_tweet_id>

SQL JOIN with conditions

I have a conversation table which contains two users ids as foreign keys, and the user table which contains the users details. I want to write a query which returns the conversation table joined to the user table but displaying the name and surname of the user whose id wasn't sent as the parameter.
CREATE TABLE `conversation` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_one_id` int(11) NOT NULL,
`user_two_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`surname` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
)
For example I have
Conversation:
id user_one_id user_two_id
1 1 2
User:
id name surname
1 userone_name userone_surname
2 usertwo_name usertwo_surname
I want a query that will return user_two's name and surname in the join, not user one.
My current query:
SELECT c.id, c.user_one_id, c.user_two_id, u.name, u.surname * FROM conversation c
JOIN user u
WHERE c.user_one_id = 1
OR c.user_two_id = 1
AND IF (c.user_one_id = u.id, c.user_two_id = u.id, c.user_one_id = u.id)
GROUP BY c.id
ORDER BY c.date DESC;
[INNER] JOIN should have an ON clause. (I consider it a flaw that MySQL allows you to omit it.)
The join criteria would have to be: Give me the user of the conversation that is not user 1.
SELECT c.id, c.user_one_id, c.user_two_id, u.name, u.surname
FROM conversation c
JOIN user u ON u.id IN (c.user_one_id, c.user_two_id) AND u.id <> 1
WHERE c.user_one_id = 1 OR c.user_two_id = 1
ORDER BY c.date DESC;

complicated sql query returns a result with empty tables

I have three empty tables
--
-- Tabellenstruktur für Tabelle `projects`
--
CREATE TABLE IF NOT EXISTS `projects` (
`id_project` int(11) NOT NULL AUTO_INCREMENT,
`id_plan` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
`description` longtext NOT NULL,
PRIMARY KEY (`id_project`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `project_plans`
--
CREATE TABLE IF NOT EXISTS `project_plans` (
`id_plan` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` longtext NOT NULL,
`max_projects` int(11) DEFAULT NULL,
`max_member` int(11) DEFAULT NULL,
`max_filestorage` bigint(20) NOT NULL DEFAULT '3221225472' COMMENT '3GB Speicherplatz',
PRIMARY KEY (`id_plan`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
-- --------------------------------------------------------
--
-- Tabellenstruktur für Tabelle `project_users`
--
CREATE TABLE IF NOT EXISTS `project_users` (
`id_user` int(11) NOT NULL,
`id_project` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
All these tables are empty but i get a result with my query?
my query:
SELECT
A.id_plan,
A.name AS plan_name,
A.description AS plan_description,
A.max_projects,
A.max_member,
A.max_filestorage,
B.id_plan,
B.name AS project_name,
B.description AS project_description,
C.id_user,
C.id_project,
COUNT(*) AS max_project_member
FROM
".$this->config_vars["projects_plans_table"]." AS A
LEFT JOIN
".$this->config_vars["projects_table"]." AS B
ON
B.id_plan = A.id_plan
LEFT JOIN
".$this->config_vars["projects_user_table"]." AS C
ON
C.id_project = B.id_project
WHERE
C.id_project = '".$id."'
&& B.deleted = '0'
i think the problem is the COUNT (*) AS ...
how i can solve the problem?
For one, you are getting a record explicitly due to the COUNT(). Even though you have no records, you are asking the engine how many records which at worst case will return zero. Count(), like other aggregates are anticipated to have a group by, so even though you don't have one, you are still asking.
So the engine is basically stating hey... there are no records, but I have to send you a record so you can get the count() column to look at and do with what you will. So, it is doing what you asked.
Now, for the comment to the other question where you asked...
Yes but i want to count the project member from a project, how i can count the users from project_users where all users have the id_project 1.
Since you only care about a count, and not the specific WHO involved, you can get this result directly from the project_users table (which should have an index on both the ID_User and another on the ID_Project. Then
select count(*)
from project_users
where id_project = 1
To expand from basis of your original question to get the extra details, I would do...
select
p.id_project,
p.id_plan,
p.name as projectName,
p.description as projectDescription,
pp.name as planName,
pp.description as planDescription,
pp.max_projects,
pp.max_member,
pp.max_filestorage,
PJCnt.ProjectMemberCount
from
( select id_project,
count(*) as ProjectMemberCount
from
project_users
where
id_project = 1 ) PJCnt
JOIN Projects p
on PJCnt.id_project = p.id_project
JOIN Project_Plans PP
on p.id_plan = pp.id_plan
Now, based on this layout of tables, a plan can have a max member count, but there is nothing indicating max members for the plan based on all projects, or max per SINGLE project. So, if a plan allows for 20 people, can there be 20 people for 10 different projects under the same plan? That's something only you would know the impact of... just something to consider what you are asking for.
Your cleaned-up query should look like :
See sqlfidle demo as well : http://sqlfiddle.com/#!2/e693f5/9
SELECT
A.id_plan,
A.name AS plan_name,
A.description AS plan_description,
A.max_projects,
A.max_member,
A.max_filestorage,
B.id_plan,
B.name AS project_name,
B.description AS project_description,
C.id_user,
C.id_project,
COUNT(*) AS max_project_member
FROM
project_plans AS A
LEFT JOIN
projects AS B
ON
B.id_plan = A.id_plan
LEFT JOIN
project_users AS C
ON
C.id_project = B.id_project
WHERE
C.id_project = '".$id."';
This will return you null values for all the cols from the select because you have one legit return form the result set and that is the count(*) output 0.
To fix this just add a group by at the end (see group by example http://sqlfiddle.com/#!2/14d46/2) or
Remove the count(*) and the null values will be gone as well as the count(*) values 0
See simple sql example here : http://sqlfiddle.com/#!2/ab7dd/5
Just comment the count() and you fixed you null problem!

MySQL Query with Left Join and having clause

I have a problem with a query can you tell me where I'm wrong?
I have 3 tables:
users:
`users` (
`id` int(11) AUTO_INCREMENT,
`first_name` varchar(16),
`last_name` varchar(16)
`email` varchar(16)
`password` varchar(32)
`phone` varchar(13)
`age` tinyint(4)
`gender` varchar(6)
)
users_eyes:
`users_eyes` (
`user_id` int(4),
`user_eyescolor_id` tinyint(2),
UNIQUE KEY `user_id` (`user_id`,`user_eyescolor_id`)
)
users_eyestype:
`users_eyestype` (
`user_id` int(4),
`user_eyestype_id` tinyint(2),
UNIQUE KEY `user_id` (`user_id`,`user_eyestype_id`)
)
This is my query
SELECT
SQL_CALC_FOUND_ROWS
u.first_name,
u.last_name,
u.age,
u.gender,
u.phone,
u.id
, GROUP_CONCAT(DISTINCT ue.user_eyescolor_id SEPARATOR ' ' ) as eyes_color
, GROUP_CONCAT(DISTINCT uet.user_eyestype_id SEPARATOR ' ' ) as eyes_type
FROM users u
LEFT JOIN users_eyes ue ON u.id = ue.user_id
LEFT JOIN users_eyestype uet ON u.id = uet.user_id
WHERE
ue.user_eyescolor_id IN (1,2,3,4)
GROUP BY u.id
HAVING
COUNT(ue.user_id) = 4
and the result is a guy with 2 kinds of eyes, not this one with 4 kinds of eyes
everything is just perfect before i join and eyes_type.
It would be nice to have test data to get you correctly.
Possibly you need COUNT(distinct ue.user_id) instead.
And no need LEFT JOIN users_eyes - inner join fits better as users_eyes fields used in where and having clauses.
UPDATE:
See a fixed query
EXPLANATION:
If user has e.g. 4 users_eyes and 3 users_eyestype after join he/she gets 12 records (cartesian product). That's why COUNT(ue.user_id) failed.
SOLUTION:
Use COUNT(user_eyescolor_id) = 4 instead. In a future I would advice to change the query, though there is no easy workarounds.

How to create a nested select query in sql

What I am trying to do is create a comments section for a website,
The comments consist of a user's name, email and comment. I store this data in the 'comments' table
CREATE TABLE `comments` (
`commentid` int(5) NOT NULL auto_increment,
`user` varchar(40) NOT NULL default '',
`email` varchar(100) NOT NULL default '',
`comment` text NOT NULL,
PRIMARY KEY (`commentid`)
)
What i want to do is execute a query that grabs all this data but also checks the email address in the 'users' table to see if it exists. If it does, grab the avatar from the 'misc' table. If the email doesn't exist in the 'users' table, it's just left blank.
At the moment with the query i tried, it only grabs the data from the 3 tables if the email exists in the 'users' table. I have another comment which as anonymous user left but that's not getting grabbed by the query.
CREATE TABLE `users` (
`userid` int(25) NOT NULL auto_increment,
`email` varchar(255) NOT NULL default '',
`username` varchar(25) NOT NULL default '',
PRIMARY KEY (`userid`)
)
CREATE TABLE `misc` (
`miscid` int(4) NOT NULL auto_increment,
`userid` varchar(3) NOT NULL default '',
`avatar` varchar(100) NOT NULL default '',
PRIMARY KEY (`miscid`)
)
I am pretty sure i need a nested select as a column name so that if there is an email it displays there...if not it's left blank.
EDIT:
Made the table structures how it should be.
This is a query I have just tried but it only displays a row which has an email address. there should be another without email address
SELECT c.comment, c.user, av.avatar
FROM comments c
INNER JOIN users u ON c.email = u.email
LEFT OUTER JOIN (
SELECT userid, avatar
FROM misc
) AS av ON av.userid = u.userid
If I correctly understood your issue, the problem is that you are using an INNER JOIN between comments and users, which means that it will only return matching rows on email. Thus the reason why it does not return comments that are without email addresses or non-matching email addresses.
Replace your INNER JOIN with a LEFT JOIN. Try out this query:
SELECT `c`.`comment`, `c`.`user`, `m`.`avatar`
FROM `comments` `c`
LEFT JOIN `users` `u` ON `c`.`email` = `u`.`email`
LEFT JOIN `misc` `m` ON `m`.`userid` = `u`.`userid`;
Hope that should help you get all comments.
Not really sure what your desired output, how you get the right misc for a given user, but here is the general idea
SELECT userid, email, username, IF(email<>'',(SELECT avatar from misc where miscid = users.userid),null) avater FROM users;
this is a more readable version
SELECT
userid,
email,
username,
IF(email<>''
,/*then*/(SELECT avatar from misc where miscid = users.userid)
,/*else*/null)
as avater
FROM users;
Please provide a clear list of your tables, and an example desired output, and we can better assist.
The final desired example output is very helpful when designing MySQL statements.
SELECT * FROM comments LEFT JOIN users
ON users.email=comments.email
Not really sure if this is what you mean, but I guess you just want some extra columns in your query result with the email address (empty if not available) and avatar (empty if not available), if that's right you can work with a LEFT JOIN.
SELECT
c.*,
u.email,
m.avatar
FROM
comments as c LEFT JOIN
users as u ON (u.userid = c.user) LEFT JOIN
misc as m ON (m.miscid = u.userid)
Please not that the column names you are using are quite weird and inconsistent; use just the name id for the id of the column, and reference only to those id's in other models.
No, what you actually need is LEFT OUTER JOIN.
Its purpose is exactly what you need - when joining two (or three) tables on some key and the left table has no correspondent key it's columnsare filled with NULL in the result set for that key.