Storing array of IDs and how to correctly unpack them on a select out - postgresql

This might be partly a design question (new to PostgreSQL) as well.
I have three tables - Users, Groups and User_Group. User_Group represents a combination of 1 user_id being linked to 0..X Group IDs.
The tables are as simple as you think (for now, building out this thing):
User: ID, Name, ....
Group: ID, Name, ...
User_Group: UserID, GroupID int[], ...
So right now, the GroupID field in User_Group is an Integer array. UserID 1 has a value of {1,2,10,19,28} for example.
Goal:
In my UI, I need to represent that list as the group names (ie: {Group1, Group2, Group10, Group19, Group28}).
So, because I am new to PostgreSQL, I'm researching and a couple ideas pop into my mind - unnest, ANY and array replacement. All scream performance issues to me, but I might be wrong (this is the design question, is it smart to store array?)
My query right now:
select
u.*,
g.group_ids
from users u
left join user_group g
on u.id = g.user_id
Piece I'm trying to figure out how to push into:
select ug.group_id
from (select unnest(group_ids) group_id FROM user_group) as ug
left join groups g
on g.id = ug.group_id
This will just result in (obviously) an additional row for each group ID the person is associated with.
Which is the best way to do this?

( Personally I would have a column on Users table as Groups (int array) but your choice is fine too).
It would look like (I used table and field names off the top of my head, slightly modified than yours):
select u.*, g.Name as GroupName
from users u
left join usergroups ug on ug.UserId = u.UserId
left join groups g on g.groupId = ANY( ug.groups );
Update: I might have misunderstood your need. Maybe you meant this:
select u.*,
(select string_agg(g.name,',')
from groups g
inner join usergroups ug on ug.groupId = g.GroupId
where ug.UserId = u.UserID and
g.groupId = ANY( ug.groups )) as Groups
from users u;

There, you have a one-to-many relationship:
User (1)->(*) Groups
This kind of relation doesn't need an intermediary table for the link definition. The one-to-many relation use to have a foreign key in the child table (in this case is Groups).
The result will be:
User: id, name
Group: id, name, user_id
And you can add a constraint to the database as: ALTER TABLE user ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES group;

Related

Check whether there exist at least one b for every a where each b has a foreign key reference to a

Say that you have a table of a's and a table of b's where each b has a foreign key reference to the table of a's. How would you write a SQL statement answering the question whether or not there exists atleast one b for every a?
To reify: Say that you have a table of users:
create table users (
id bigserial primary key,
name text
);
and a table of hats that these users wear:
create table hats (
id bigserial primary key,
user_id bigserial references users,
description text
);
How would you write a query answering whether or not each user has at least one hat, or to rephrase: Is there any user without a hat?
You could use LEFT JOIN to find users without hat:
SELECT u.*
FROM users u
LEFT JOIN hats h
ON u.id = h.user_id
WHERE h.user_id IS NULL;
RhodiumToad on #postgresql#freenode answered:
Do you want a result like (user_id, has_hat)? Or just a list of users with hats, or users without hats? Or a single true/false result for "does any user not have a hat?"
The most efficient answer of the various different queries will be that to the first question:
select exists(
select 1
from users u
where not exists(
select 1
from hats h
where h.user_id=u.id));
This because it's (a) plannable as an anti-join and (b) stops on first match. There's also the added benefit of it literally saying "does a user exist such that no hat exists for that user" so it should be easy to understand for future readers.
The next best if you want more detail is the middle option (users with/without hats) like so:
-- shows all users with at least one hat
select *
from users u
where exists(
select 1
from hats h
where h.user_id=u.id);
-- shows all users with no hat
select *
from users u
where not exists(
select 1
from hats h
where h.user_id=u.id);
The first option doesn't plan as efficiently so it should usually be avoided:
-- shows all users, with a flag for whether they have a hat
select u.id, exists(
select 1
from hats h
where h.user_id=u.id) as has_hat
from users u;

How do we do LEFT JOIN with old syntax?

How do we do LEFT JOIN with old syntax?
Let's say you have a User table and UserRole table, and you are holding the ID of the UserRole in User table.
Here is the query to retrieve all User's names, and the Role Names with the new notation:
SELECT U.Username, US.[Desc] FROM [User] U
INNER JOIN UserRole US ON U.UserRoleId = US.Id
And here is the old notation:
SELECT U.Username, US.[Desc] FROM [User] U, UserRole US
WHERE U.UserRoleId = US.Id
Now, let's assume that all users don't have a role, the UserRoleId is either 0 or NULL.
Here is the query to retrieve all User's names, and the Role Names with the new notation:
SELECT U.Username, US.[Desc] FROM [User] U
LEFT JOIN UserRole US ON U.UserRoleId = US.Id
Question is: How do we do the same with old syntax, without using the word JOIN?
The operators are *= and =* (depending on which side of the predicate each column is):
SELECT U.Username, US.[Desc]
FROM [User] U, UserRole US
WHERE U.UserRoleId *= US.Id
These have been deprecated since SQL Server 2012 though, since then there is no backward compatibility for a join syntax that was discontinued 24 years ago.
I have no idea why you might want to use this, but here are some reasons to sway you back from the dark side:
Bad habits to kick : using old-style JOINs
Or, if you want an alternative way without joins, or proprietary syntax you can use:
SELECT U.Username, US.[Desc]
FROM [User] U, UserRole US
WHERE U.UserRoleId = US.Id
UNION ALL
SELECT U.Username, NULL
FROM [User] U
WHERE NOT EXISTS (SELECT 1 FROM UserRole US WHERE U.UserRoleId = US.Id);
But once again, why bother, the LEFT JOIN syntax was introduced in ANSI 92, if you can't use it with your database, it is time to change your database vendor, and not your syntax.
Use the PLUS(+) sign :
SELECT U.Username, US.[Desc] FROM [User] U, UserRole US
WHERE U.UserRoleId = US.Id(+)
The + should be placed on any column of the right table of the LEFT JOIN that appears in the WHERE clause.
Though - this is not suggested, this form of syntax usually leads to errors due to the messy code it creates

DB2 Lookup table using two columns of same table

I have a lookup table for institution id, name, address and another table for course details.
In each course record there will be two columns pointing primary and secondary institution ids.
My select query should look like ->
Select course_id,
name,
primary_Institution_id,
Primary_Institution_name,
primary_Institution_address,
Secondary _Institution_id,
Secondary _Institution_name,
Secondary_Institution_address
from [JOIN MAY BE]
where course_id in ('1223','34234','43432')
How to achieve this? I have no control over the tables and I can only select from them and cannot modify their structure.
If you are trying to ask how to do the join, it might look something like this
Select c.course_id,
c.name,
c.primary_Institution_id,
i.name as primary_Institution_name,
i.address as primary_Institution_address
c.secondary_Institution_id
k.name as Secondary _Institution_name,
k.address as Secondary_Institution_address
from courses as c
join institutions as i
on i.id = c.primary_Institution_id
left
join institutions as k
on i.id = c.secondary_Institution_id
where course_id in ('1223','34234','43432')
This assumes that the first institution id is mandatory (never null) so the join is implied as an inner join, but that perhaps the second might be optional (null allowed) so it uses a left join, in case there is nothing to match to.

PostgreSQL: custom logic for determining distinct rows?

Here's my problem. Suppose I have a table called persons containing, among other things, fields for the person's name and national identification number, with the latter being optional. There can be multiple rows for each actual person.
Now suppose I want to select exactly one row for each actual person. For the purposes of the application, two rows are considered to refer to the same person if a) their ID numbers match, or b) their names match and the ID number of one or both is NULL. SELECT DISTINCT is no good here: I cannot do a DISTINCT ON (name, id) because then two rows with the same name where the ID of one is NULL wouldn't match (which is incorrect, they should be considered the same). I cannot do a DISTINCT ON (name) because then rows with the same name but different IDs would match (again incorrect, they should be considered different). And I cannot do a DISTINCT ON (id) because then all the rows where ID is NULL would be considered the same (obviously incorrect).
Is there any way to redefine the way PostgreSQL compares rows to determine whether or not they're identical? I guess the default behaviour for DISTINCT ON (name, id) would be something like IF a.name = b.name AND a.id = b.id THEN IDENTICAL ELSE DISTINCT. I'd like to redefine it to something like IF a.id = b.id OR (a.name = b.name AND (a.id IS NULL OR b.id IS NULL)) THEN IDENTICAL ELSE DISTINCT.
It's pretty late and I might have missed something obvious, so other suggestions on how to achieve what I want would also be welcome. Anything to enable me to select distinct rows based on more complex criteria than a simple list of columns. Thanks in advance.
With Window Functions
--
-- First, SELECT those names with NULL national IDs not shadowed by the same
-- name with a national ID. Each one is a unique person.
--
SELECT name, id
FROM persons
WHERE NOT EXISTS (SELECT 1
FROM persons p
WHERE p.name = persons.name AND p.id IS NOT NULL)
--
-- Second, collapse each national ID into the "first" row with that ID,
-- whatever the name. Each ID is a unique person.
--
UNION ALL
SELECT name, id
FROM (SELECT name, id, ROW_NUMBER() OVER (PARTITION BY id)
FROM persons
WHERE id IS NOT NULL) d
WHERE d.row_number = 1;
Without Window Functions
Replace the above UNION with a GROUP BY the first (MIN()) name for each non-NULL id:
...
UNION ALL
SELECT MIN(name) AS name, id
FROM persons
WHERE id IS NOT NULL
GROUP BY id
It seems like the main problem is the layout of your database. I don't know the details of your specific application, but having multiple rows and null IDs for the same person is usually a bad idea. If possible you may want to consider creating a separate table for any of the information that requires multiple rows, with persons only containing one row per person and a unique identifier for each row.
But, if you can't do that... I don't think just a distinct is going to solve this problem.
What's the problem with:
select distinct name, id
from persons
where id is not null
Do you have some persons that have a name, but not an ID? Or do you need some specific data from the other rows?
Here's another problem: if there are two rows with the same name and null IDs, and multiple people with the same name and different IDs, how do you know which person the null rows match?

T-SQL - How to write query to get records that match ALL records in a many to many join

(I don't think I have titled this question correctly - but I don't know how to describe it)
Here is what I am trying to do:
Let's say I have a Person table that has a PersonID field. And let's say that a Person can belong to many Groups. So there is a Group table with a GroupID field and a GroupMembership table that is a many-to-many join between the two tables and the GroupMembership table has a PersonID field and a GroupID field. So far, it is a simple many to many join.
Given a list of GroupIDs I would like to be able to write a query that returns all of the people that are in ALL of those groups (not any one of those groups). And the query should be able to handle any number of GroupIDs. I would like to avoid dynamic SQL.
Is there some simple way of doing this that I am missing?
Thanks,
Corey
select person_id, count(*) from groupmembership
where group_id in ([your list of group ids])
group by person_id
having count(*) = [size of your list of group ids]
Edited: thank you dotjoe!
Basically you are looking for Persons for whom there is no group he is not a member of, so
select *
from Person p
where not exists (
select 1
from Group g
where not exists (
select 1
from GroupMembership gm
where gm.PersonID = p.ID
and gm.GroupID = g.ID
)
)
You're basically not going to avoid "dynamic" SQL in the sense of dynamically generating the query at query time. There's no way to hand a list around in SQL (well, there is, table variables, but getting them into the system from C# is either impossible (2005 & below) or else annoying (2008)).
One way that you could do it with multiple queries is to insert your list into a work table (probably a process-keyed table) and join against that table. The only other option would be to use a dynamic query such as the ones specified by Jonathan and hongliang.