Subqueries in activerecord - ruby-on-rails-3.1

With SQL I can easily do sub-queries like this
User.where(:id => Account.where(..).select(:user_id))
This produces:
SELECT * FROM users WHERE id IN (SELECT user_id FROM accounts WHERE ..)
How can I do this using rails' 3 activerecord/ arel/ meta_where?
I do need/ want real subqueries, no ruby workarounds (using several queries).

Rails now does this by default :)
Message.where(user_id: Profile.select("user_id").where(gender: 'm'))
will produce the following SQL
SELECT "messages".* FROM "messages" WHERE "messages"."user_id" IN (SELECT user_id FROM "profiles" WHERE "profiles"."gender" = 'm')
(the version number that "now" refers to is most likely 3.2)

In ARel, the where() methods can take arrays as arguments that will generate a "WHERE id IN..." query. So what you have written is along the right lines.
For example, the following ARel code:
User.where(:id => Order.where(:user_id => 5)).to_sql
... which is equivalent to:
User.where(:id => [5, 1, 2, 3]).to_sql
... would output the following SQL on a PostgreSQL database:
SELECT "users".* FROM "users" WHERE "users"."id" IN (5, 1, 2, 3)"
Update: in response to comments
Okay, so I misunderstood the question. I believe that you want the sub-query to explicitly list the column names that are to be selected in order to not hit the database with two queries (which is what ActiveRecord does in the simplest case).
You can use project for the select in your sub-select:
accounts = Account.arel_table
User.where(:id => accounts.project(:user_id).where(accounts[:user_id].not_eq(6)))
... which would produce the following SQL:
SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT user_id FROM "accounts" WHERE "accounts"."user_id" != 6)
I sincerely hope that I have given you what you wanted this time!

I was looking for the answer to this question myself, and I came up with an alternative approach. I just thought I'd share it - hope it helps someone! :)
# 1. Build you subquery with AREL.
subquery = Account.where(...).select(:id)
# 2. Use the AREL object in your query by converting it into a SQL string
query = User.where("users.account_id IN (#{subquery.to_sql})")
Bingo! Bango!
Works with Rails 3.1

Another alternative:
Message.where(user: User.joins(:profile).where(profile: { gender: 'm' })

This is an example of a nested subquery using rails ActiveRecord and using JOINs, where you can add clauses on each query as well as the result :
You can add the nested inner_query and an outer_query scopes in your Model file and use ...
inner_query = Account.inner_query(params)
result = User.outer_query(params).joins("(#{inner_query.to_sql}) alias ON users.id=accounts.id")
.group("alias.grouping_var, alias.grouping_var2 ...")
.order("...")
An example of the scope:
scope :inner_query , -> (ids) {
select("...")
.joins("left join users on users.id = accounts.id")
.where("users.account_id IN (?)", ids)
.group("...")
}

Related

Project a single column of a table filtered by tag from act as taggale

Currently the only way I know to sub select in rails is with arel,
for exmaple -
sub = x.where(y:'x').project(:id)
select = a.where(a[:x_id].in(sub))
question is,
if x is using the acts as taggable on gem and need to filtered by a specific tag, use with tagged_with method.
How can I still achive same database efficiency, it looks like the tagged with method override the projection.
thanks,
You don't need Arel to build sub selects in Rails:
sub = X.where(y: 'x')
select = A.where(x_id: sub)
generates the following SQL, assuming A's table name is as and X's is xs:
SELECT "as".* FROM "as" WHERE "as"."x_id" IN (SELECT "xs"."id" FROM "xs" WHERE "xs"."y" = 'x')
Testing with tagged_with worked: A.where(x_id: X.tagged_with('my_tag')) generates the expected SQL, at least for Rails 5.1, version on which I've tested.
Edit
You can specify the column used inside the subselect if needed. If you don't specify it, the primary key column is the default:
sub = X.where(y: 'x').select(:x_y_id)
select = A.where(x_id: sub)
will generate the following SQL:
SELECT "as".* FROM "as" WHERE "as"."x_id" IN (SELECT "xs"."x_y_id" FROM "xs" WHERE "xs"."y" = 'x')

How to solve AmbiguousColumn error in postgresql with ruby project

I would like to join two query result.
First one is
Gig.where(id:gigsInRadioIDS)
When I convert result to json I get this result
[{"id":1,"address":"test1"},{"id":2,"address":"test2"}
Second is.
Slot.where(status:"available").where(Sequel.lit('("from" > ?) AND ("to" < ?)', fromdate, todate))
when I convert result to json I get this result
[{"id":15,"status":"available","gig_id":1}]
What I want is to inner join to result
so I hope to get this result(I abbreviated some non related snippet)
[{"id":15,"status":"available","gig_id":1,"id":1,"address":"test1"}]
What I have tried is
Gig.where(id:gigsInRadioIDS).as(:tb_gig).join(Slot.where(status:"available").where(Sequel.lit('("from" > ?) AND ("to" < ?)', fromdate, todate)), gig_id: :id)
And I got this error
Sequel::DatabaseError - PG::AmbiguousColumn: ERROR: column reference "id" is ambiguous
LINE 1: ...) AS "t1" ON ("t1"."gig_id" = "gigs"."id") WHERE ("id" IN (2...
Thanks for reading this complex code.
Any news will be big help. Thanks.
There are a bunch of ways to approach it. It looks like you are querying Slots with some Gigs data mixed in, so you may want to start with your Slot model. The regular join syntax is a little easier to understand and makes it easier to select columns from both tables (i.e. instead of WHERE fk IN (..)). As a bonus, you can mix your gig_id filter into the JOIN clause. Finally, you don't need to use Sequel.lit to do greater than / less than in your WHERE clause, and you should be explicit about the columns you want to select.
Here's how I would write it:
Slot.join(:gigs, [[:id, :gig_id], [:id, gigsInRadioIDS]])
.where(status: 'available') { (from() > fromdate) & (to() < todate) }
.select(Sequel[:slots][:id].as(:slot_id), :status, :gig_id, :address)
Getting .all of the records and outputting JSON should yield something like:
[{"slot_id":15,"status":"available","gig_id":1,"address":"test1"}]
Note the Sequel[:table_name][:column_name] syntax. This is the new style of fully-qualifying identifiers in Sequel 4.49+. (In past versions of Sequel, it would have been written like :table_name__column_name, but that syntax is now deprecated.)
Your where(id:gigsInRadioIDS) translates into WHERE ("id" IN (...)), and since both gigs and slots have an "id" column, the database doesn't know which id you want.
You need to explicitly specify the table with where{{gigs[:id] => gigsInRadioIDS}}
Querying with Sequel

ActiveRecord Subquery Inner Join

I am trying to convert a "raw" PostGIS SQL query into a Rails ActiveRecord query. My goal is to convert two sequential ActiveRecord queries (each taking ~1ms) into a single ActiveRecord query taking (~1ms). Using the SQL below with ActiveRecord::Base.connection.execute I was able to validate the reduction in time.
Thus, my direct request is to help me to convert this query into an ActiveRecord query (and the best way to execute it).
SELECT COUNT(*)
FROM "users"
INNER JOIN (
SELECT "centroid"
FROM "zip_caches"
WHERE "zip_caches"."postalcode" = '<postalcode>'
) AS "sub" ON ST_Intersects("users"."vendor_coverage", "sub"."centroid")
WHERE "users"."active" = 1;
NOTE that the value <postalcode> is the only variable data in this query. Obviously, there are two models here User and ZipCache. User has no direct relation to ZipCache.
The current two step ActiveRecord query looks like this.
zip = ZipCache.select(:centroid).where(postalcode: '<postalcode>').limit(1).first
User.where{st_intersects(vendor_coverage, zip.centroid)}.count
Disclamer: I've never used PostGIS
First in your final request, it seems like you've missed the WHERE "users"."active" = 1; part.
Here is what I'd do:
First add a active scope on user (for reusability)
scope :active, -> { User.where(active: 1) }
Then for the actual query, You can have the sub query without executing it and use it in a joins on the User model, such as:
subquery = ZipCache.select(:centroid).where(postalcode: '<postalcode>')
User.active
.joins("INNER JOIN (#{subquery.to_sql}) sub ON ST_Intersects(users.vendor_coverage, sub.centroid)")
.count
This allow minimal raw SQL, while keeping only one query.
In any case, check the actual sql request in your console/log by setting the logger level to debug.
The amazing tool scuttle.io is perfect for converting these sorts of queries:
User.select(Arel.star.count).where(User.arel_table[:active].eq(1)).joins(
User.arel_table.join(ZipCach.arel_table).on(
Arel::Nodes::NamedFunction.new(
'ST_Intersects', [
User.arel_table[:vendor_coverage], Sub.arel_table[:centroid]
]
)
).join_sources
)

select two calculations with one query in active record

In SQL I would do a "select count(*), min(price) from products". What is the best way to do this with one query in active records?
Right now I have to do two queries, which doesn't feel right.
result_count = Product.where(filter_string).count
result_min = Product.where(filter_string).minimum(:price)
You can add the string in a select method like this:
result_stats = Product.select("count(*) as product_count, min(price) as price_min").where(filter_string)[0]
result_stats["product_count"] # => 123
result_stats["price_min"] # => 12.35
The limitation here is that this will initialize Product objects with only those fields accessible (in this case only 1 object since there's no group by clause). In this case, it's not really an issue, but something worth knowing if you get an error when you try to access relations.
http://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields

How to do joins on subqueries in AREL within Rails

I have a simple model
class User
has_many :logs
class Logs
related in the usual way through the foreign key logs.user_id. I'm trying to do the following using Arel and according to the Arel doc it should work.
u_t = Arel::Table::new :users
l_t = Arel::Table::new :logs
counts = l_t.
group(l_t[:user_id]).
project(
l_t[:user_id].as("user_id"),
l_t[:user_id].count.as("count_all")
)
l_t.joins(counts).on(l_t[:id].eq(counts[:user_id]))
When I do that I get the error
TypeError: Cannot visit Arel::SelectManager
However the author of Arel explicitly suggests that Arel can do this kind of thing.
Please do not write responses on how I can achieve the same query with raw sql, another type of Arel query etc. It is the pattern I am interested in not the specific results of this query.
You can use join_sources to retrieve the Arel::Nodes::Join from the instance of Arel::SelectManager, and pass that to joins
Using your example:
l_t.joins(counts.join_sources).on(l_t[:id].eq(counts[:user_id]))
This achieves a join of nested select subquery with Arel:
You can add the nested inner_query and an outer_query scope in your Model file and use ...
inner_query = Model.inner_query(params)
result = Model.outer_query(params).joins(Arel.sql("(#{inner_query.to_sql})"))
.group("...")
.order("...")
For variations on this, for example to use INNER JOIN on the subquery, do the following:
inner_query = Model.inner_query(params)
result = Model.outer_query(params).joins(Arel.sql("INNER JOIN (#{inner_query.to_sql}) tablealias ON a.id = b.id"))
.group("...")
.order("...")
Add in the specific joins, constraints and groupings to each of the queries' scopes to modify the sql statement further ie:
scope :inner_query , -> (company_id, name) {
select("...")
.joins("left join table1 on table1.id = table2.id")
.where("table1.company_id = ? and table1.name in (?)", company_id, name)
.group("...")
}
This allows you to put WHERE conditions on the nested query as well as the outer query

Resources