Join Query in Sql server - sql

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))

Related

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.

How to merge users in PostgreSQL

I need to make something to merge some users in PGSQL but I think that pgsql don't own the MERGE property. I just want to know how to make two users to be matched like this :
id | name | username | mail
1 | toto | tata | toto.tata#gmail.com
2 | titi | tutu | titi.tutu#gmail.com
Here I want to chose which data I would like I want to say that I want to merge only username from 2 to 1 so the result would be :
id | name | username | mail
1 | toto | tutu | toto.tata#gmail.com
You just need to select all the columns for first id and the column you need with second id will be a subquery in select list. Please check below answer for selecting merged result.
Schema and insert statements:
create table users (id int , name varchar(50), username varchar(50), mail varchar(50));
insert into users values (1 , 'toto' , 'tata' , 'toto.tata#gmail.com');
insert into users values (2 , 'titi' , 'tutu' , 'titi.tutu#gmail.com');
Query:
select id,name,(select username from users where id=2) username,mail from users where id=1
Output:
id
name
username
mail
1
toto
tutu
toto.tata#gmail.com
db<fiddle here
To merge the rows within the table you can first update first row with data from second row then delete the second row. Try this:
Schema and insert statements:
create table users (id int , name varchar(50), username varchar(50), mail varchar(50));
insert into users values (1 , 'toto' , 'tata' , 'toto.tata#gmail.com');
insert into users values (2 , 'titi' , 'tutu' , 'titi.tutu#gmail.com');
Update query:
update users set username=(select username from users where id=2) where id=1;
delete from users where id=2;
Select query:
select * from users
id
name
username
mail
1
toto
tutu
toto.tata#gmail.com
db<fiddle here
You could use aggregation:
select min(id) as id,
max(name) filter (where id = 1) as name,
max(username) filter (where id = 2) as username,
max(mail) filter (where id = 1) as mail
from t
where id in (1, 2);
This assumes that you want to pull particular column values from particular ids.
Or you could use join:
select t1.id, t1.name, t2.username, t1.mail
from t t1 join
t t2
on t1.id = 1 and t2.id = 2;
If you actually want to change the data, use update and delete:
update t t1
set username = t2.username
from t t2
where t1.id = 1 and t2.id = 2;
delete from t
where t.id = 2;
Here is a db<>fiddle.

Insert rows depending on select query return

i have a table user activity ( iuser_id, sactivity)
I would need to insert multiple instances to this table using user id ids from table user whose job is X . How do I do that?
Insert into tbluseractivity
(Select iuser_id from tbl_user where activity = x , 'sample activity')
Given that tbl_user would return 100 records, how can insert it.thank you.
You can bulk insert as below
insert into tbluseractivity(iuser_id, sactivity)
Select iuser_id, 'sample activity'
from tbl_user
where activity = x

Specific complex SQL query and Django ORM?

I have a set of tables that contain content that is created and voted on by users.
Table content_a
id /* the id of the content */
user_id /* the user that contributed the content */
content /* the content */
Table content_b
id
user_id
content
Table content_c
id
user_id
content
Table voting
user_id /* the user that made the vote */
content_id /* the content the vote was made on */
content_type_id /* the content type the vote was made on */
vote /* the value of the vote, either +1 or -1 */
I want to be able to select a set of users and order them by the sum of the votes on the content they have produced. For example,
SELECT * FROM users ORDER BY <sum of votes on all content associated with user>
Is there a specific way this can be achieved using Django's ORM, or do I have to use a raw SQL query? And what would the most efficient way be to achieve this in raw SQL?
Update
Assuming the models are
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
class ContentA(models.Model):
user = models.ForeignKey(User)
content = models.TextField()
class ContentB(models.Model):
user = models.ForeignKey(User)
content = models.TextField()
class ContentC(models.Model):
user = models.ForeignKey(User)
content = models.TextField()
class GenericVote(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
user = models.ForeignKey(User)
vote = models.IntegerField(default=1)
Option A. Using GenericVote
GenericVote.objects.extra(select={'uid':"""
CASE
WHEN content_type_id = {ct_a} THEN (SELECT user_id FROM {ContentA._meta.db_table} WHERE id = object_id)
WHEN content_type_id = {ct_b} THEN (SELECT user_id FROM {ContentB._meta.db_table} WHERE id = object_id)
WHEN content_type_id = {ct_c} THEN (SELECT user_id FROM {ContentC._meta.db_table} WHERE id = object_id)
END""".format(
ct_a=ContentType.objects.get_for_model(ContentA).pk,
ct_b=ContentType.objects.get_for_model(ContentB).pk,
ct_c=ContentType.objects.get_for_model(ContentC).pk,
ContentA=ContentA,
ContentB=ContentB,
ContentC=ContentC
)}).values('uid').annotate(vc=models.Sum('vote')).order_by('-vc')
The above ValuesQuerySet,(or use values_list()) gives you a sequence of IDs of User()s in the order of descending votes count. You could then use it to fetch top users.
Option B. Using User.objects.raw
When I use User.objects.raw, I got almost same query w/ the answer given by forsvarir :
User.objects.raw("""
SELECT "{user_tbl}".*, SUM("gv"."vc") as vote_count from {user_tbl},
(SELECT id, user_id, {ct_a} AS ct FROM {ContentA._meta.db_table} UNION
SELECT id, user_id, {ct_b} AS ct FROM {ContentB._meta.db_table} UNION
SELECT id, user_id, {ct_c} as ct FROM {ContentC._meta.db_table}
) as c,
(SELECT content_type_id, object_id, SUM("vote") as vc FROM {GenericVote._meta.db_table} GROUP BY content_type_id, object_id) as gv
WHERE {user_tbl}.id = c.user_id
AND gv.content_type_id = c.ct
AND gv.object_id = c.id
GROUP BY {user_tbl}.id
ORDER BY "vc" DESC""".format(
user_tbl=User._meta.db_table, ContentA=ContentA, ContentB=ContentB,
ContentC=ContentC, GenericVote=GenericVote,
ct_a=ContentType.objects.get_for_model(ContentA).pk,
ct_b=ContentType.objects.get_for_model(ContentB).pk,
ct_c=ContentType.objects.get_for_model(ContentC).pk
))
Option C. Other possible ways
De-normalize vote_count to User or profile model, for example, UserProfile, or other relative model, as suggested by Michael Dunn. This behaves much better if you access vote_count on-fly frequently.
Build a DB view which does the UNIONs for you, then map a model to it, this could make the construction of the query easier.
Sort in Python, usually it's best way to work for large-scale data, because of dozen of toolkits and extension ways.
You need some Django Models mapping those tables before use Django ORM to query. Assuming they are User and Voting models that matching users and voting tables, you could then
User.objects.annotate(v=models.Sum('voting__vote')).order_by('v')
For a raw SQL solution, I've created a rough replication of your problem on ideone here
Data setup:
create table content_a(id int, user_id int, content varchar(20));
create table content_b(id int, user_id int, content varchar(20));
create table content_c(id int, user_id int, content varchar(20));
create table voting(user_id int, content_id int, content_type_id int, vote int);
create table users(id int, name varchar(20));
insert into content_a values(1,1,'aaaa');
insert into content_a values(2,1,'bbbb');
insert into content_a values(3,1,'cccc');
insert into content_b values(1,2,'dddd');
insert into content_b values(2,2,'eeee');
insert into content_b values(3,2,'ffff');
insert into content_c values(1,1,'gggg');
insert into content_c values(2,2,'hhhh');
insert into content_c values(3,3,'iiii');
insert into users values(1, 'first');
insert into users values(2, 'second');
insert into users values(3, 'third');
insert into users values(4, 'voteonly');
-- user 1 net votes (2)
insert into voting values (1, 1, 1, 1);
insert into voting values (2, 3, 1, -1);
insert into voting values (3, 1, 1, 1);
insert into voting values (4, 2, 1, 1);
-- user 2 net votes (3)
insert into voting values (1, 2, 2, 1);
insert into voting values (1, 1, 2, 1);
insert into voting values (2, 3, 2, -1);
insert into voting values (4, 2, 2, 1);
insert into voting values (4, 2, 3, 1);
-- user 3 net votes (-1)
insert into voting values (2, 3, 3, -1);
I've basically assumed that content_a has a type of 1, content_b has a type of 2 and content_c has a type of 3. Using raw SQL, there seems to be two obvious approaches. The first is to union all of the content together, then join it with the users and voting tables. I've tested this approach below.
select users.*, sum(voting.vote)
from users,
voting, (
SELECT id, 1 AS content_type_id, user_id
FROM content_a
UNION
SELECT id, 2 AS content_type_id, user_id
FROM content_b
UNION
SELECT id, 3 AS content_type_id, user_id
FROM content_c) contents
where contents.user_id = users.id
and voting.content_id = contents.id
and voting.content_type_id = contents.content_type_id
group by users.id
order by sum(voting.vote) desc;
The alternative would seem to be to outer join the content tables to the voting tables, without the union step. This may be more performant, but I haven't been able to test it because visual studio keeps rewriting my sql for me... I'd expect the SQL to look something like this (but I haven't tested it):
select users.*, sum(voting.vote)
from users, voting, content_a, content_b, content_c
where users.id = content_a.user_id (+)
and users.id = content_b.user_id (+)
and users.id = content_c.user_id (+)
and ((content_a.id = voting.content_id and voting.content_type_id = 1) OR
(content_b.id = voting.content_id and voting.content_type_id = 2) OR
(content_c.id = voting.content_id and voting.content_type_id = 3))
group by users.id
order by sum(voting.vote) desc;
I would do this using precalculated values. First make a separate table to store the votes that each user has received:
class VotesReceived(models.Model):
user = models.OneToOneField(User, primary_key=True)
count = models.IntegerField(default=0, editable=False)
then use a post_save signal to update the count every time a vote is made:
def update_votes_received(sender, instance, **kwargs):
# `instance` is a Voting object
# assuming here that `instance.content.user` is the creator of the content
vr, _ = VotesReceived.objects.get_or_create(user=instance.content.user)
# you should recount the votes here rather than just incrementing the count
vr.count += 1
vr.save()
models.signals.post_save.connect(update_votes_received, sender=Voting)
Usage:
user = User.objects.get(id=1)
print user.votesreceived.count
If you already have data in your database you'd have to update the vote counts manually the first time of course.

How can I change my INNER JOIN to an OUTER JOIN?

I have 2 tables : Users and Documents.
1 User can have 0 or several documents.
I would like to display each user and their documents, but the problem : I would like to display users that have no documents, in a result like this (assume IdUsers 3 and 5 have no documents):
IdUser IdDocument DocumentName
====== ========== ============
1 1 test11.pdf
1 2 test12.pdf
1 3 test13.pdf
2 4 test21.pdf
2 5 test21.pdf
3 NULL NULL
4 6 test41.pdf
5 NULL NULL
You will use a LEFT JOIN to perform this operation.
create table users
(
userid int
)
create table documents
(
documentid int,
userid int,
documentname varchar(10)
)
insert into users values (1)
insert into users values (2)
insert into users values (3)
insert into users values (4)
insert into documents values (1, 1, 'test')
insert into documents values (2, 1, 'test 1')
insert into documents values (3, 2, 'test 2')
insert into documents values (3, 3, 'test 3')
select *
from users u
left join documents d
on u.userid = d.userid
see a sqlfiddle for a test
You should do some research on JOINs, here is a good description of the JOINs:
A Visual Explanation of SQL Joins