Most efficient way to set up MySQL Tables? - mysql

I have a table called 'users' that contains fields such as
username
DOB
gender
etc
Each user can save their plants on a table called 'plants' that has the following fields
username
plant
So user 'johndoe' would show on table 'plants' as follows:
johndoe -> lilly
johndoe -> orchid1
johndoe -> orchid2
johndoe -> fern1
johndoe -> fern2
Questions:
Is this a bad way of setting up my tables? I ask because when the number of users grows, then there will be hundreds of rows, several of which with repeated usernames because each user will likely have more than one plant. Would this slow down queries?
If I make one plant table per user, I think that would increase dramatically the number of DB requests and create an enormous amount of tables in the DB.
Another option would be to have separate tables for lilly, orchid, fern, etc with fields such as
username
but then I would still have several rows with repeated usernames.
Any suggestions?

Normalize your tables. Add id field to users and store that id in plants instead of the username.
Further, read more about normalization principles and try to apply them in practice.

In stead of using the username I would use an INT id. Like so:
userid
username
DOB
gender
etc
and for your plants:
plantid
userid_fk
plantname
And then join on users.userid = plants.userid_fk to find the matches :)

That's a perfectly reasonable way (your initial proposal) to set things up; that's what relational databases are for.
You're doing a classic many-to-many relationship in your plants table. Each user can have several plants, and each plant may have several users.
For better efficiency, you could use an id to refer to your users instead of a user name. In most relational databases, an id is a row number, and is a very efficient (and compact) reference.

Here's how I'd go about this, with an example database script and query:
Create/Load Users Table - Using a surrogate primary key instead of the username since queries will be faster against INTs and they can conceivably change.
CREATE TABLE users (
id int(11) AUTO_INCREMENT NOT NULL,
username varchar(255) NOT NULL,
gender char(1) NULL,
PRIMARY KEY(id)
) ENGINE = MyISAM AUTO_INCREMENT = 3
Load Users
INSERT INTO users(id, username, gender)
VALUES(1, 'john', NULL)
GO
INSERT INTO users(id, username, gender)
VALUES(2, 'jane', NULL)
GO
Create Plants
CREATE TABLE plants (
id int(11) AUTO_INCREMENT NOT NULL,
name varchar(200) NOT NULL,
PRIMARY KEY(id)
) ENGINE = MyISAM AUTO_INCREMENT = 6
Load Plants
INSERT INTO plants(id, name)
VALUES(1, 'fern1')
GO
INSERT INTO plants(id, name)
VALUES(2, 'fern2')
GO
INSERT INTO plants(id, name)
VALUES(3, 'orchid1')
GO
INSERT INTO plants(id, name)
VALUES(4, 'orchid2')
GO
INSERT INTO plants(id, name)
VALUES(5, 'lilly1')
Create Join Table I use a surrogate primary key here since so many web platforms won't properly update a join table with a 2 column primary. Also, I've ended up adding meta data / details to these types of joins quite often, and it gets messy without it. Note I don't restrict the combination of the same user adding the same plant type, but you can easily.
CREATE TABLE plants_users (
id int(11) AUTO_INCREMENT NOT NULL,
plant_id int(11) NOT NULL,
user_id int(11) NOT NULL,
PRIMARY KEY(id)
)
ENGINE = MyISAM
AUTO_INCREMENT = 8
GO
CREATE INDEX fk_plant_id USING BTREE
ON plants_users(plant_id)
GO
CREATE INDEX fk_user_id USING BTREE
ON plants_users(user_id)
Load Join Table
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(1, 1, 1)
GO
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(2, 2, 1)
GO
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(3, 3, 1)
GO
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(4, 4, 1)
GO
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(5, 2, 2)
GO
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(6, 3, 2)
GO
INSERT INTO plants_users(id, plant_id, user_id)
VALUES(7, 5, 2)
Final Query:
select
users.username,
plants.name
from
users,plants,plants_users
where
users.id = plants_users.user_id and
plants.id = plants_users.plant_id
Result:
user name
john fern1
john fern2
john orchid1
john orchid2
jane fern2
jane orchid1
jane lilly1

Related

How do you ON DUPLICATE KEY UPDATE a user info without breaking the database?

I have a simple table with students in MySQL database. Similar to:
student_id
student_name
teacher_id
1
Adam
100
2
Bob
100
3
Carl
100
4
Dan
200
Teachers can input new students or change existing student's names. Currently I have this set up to update via:
INSERT INTO student_list (name) VALUES (:name) ON DUPLICATE KEY UPDATE student name= values(name)
The problem is that this completely breaks any associated student_ids in other tables (as the ON DUPLICATE KEY UPDATE changes the student_id). I could look up each entry before updating, but it seems messy. Surely there's a better way.
CREATE TABLE `sabrep_db`.`students` ( `student_id` INT NOT NULL AUTO_INCREMENT , `name` TEXT NOT NULL , `teacher_id` INT NOT NULL , PRIMARY KEY (`student_id`)) ENGINE = InnoDB;
INSERT INTO `students` (`student_id`, `name`, `teacher_id`) VALUES (NULL, 'Adam', '100'), (NULL, 'Bob', '100');
Should also pass a key (usually the primary key) as part of the insert so that it knows the key reference to update when there is a duplicate, name is passed twice for the update portion:
INSERT INTO student_list (student_id, name)
VALUES (:student_id, :name) ON DUPLICATE KEY UPDATE student_name = :name;

How can I find out which table uses a specific foreign key value?

In my MariaDB database, I have three tables, Users, Workers and Admins, where the Users table has the column ID as primary key. Workers and Admins use a Users.ID as foreign (and primary) key. Users can either be Workers or Admins, not both. How can I find out, using a Users.ID, whether they are Workers or Admins?
I've tried this function:
CREATE FUNCTION workeroradmin(id INT)
RETURNS VARCHAR(20)
BEGIN
DECLARE type VARCHAR(20)
IF (SELECT COUNT(*) FROM Workers WHERE ID = id) = 1
THEN
SET type='Worker'
ELSEIF (SELECT COUNT(*) FROM Admins WHERE ID = id) = 1
THEN
SET type='Admin'
RETURN type;
END;
But this would just give me SQL syntax errors. Is a function the way to go here or is a simple IF clause enough?
Are you sure that one user cannot be Worker AND Admin simultaneously?
I'd do something like this instead:
SELECT
Users.Id
, IF(Workers.Id IS NOT NULL, 1, 0) AS IsWorker
, IF(Admins.Id IS NOT NULL, 1, 0) AS IsAdmin
FROM
Users
LEFT JOIN Workers ON (Workers.Id = Users.Id)
LEFT JOIN Admins ON (Admins.Id = Users.Id)
WHERE
...
;
You can do the same using sub-query instead of LEFT JOIN.
If you realy would like to use your way, you can do something like this:
DELIMITER $$
CREATE FUNCTION `workeroradmin`(`_id` INT)
RETURNS varchar(20)
LANGUAGE SQL
BEGIN
IF((SELECT `Id` FROM `Workers` WHERE `Id` = _id) IS NOT NULL) THEN
SET #result = 'Worker';
ELSEIF((SELECT `Id` FROM `Admins` WHERE `Id` = _id) IS NOT NULL) THEN
SET #result = 'Admin';
END IF;
RETURN #result;
END$$
DELIMITER ;
I would not use COUNT just to check existence as this is sub-optimal, Id would to the same.
I don't know if this solution is overkill to you but you can actually enforce a user to be either a worker "xor" an admin. This solution works starting on MariaDB 10.2.1.
You can use composite primary keys and CHECK constraints (that actually work in MariaDB 10.2.1 and newer).
For example:
create table users (
id int not null,
type int not null check (type in (1, 2)),
name varchar(20),
primary key (id, type)
);
create table workers (
id int not null,
type int not null check (type = 1),
position varchar(20),
primary key (id, type),
foreign key (id, type) references users (id, type)
);
create table admins (
id int not null,
type int not null check (type = 2),
server varchar(20),
primary key (id, type),
foreign key (id, type) references users (id, type)
);
insert into users (id, type, name) values (100, 1, 'Anne'); -- worker
insert into workers (id, type, position) values (100, 1, 'Manager');
insert into users (id, type, name) values (105, 2, 'Tom'); -- admin
insert into admins (id, type, server) values (105, 2, 'server1');
insert into workers (id, type, position) values (105, 2, 'Programmer'); -- fails!
See? The last insert fails, so Tom cannot be an admin and a worker at the same time, and this is enforced at the database level, not your code.
Then, how do you know if it's an admin or a worker? Just look at the column type in the table users. Much simpler and cheap.
Cheers!

Primary / foreign key; joining multiple tables using subqueries

I have a question for an assignment with 5 tables as shown below. I need to write a query with the minimum cost for each sport:
2nd column is equipment_name:
I think I need to do a bunch of joins in subqueries with the primary keys being the id columns and the foreign keys the name_id columns. Is this the right approach?
You don't need a bunch of joins; minimally this question can be solved by one join between the store_equipment_price and the sports_equipment tables - if these two are joined on equipment id then you'll effectively get rows that can give the cost of starting up in each sport per store. You'll need to group by the sport id and the store id; don't forget that it might be cheaper to start soccer by getting all the gear from store A but it might be cheaper to start golf by going to tore B - tho I how I read the question. If however you're prepared to get your gloves from store A and your bat from store B etc then we don't even group by the store when summing, instead we work out which store is cheapest for each component rather than which store is cheapest for each sport overall.
If you're after producing named stores/sports on your result rows then you'll need more joins but try getting the results right based on the fewest number of joins possible to start with
Both these queries will ultimately be made a lot easier by the use of an analytic/windowing function but these are database dependent; never post an sql question up without stating what your db vendor is, as there are few questions that are pure ISO SQL
You question is not completely clear, I assume you need to find stores from which to buy each equipment for all sports so as to incur minimum expense. Following query will achieve this
select s.sports, e.equipment_name, min(sep.price),
(select store_name from stores st where st.id = sep.store_id) store_name
from sports s
join sports_equipment se on s.id = se.sport_id
join equipment e on e.id = se.equipment_id
join sports_equipment_prices sep on sep.equipment_id = se.equipment_id
group by s.sports, e.equipment_name
order by s.sports, e.equipment_name
;
Following 'create table' and 'insert data' script are based on your screen images
create table sports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sports varchar(50)
);
insert into sports(sports) values('golf');
insert into sports(sports) values('baseball');
insert into sports(sports) values('soccer');
create table stores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
store_name varchar(50)
);
insert into stores(store_name) values('A');
insert into stores(store_name) values('B');
insert into stores(store_name) values('C');
create table equipment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
equipment_name varchar(50)
);
insert into equipment(equipment_name) values('shoes');
insert into equipment(equipment_name) values('ball');
insert into equipment(equipment_name) values('clubs');
insert into equipment(equipment_name) values('glove');
insert into equipment(equipment_name) values('bat');
create table sports_equipment (
sport_id INTEGER not null,
equipment_id INTEGER not null,
FOREIGN KEY(sport_id) REFERENCES sports(id),
FOREIGN KEY(equipment_id) REFERENCES equipment(id)
);
insert into sports_equipment values(1, 1);
insert into sports_equipment values(1, 2);
insert into sports_equipment values(1, 3);
insert into sports_equipment values(2, 2);
insert into sports_equipment values(2, 4);
insert into sports_equipment values(2, 5);
insert into sports_equipment values(3, 1);
insert into sports_equipment values(3, 2);
create table sports_equipment_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
store_id INTEGER not null,
equipment_id INTEGER not null,
price INTEGER not null,
FOREIGN KEY(store_id) REFERENCES stores(id),
FOREIGN KEY(equipment_id) REFERENCES equipment(id)
);

How do i concatenate information from a foreign key?

i just realised i've worded this very poorly
i'm trying to find out if i can do this within an insert statement, not when trying to output the data
This might sound very confusing but hopefully i can explain it.
I have two tables, expertise (parent) and department (child)
In expertise I have :
exp_id int(2) primary key
exp_name varchar(30)
In department I have:
dep_id int(2) primary key
dep_name varchar(30)
exp_id int(2) foreign key
I DO NOT want the outcome for the department rows to look like this:
dep_id dep_name exp_id
1 accounting 32
1 accounting 27
1 accounting 29
I WANT IT to look like this
dep_id dep_name exp_id
1 accounting 32, 27, 29
So there is multiple rows within a row, if that makes any sense.
I believe its concatenation i have to work with, but I've never used this before and was looking for some help
i just realised i've worded this very poorly
i'm trying to find out if i can do this within an insert statement, not when trying to output the data
you should be able to use group_concat, ie:
select dep_id, dep_name, group_concat(exp_id) exp_id
from department
group by dep_id, dep_name
You should also consider normalisation your department table, it is currently violating first normal form due to the repeating groups of dep_id, dep_name. reference here
edit
I see you updated your question to say you want the data stored in this way in the table, not displayed.
The answer to that is: you really don't.
delimited fields are all but impossible to deal with, and are just all around horrible in an rdbms. If you absolutely must have it this way, you could use the above query to create a view, and at least store the real data in a proper, normalised fashion.
To re-iterate. Please don't do this. Kittens will die.
In the spirit of normalized data
-- drop table expertise
create table expertise
( expId int not null,
expName varchar(100) not null
);
insert expertise (expId,expName) values (32,'number massaging');
insert expertise (expId,expName) values (27,'misrepresentation');
insert expertise (expId,expName) values (29,'embezzlement');
-- select * from expertise
-- drop table department;
create table department
( deptId int not null,
deptName varchar(100) not null
);
insert into department (deptId,deptName) values (1,'Accounting');
insert into department (deptId,deptName) values (2,'Executive');
insert into department (deptId,deptName) values (3,'Food Service');
-- select * from department
-- drop table expDeptIntersect;
create table expDeptIntersect
( expId int not null,
deptId int not null
);
insert expDeptIntersect (expId,deptId) values (27,1);
insert expDeptIntersect (expId,deptId) values (32,1);
insert expDeptIntersect (expId,deptId) values (29,1);
--select * from expdeptintersect
select d.deptId,d.deptName,group_concat(i.expId) expId
from department d
join expDeptIntersect i
on i.deptId=d.deptId
group by d.deptId,d.deptName

Generate Unique AI ID over multiple data tables

I am designing a database but I come to a struggle at this moment,
Currently I have 3 tables:
ex_artists
ex_tracks
ex_labels
Now I wish to have a Unique ID throughout these 3(or more) tables
So if 'Example Artist' had ID '1', then 'Example Track' can not also have ID '1'
but should get ID '2' since '1' already exists
I understand your concerns. Once you decide to design your database with technical IDs, there is always the danger of confusing IDs. While
insert into album_track (album, artist, track, no)
values ('B0016991-00', 'JBIEBER', 'BOYFRIEND0001', 2);
instead of
insert into album_track (album, artist, track, no)
values ('B0016991-00', 'BOYFRIEND0001', 'JBIEBER', 2);
would probably through an error,
insert into album_track (album_id, artist_id, track_id, no) values (40, 22, 12, 2);
instead of
insert into album_track (album_id, artist_id, track_id, no) values (40, 12, 22, 2);
would probably not, and the time you notice your program error it may be too late to tell the bad records from the good ones. Your data would be technically consistent, but a mess really.
To overcome this problem, you need one source to pull your IDs from. In Oracle for instance you would use a sequence. In MySQL you can create an ID table for this only purpose:
create table ids(id int auto_increment primary key);
create table album(id int primary key, album_no text, album_name text,
foreign key (id) references ids(id));
create table track(id int primary key, track_name text, record_date date, take int,
foreign key (id) references ids(id));
insert into ids values ();
insert into album (id, album_no, album_name) values
((select last_insert_id), 'B0016991-00', 'Justin Bieber – Believe - Deluxe Edition');
So whenever you insert a record in one of your tables, you must specify an ID (because it is not automatically got). You get the ID with an insert into your IDs table and then call MySQL's LAST_INSERT_ID().
An less safe, but simpler alternative would be to start the IDs at different offsets:
create table album(id int auto_increment primary key, album_no text, album_name text);
create table track(id int auto_increment primary key, track_name text, record_date date);
alter table track auto_increment=10000001;
create table artist(id int auto_increment primary key, artist_name varchar(100));
alter table artist auto_increment=20000001;
insert into artist (artist_name) values ('Justin Bieber');
This works as long as your IDs stay in the desired range.
it is not that i didn't read what you wrote :>
but you don't need a unique ID across them all, just unique ways of identifying rows.
consider the following:
-- drop table artist
create table artist
(
artist_id int unsigned auto_increment PRIMARY KEY,
artist_name varchar(255) not null
);
insert into artist (artist_name) values ('U2');
insert into artist (artist_name) values ('Cranberries');
-- drop table label
create table label
(
label_id int unsigned auto_increment PRIMARY KEY,
artist_id int not null, -- will leave FK RI to developer
label_name varchar(255) not null,
release_date datetime not null
);
insert into label(artist_id,label_name,release_date) values (1,'Boy','1980-10-20');
insert into label(artist_id,label_name,release_date) values (2,'No Need to Argue','1994-10-03');
create table track
(
id int unsigned auto_increment PRIMARY KEY, -- not completely necessary, will explain
track_id int not null, -- u number this 1 to n consistent with label layout
label_id int not null, -- will leave FK RI to developer
track_name varchar(255) not null
);
-- Cranberries:
insert track (track_id,label_id,track_name) values (1,2,'Zombie');
-- U2:
insert track (track_id,label_id,track_name) values (1,1,'I Will Follow');
insert track (track_id,label_id,track_name) values (2,1,'Twilight');
-- select * from track
artist and label rather obvious. track does not need the 'id' column but i threw it in anyway. That is, track can be identified as combo of artist/label id's
foreign key (FK) referential integrity is up to you but i could plop it in if you want