Create an aggregation query of a table linked to another 2 tables - sql

I have 3 tables, the first one stores information about a user, such as an email address, etc, and it updates dynamically (when a new user has registered). The second one stores the user's roles and it is a static table comes from the init SQL script. And the last one, named user_status, keeps track of changing a particular user's role by adding a new entry with the current timestamp.
Need to gather the current status (the latest created status entries) of all users grouped by role pointed to a number, which is a count of corresponding users.
/* table user_account stores top user information */
CREATE TABLE IF NOT EXISTS user_account (
id serial PRIMARY KEY,
email text NOT NULL
);
/* table user_role keeps user's role ids */
CREATE TABLE IF NOT EXISTS user_role (
id serial PRIMARY KEY,
role text NOT NULL
);
/* table user_status track a user role on change */
CREATE TABLE IF NOT EXISTS user_status (
id serial PRIMARY KEY,
user_account_id int NOT NULL REFERENCES user_account(id),
user_role_id int NOT NULL REFERENCES user_role(id),
created timestamptz NOT NULL DEFAULT clock_timestamp()
);
INSERT INTO user_account(email) VALUES
( 'user1#myorg.com' ),
( 'user2#myorg.com' ),
( 'user3#myorg.com' );
INSERT INTO user_role (role) VALUES
( 'activation_required' ),
( 'regular' ),
( 'forum_only' ),
( 'moderator' ),
( 'admin' );
INSERT INTO user_status (user_account_id, user_role_id) VALUES
(1, 1), -- now user `user1#myorg.com` has `activation_required` role
(1, 5), -- now user `user1#myorg.com` has `admin` role
(2, 1), -- now user `user2#myorg.com` has `activation_required` role
(2, 2), -- now user `user2#myorg.com` has `regular` role
(3, 1), -- now user `user3#myorg.com` has `activation_required` role
(1, 4), -- now user `user1#myorg.com` has `moderator` role
(3, 2); -- now user `user2#myorg.com` has `regular` role
So after the latter insert query I expect to see
moderator | 1
regular | 2
Because there is only 3 users (1+2), and at the current moment is one moderator and two regular users.

So I've contrived a query that solves the issue:
SELECT user_role.role, COUNT(user_status.*)
FROM user_account, user_status
JOIN user_role ON user_role.id = user_status.user_role_id
WHERE user_status.created = (SELECT MAX(created) FROM user_status WHERE user_account.id = user_status.user_account_id)
GROUP BY user_role.role
ORDER BY user_role.role;

Related

How to differentiate between no rows and foreign key reference not existing?

My users table contains Alice, Bob and Charles. Alice and Bob have a 3 and 2 fruits respectively. Charles has none. A relationship is established using a foreign key constraint foreign key (user_id) references users (id) and a unique (user_id, name) constraint, allowing zero or one fruit per user.
create table users (
id integer primary key,
firstname varchar(64)
);
create table fruits (
id integer primary key not null,
user_id integer not null,
name varchar(64) not null,
foreign key (user_id) references users (id),
unique (user_id, name)
);
insert into users (id, firstname) values (1, 'Alice');
insert into users (id, firstname) values (2, 'Bob');
insert into users (id, firstname) values (3, 'Charles');
insert into fruits (id, user_id, name) values (1, 1, 'grape');
insert into fruits (id, user_id, name) values (2, 1, 'apple');
insert into fruits (id, user_id, name) values (3, 1, 'pear');
insert into fruits (id, user_id, name) values (4, 2, 'orange');
insert into fruits (id, user_id, name) values (5, 2, 'cherry');
Charles does not have an orange, so there is no resulting row (first query below). However, running the same query for a user that does not exist (second query below) also returns no result.
test=# select * from fruits where user_id = 3 and name = 'orange';
id | user_id | name
----+---------+------
(0 rows)
test=# select * from fruits where user_id = 99 and name = 'orange';
id | user_id | name
----+---------+------
(0 rows)
Is it possible to perform a single query whilst simultaneously differentiating between the user not existing vs the user existing and not having a fruit?
If so, can this also be done to find all the fruits belonging to a particular user (i.e. select * from fruits where user_id = 3 vs select * from fruits where user_id = 99.
Use a LEFT [OUTER] JOIN:
SELECT u.id, u.firstname, f.name AS fruit
-- , COALESCE(f.name, '') AS fruit -- alternatively an empty string
FROM users u
LEFT JOIN fruits f ON f.user_id = u.id
AND f.name = 'orange'
WHERE u.id = 3;
If the user exists, you always get a row.
If the user has no fruit (of that name), fruit defaults to NULL - which is otherwise impossible since fruit.name is defined NOT NULL.
To get an empty string instead, throw in COALESCE(). But no fruit can be named '' then or it'll be ambiguous again. Add a CHECK constraint to be sure.
Related:
Postgres Left Join with where condition
Aside 1: "name" is not a good name.
Aside 2: Typically, this would be a many-to-many design with three tables: "users", "fruits" and "user_fruits". See:
How to implement a many-to-many relationship in PostgreSQL?

How to add N entries to json{b}_build_object where N is number of rows in table?

Given this table setup:
create table accounts (
id char(4) primary key,
first_name varchar not null
);
create table roles (
account_id char(4) references accounts not null,
role_type varchar not null,
role varchar not null,
primary key (account_id, role_type)
);
and initial account insertion:
insert into accounts (id, first_name) values ('abcd', 'Bob');
I want to get all the account info of someone, along w/ the roles they have as key-value pairs. Using a join for this one-to-many relationship would duplicate the account information across each row containing the role, so I want to create a JSON object instead. Using this query:
select
first_name,
coalesce(
(select jsonb_build_object(role_type, role) from roles where account_id = id),
'{}'::jsonb
) as roles
from accounts where id = 'abcd';
I get this expected result:
first_name | roles
------------+-------
Bob | {}
(1 row)
After adding a first role:
insert into roles (account_id, role_type, role) values ('abcd', 'my_role_type', 'my_role');
I get another expected result:
first_name | roles
------------+-----------------------------
Bob | {"my_role_type": "my_role"}
(1 row)
But after adding a second role:
insert into roles (account_id, role_type, role) values ('abcd', 'my_other_role_type', 'my_other_role');
I get:
ERROR: more than one row returned by a subquery used as an expression
How do I replace this error with
first_name | roles
------------+-----------------------------
Bob | {"my_role_type": "my_role", "my_other_role_type": "my_other_role"}
(1 row)
?
I'm on Postgres v13.
You may use json_object and array_agg with a group by to achieve this outcome. See example with working fiddle below:
Query #1
select
a.first_name,
json_object(
array_agg(role_type),
array_agg(role)
)
from accounts a
inner join roles r on r.account_id = a.id
where a.id = 'abcd'
group by a.first_name;
first_name
json_object
Bob
{"my_role_type":"my_role","my_other_role_type":"my_other_role"}
View on DB Fiddle
Edit 1:
The following modification using a left join and case expression to provide an alternative for results containing null values:
select
a.first_name,
CASE
WHEN COUNT(role_type)=0 THEN '{}'::json
ELSE
json_object(
array_agg(role_type),
array_agg(role)
)
END as role
from accounts a
left join roles r on r.account_id = a.id
group by a.first_name;
View on DB Fiddle
Let me know if this works for you.

Get the count of a many to many relationship in two levels in the same query

I am getting the list of friends of a particular user using this query.
SELECT users.* from users
inner join friends ON user_id = id OR user_id2 = id
WHERE (user_id = 33 OR user_id2 = 33) AND id != 33
GROUP BY user_id, user_id2, id
Now i want to include the number of friends each user have in the same query
How can i do that?
This seems like a duplicate but i couldn't apply other answers to my problem. Help is highly appreciated.
This is the schema of the database
create table if not exists users (
id serial primary key,
phone_number text,
cin text,
name text,
positive_date timestamp,
address text,
created_at timestamp default now()
);
create table if not exists friends (
user_id integer not null references users,
user_id2 integer not null references users,
created_at timestamp default now(),
distance integer
);
And a couple of insert statements to try things out
INSERT INTO users (id, phone_number, cin, name, positive_date, address, created_at) VALUES (20, '0306200503', 'k26jx2dx', 'Mathis Gerard', NULL, '2222 Dubois de Provence Suite 117', '2020-03-30 08:31:42.674733');
INSERT INTO users (id, phone_number, cin, name, positive_date, address, created_at) VALUES (21, '0306200503', 'k26jx2dx', 'Mathis Gerard', NULL, '2222 Dubois de Provence Suite 117', '2020-03-30 08:31:42.674733');
INSERT INTO friends (user_id, user_id2, created_at, distance) VALUES (20, 21, '2020-06-05 17:23:00.635', 2);
And here is an image explaining things in more details:
The result of the query Get friends of user 1 and their number of friends would return the following:
- 2: id, name ... + count = 2
- 3: id, name ... + count = 1
If you just want the number of friends, how about a subquery:
select u.*,
(select count(*)
from friends f
where u.id in (f.user_id, f.user_id2)
)
from users u
where id = 33;
I would be surprised if someone could befriend themselves. If so, you can add f.user_id <> f.user_id2 in the subquery.

How to see table according to another table's ID?

See table according to another tables ID.
Example: see what countries from country table an id = 5 from users table has visited.
I think it needs a subquery for this task.
Select * from country where (select id from users where id = 5)
It doesn't work correctly.
Edit
user table
CREATE TABLE `user` (
`id` int(6) NOT NULL,
`name` varchar(30) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `user` (`id`, `name`) VALUES
('1', 'jackie'),
('2', 'maria'),
('3', 'sandra')
country table
CREATE TABLE `country` (
`name` varchar(30) NOT NULL,
`capital` varchar(30) NOT NULL,
) ;
INSERT INTO `country` (`name`, `capital`) VALUES
('Italy', 'Rome'),
('Portugal', 'Lisbon'),
('China', 'Beijing'),
('Norway', 'Oslo');
Purpose: users can mark the countries visited so the program should have the ability to show which countries a user has visited.
Hence we want a query that shows the info for each country a user with id = 5 has visited.
Example user = 5 has visited Italy and Norway.
UPDATE NEW
to add countries a user has visited:
CREATE TABLE `country_visited`(
`userId` INT(6) not null,
`country` varchar(30) not null
);
INSERT INTO `country_visited` (`userid`,`country`) VALUES
('1', 'Spain'),
('2', 'Norway'),
('3', 'Italy'),
('4', 'Spain'),
('5', 'Italy'); #if this has visited 3 countries how do you add it?
You first need a table to store which countries a user had visited. For example
CREATE TABLE countryVisited(
userId INT not null
country varchar(30) not null
)
Insert into the table:
INSERT INTO countryVisited (userId,country) VALUES
(5, 'Norway'),
(4, 'Spain'),
(5, 'Italy')
You can then join as follows:
SELECT id, name, c.country, capital
FROM users u
INNER JOIN countryVisited cv
ON u.id = cv. userId
INNER JOIN country c
ON cv.country = c.country
WHERE u.id=5
Even better, add an id field to the country table and only store that value in the countryVisited table instead of the country name.
You should have another table which stores the relation between User and Country. Something like this:
UserCountries Table
UserID CountryId
------- -------------
1 1
1 2
2 1
2 3
Then you should write a query like this:
Select * from Country where Id in
(select CountryId from UserCountries
where UserId = 5)
After edit your question I can understand the matter. Now you can use the structure for solved your problem.
Edit your user table as like as user_id, user_name, visited_country_id.
when a user choose his visited country then update user table visited_country_id using comma separator. For an Example
user_id user_name visited_country_id
5 Selim 1,3,4
Now you execute query like select* from country where country_id in (select visited_country_id from user where user_id=5)

Join Query in Sql server

I am having trouble with a join in sql.
I have 3 tables.
1: Lists the user details
2: Lists the permissions the user group has
3: Lists the page that that group can access
Table1 users :
****************************************
username | group
****************************************
admin | administrator
Table2 groups :
*********************************************
user_group | create | view | system_admin
*********************************************
administrator | 1 | 0 | 1
Table3 urls:
*********************************************
create | view | system_admin
*********************************************
create.php | view.php | system.php
(apologies for my table drawing)
What I am doing via php , is grabbing the user_group they belong to.
I then need to check if they have access to the page they have just hit or redirect them back.
Can I accomplish this with the current table layout the way they are through a join?, Or shall I look to re-design these tables as they are not intuitive for this kind of thing.
I actually might redesign the tables to make them easier to query:
create table users
(
id int,
username varchar(10),
groupid int
);
insert into users values (1, 'admin', 1);
create table groups
(
groupid int,
groupname varchar(20)
);
insert into groups values (1, 'administrator');
create table permissions
(
permissionid int,
permissionname varchar(20)
);
insert into permissions values (1, 'create');
insert into permissions values (2, 'view');
insert into permissions values (3, 'system_admin');
create table urls
(
urlid int,
name varchar(10)
);
insert into urls values(1, 'create.php');
insert into urls values(2, 'view.php');
insert into urls values(3, 'system.php');
create table group_permission_urls
(
groupid int,
permissionid int,
urlid int
);
insert into group_permission_urls values(1, 1, 1);
insert into group_permission_urls values(1, 0, 2);
insert into group_permission_urls values(1, 3, 3);
Then your query would be similar to this:
select *
from users us
left join groups g
on us.groupid = g.groupid
left join group_permission_urls gpu
on us.groupid = gpu.groupid
left join permissions p
on gpu.permissionid = p.permissionid
left join urls u
on gpu.urlid = u.urlid
see SQL Fiddle with Demo
By comparing the $current_page with the results of an IN() subquery, you can do this in one query. If the page matches any listed in a column the user has permission for, this will return a row. It should not return any row if there is no match in an allowed column.
SELECT
groups.create,
groups.view,
groups.system_admin,
1 AS can_access
FROM
users
JOIN groups ON users.group = groups.user_group
WHERE
users.username = '$some_username'
AND (
/* Substitute the current page. Better, use a prepared statement placeholder if your API supports it */
(groups.create = 1 AND '$current_page' IN (SELECT DISTINCT create FROM urls))
OR
(groups.view = 1 AND '$current_page' IN (SELECT DISTINCT view FROM urls))
OR
(groups.system_admin = 1 AND '$current_page' IN (SELECT DISTINCT system_admin FROM urls))
)
This works by comparing the $current_page to the distinct set of possible values from each of your 3 columns. If it matches a column and also the user's group has permission on that type, a row is returned.
select case when count(1) > 0 then 'come in' else 'go away' end
from users, groups, urls
where
users.username = '$username' and
users.user_group = groups.user_group and
((urls.create = '$url' and groups.create = 1) or
(urls.view = '$url' and groups.view = 1) or
(urls.system_admin = '$url' and groups.system_admin = 1))