I am creating a fantasy football android app where getting the JSON data is done with php files and SQL. My problem involves 2 tables:
player_details
player_id (primary key)
first_name
last_name
user_team
(All id fields except PK are foreign keys linked to the player_id)
user_team_id (primary key)
goalkeeper_id
rightback_id
leftback_id
etc.
I want to be able to run a PHP script, with a simple select statement in it, that will display a user team's details. I also want to include the first and last name of the players, which are linked to the foreign keys in the user_team position id fields. However all I can do is display the user_teams details and the players_id only.
SELECT *
FROM user_team
WHERE user_id = '".$uid."'
In this case, you have to do as many JOINs as the number of positions you have for your teams.
Like,
SELECT u.user_team_name, p1.goalkeeper_name, p2.rightback_name, p3.leftback_name, ...
FROM user_team u
LEFT JOIN player_details p1 ON u.goalkeeper_id = p1.player_id
LEFT JOIN player_details p2 ON u.rightback_id = p2.player_id
LEFT JOIN player_details p3 ON u.leftback_id = p3.player_id
...
WHERE u.user_team_id = <some id>
Doable, but not really good.
Then, you may change your model, and introduce the third table, that ties the player with both team and position, like:
CREATE TABLE team_positions (
player_id INT,
team_id INT,
position TEXT,
FOREIGN KEY player_id
REFERENCES player_details(player_id)
FOREIGN KEY team_id
REFERENCES user_team(user_team_id)
);
To increase the consitancy of data, it could be further normalized by introducing the positions table, than you going to refer it in team_positions by id too.
In the meantime, user_team table would retain just the team details, like name & any other stuff you wanna put there (emblem, perhaps?), along with team id.
This way, you're going to have quite flexible structure. And that is pretty much usual the way of doing stuff in relational DB model.
Related
I have 3 tables in a MySQL database: courses, users and participants, which contains about 30mil, 30k and 3k entries respectively.
My goal is to (efficiently) figure out the number of users that have been assigned to courses that matches our criteria. The criteria is a little more complex, but for this example we only care about users where deleted_at is null and courses where deleted_at is null and active is 1.
Simplified these are the columns:
users:
id
deleted_at
1
null
2
2022-01-01
courses:
id
active
deleted_at
1
1
null
1
1
2020-01-01
2
0
2020-01-01
participants:
id
participant_id
course_id
1
1
1
2
1
2
3
2
2
Based on the data above, the number we would get would be 1 as only user 1 is not deleted and that user assigned to some course (id 1) that is active and not deleted.
Here is a list of what I've tried.
Joining all the tables and do simple where's.
Joining using subqueries.
Pulling the correct courses and users out to the application layer (PHP), and querying participants using WHERE IN.
Pulling everything out and doing the filtering in the application layer.
Calling using EXPLAIN to add better indexes - I, admittedly, do not do this often and may not have done this well enough.
A combination of all the above.
An example of a query would be:
SELECT COUNT(DISTINCT participant_id)
FROM `participants`
INNER JOIN
(SELECT `courses`.`id`
FROM `courses`
WHERE (`active` = '1')
AND `deleted_at` IS NULL) AS `tempCourses` ON `tempCourses`.`id` = `participants`.`course_id`
WHERE `participant_type` = 'Eloomi\\Models\\User'
AND `participant_id` in
(SELECT `users`.`id`
FROM `users`
WHERE `users`.`deleted_at` IS NULL)
From what I can gather doing this will create a massive table, which only then will start applying where's. In my mind it should be possible to short circuit a lot of that because once we get a match for a user, we can disregard that going forward. That would be how to handle it, in my mind, in the application layer.
We could do this on a per-user basis in the application layer, but the number of requests to the database would make this a bad solution.
I have tagged it as PHP as well as MySQL, not because it has to be PHP but because I do not mind offloading some parts to the application layer if that is required. It's my experience that joins do not always use indexes optimally
Edit:
To specify my question: Can someone help me provide a efficient way to pull out the number of non-deleted users that have been assigned to to active non-deleted courses?
I would write it this way:
SELECT COUNT(DISTINCT p.participant_id)
FROM courses AS c
INNER JOIN participants AS p
ON c.id = p.course_id
INNER JOIN users AS u
ON p.participant_id = u.id
WHERE u.deleted_at IS NULL
AND c.active = 1 AND c.deleted_at IS NULL
AND p.participant_type = 'Eloomi\\Models\\User';
MySQL may join the tables in another order, not the order you list the tables in the query.
I hope that courses is the first table MySQL accesses, because it's probably the smallest table. Especially after filtering by active and deleted_at. The following index will help to narrow down that filtering, so only matching rows are examined:
ALTER TABLE courses ADD KEY (active, deleted_at);
Every index implicitly has the table's primary key (e.g. id) appended as the last column. That column being part of the index, it is used in the join to participants. So you need an index in participants that the join uses to find the corresponding rows in that table. The order of columns in the index is important.
ALTER TABLE participants ADD KEY (course_id, participant_type, participant_id);
The participant_id is used to join to the users table. MySQL's optimizer will probably prefer to join to users by its primary key, but you also want to restrict that by deleted_at, so you might need this index:
ALTER TABLE users ADD KEY (id, deleted_at);
And you might need to use an index hint to coax the optimizer to prefer this secondary index over the primary key index.
SELECT COUNT(DISTINCT p.participant_id)
FROM courses AS c
INNER JOIN participants AS p
ON c.id = p.course_id
INNER JOIN users AS u USE INDEX(deleted_at)
ON p.participant_id = u.id
WHERE u.deleted_at IS NULL
AND c.active = 1 AND c.deleted_at IS NULL
AND p.participant_type = 'Eloomi\\Models\\User';
MySQL knows how to use compound indexes even if some conditions are in join clauses and other conditions are in the WHERE clause.
Caveat: I have not tested this. Choosing indexes may take several tries, and testing the EXPLAIN after each try.
I don't know if it's possible but this is what I want to do.
I have three tables
Accounts
id | username | pass
Cars
id | plate | id_ac | kilometers // in the id_ac I add manual the id of the car's owner.
Trips
id | plate | places | date | active
Now I want when the user is logged in to see tha Trips that are active with his car.
So I want to
SELECT from Trips the plate, places, date WHERE active=0 AND id= ? (the logged user's id)
But the table trips doesn't have the id of the owner of the car. So I want somehow to select the values of the table Trips of the car that logged user owns.
Any idea how can I do that?
You should have a look at table joins. This looks like it is what you are looking for:
SELECT t.plate, t.places, t.date FROM Trips as t JOIN Cars as c ON t.plate = c.plate WHERE t.active = 0 AND c.id_ac = ?
Have a look at this. This is a pretty nice way of explaining the table joins.
http://www.sql-join.com/sql-join-types
I am not getting into DB architecture and deeper stuff, but it can help you with your task at hand
What you are referring to is primary and forgein key.
Your primary key is a unique key, which can identify every record in your table. In your user table that is the id. It has to be unique.
When you are now creating trips in your trips table, you will have to "link" your record to the specific user. Here you are referring to the primary key in user's table. So you have a new column user_id inside your trips table, where you will store the user id, who is connected to that trip.
When using the user id in other tables, you are having a "foreign key".
For each trip you will have to create a foreign key for the car and for the user. This can be the number plate and the user id.
If you want to see all the tracks for a specific user, you can make a JOIN with the user id in tracks. If you want to see all tracks for one car, you will just JOIN with the plate.
(I can just suggest reading some information about primary/ foreign key. It is fundamental when designing a database structure as you did above.)
From what I've been reading online, I understood that it's better to split the data into more tables, if possible because of the access times.
Right now I have a table in which I am storing usernames, passwords and join date
This is how my table looks:
'user'
'user_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
user_username VARCHAR(80) NOT NULL,
user_password VARCHAR(255) NOT NULL,
user_join_date INTEGER UNSIGNED NOT NULL,
PRIMARY KEY (user_id) ');
I am trying to create a new table called profiles in which I want to store first name, last name, email, age and gender. What I think is that I need a one-to-one relationship, so I should be using the user_id but I'm not really sure how to implement it.
Should I create another table called profiles with
profiles
profiles_id
first_name
last_name
email
age
gender
and another one which should be the relationship ? e.g.
user_profiles
----------
user_id
profiles_id
Is this right? How would the SQL look for the user_profiles?
Thanks
Don't split the tables. Just add the new columns to your existing user table. You might find later on that splitting tables is a good idea based on actual queries and usage patterns but until you have that kind of data, keep things simple.
If you must create a profile table, don't create a user_profiles table. That would allow an m-to-n relationship which is probably not what you want. A simple user_id column in profiles is better. In fact, it could be both a foreign key and the primary key to make sure that each user row only have one and only one profile row (although by splitting the tables you might still have a user with no profile).
Usually, you create an association table, like user_profiles you have described when one user could have more than one profile, and/or one profile could belong to one or more user.
As you have said, here you have a one-to-one relationship between user and profile. So, you can simply add a user_id column to your profile table, and define it as a foreign key to user table.
Then, a simple JOIN will allow you to query both tables at the same time:
SELECT u.*, p.*
FROM user u
JOIN profile p ON u.user_id = p.user_id
Add a new field in the User table, ProfileId, and set it as Foreign Key (FK). Each time you create a User, you have to assign to it a profile (which will be the ProfileId PK from profile table).
If you want to see also the profile information of a user, you have to do a join
Select username, first_name,second_name
From user u, profile p
Where u.profileId = p.profileId
this
user_profiles
----------
user_id
profiles_id
is used in a many-to-many relationship. By example, you want to assign to an admin some privileges, but those privileges can be also assigned to more admins. Then, you have to create a 3rd table to solve this problem. Here is an example, but you don't need to do this.
You could add a user_id field to your profiles table and JOIN the tables on user_id.
SELECT user.user_username, ..., profiles.first_name, ...
FROM user
INNER JOIN profiles
ON user.user_id = profiles.user_id
This should fetch data combining information from those rows where the JOIN condition is met (i.e. user.user_id = profiles.user_id).
It is true that having more than one tables is a good idea. I am not sure what you mean about access time, but there are other advantages.
- Your users database containing passwords etc is "sacred", you never change its structure and you limit the rights to it (read, write) to the strict minimum.
- You can then have several "satelites" tables such as profiles, private messages, etc which are more flexible, less sensitive and which you can change all the time.
About your question per se, there is no need for a separate table with the relationships. In fact is a very bad idea which will complicate your queries and doesn't have any advantage. Instead, in your profiles database you will have one column that refers back to the user id.
users
--------
id
user_name
email
password
users_profiles
---------
id
user_id
favourite_animal
Table user
user_id |user_username |user_password |user_join_date |profile_id
Table profile
profile_id |first name |last name |email |age |gender
When selecting a user by user id:
SELECT u.*, p.* FROM user AS u INNER JOIN `profile` AS p ON u.profile_id = p.profile_id WHERE u.user_id = 1
But a user should only one gender, one age, one name and surname. Maybe e-mail adresses might be many. I suggest you there is no need to join tables which have a 1-to-1 relation. Instead merge those tables.
Okay I am making a profile page where I will call on all of the user's information. Within the website users will gain points and earn badges. I have something like 35 badges. Rather than have a row in my users table for every badge(yes/no to decide whether user has earned badge) for every single user, I was wondering how I could do this without blowing up my users table.
I have a badge table with index, name, description, and photo. I was wondering can I make a single row in my users table for badges and separate badge numbers by ",". Then decipher the badges so they all print on the page. I feel I can do this but don't know how.
Please help. Open to other suggestions
You are describing a many-to-many relationship; use a junction table to represent it.
Create a table of users and a table of badges. Then create a Users_Badges table with user_id and badge_id as foreign keys in it (together they will form a composite key).
you can create table structures like these.
table structures
users
userId
userName
badges
badgeId
badgeName
user_badges
userId
badgeID
-- returns given users all badges within single row and comma seperated field.
select group_concat(b.badgeName) as usersAllBadges
from user_badges ub
inner join users u on u.userId = ub.userId
inner join badges b on b.badgeId = ub.badgeID
where ub.userID=1
-- returns given users all badges seperate rows.
select b.badgeName
from user_badges ub
inner join users u on u.userId = ub.userId
inner join badges b on b.badgeId = ub.badgeID
where ub.userID=1
You tagged php so I am assuming php
explode(',',$data);
http://www.php.net
When building a category navigation system for a business directory with a many to many relationship, I understand that it is good practise to create a mapping table.
Category Table ( CategoryId, CategoryName )
Business Table ( BusinessId, BusinessName )
Category Mapping Table ( BusinessId, CategoryId )
When I join the Category table and Business table to create the mapping table would this then give me a table which contains every possible business and category relationship?
I have 800 categories and 1000 business listings. Would that then give me a table containing 800,000 possible relationships. If so how would I focus on only the relationships that exist? Would I have to go through all listings (800,000) marking them as true or false?
I have been getting really confused about this so any help would be much appreciated.
When using many-to-many relationships, the only realistic way to handle this is with a mapping table.
Lets say we have a school with teachers and students, a student can have multiple teachers and visa versa.
So we make 3 tables
student
id unsigned integer auto_increment primary key
name varchar
teacher
id unsigned integer auto_increment primary key
name varchar
link_st
student_id integer not null
teacher_id integer not null
primary key (student_id, teacher_id)
The student table will have 1000 records
The teacher table will have 20 records
The link_st table will have as many records as there are links (NOT 20x1000, but only for the actual links).
Selection
You select e.g. students per teacher using:
SELECT s.name, t.name
FROM student
INNER JOIN link_st l ON (l.student_id = s.id) <--- first link student to the link-table
INNER JOIN teacher t ON (l.teacher_id = t.id) <--- then link teacher to the link table.
ORDER BY t.id, s.id
Normally you should always use an inner join here.
Making a link
When you assign a teacher to a student (or visa versa, that's the same).
You only need to do:
INSERT INTO link_st (student_id, teacher_id)
SELECT s.id, t.id
FROM student s
INNER JOIN teacher t ON (t.name = 'Jones')
WHERE s.name = 'kiddo'
This is a bit of a misuse of an inner join, but it works as long as the names are unique.
If you know the id's, you can just insert those directly of course.
If the names are not unique this will be a fail and should not be used.
How to avoid duplicate links
It's very important to avoid duplicate links, all sorts of bad things will happen if you have those.
If you want to prevent inserting duplicate links to your link table, you can declare a unique index on the link (recommended)
ALTER TABLE link_st
ADD UNIQUE INDEX s_t (student_id, teacher_id);
Or you can do the check in the insert statement (not really recommended, but it works).
INSERT INTO link_st (student_id, teacher_id)
SELECT s.id, t.id
FROM student s
INNER JOIN teacher t ON (t.id = 548)
LEFT JOIN link_st l ON (l.student_id = s.id AND l.teacher_id = t.id)
WHERE (s.id = 785) AND (l.id IS NULL)
This will only select 548, 785 if that data is not already in the link_st table, and will return nothing if that data is in link_st already. So it will refuse to insert duplicate values.
If you have a table schools, it depends if a student can be enrolled in multiple schools (unlikely, but lets assume) and teachers can be enrolled in multiple schools. Very possible.
table school
id unsigned integer auto_increment primary key
name varchar
table school_members
id id unsigned integer auto_increment primary key
school_id integer not null
member_id integer not null
is_student boolean not null
You can list all students in a school like so:
SELECT s.name
FROM school i
INNER JOIN school_members m ON (i.id = m.school_id)
INNER JOIN student s ON (s.id = m.member_id AND m.is_student = true)
When I join the Category table and
Business table to create the mapping
table would this then give me a table
which contains every possible business
and category relationship?
Yes.
Would I have to go through all listings (800,000) marking them as true or false?
No, you need to use the ON-clause to set join-conditions.
SELECT <columns> FROM categories as c
INNER JOIN mapping AS m
ON m.CategoryId = c.CategoryId
INNER JOIN businesses as b
ON m.BusinessId = b.BusinessId
You should use mapping tables when you are trying to model a many-to-many or one-to-many relationship.
For example, in an address book application, a particular contact could belong to zero, one or many categories. If you set your business logic that a contact can only belong to one category, you would define your contact like:
Contact
--------------
contactid (PK)
name
categoryid (FK)
Category
--------------
categoryid (PK)
categoryname
But if you wanted to allow a contact to have more than one email address, use a mapping table:
Contact
--------------
contactid (PK)
name
Category
--------------
categoryid (PK)
categoryname
Contact_Category
--------------
contactid (FK)
categoryid (FK)
Then you can use SQL to retrieve a list of categories that a contact is assigned to:
select a.categoryname from Category a, Contact b, Contact_Category c where a.categoryid=c.categoryid and b.contactid=c.contactid and b.contactid=12345;
select a.categoryname
from Category a
inner join Contact_Category c on a.categoryid=c.categoryid
inner join Contact b on b.contactid=c.contactid
where b.contactid=12345;
you only put the real relationships in the mapping table. so on average fi a business is in 2 categories, then in your example, there would only be 2000 records in the mapping table, not 800,000
"When I join the Category table and Business table to create the mapping table" you don't join those two tables to create the mapping table. You create an actual physical table.