Generating array-agg(...)s in postgres - sql

Using Postgres 10 I have tables representing 'units', the 'group' to which each unit belongs, and the 'distance' between each pair of units.
I now want a group-to-group distance table that aggregates all the unit-to-unit distances for each pair of groups - i.e. an aggregate array of the distances from all the units in one group to all the units in the other group.
What I have
'units' 'unit_distances'
id | group this_unit_id | that_unit_id | distance
---------- --------------------------------------
1 | 1 1 | 2 | d_12
2 | 1 1 | 3 | d_13
3 | 2 1 | 4 | d_14
4 | 3 2 | 3 | d_23
... | ... ... | ... | ...
What I want
'group_distances'
this_group_id | that_group_id | all_distances
---------------------------------------------------
1 | 2 | {d_13, d_23}
1 | 3 | {d_14, d_24}
2 | 3 | {d_34}
... | ... | {..., ...
I'm not sure how to return all such arrays for all group-group pairings and generate the table depicted above. At the moment I can only get these arrays by defining the sites individually (as x and y below).
with dist as (
select distance
from unit_distances
inner join units
on unit_distances.this_unit_id = units.id
WHERE units.group = x
intersect
select distance
from unit_distances
inner join units
on unit_distances.that_unit_id = units.id
WHERE units.group = y)
select array_agg(distance) from dist;
Thanks

If I understand correctly, this is just two joins to get the groups and an aggregation for the distances:
select u1.group, u2.group, array_agg(ud.distance) as all_distances
from unit_distances ud join
units u1
on ud.this_unit_id = u1.id join
units u2
on ud.that_unit_id = u2.id
group by u1.group, u2.group;

Related

SQLite How do i join two tables

I have two tables that contains translations of different words in english and french
words : contains all the words used in the app
+----+-------+---------+--
| ID | TEXT | LANG_ID |
+----+-------+---------+--
| 1 | partir | 4 |
| 2 | manger | 4 |
| 3 | go | 5 |
| 4 | eat | 5 |
+----+-------+---------+--
Translated_word : contains the translations (english to french and vice versa)
+----+-------+---------+--
| ID | SOURCE | TO |
+----+-------+---------+--
| 10 | 1 | 3 |
| 12 | 2 | 4 |
| 13 | 3 | 1 |
| 14 | 4 | 2 |
+----+-------+---------+--
I need to get all the contents of the word table in this format
1 partir, go
2 manger, eat
3 go, partir
4 eat, manger
I have tried several queries and i am not able to achieve the desired results. For example, when i try this one
SELECT lp.id, tw.id, lp.text, tw.text FROM words AS lp JOIN words AS tw ON (tw.id = lp.id)
i get the following results
1 partir, partir
2 manger, manger
...
...
Can someone let me know what i am doing wrong ?
Thanks
This would use two joins:
select tw.id, w1.*, w2.*
from translated_word tw join
words w1
on tw.source = w1.id join
words w2
on tw.to = w2.id and w2.lang_id <> w1.lang_id
where lang_id in (4, 5);
Note the additional condition on the second join . . . the language ids are not equal. I might expect that you want English and French in different columns. For that:
select tw.id, w1.*, w2.*
from translated_word tw join
words wf
on tw.source = wf.id and wf.lang_id = 4 join
words we
on tw.to = we.id and we.lang_id = 5
where lang_id in (4, 5);
Join translated_word to 2 copies of words:
select w1.id,
w1.text || ', ' || w2.text
from words w1
inner join translated_word tw on tw.source = w1.id
inner join words w2 on w2.id = tw.`to`
See the demo.

Displaying a pair that have same value in another table

I'm trying to make a query that pair a worker that work on the same place. The relational model I'm asking looks like this:
Employee(EmNum, name)
Work(FiNum*, EmNum*)
Field(FiNum, Title)
(bold indicates primary key)
right now my code looks like
SELECT work.finum, e1.name,e1.emnum,e2.name,e2.emnum
FROM employee e1
INNER JOIN employee e2
on e1.EmNum = e2.EmNum
INNER JOIN work
on e1.emnum = work.emnum
This gives me result like
| finum | name | emnum | name_1 | emnum_1 |
| 1 | a | 1 | a | 1 |
| 1 | b | 2 | b | 2 |
| 2 | c | 3 | c | 3 |
| 3 | d | 4 | d | 4 |
| 3 | e | 5 | e | 5 |
while I want the result to be like
| finum | name | emnum | name_1 | emnum_1 |
| 1 | a | 1 | b | 2 |
| 1 | b | 2 | a | 1 |
| 3 | d | 4 | e | 4 |
| 3 | e | 5 | d | 5 |
I'm quite new at sql so I can't really think of a way to do this. Any help or input would be helpful.
Thanks
Your question is slightly unclear, but my guess is that you're trying to find employees that worked on the same place = same finum in work, but different row. That you can do this way:
SELECT w1.finum, e1.name,e1.emnum, e2.name,e2.emnum
from work w1
join work w2 on w1.finum = w2.finum and w1.emnum != w2.emnum
join employee e1 on e1.emnum = w1.emnum
join employee e2 on e2.emnum = w2.emnum
If you don't want to repeat the records (1 <-> 2 + 2 <-> 1 change the != in the join to > or <)
I'm trying to make a query that pair a worker that work on the same place.
Presumably the "places" are represented by the Field table. If you want to pair up employees on that basis then you should be performing a join conditioned on field numbers being the same, as opposed to one conditioned on employee numbers being the same.
It looks like your main join wants to be a self-join of Work to Work of records with matching FiNum. To get the employee names in the result then you will need also to join Employee twice. To avoid employees being paired with themselves, you will want to filter those cases out via a WHERE clause.

Active Record Query, return Users with over a certain number of check_ins at a restaurant

In my db I have Users that have check_ins. A check_in is tied to one restaurant with restaurant_id. What's the most efficient way to get all Users who have checked in at a given restaurant more then X times?
To write effect Active Record queries, you must first know how to write effective SQL queries. As with any programming problem, the first step is to break it down into smaller tasks.
TL;DR
Don't do two queries when you just need one.
users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: 1 })
.group(:id)
.having('check_in_count > ?', 3)
.all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
# #<User id=4, name="Jordan", ..., check_in_count=4> ]
nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4
Prelude
Your check_ins table probably looks something like this:
id | restaurant_id | user_id | ...
-----+---------------+---------+-----
1 | 1 | 1 | ...
2 | 1 | 2 |
3 | 1 | 2 |
4 | 1 | 2 |
5 | 1 | 2 |
6 | 1 | 3 |
7 | 1 | 3 |
8 | 1 | 3 |
9 | 1 | 4 |
10 | 1 | 4 |
11 | 1 | 4 |
12 | 1 | 4 |
13 | 2 | 1 |
... | ... | ... | ...
In the above table we have 12 check-ins at the restaurant with restaurant_id = 1. The user with user_id = 1 checked in once, 2 checked in four times, 3 checked in twice, and 4 checked in four times.
The naïve way
The naive way to do this would be to break it down into the following tasks:
Get the check_ins records for the restaurant:
SELECT * FROM check_ins WHERE restaurant_id = 1;
Get the number of check-ins for each user for the restaurants by grouping by user_id and counting the number of records in each group:
SELECT check_ins.*, COUNT(user_id) AS check_in_count
FROM check_ins
WHERE restaurant_id = 1
GROUP BY user_id
Restrict the results to groups with at least than N records, e.g. N = 3:
SELECT check_ins.*, COUNT(user_id) AS check_in_count
FROM check_ins
WHERE restaurant_id = 1
GROUP BY user_id
HAVING check_in_count >= 3
Translate that into an Active Record query:
check_in_counts = CheckIn.where(restaurant_id: 1).group(:user_id)
.having("user_count > ?", 3).count
# => { 2 => 4, 4 => 4 }
Write a second query to get the associated users:
User.find(check_in_counts.keys)
# => [ #<User id=2, ...>, #<User id=4, ...> ]
That works, but there's something smelly about it—oh, it's that we're using a relational database. If we have a query that gets records from check_ins, we should just get the related users in the same query.
A better way
Now, it's relatively obvious that we could take our SQL query from (3) above and add a JOIN users ON check_ins.user_id = users.id to get the associated users records, but that leaves us in a bind because we still want Active Record to give us User objects, not CheckIn objects. To do that we need a different query, one that starts with users and joins check_ins.
To get there, we use LEFT OUTER JOIN:
SELECT *
FROM users
LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
WHERE restaurant_id = 1;
The above query will give us results like this:
id | name | ... | restaurant_id | user_id
----+--------+-----+---------------+---------
1 | Sarah | 1 | 1 | 1
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
3 | Carmen | 1 | 1 | 3
3 | Carmen | 1 | 1 | 3
3 | Carmen | 1 | 1 | 3
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
This looks familiar: it has all of the data from check_ins, with the data from users added on to each row. That's what LEFT OUTER JOIN does. Now, just like before, we can use GROUP BY to group by user IDs and COUNT to count the records in each group, with HAVING to restrict the results to users with a certain number of check-ins:
SELECT users.*, COUNT(*) AS check_in_count
FROM users
LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
WHERE restaurant_id = 1
GROUP BY users.id
HAVING check_in_count >= 3;
This gives us:
id | name | ... | check_in_count
----+--------+-----+----------------
2 | Nick | ... | 4
4 | Jordan | | 4
Perfect!
Finally...
Now all we have to do is translate this into an Active Record query. It's pretty straightforward:
users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: 1 })
.group(:id)
.having('check_in_count > ?', 3)
.all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
# #<User id=4, name="Jordan", ..., check_in_count=4> ]
nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4
And best of all, it performs just one query.
Bonus: Scope it
That's a pretty long Active Record query. If there's only one place in your app where you're going to have a query like this, it might be okay to use it that way. If I were you, though, I would turn it into a scope:
class User < ActiveRecord::Base
scope :with_check_in_count, ->(opts) {
opts[:at_least] ||= 1
select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: opts[:restaurant_id] })
.group(:id)
.having('check_in_count >= ?', opts[:at_least])
}
# ...
end
Then:
User.with_check_in_count(at_least: 3, restaurant_id: 1)
# ...or just...
User.with_check_in_count(restaurant_id: 1)
I cannot check this with your exact model schema, but something like this should work:
check_in_counts = CheckIn.group(:user_id).having(restaurant_id: 3).having('COUNT(id) > 10').count
This will return a Hash with user_id => check_in_count values, which you can use to fetch all the User objects:
users = User.find(check_in_counts.keys)

list combinations in SQL Server 2000

I have a table which lists combinations of profiles
id | p1 | p2
1 | 1 | 2
2 | 2 | 3
3 | 1 | 3
and a table which lists users, and adds a profile to it.
As you can see, a user can have multiple profiles
id | user | comb
1 | John | 1
2 | John | 3
3 | John | 2
4 | Jef | 1
5 | Jef | 2
Now, I'd like to see per user, the combinations that are equal to the first table.
excepted output:
| user | comb
| John | 1,2
| John | 1,3
| Jef| 2,3
How can I best do this in SQL Server 2000 (so no CTE :( )?
I can't get further than this:
select * from users where comb in (
select p2 from combinations c inner join users u on u.comb = c.p1
)
If I understand you right, you are looking for users that have an illegal combination of profiles. You can do that by starting from the illegal_combinations table:
SELECT *
FROM dbo.illegal_combinations ic
JOIN dbo.user_profile p1
ON ic.p1 = p1.comb
JOIN dbo.user_profile p2
ON ic.p2 = p2.comb
AND p1.user = p2.user;
That gives you for each combination all users, so you might end up with a user more than once. But that can be fixed easily:
SELECT DISTINCT p1.*
FROM dbo.illegal_combinations ic
JOIN dbo.user_profile p1
ON ic.p1 = p1.comb
JOIN dbo.user_profile p2
ON ic.p2 = p2.comb
AND p1.user = p2.user;
Instead of p1.* you should use a specific column list. You can include columns from one of the other tables, but then you might get duplicate users again.

SQL LEFT JOIN help

My scenario: There are 3 tables for storing tv show information; season, episode and episode_translation.
My data: There are 3 seasons, with 3 episodes each one, but there is only translation for one episode.
My objetive: I want to get a list of all the seasons and episodes for a show. If there is a translation available in a specified language, show it, otherwise show null.
My attempt to get serie 1 information in language 1:
SELECT
season_number AS season,number AS episode,name
FROM
season NATURAL JOIN episode
NATURAL LEFT JOIN episode_trans
WHERE
id_serie=1 AND
id_lang=1
ORDER BY
season_number,number
result:
+--------+---------+--------------------------------+
| season | episode | name |
+--------+---------+--------------------------------+
| 3 | 3 | Episode translated into lang 1 |
+--------+---------+--------------------------------+
expected result
+-----------------+--------------------------------+
| season | episode| name |
+-----------------+--------------------------------+
| 1 | 1 | NULL |
| 1 | 2 | NULL |
| 1 | 3 | NULL |
| 2 | 1 | NULL |
| 2 | 2 | NULL |
| 2 | 3 | NULL |
| 3 | 1 | NULL |
| 3 | 2 | NULL |
| 3 | 3 | Episode translated into lang 1 |
+--------+--------+--------------------------------+
Full DB dump
http://pastebin.com/Y8yXNHrH
I tested the following on MySQL 4.1 - it returns your expected output:
SELECT s.season_number AS season,
e.number AS episode,
et.name
FROM SEASON s
JOIN EPISODE e ON e.id_season = s.id_season
LEFT JOIN EPISODE_TRANS et ON et.id_episode = e.id_episode
AND et.id_lang = 1
WHERE s.id_serie = 1
ORDER BY s.season_number, e.number
Generally, when you use ANSI-92 JOIN syntax you need to specify the join criteria in the ON clause. In MySQL, I know that not providing it for INNER JOINs results in a cross join -- a cartesian product.
LEFT JOIN episode_trans
ON episode_trans.id_episode = episode.id_episode
AND episode_trans.id_lang = 1
WHERE id_serie=1
You probably need to move the id_lang = 1 into the LEFT JOIN clause instead of the WHERE clause. Think of it this way... for all of those rows with no translation the LEFT JOIN gives you back NULLs for all of those translation columns. Then in the WHERE clause you are checking to see if that is equal to 1 - which of course evaluates to FALSE.
It would probably be easier if you included your code in the question next time instead of in a link.
Can you try using
LEFT OUTER JOIN
instead of
NATURAL LEFT JOIN