Select associated records based on comparison with array column - sql

Suppose a hypothetical scenario with following tables
Vehicles
+----+--------+
| id | number |
+----+--------+
| 1 | v1 |
| 2 | v2 |
| 3 | v3 |
+----+--------+
Users
+-----+------+-------------+
| id | name | vehicle_ids |
+-----+------+-------------+
| 100 | u1 | {1} |
| 200 | u2 | {1,2} |
| 300 | u3 | {2,3} |
| 400 | u4 | {} |
+-----+------+-------------+
Given number of a vehicle I am looking for a query that returns all the users associated with that vehicle,
where vehicle_ids contains the id of vehicle
where vehicle_ids only has the id of vehicle (i.e single element)

We can use ANY and ALL constructs (more details here Row and Array Comparisons)
USING "ANY"
http://sqlfiddle.com/#!17/ec886/21
For first case where we need to return users that are associated with vehicle represented by search query (along with others) we can use ANY. So, following query will return user rows with name u2 and u3 when supplied with v2 as vehicle name
SELECT "users".*
FROM "users"
WHERE (
(SELECT "vehicles"."id" FROM "vehicles" WHERE "vehicles"."number" = 'v2') =
ANY("users"."vehicle_ids")
)
You can replace "number" = 'v2' with any other vehicle name to get all the users which are associated with this vehicle number.
USING "ALL"
http://sqlfiddle.com/#!17/ec886/27
For second case where we need exact match i.e. only one element in the array, we will use All. So following query will return only user row with name u1 when v1
SELECT "users".*
FROM "users"
WHERE (
"users"."vehicle_ids" != '{}' and
(SELECT "vehicles"."id" FROM "vehicles" WHERE "vehicles"."number" = 'v1') = ALL("users"."vehicle_ids")
)
If we don't use "users"."vehicle_ids" != '{}' then the result will also contain user u4
Note:
This subquery method will only work for single vehicle i.e. we can't do WHERE "vehicles"."number" in ('v2', v3) as the subquery will return multiple rows and Postgres will throw an error. We might have to explore unnest if it can serve the same purpose as original question and also if it is generic enough to support aforementioned use case.

You can use unnest() as well:
select u.*
from users u cross join lateral
unnest(vehicle_ids) x(vehicle_id) join
vehicles v
on v.id = x.vehicle_id
where v.number = 'v2';
Here is a db<>fiddle.

Question #1: "where vehicle_ids contains the id of vehicle"
You can join the two tables with a condition on the array
select u.*
from users u
join vehicles v on v.id = any(u.vehicle_ids)
where v.number = 'v1';
Question #2: "where vehicle_ids only has the id of vehicle (i.e single element)"
This can be achieved by create a single element array from a SELECT statement
select u.*
from users u
where u.vehicle_ids = array(select v.id from vehicle v where v.number = 'v1');

Related

SQL Query to show results that don't have a relation to variable

For an assignment I have which includes a delete and add friend system (like Facebook), I've made a query that works by using two SQL tables, one which includes a friend_id, name and other information, and another which holds two friend_id columns, that show the relationship with the users and if they're friends.
User Table (friends)
| friend_id | profile_name |
|:---------- |:------------:|
| 1 | John |
| 2 | Peter |
| 3 | Alex |
| 4 | Nick |
---------------------------
Friendship Table (myfriends)
| friend_id1 | friend_id2 |
|:---------- |:----------:|
| 1 | 3 |
| 2 | 4 |
| 3 | 1 |
| 4 | 2 |
-------------------------
I am wanting to get a query which selects people that don't have a connection with a result (I want to show anyone who doesn't have a connection to friend_id '1', so only want to show users 2 and 4), and then display their name.
I have a query that selects the ones which have the relation which is:
SELECT friends.profile_name,friends.friend_id FROM `myfriends` JOIN `friends` ON friends.friend_id = myfriends.friend_id2 WHERE `friend_id1` = 1;
The query bellow shows all results from the table, and even using '!=', it doesn't select those who don't have a relation to friend_id '1'
SELECT friends.profile_name,friends.friend_id FROM `myfriends` JOIN `friends` ON friends.friend_id = myfriends.friend_id2 WHERE `friend_id1` != 1;
How can I fix this query so it shows all results but those connected to ‘friend_id1’ = 1
with connected as (SELECT friend_id,
myfriends.friend_id2 friend
FROM myfriends
JOIN friends
ON friends.friend_id = myfriends.friend_id1
WHERE friend_id1 = 1)
select *
from friends
where friend_id not in (select distinct friend from connected union all select distinct friend_id from connected)
you cannot change the where clause as it specifies which user you want to focus on.
So first get the users that are connected (in the first cte), and then select all users except those found in the first result of the connected users.
By the way, your example is misleading as it can be solved with a bug by doing something simple in the join.
edit
while it wasn't clease which version you were using, (I thought with clause is available in the newer mysql versions) I created another solution that is working on mysql 5.6 and should work for you as well:
select f.*
from friends f
left join (
SELECT friend_id, myfriends.friend_id2 friend
FROM myfriends
JOIN friends
ON friends.friend_id = myfriends.friend_id1
WHERE 1 in (friend_id,friend_id2)) f1
on f1.friend = f.friend_id
where f1.friend is null
it has a nicer implementation in one part (1 in one of 2 columns), and uses a left join that takes the nulls from the right table.

Redshift join each values in an array

I have a table like below (its actually the pg_group table)
group_id | group_name | userid
_____________________________________
101 | gr1 | {100,101}
102 | gr2 | {100,110,120}
I have another table where I can see the name of the user id.
userid | username
______________________
100 | user1
101 | user2
110 | user3
120 | user4
I want to join these 2 tables and generate the output like this.
group_id | group_name | username
_____________________________________
101 | gr1 | user1,user2
102 | gr2 | user1,user3,user4
I tried listagg and etc, but it didn't work as expected.
Update:
I tried this one, but list agg seems not working.
SELECT I.group_name, listagg(J.username,',')
FROM pg_group I
LEFT JOIN pg_user J
ON J.userid = ANY(I.userid)
GROUP BY I.group_name
ERROR: One or more of the used functions must be applied on at least one user created tables. Examples of user table only functions are LISTAGG, MEDIAN, PERCENTILE_CONT, etc;
Here first I have converted arrays of user_ID INTO ROWS WITH UNNEST THEN COLLECTED username against those user_id and at last with string_agg() function again those usernames have been grouped into a comma separated column.
select group_id,group_name,string_agg(username,',')usrname from
(select group_id,group_name,unnest(userid::text[])user_id from pg_group )pg
inner join pg_user u
on pg.user_id::int = u.userid
group by group_id,group_name
From googling so far I have understood that you cannot use listagg() if there is no user defined table is involved. I have found a way around. But I cannot check it since I don't have Redshift platform. Please check it out:
select group_name,listagg(username, ', ') within group (order by column_name)
from
(
SELECT I.group_name,J.username
FROM pg_group I
LEFT JOIN pg_user J
ON J.userid = ANY(I.userid)
left join (select top 1 1 from my_schema.my_table)
on 1=1
)
Instead of my_schema.my_table Please use any of your user defined table

PostgreSQL - How to find the row with a record that matches it with a value higher than given value?

Let's say I have two tables with a 1-to-many relation.
Table A (user):
id INT,
name TEXT
Table B (skill):
id INT,
user_id INT,
skill_name TEXT,
skill_level INT
Each user may have multiple skills. And now I wish to gather the users that have a certain skill that at least at a certain level, and maybe the users that have all the skills that matches the condition.
For example, let's say I have the following data in my database:
User:
id | name
1 | Merlin
2 | Morgan
Skill:
id | user_id | skill_name | skill_level
1 | 1 | Fireball | 2
2 | 1 | Thunderbolt | 3
2 | 2 | Thunderbolt | 2
2 | 2 | Firestorm | 1
2 | 2 | Curse | 3
And if I search for user who has thunderbolt at level 2 or more, I should get both Merlin and Morgan; if if I search for user who knows both thunderbolt at level 1 or more and curse at level 2 or more, then only Morgan should appear.
Also, I am hoping the result could also contain the content of all skills the users have. For now I am using
ARRAY_AGG(JSON_BUILD_OBJECT('skill_name', skill_name, 'skill_level', skill_level') to gather all skills by users' id. But I don't know how to filter the data based on those skills I get.
One approach uses aggregation, and a having clause to filter on skills names and levels. Boolean aggregate functions come handy here:
select u.*,
jsonb_agg(jsonb_build_object('skill_name', skill_name, 'skill_level', skill_level)) as skills
from users u
inner join skills s on s.user_id = u.id
group by u.id
having bool_or(s.skill_name = 'Thunderbolt' and s.skill_level >= 1)
and bool_or(s.skill_name = 'Curse' and s.skill_level >= 2)
This also adds a column to the resultset, called skills, that is a JSONB array containing one object for each skill name and level, as requested in your question: note that it makes more sense to generate a JSON(B) array rather than an array of JSON objects, as your originally intended.
One method uses aggregation. For one skill:
select user_id
from skills
group by user_id
having count(*) filter (where skill_name = 'thunderbold' and skill_level >= 2) > 0;
And for multiple skills, just add more clauses:
select user_id
from skills
group by user_id
having count(*) filter (where skill_name = 'thunderbold' and skill_level >= 1) > 0 and
count(*) filter (where skill_name = 'curse' and skill_level >= 2) > 0 ;
This returns the user_id. You can join back to the users table to get the name.

How to fetch data while where condition in jsonb in Postgresql

I have a table data_table like this
| id | reciever
| (bigint) |(jsonb)
----------------------------------------------------------------------
| 1 | [{"name":"ABC","email":"abc#gmail.com"},{"name":"ABDFC","email":"ab34c#gmail.com"},...]
| 2 | [{"name":"DEF","email":"deef#gmail.com"},{"name":"AFDBC","email":"a45bc#gmail.com"},...]
| 3 | [{"name":"GHI","email":"ghfi#gmail.com"},{"name":"AEEBC","email":"5gf#gmail.com"},...]
| 4 | [{"name":"LMN","email":"lfmn#gmail.com"},{"name":"EEABC","email":"gfg5#gmail.com"},...]
| 5 | [{"name":"PKL","email":"dfdf#gmail.com"},{"name":"ABREC","email":"a4rbc#gmail.com"},...]
| 6 | [{"name":"ANI","email":"fdffd#gmail.com"},{"name":"ABWC","email":"abrtc#gmail.com"},...]
when i run on pg admin it works fine
I want to fetch row by putting email in where condition like select * from data_table where receiver = 'abc#gmail.com'. there can be more data in array so i have shown "...".
I have tried like where receiver-->>'email'='abc#gmail.com' but it is working in the case {"name":"ABC","email":"abc#gmail.com"} only not in array where i have to chaeck every email in array
Help will be appreciated.
One option is to use exists and jsonb_array_elements():
select t.*
from mytable t
where exists (
select 1
from jsonb_array_elements(t.receiver) x(elt)
where x.elt ->> 'email' = 'abc#gmail.com'
)
This gives you all rows where at least one element in the array has the given email.
If you want to actually exhibit the matching elements, then you can use a lateral join instead (if more than one element in the array has the given email, this duplicates the row):
select t.*, x.elt
from mytable t
cross join lateral jsonb_array_elements(t.receiver) x(elt)
where x.elt ->> email = 'abc#gmail.com'

Problem with query to select distinct login

I need to select for each cod_user login that didn`t match to his login. Example: a -> acc (where in the table actually is abb).
I need it for some tests in data base in SoapUi.
I start with this, but can´t go any further for now:
SELECT U1.COD_USER, U2.LOGIN
FROM USERS U1
INNER JOIN USER U2
ON U1.LOGIN != U2.LOGIN
table name users
+----------+-------+
| cod_user | login |
+----------+-------+
| a | abb |
| b | acc |
| c | add |
| d | ahh |
| e | agg |
| f | ann |
+----------+-------+
But that query gives me all logins for each users that he didnt use and i only need one. Thanks you.
Does this do what you want?
SELECT U1.COD_USER,
MAX(U1.LOGIN) KEEP (DENSE_RANK FIRST ORDER BY DBMS_RANDOM.RANDOM) as UNUSED_LOGIN
FROM USERS U1
WHERE NOT EXISTS (SELECT 1
FROM USER U2
WHERE U1.COD_USER = U2.COD_USER AND
U1.LOGIN = U2.LOGIN
)
GROUP BY U1.COD_USER
In your both table, you must have a column which match. Like Cod_user must be common field and that needs to be used on join logic. So, just modify the your SQL like below
SELECT U1.COD_USER, U2.LOGIN
FROM USERS U1 INNER JOIN
USER U2
ON ( U1.COD_USER=U2.COD_USER and U1.LOGIN!= U2.LOGIN)
I understand that you want to assign to each user some random login belonging to other user from the same table and this assigned values should be distinct. So:
with t as (
select cod_user, login, count(1) over () cnt,
row_number() over (order by dbms_random.value) rn
from users )
select a.cod_user, a.login, b.login as random_login
from t a
left join t b on a.rn = b.rn + 1 or (a.rn = 1 and b.rn = b.cnt)
order by a.cod_user
dbfiddle demo
I assigned random row numbers to rows, then made self join on a.rn = b.rn + 1. First row must be joined exceptionally to the last, this is why count() over () was used. Probably you could also use mod() for this.
Assignment is random (due to dbms_random used for ordering) and unique. If you run this query severeal times you will get different, random, unique values.