How to do an inclusion AND exclusion join query in SQL/Active Record? - sql

The Problem
I have three models, greatly simplified for the purpose of this question:
User:
- id
Event:
- id
- user_id (FK)
Stream:
- id
- event_id (FK)
- user_id (FK)
I'm using PostgresQL
Lets say I have two users, with IDs: 1, 2.
I want to retrieve all Events that has at least one Stream by User 1. I also want to exclude any Events that have at least one Stream by User 2.
Data Set
Stream: ID: 1, User: 1, Event: 1
Stream: ID: 2, User: 1, Event: 1
Stream: ID: 3, User: 1, Event: 2
Stream: ID: 4, User: 2, Event: 2
Stream: ID: 5, User: 2, Event: 3
Stream: ID: 6, User: 2, Event: 3
For purposes of this example, let's say these are all of the Streams in the system.
Current Active Record Query
# Keep in mind this is on the User model. So self=User 1
Event.joins(:streams)
.merge( Stream.where(user: self).where.not(user: User.find(2)) )
.....
Resulting Query:
SELECT events.*
FROM events
INNER JOIN streams ON streams.event_id = events.id
WHERE streams.user_id = 1 AND streams.user_id != 2
Current Result
Expected:
Event 1
Actual:
Event 1
Event 2
Currently this appropriately only returns Events having at least one Stream by User 1, however it does not exclude those Events which also have at least one Stream by User 2.
How can you properly execute this in SQL, and an unnecessary bonus, in Active Record?

Try this
SELECT events.*
FROM events ev
INNER JOIN streams st
ON st.event_id = ev.id
and st.user_id = 1
left join streams st1
ON st1.event_id = ev.id
and st1.user_id = 2
where st1.event_id is null

Related

postgreSQL - How to have condition on Join model result

I have been pulling my hair on how to translate my business requirement into a SQL query.
The case in itself is not that complex. I have two main tables user and list and a many to many relationship between the 2 of them though the userList table, where a user can be connected to a list by having a specific role, like being the owner or a collaborator.
Relevant properties for that example:
user -> id
list -> id | isPublic (BOOL)
userList -> id | userId | listId | role (OWNER|COLLABORATOR)
I have an API endpoint that allows for a user (requester) to retrieve all the lists of another user (target). I have the following 2 cases:
Case 1: requester = target
I have that under control with:
`SELECT
"list".*,
"userList"."id" AS "userList.id",
"userList"."listId" AS "userList.listId"
FROM
"list" AS "list"
INNER JOIN
"userList" AS "userList"
ON "list"."id" = "userList"."listId" AND "userList"."userId" = '${targetUserId}'`;
Case 2: requester =/= target
This is where I'm stuck as I have the following additional constraints:
a user can only see other users' lists that are public
OR lists for which they have the role of COLLABORATOR or OWNER
So I'm looking to do something like:
Get all the lists for which target is connected to (no matter the role) AND for which, EITHER the list is public OR the requester is also connected to that list.
Sample Data
user table
id
----------
john
anna
list table
id | isPublic
------------------
1 false
2 true
3 false
userList table
id | userId | listId | role
- john 1 OWNER
- john 2 OWNER
- john 3 OWNER
- anna 1 COLLABORATOR
Anna requests to see all of John's lists
desired result:
[{
id: 1,
isPublic: false
}, {
id: 2,
isPublic: true
}]
Any help would be greatly appreciated :)
Many thanks
Does this do what you want?
select l.*
from list
inner join userlist ul on ul.listid = l.id
group by l.id
having
bool_or(ul.role = 'OWNER' and ul.userid = 'john') -- owned by John
and (l.ispublic or bool_or(ul.userid = 'anna') -- public or allowed to Anna

SQL: How to virtually combine two tables and link to the result?

Imagine three tables:
user:
- id
- name (string)
event:
- id
- description (string)
- points (int)
history:
- user_id
- event_id
Now imagine that the tables are currently filled with the following data:
user:
id: 1, name: foo
id: 2, name: bar
id: 3, name: beef
event:
id: 1, description: "walked 5 miles", points: +10
id: 2, description: "awake the whole night", points: +15
id: 3, description: "wasn't naughty", points: +20
history:
id: 1, user_id: 1, event_id: 1
id: 2, user_id: 1, event_id: 3
id: 3, user_id: 2, event_id: 1
So the schema is something like "how many points does a user have". Currently, e.g., the user 1 has in sum 10+20 = 30 points, right?
I would like to add another table with things the user can "buy" for his/her points.
gift:
- id
- points (int)
- description (string)
which is filled with e.g.
gift:
id: 1, points: -30, description: "a bottle of beer"
id: 2, points: -5, description: "coffee"
My problem:
Currently I am creating a new entry in history when the user gets points. But how would you substract points when he buys a gift?
I thought about something like a combined table which includes the events and the gifts entries:
[USER]-----[HISTORY]-----[COMBINED]--+--[EVENTS]
|
+--[GIFTS]
But I cannot really join them because I need the gift table somewhere else.
I have absolutely no clue how to do that in SQL, it's a long time since I learned it and I unfortunately never used it since then. I hope somebody can point me into the right direction :)
Thanks!

Can PostgreSQL JOIN on jsonb array objects?

I am considering switching to PostgreSQL, because of the JSON support. However, I am wondering, if the following would be possible with a single query:
Let's say there are two tables:
Table 1) organisations:
ID (INT) | members (JSONB) |
------------+---------------------------------------------------------|
1 | [{ id: 23, role: "admin" }, { id: 24, role: "default" }]|
2 | [{ id: 23, role: "user" }]
Table 2) users:
ID (INT) | name TEXT | email TEXT |
------------+-----------+---------------|
23 | Max | max#gmail.com |
24 | Joe | joe#gmail.com |
Now I want to get a result like this (all i have is the ID of the organisation [1]):
ID (INT) | members (JSONB) |
------------+--------------------------------------------------------|
1 | [{ id: 23, name: "Max", email: "max#gmail.com", role:
"admin" },
{ id: 24, name: "Joe", email: "joe#gmail.com ", role:
"default" }]
(1 row)
I know this is not what JSONB is intended for and that there is a better solution for storing this data in SQL, but I am just curious if it would be possible.
Thanks!
Yes it is possible to meet this requirement with Postgres. Here is a solution for 9.6 or higher.
SELECT o.id, JSON_AGG(
JSON_BUILD_OBJECT(
'id' , u.id,
'name' , u.name,
'email' , u.email,
'role' , e.usr->'role'
)
)
FROM organisations o
CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(o.data) AS e(usr)
INNER JOIN users u ON (e.usr->'id')::text::int = u.id
GROUP BY o.id
See this db fiddle.
Explanation :
the JSONB_ARRAY_ELEMENTS function splits the organisation json array into rows (one per user) ; it is usually used in combination with JOIN LATERAL
to join the users table, we access the content of the id field using the -> operator
for each user, the JSONB_BUILD_OBJECT is used to create a new object, by passing a list of values/keys pairs ; most values comes from the users table, excepted the role, that is taken from the organisation json element
the query aggregates by organisation id, using JSONB_AGG to generate a json array by combining above objects
For more information, you may also have a look at Postgres JSON Functions documentation.
There might be more ways to do that. One way would use jsonb_to_recordset() to transform the JSON into a record set you can join. Then create a JSON from the result using jsonb_build_object() for the individual members and jsonb_agg() to aggregate them in a JSON array.
SELECT jsonb_agg(jsonb_build_object('id', "u"."id",
'name', "u"."name",
'email', "u"."email",
'role', "m"."role"))
FROM "organisations" "o"
CROSS JOIN LATERAL jsonb_to_recordset(o."members") "m" ("id" integer,
"role" text)
INNER JOIN "users" "u"
ON "u"."id" = "m"."id";
db<>fiddle
What functions are available in detail depends on the version. But since you said you consider switching, assuming a more recent version should be fair.

One to many relationship on the same table

Here is the situation:-
I have a table called Users. This contains user data for students and tutors as most of the data required is the same.
Having completed the system I am now told that the client would like to be able to assign students to tutors.
Is there a legitimate/ clean way I can create a one to many relationship within a single table, perhaps via a link table?
I've tried to think this through but whatever solution I come up with seems messy.
I would be grateful for any input.
Thanks
Phill
Have you tried the following approach?
Make a new table, for example TutorStudent (choose a more appropriate name if needed). It should have two columns:
Tutor_ID
Student_ID
Both columns shall be the (composite) primary key, each column will be a foreign key to your Users table User_ID (I assume this is what you have).
So, if you have a tutor named Newton that has two students, Tesla and Edison, your Users table will have something like this:
User_ID, Name
1, Newton
2, Tesla
3, Edison
and your TutorStudent table will have following values:
Tutor_ID, Student_ID
1, 2
1, 3
Relatively simple and doesn't require any modifications to your existing table.
Do take care when deleting users - use the delete cascade feature of your database system or do some maintenance work afterwards so your TutorStudent table doesn't go stale when updating/removing your users.
My ideal for the same situation
Example: one book have many category:
Basic solution:
book table has recorded book information
category table has recored category information ex: 100 documents
book_category_relation table has single book (book_id) has category(category_id) 1 book may be have 100 category_id
Ideal solution:
First calculate total your category: ex 100 document. Each category equal value 1 bit: max 31 bit so 100 category we have ceil floor(100%31) = 4 groups
category_id = 1 : 1 (1%31) <=> 000000001 group 0 = floor(1/31)
category_id = 2 : 2 (2%31)<=> 000000010 group 0 = floor(2/31)
category_id = 3 : 4 (3%31)<=> 000000100 group 0 = floor(3/31)
category_id = 4 : 8(4%31)<=> 000001000 group 0 = floor(4/31)
...........................
category_id = 31: 2^31(31%31) <=>1000..000 group 0 if moduler 31 equal zero so number group = (31/31 -1)=0;
category_id = 32: 1(32%31) <=> 0000000001 group 1 = floor(32/31)
category_id = 33: 2(33%31) <=> 0000000010 group 1 = floor(33/31)
Ok now we add 4 fields in design book table (group_0,group_1,group_2,group_3) with int(11) unsigned and add index that fields
if book has category id = n so we can the following calculate formula:
bit code = (n%31 ==0)?31: (n%31)
number group field = (n%31==0)?(n/31 -1):floor(n/31)
ex: book in category_id = 100 so:
bit code = (100%31) =7 <=>2^7 = 128,
group = floor(100%31) = 3 <=> in group_3
so if you need query all book in category_id = 100, query string is:
SELECT * FROM book WHERE group_3&128
Note: MySQL not index working if bitwise in where.
But you can check in this link:
Bitwise operations and indexes

Issue to build an sql query - running in sql developer

I am new to sql and have the a table called Event with the following column:
ID
Stage
Type
Location
Country
So a country can be Italy,Brazil,England or All.
Basically I need to build a query that i will pass country as parameter and it will return :
All country if All is passed as parameter and return only the
countries which their stage and type are not the same
If any other country other than All is passed as parameter then it should return
for the choosen country and All country also with their stage and
type not the same
So for example if All is passed i will recieve data for All countries only which there stage and type are not the same
If Brazil is passed then i will receive for Brazil and All countries which there stage and type are not the same.
So If I have query for country Brazil on the following record:
ID: 1
Stage: C
Type: Football
Location:City
Country:All
ID: 2
Stage: C
Type: Football
Location:City
Country:Brazil
ID 3
Stage: D
Type: VolleyBall
Location:City
Country:All
ID 4
Stage: E
Type: handball
Location:City
Country:Brazil
It will return ID 3 and ID 4 only because for the others the Type and the stage are the same.
If for All countries:
ID: 1
Stage: C
Type: basketball
Location:City
Country:All
ID: 2
Stage: D
Type: handball
Location:City
Country:Brazil
ID 3
Stage: C
Type: VolleyBall
Location:City
Country:ALL
It should return :
Only ID 1 and 3.
I Know it will be something like this :
select *
from Event
where country =
Any help will be most welcome thanks in advance
The country part is easy.
You want to get all rows where either the country is All or the country matches your parameter
SELECT *
FROM Event
WHERE Country = #Country or Country = 'All'
Your second part is considerably more difficult.
If I understand you want to return all rows where that Stage/Type combo appears only once. To do this you would need to first go get the count of each of those combos, and then select the ones where that count = 1
SELECT Stage, Type, count(*) occurrences
FROM Events
GROUP BY Stage, Type
HAVING count(*) = 1
This query groups them by Stage and Type and counts them and selects only those that have 1 occurrence.
You can combine this with the first query in a join to get what I think you want.
SELECT *
FROM Event
JOIN (
SELECT Stage, Type, count(*) occurrences
FROM Events
GROUP BY Stage, Type
HAVING count(*) = 1
) uniqueEvents ON uniqueEvents.Stage = Event.Stage AND uniqueEvents.Type = Event.Type
WHERE Country = #Country or Country = 'All'