I have an SQL problem i was wondering if someone could help me out. Below is the schema of the database
Player
playerid (primary key)
playerName
PlaysAt
clubId (primary key)
playerId (fk to PLAYER.playerid)
yearsAtClub
My question is how do i select the clubId of the club where player named john and stephen play where john and stephen play at the same club. I have no idea how to get the club id in this case. i have managed to get the join part correct as im able to select the club id of john but cant get it when i specify the both players using WHERE playerName = john AND playerName = stephen.
Use:
SELECT c.clubid
FROM PLAYSAT c
JOIN PLAYER p ON p.playerid = c.playerid
WHERE p.playername IN ('john', 'stephen')
GROUP BY c.clubid
HAVING COUNT(DISTINCT p.playername) = 2
The key is that the number of parameters in the IN clause needs to match the COUNT in the HAVING clause -- in this case, two.
The DISTINCT helps in case there isn't a primary key or unique constraint on both the PLAYSAT.clubid and PLAYSAT.playerid columns -- two entries for "John" otherwise would be considered valid if for the same clubid value. Otherwise, the DISTINCT can be omitted from the query.
You can join twice to the same table by using the keyword as to create aliases:
select clubId
from PlaysAt
join Player as Player1 using (playerId)
join Player as Player2 using (playerId)
where Player1.playerName = 'john'
and Player2.playerName = 'stephen';
This assumes that the table Player has the primary key playerId (with a capital "I"). It doesn't in your code, but I couldn't tell if this was a typo. If you use the same name for primary key and foreign key you can take advantage of the using keyword, which makes queries much simpler.
Related
I'm trying to build school management system and I'm having trouble designing an optimal database structure. I have Students, Staff and Users tables for login. User table will have login information only (userNumber, password) and Students and Staff will contain personal information. I separated Students and Staff because they contain different personal data. But they both have a userNumber.
users(
id,
userNumber,
password
)
students(
id,
studentNumber,
name,
age
)
staff(
id,
staffNumber,
name,
age,
salary,
dateOfHiring,
staffType
)
Let's say I'm login in with a userNumber 98242, how can let the system know where should I look, in Students table or Staff table?
I would like some recommendations on database structures.
just add column userType to users table
You could do a few things. You could create a type in the users table and look that up. You could also join the both the tables and then on recieving a record check if the student id or staff id has been returned.
Then your query could be something like
SELECT users.id as user_id, students.id, staff.id FROM users
LEFT JOIN students ON users.id = students.id
LEFT JOIN staff ON users.id = staff.id
WHERE id = 98242
Inheritance:
create table persons (
id,
name,
age
);
create table users (
number,
password
) inherits (persons);
create table students (
) inherits (users);
create table stuff (
salary,
dateOfHiring,
staffType
) inherits (users);
Schematically, something like this.
Using tableoid system column you could to know the origin of the particular row:
select
*,
tableoid::regclass -- Prints the origin table name (users, students, stuff, ...)
from users
where number = 98242;
While there are only few separate columns for students and staff members, I would keep it simple:
CREATE TABLE person (
person_id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY
, name text
, birthday date -- never age! bitrots in no time
, student_number int
, staff_number int
, salary numeric
, hired_at date
, staff_type text
, CONSTRAINT one_role_max CHECK (student_number IS NULL
OR (staff_number, salary, hired_at, staff_type) IS NULL)
, CONSTRAINT one_role_min CHECK (student_number IS NOT NULL
OR (staff_number, salary, hired_at, staff_type) IS NOT NULL)
);
CREATE TABLE users (
user_number int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
, person_id int NOT NULL REFERENCES person
, password text -- encrypted !
);
This way, one person can have 0-n user accounts - which is the typical reality. You can restrict to a single account per person by adding UNIQUE (person_id) to table users.
The CHECK constraint one_role_max enforces that either student columns or staff columns must stay NULL.
The CHECK constraint one_role_min enforces that at least one of both must have any values.
Adapt what must/can be filled in to your needs. The expressions work excellently for the current design. See:
NOT NULL constraint over a set of columns
While it's strictly "either/or" and the only student column is student_number, this query answers your question:
SELECT CASE WHEN student_number IS NULL THEN 'staff' ELSE 'student' END AS user_role
FROM person
WHERE person_id = (SELECT person_id FROM users WHERE user_number = 98242);
Or remove one or both CHECK constraints to allow the same person to be student and staff, or neither. Adapt above query accordingly.
You could use inheritance for this (like Abelisto demonstrates), but I'd rather stay away from it. There once was the idea of an object-relational DBMS. But the community has largely moved on. It works, but with caveats. Partitioning used to be a major use case. But declarative partitioning in Postgres 10 mostly superseded the inheritance-based implementation. There is not too much interest in it any more.
What about all those empty columns? Am I wasting a lot of space there? The opposite is the case. The disk footprint won't get much smaller than this. NULL storage is very cheap. See:
Does not using NULL in PostgreSQL still use a NULL bitmap in the header?
I am trying to filter my results to only show a person's name who is staying at a hotel in a specific city. What I have so far is
select guestName
from Guest
join Hotels on hotelNo=1
However, when I do this, the result page lists every guest name in the database, rather than the ones that are staying in hotelNo 1.
You should put your condition in Where clause instead.
And Join should look like this
select guestName
from Guest g
join Hotels h on g.HotelId = h.HotelId -- Primary Key of Hotels equals Foreign Key of Guest
where hotelNo = 1
Explanation
In Join clause, You should address the condition between 2 tables (It is often between Primary key (Hotel table) and Foreign Key (Guest table))
More details in https://www.w3schools.com/sql/sql_join_inner.asp
I have a SQLite database with Movies, Actors, and Tags.
There is a many-to-many relation between movies and actors, and movies and tags.
In my app, I want to list all movies with their corresponding actors and tags, for example:
Mr. & Mrs. Smith: Actors: Brad Pitt, Angelina Jolie, Tags: Action, Comedy, Crime
Passengers: Actors: Jennifer Lawrence, Chris Pratt, Tags: Adventure, Drama, Romance
And I'm wondering what are the correct SQL statements to achieve that.
The tables in my database are defined as follows :
CREATE TABLE "Movie"
(
id INTEGER PRIMARY KEY,
name VARCHAR
)
CREATE TABLE "Actor"
(
id INTEGER PRIMARY KEY,
name VARCHAR
)
CREATE TABLE "Tag"
(
id INTEGER PRIMARY KEY,
name VARCHAR
)
CREATE TABLE "Movie_Actor"
(
movie_id INTEGER,
actor_id INTEGER,
FOREIGN KEY(movie_id) REFERENCES "Movie" (id),
FOREIGN KEY(actor_id) REFERENCES "Actor" (id),
UNIQUE(movie_id, actor_id)
);
CREATE TABLE "Movie_Tag"
(
movie_id INTEGER,
tag_id INTEGER,
FOREIGN KEY(movie_id) REFERENCES "Movie" (id),
FOREIGN KEY(tag_id) REFERENCES "Tag" (id),
UNIQUE(movie_id, tag_id)
);
To get a single movie with it's actors and tags I use the following 3 queries (for example Movie.id = 1):
To get the movie row:
SELECT *
FROM Movie
WHERE id = 1
To get the actors:
SELECT *
FROM
(SELECT * FROM Actor) AS T1
JOIN
(SELECT * FROM Movie_Actor WHERE Movie_Actor.movie_id = 1) AS T2 ON T1.id = T2.actor_id
To get tags:
SELECT *
FROM
(SELECT * FROM Tag) AS T1
JOIN
(SELECT * FROM Movie_Tag WHERE Movie_Tag.movie_id = 1) AS T2 ON T1.id = T2.tag_id
My question is, how should I go about retrieving the tags and actors when I'm getting a list of movies such as SELECT * FROM Movie?
Many ORMs have an option to 'eager load' relations, and I'm wondering how can I do it in plain SQL?
Do I need to execute extra 2 queries on each row I get from SELECT * FROM Movie?
Thank You.
To get movie with id = 1 along with all of the actors associated with that movie you do the following:
SELECT * FROM Movie
LEFT JOIN Movie_Actor ON Movie_Actor.movie_id = Movie.id
LEFT JOIN Actor ON Actor.id = Movie_Actor.actor_id
WHERE id = 1
To also get all the tags, keep joining the associated tables Movie_Tag and Tag.
You might think that this would be terribly inefficient because a lot of information is going to be duplicated, for example the name of a movie is going to be fetched not just once, but NA * NT times, where NA is the number of fetched actors and NT is the number of fetched tags.
Actually, databases tend to be smart about that, (precisely because this is a very popular mechanism of retrieving data with as few as possible roundtrips to the database,) so within their communication protocols they contain special measures to avoid transmitting field values that are identical from row to row. So, the actual amount of data transmitted is very close to exactly the amount of data that would have been transmitted if you queried each table separately.
The benefit, of course, is that you suffer the penalty of a single round-trip to the database, instead of several round-trips, one for each table.
I have a "raw" table that looks like this (among other many fields):
team_id | team_name
---------+-------------------------
1 | Team1
1 | Team1
2 | Team2
2 | Team2
I want to extract the team names and their id codes and create another table for them, so I created:
CREATE TABLE teams (
team_id integer NOT NULL,
team_name varchar(50) NOT NULL,
CONSTRAINT team_pkey PRIMARY KEY (team_id)
);
And I am planning to copy the data from the old table to the recently created one like this:
INSERT INTO teams(team_id,team_name)
SELECT team_id,team_name FROM rawtable
GROUP BY team_id, team_name;
At first I wasn't adding the GROUP BY part, and I was getting a message:
ERROR: duplicate key value violates unique constraint "team_pkey"
I added the GROUP BY so it doesn't try to insert more than one row for the same team, but the problem still persist and I keep getting the same message.
I don't understand what is causing it. It looks like I am inserting single non duplicate rows into the table. What's the best way to fix this?
If two different teams with the same id are in raw_table e.g. (1, 'foo') and (1, 'bar') the group by will still return both, because those two are different.
If you just want to pick one of the rows for duplicate values of team_id then you should use something like this:
insert into teams (team_id,team_name)
select distinct on (team_id) team_id, team_name
from rawtable
order by team_id;
The Postgres specific distinct on operator will make sure that only distinct values for team_id are returned.
My best guess is that you have the same team_id for more then one team_name at least somewhere in your table. Try to add `Having count(*)=1 to your select statement
Since the team_id is unique in the destination table, two separate team names with the same id will create duplicates, one row for each name.
A simple fix is to group by team_id so that you only get a single row per id, and pick one of the names the team has (here we somewhat arbitrarily use MAX to get the last in alphabetical order)
INSERT INTO teams(team_id,team_name)
SELECT team_id, MAX(team_name) FROM rawtable
GROUP BY team_id
One of your Team1 or Team2 probably has some extra spaces or nonprintable characters. This would cause your group by to return multiple rows for Team_ID 1 or 2 causing the problem.
Try to use distinct in your query :
insert into teams (team_id,team_name) select distinct on (team_id)
team_id, team_name from order by team_id;
So I have two tables in this simplified example: People and Houses. People can own multiple houses, so I have a People.Houses field which is a string with comma delimeters (eg: "House1, House2, House4"). Houses can have multiple people in them, so I have a Houses.People field, which works the same way ("Sam, Samantha, Daren").
I want to find all the rows in the People table corresponding to the the names of people in the given house, and vice versa for houses belong to people. But I can't figure out how to do that.
This is as close as I've come up with so far:
SELECT People.*
FROM Houses
LEFT JOIN People ON Houses.People Like CONCAT(CONCAT('%', People.Name), '%')
WHERE House.Name = 'SomeArbitraryHouseImInterestedIn'
But I get some false positives (eg: Sam and Samantha might both get grabbed when I just want Samantha. And likewise with House3, House34, and House343, when I want House343).
I thought I might try and write a SplitString function so I could split a string (using a list of delimiters) into a set, and do some subquery on that set, but MySQL functions can't have tables as return values.
Likewise you can't store arrays as fields, and from what I gather the comma-delimited elements in a long string seems to be the usual way to approach this problem.
I can think of some different ways to get what I want but I'm wondering if there isn't a nice solution.
Likewise you can't store arrays as fields, and from what I gather the comma-delimited elements in a long string seems to be the usual way to approach this problem.
I hope that's not true. Representing "arrays" in SQL databases shouldn't be in a comma-delimited format, but the problem can be correctly solved by using a junction table. Comma-separated fields should have no place in relational databases, and they actually violates the very first normal form.
You'd want your table schema to look something like this:
CREATE TABLE people (
id int NOT NULL,
name varchar(50),
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE houses (
id int NOT NULL,
name varchar(50),
PRIMARY KEY (id)
) ENGINE=INNODB;
CREATE TABLE people_houses (
house_id int,
person_id int,
PRIMARY KEY (house_id, person_id),
FOREIGN KEY (house_id) REFERENCES houses (id),
FOREIGN KEY (person_id) REFERENCES people (id)
) ENGINE=INNODB;
Then searching for people will be as easy as this:
SELECT p.*
FROM houses h
JOIN people_houses ph ON ph.house_id = h.id
JOIN people p ON p.id = ph.person_id
WHERE h.name = 'SomeArbitraryHouseImInterestedIn';
No more false positives, and they all lived happily ever after.
The nice solution is to redesign your schema so that you have the following tables:
People
------
PeopleID (PK)
...
PeopleHouses
------------
PeopleID (PK) (FK to People)
HouseID (PK) (FK to Houses)
Houses
------
HouseID (PK)
...
Short Term Solution
For your immediate problem, the FIND_IN_SET function is what you want to use for joining:
For People
SELECT p.*
FROM PEOPLE p
JOIN HOUSES h ON FIND_IN_SET(p.name, h.people)
WHERE h.name = ?
For Houses
SELECT h.*
FROM HOUSES h
JOIN PEOPLE p ON FIND_IN_SET(h.name, p.houses)
WHERE p.name = ?
Long Term Solution
Is to properly model this by adding a table to link houses to people, because you're likely storing redundant relationships in both tables:
CREATE TABLE people_houses (
house_id int,
person_id int,
PRIMARY KEY (house_id, person_id),
FOREIGN KEY (house_id) REFERENCES houses (id),
FOREIGN KEY (person_id) REFERENCES people (id)
)
The problem is that you have to use another schema, like the one proposed by #RedFilter. You can see it as:
People table:
PeopleID
otherFields
Houses table:
HouseID
otherFields
Ownership table:
PeopleID
HouseID
otherFields
Hope that helps,
Hi you just change the table name places, left side is People and then right side is Houses:
SELECT People.*
FROM People
LEFT JOIN Houses ON Houses.People Like CONCAT(CONCAT('%', People.Name), '%')
WHERE House.Name = 'SomeArbitraryHouseImInterestedIn'