SQL many-to-many relationship with members of same table - sql

I have an application that tracks siblings. There is a "Members" table. That's it for now. Members can be "Siblings" of one another. I want to somehow track relationships between different members of the "Members" table. I tried doing it like this:
CREATE TABLE members (
id SERIAL PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
);
CREATE TABLE siblings (
member_one_id INTEGER REFERENCES members (id),
member_two_id INTEGER REFERENCES members (id),
PRIMARY KEY (member_one_id, member_two_id)
);
But I have no idea how to query this structure. I tried this
SELECT * FROM members
INNER JOIN siblings
ON members.id = siblings.member_one_id OR members.id = siblings.member_two_id
INNER JOIN members
ON members.id = siblings.member_one_id OR members.id = sibling.member_two_id;
and I keep getting the error:
ERROR: table name "members" specified more than once
Is there a way to both store this type of relationship and query it effectively? My SQL knowledge gets thinner with more complex structures like this. Thanks!

Use table aliases like this:
SELECT * FROM members one
INNER JOIN siblings
ON one.id = siblings.member_one_id OR one.id = siblings.member_two_id
INNER JOIN members two
ON two.id = siblings.member_one_id OR two.id = sibling.member_two_id

Table structure looks good. You can use a CTE to traverse relationship(https://www.postgresql.org/docs/9.1/static/queries-with.html)

Related

SQLite : how to achieve ORM "eager loading" in plain SQL?

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.

A query with primary key field used twice

In my database I have two tables:
relationship table:
organization_id_first, organization_id_second, relationship_type
organization table:
primary key = org_id ; org_id, org_name, ...
How would I be able to join the organization table so that I could get the org_name for both organizations that have an entry in the relationship table? I don't think I can join on the same primary key. Would I have to do a subquery of some sort?
Thanks!
This is how i would do it in T-SQL ... just join it twice and make two different object
select or1.org_name, or2.org_name, rel.relationship_type from relationship rel
join organization or1 on rel.organization_id_first = or1.org_id
join organization or2 on rel.organization_id_second = or2.org_id

Join tables on columns of composite foreign / primary key in a query

CREATE TABLE subscription (
magazine_id bigint,
user_id bigint,
PRIMARY KEY (magazine_id, user_id)
);
CREATE TABLE delivery (
magazine_id bigint,
user_id bigint,
FOREIGN KEY (subscription) REFERENCES subscription (magazine_id, user_id)
);
What is a good way to query for deliveries given a particular subscription? Is there a way to assign a column name to PRIMARY KEY (magazine_id, user_id) and the corresponding foreign key so that I can query like this:
SELECT *
FROM subscription
JOIN delivery ON (delivery.subscription_fk = delivery.subscription_pk);
Note: I can write something like this:
SELECT *
FROM subscription
JOIN delivery ON (delivery.magazine_id = subscription.magazine_id
AND delivery.user_id = subscription.user_id);
However, I am under the impression that there is a less verbose way to achieve this.
There is a NATURAL JOIN:
SELECT *
FROM subscription
NATURAL JOIN delivery;
Quoting the manual on SELECT:
NATURAL
NATURAL is shorthand for a USING list that mentions all columns in the two tables that have the same names.
It would work for your test setup, but it's not strictly doing what you ask for. The connection is based on all columns sharing the same name. Foreign keys are not considered. The cases where NATURAL JOIN is a good idea are few and far between.
Simplify code / less verbose
For starters, you could use table aliases and you don't need parentheses around the join conditions with ON (unlike with USING):
SELECT *
FROM subscription s
JOIN delivery d ON d.magazine_id = s.magazine_id
AND d.user_id = s.user_id;
Since column names in the join conditions are identical, you can further simplify with USING:
SELECT *
FROM subscription s
JOIN delivery d USING (magazine_id, user_id);
There is no syntax variant making joins based on foreign key constraints automatically. You would have to query the system catalogs and build the SQL dynamically.
Doesn't delivery has two columns representing the foreign key? Then it should work like with a non-composite primary key SELECT * FROM subscription JOIN delivery ON (delivery.magazine_id = subscription.magazine_id AND delivery.user_id = subscription.user_id).

Two foreign keys reference the primary key of another table

So I have two tables
Person(personID, first_name, last_name);
Relation(relationID, child_personID, parent_personID);
personID and relationID are both primary keys. child_personID and parent_personID are both foreign keys.
I want to make a query so I have the first names and last names of both the child and parent.
child.first_name child.last_name and parent.first_name, parent.last_name
One way to go about this is using joins and table aliases. Something like this:
select
child.first_name,
child.last_name,
parent.first_name,
parent.last_name
from relation r
join person child on r.child_personID = child.id
join person parent on r.parent_personID = parent.id

Matching delimited string to table rows

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'