How to group Left Outer Join - ruby-on-rails

I have two models, User and Telephone. User has many telephones.
I'm making a list on active admin and if user has two phones I would like to group them together. I need a raw sql query, and the one that I wrote gives two separate rows.
What I want:
John Legend 123456, 654321
What I get:
John Legend 123456
John Legend 654321
This is my query:
a = "SELECT user.id, user.first_name, user.last_name, telephone.id AS telephone_id, telephone.number AS telephone_number,
FROM users
LEFT OUTER JOIN telephones ON telephones.user_id = user.id
GROUP BY users.id, telephones.user_id, telephones.number, telephones.id
ORDER BY user.id DESC"

If you are using Postgres 9.0+, then you may use the STRING_AGG function here:
SELECT
u.id,
u.first_name,
u.last_name,
STRING_AGG(t.number, ',') AS telephone_number
FROM users u
LEFT JOIN telephones t
ON t.user_id = u.id
GROUP BY
u.id
ORDER BY
u.id DESC;
I only included the three columns which your expected actually shows, though you may add other columns if you want.

Related

How to write sub query in active record?

I have two tables users and posts and they have association of has_many. I want to fetch details of both users and posts in a single query. I'm able to manage the sql query but I don't want to use the raw query in the code (using execute method) as i think it is kind of simple thing and can be written using active record.
Here is the sql query
SELECT a.id, a.name, a.timestamp, b.id, b.user_id, b.title
FROM users a
INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = a.id
where id IN ( 1, 2, 3);
I think includes does not help here because i'm dealing with large data.
Can any one help me ?
If you just want those specific columns and nothing else then this will work
User.joins(:post)
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
posts.id as post_id, posts.user_id as post_user_id,
posts.title as post_title")
This will return an ActiveRecord::Relation of User objects with virtual attributes for post_id, post_user_id (Not sure why you need this one since you already selected users.id), and post_title.
The query produced will be
SELECT users.id,
users.name,
users.timestamp,
posts.id as post_id,
posts.user_id as post_user_id,
posts.title as post_title
FROM users
INNER JOIN posts on posts.user_id = users.id
where users.id IN ( 1, 2, 3);
Please note you may have multiple User objects, one for each Post, just as the SQL query does.
You can execute your exact query using the string version of joins e.g.
User.joins("INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = users.id")
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
b.id as post_id, b.user_id as post_user_id,
b.title as post_title")
Additionally to avoid some of the overhead you can use arel instead e.g.
users_table = User.arel_table
posts_table = Post.arel_table
query = users_table.project(Arel.star)
.join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql)
This will return an ActiveRecord::Result with 2 useful methods columns (the columns selected) and rows. You can convert this to a Hash(#to_hash) but note that any columns with duplicate names (id for instance) will overwrite one another.
You could fix this by specifying the colums you want selected in the project portion. e.g. your current query would be:
query = users_table.project(
users_table[:id],
users_table[:name],
users_table[:timestamp],
posts_table[:id].as('post_id'),
posts_table[:user_id].as('post_user_id'),
posts_table[:title].as('post_title')
).join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql).to_hash
Since none of the names collide now it can be structured into a nice Hash where the keys are the column names and the values or the row value for that record.
users = User.joins(:posts).includes(:posts).where(id: [1, 2, 3])
Will give you all the users with theirs posts.
then you can do whatever you want with them, but to access posts data for first retrieved user
first_user_posts = users.first.posts # this will not make additional DB queries as you used includes and data is already added
We use joins to have INNER JOIN statement in the SQL
We use includes to load all posts in the memory
I have two tables users and posts and they have association of
has_many. I want to fetch details of both users and posts in a single
query.
can be done with includes like
users = User.includes(:posts).where({posts: {user_id: [1,2,3]}})
other is eager_load and preload you can use as per your requirements, for more https://blog.arkency.com/2013/12/rails4-preloading/

PHP/MYSQL: Obtain two items from table in join using different criteria

I am doing a join between tables to get a user pic from a user table about someone who comments on an article. However, I need to get a separate picture from the user table for a different user, the person who wrote the article at the same time. (There is a strong reason to do this in one query, as it's actually only one part of a more complex request).
Can anyone suggest how I could get two user pics from the users table during the join? This code gives me the first user pic, the commenter's pic or upic. But how do I also get the second user pic?
commments
id|comment|articleid|userid
1|great article|1|2
article
id|article|userid
1|Restaurant Openings|1
users
id|pic
1|mary.png
2|joe.png
$sql = "SELECT c.comment,c.articleid,c.userid,u.id, u.pic as upic,a.id,
FROM `comments` c
LEFT JOIN `articles` a
ON c.articleid = a.id
LEFT JOIN `users` u
ON c.userid= u.id
WHERE c.id=1";
A separate query to get the author's pic for would be:
$sqlauthorpic = "SELECT u.pic,u.id from `users` u
WHERE u.id = 1";
Thanks for any suggestions.
Join users again this time with article table, since you are looking for a user that wrote the article:
SELECT c.comment,c.articleid,c.userid,u.id, u.pic as upic,a.id, w.pic as wpic
FROM `comments` c
LEFT JOIN `articles` a ON c.articleid = a.id
LEFT JOIN `users` u ON c.userid= u.id
JOIN `users` w ON w.id = a.userid
You can also get rid of LEFT joins because, i think there could be no comments with out an article (articleid) and no comment without userid.

How can I apply LIMIT/ORDER conditions to an ActiveRecord Join?

Currently I have a Conversation has many Messages relationship, and a User has many Conversations relationship.
I would like to create an ActiveRecord Query to get The Last Message of each conversation that a user has.
Let's say I have the conversations ids in an array...
ids = [24, 22, 23]
This query:
Message.where(conversation_id: ids).joins(:conversation).order(created_at: :desc)
... is correct in terms that it returns ALL the Messages across all the user's conversations.
Using the same query above, If I map an array of the conversation_ids:
Message.where(conversation_id: ids).joins(:conversation).order(created_at: :desc).map(&:conversation_id)
I get an array like this: [24, 24, 22, 22, 23, 22] that tells me there are 3 messages in conversation with conversation_id=22, 2 messages with conversation_id=24, 1 with conversation_id=23.
This is good, But now my Question now is, How can I create an ActiveRecord Query to get just One Message from each Conversation? (the last one that was created)
I assume I have to use the limit()/order() methods, but I have no idea how to do it, it's a little too advanced for me.
Thanks for all your help in advance.
joins can accept a string, and you can specify any join you want as plain text. See doco.
Example:
User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
# SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
As for the problem of joining to the latest record, that's another question in it's own right, and has an answer on stackoverflow here.
Example:
SELECT c.*, p.*
FROM customer c INNER JOIN
(
SELECT customer_id,
MAX(date) MaxDate
FROM purchase
GROUP BY customer_id
) MaxDates ON c.id = MaxDates.customer_id INNER JOIN
purchase p ON MaxDates.customer_id = p.customer_id
AND MaxDates.MaxDate = p.date

Sequel -- How To Construct This Query?

I have a users table, which has a one-to-many relationship with a user_purchases table via the foreign key user_id. That is, each user can make many purchases (or may have none, in which case he will have no entries in the user_purchases table).
user_purchases has only one other field that is of interest here, which is purchase_date.
I am trying to write a Sequel ORM statement that will return a dataset with the following columns:
user_id
date of the users SECOND purchase, if it exists
So users who have not made at least 2 purchases will not appear in this dataset. What is the best way to write this Sequel statement?
Please note I am looking for a dataset with ALL users returned who have >= 2 purchases
Thanks!
EDIT FOR CLARITY
Here is a similar statement I wrote to get users and their first purchase date (as opposed to 2nd purchase date, which I am asking for help with in the current post):
DB[:users].join(:user_purchases, :user_id => :id)
.select{[:user_id, min(:purchase_date)]}
.group(:user_id)
You don't seem to be worried about the dates, just the counts so
DB[:user_purchases].group_and_count(:user_id).having(:count > 1).all
will return a list of user_ids and counts where the count (of purchases) is >= 2. Something like
[{:count=>2, :user_id=>1}, {:count=>7, :user_id=>2}, {:count=>2, :user_id=>3}, ...]
If you want to get the users with that, the easiest way with Sequel is probably to extract just the list of user_ids and feed that back into another query:
DB[:users].where(:id => DB[:user_purchases].group_and_count(:user_id).
having(:count > 1).all.map{|row| row[:user_id]}).all
Edit:
I felt like there should be a more succinct way and then I saw this answer (from Sequel author Jeremy Evans) to another question using select_group and select_more : https://stackoverflow.com/a/10886982/131226
This should do it without the subselect:
DB[:users].
left_join(:user_purchases, :user_id=>:id).
select_group(:id).
select_more{count(:purchase_date).as(:purchase_count)}.
having(:purchase_count > 1)
It generates this SQL
SELECT `id`, count(`purchase_date`) AS 'purchase_count'
FROM `users` LEFT JOIN `user_purchases`
ON (`user_purchases`.`user_id` = `users`.`id`)
GROUP BY `id` HAVING (`purchase_count` > 1)"
Generally, this could be the SQL query that you need:
SELECT u.id, up1.purchase_date FROM users u
LEFT JOIN user_purchases up1 ON u.id = up1.user_id
LEFT JOIN user_purchases up2 ON u.id = up2.user_id AND up2.purchase_date < up1.purchase_date
GROUP BY u.id, up1.purchase_date
HAVING COUNT(up2.purchase_date) = 1;
Try converting that to sequel, if you don't get any better answers.
The date of the user's second purchase would be the second row retrieved if you do an order_by(:purchase_date) as part of your query.
To access that, do a limit(2) to constrain the query to two results then take the [-1] (or last) one. So, if you're not using models and are working with datasets only, and know the user_id you're interested in, your (untested) query would be:
DB[:user_purchases].where(:user_id => user_id).order_by(:user_purchases__purchase_date).limit(2)[-1]
Here's some output from Sequel's console:
DB[:user_purchases].where(:user_id => 1).order_by(:purchase_date).limit(2).sql
=> "SELECT * FROM user_purchases WHERE (user_id = 1) ORDER BY purchase_date LIMIT 2"
Add the appropriate select clause:
.select(:user_id, :purchase_date)
and you should be done:
DB[:user_purchases].select(:user_id, :purchase_date).where(:user_id => 1).order_by(:purchase_date).limit(2).sql
=> "SELECT user_id, purchase_date FROM user_purchases WHERE (user_id = 1) ORDER BY purchase_date LIMIT 2"

How would I do this query with Arel?

I have a User model and an Item model. I want to rank users according to the value of the items they have. I want to do the equivalent of this query:
SELECT rank() OVER (ORDER BY grand_total DESC), u.*, grand_total
FROM users AS u
JOIN
(SELECT user_id, SUM(amount) AS grand_total FROM items WHERE EXTRACT(YEAR FROM sold_at)='2012' GROUP BY user_id) AS i
ON u.id = i.user_id;
Specifically, I don't know how to join on my select.
Given the problem as you describe it, I would write the query thus:
select users.*, sum(items.amount) as rank
from users
join items on items.user_id = users.id
group by users.id
order by rank desc;
Which would translate into AREL as:
User.select('users.*, sum(items.amount) as rank').joins('join items on items.user_id = users.id').group('users.id').order('rank desc')
This has the handy side-effect that you can call .rank on the resulting User objects and get the value of the rank column from the query, in case you need to display it.
Is there something about your situation I'm not grasping here, or would this work?

Resources