SQL: how to find a complement to a set with a derived function/value - sql

This one has me stumped, so I'm hoping someone who's smarter than me can help me out.
I'm working on a rails project in which I've got a User model which has an association of clock_periods joined to it, having the following partial definition:
User
has_many :clock_periods
#clock_periods has the following properties:
#clock_in_time:datetime
#clock_out_time:datetime
named_scope :clocked_in, :select => "users.*",
:joins => :clock_periods, :conditions => 'clock_periods.clock_out_time IS NULL'
def clocked_in?
#default scope on clock periods sorts by date
clock_periods.last.clock_out_time.nil?
end
The SQL query to retrieve all clocked in users is trivial:
SELECT users.* FROM users INNER JOIN clock_periods ON clock_periods.user_id = users.id
WHERE clock_periods.clock_out_time IS NULL
The converse however--finding all users who are currently clocked out--is deceptively difficult. I ended up using the following named scope definition, though its hackish:
named_scope :clocked_out, lambda{{
:conditions => ["users.id not in (?)", clocked_in.map(&:id)+ [-1]]
}}
What bothers me about it is that it seems like there ought to be a way to do this in SQL without resorting to generating statements like
SELECT users.* FROM users WHERE users.id NOT IN (1,3,5)
Anybody got a better way, or is this really the only way to handle it?

Besides #Eric's suggestion there's the issue (unless you've guaranteed against it in some other way you're not showing us) that a user might not have any clock period -- then the inner join would fail to include that user and he wouldn't show either as clocked in or as clocked out. Assuming you also want to show those users as clocked out, the SQL should be something like:
SELECT users.*
FROM users
LEFT JOIN clock_periods ON clock_periods.user_id = users.id
WHERE (clock_periods.clock_user_id IS NULL) OR
(getdate() BETWEEN clock_periods.clock_out_time AND
clock_periods.clock_in_time)
(this kind of thing is the main use of outer joins such as LEFT JOIN).

assuming getdate() = the function in your SQL implementation that returns a datetime representing right now.
SELECT users.* FROM users INNER JOIN clock_periods ON clock_periods.user_id = users.id
WHERE getdate() > clock_periods.clock_out_time and getdate() < clock_periods.clock_in_time

In rails, Eric H's answer should look something like:
users = ClockPeriod.find(:all, :select => 'users.*', :include => :user,
:conditions => ['? > clock_periods.clock_out_time AND ? < clock_periods.clock_in_time',
Time.now, Time.now])
At least, I think that would work...

Related

How to convert this sql joins/count statements in clean ruby ActiveRecord

I'm trying to convert with no success my find_by_sql statement into a pure ActiveRecord query.
It is:
Corner.find_by_sql('SELECT corners.id, corners.name, count(members.*) FROM corners LEFT JOIN places ON corners.id = places.ubicacion_id LEFT JOIN members ON places.id = members.place_id GROUP BY corners.id,corners.name ORDER BY corners.name;')
nicely formatted, the sql expression would be:
SELECT corners.id,
corners.name,
count(members.*)
FROM corners
LEFT JOIN places ON corners.id = places.ubicacion_id
LEFT JOIN members ON places.id = members.place_id
GROUP BY corners.id,
corners.name
ORDER BY corners.name;
In very old versions of ActiveRecord, my approach would be using the find :all and then passing a hash of options, but this way is deprecated:
Corner.find( :all,
:joins => "LEFT JOIN places ON corners.id = places.ubicacion_id",
:joins => "LEFT JOIN members ON places.id = members.place_id",
:group => "corners.id,corners.name",
:order => "corners.name",
:select => "corners.id, corners.name, count(members.*)"
)
Which one would be the best approach to rewrite in the ActiveRecord way the query? This last snippet works well, but it makes no difference on using it rather than the plain sql one:
Corner.joins("LEFT JOIN places ON corners.id = places.ubicacion_id").joins("LEFT JOIN members ON places.id = members.place_id").group("corners.id,corners.name").order("corners.name").select("corners.id, corners.name, count(members.*)")
Many thanks!
It looks like you probably want to set up some model relationships to make this more ActiveRecord-like. You can find descriptions of how to do this in the Active Record Associations documentation.
Consider these relationships:
class Member < ActiveRecord::Base
belongs_to :place
end
class Place < ActiveRecord::Base
belongs_to :corner
has_many :members
end
class Corner < ActiveRecord::Base
has_many :places, foreign_key: "ubicacion_id"
end
Given those, you should be able to do something like this:
Corner.
select("corners.id, corners.name, count(members.*)").
joins(:places, :members).
group("corners.id, corners.name").
order("corners.name")
Each of the methods chained in the query will refine the query incrementally, much like building a native SQL statement. You can find the official documentation for these methods in the Active Record Query Interface

Rails join - find records that don't have a join matching a specific condiiton

In my Rails 2.2.2 app I have two tables/models joined like so:
School
has_many :licenses, :as => :licensable
License
belongs_to :licensable, :polymorphic => true
#important fields for this question:
#start_date: datetime
#end_date: datetime
If i want to search for all schools with a current license, it's simple enough:
licensed_schools = School.find(:all, :include => [:licenses], :conditions => ["licenses.start_date < ? and licenses.end_date > ?", Time.now, Time.now])
This finds me all the schools that have a valid license in the join table. So far so good.
However, if i want to find all the schools that don't have a valid license, it's more difficult (so far): for example if i do
unlicensed_schools = = School.find(:all, :include => [:licenses], :conditions => ["licenses.id is null or licenses.start_date > ? or licenses.end_date < ?", Time.now, Time.now]
then i get back any school that a) don't have any licenses at all (fine) or b) has at least one invalid license, including schools which have an old (invalid) license AND a new (valid) license.
In other words it's returning all schools that either have no license OR have one or more invalid licenses (regardless of whether they have a valid license as well). It should be returning all schools that have 0 valid licenses.
I can't quite figure out how to do this. Any help anyone?
You can use Arel for this using exists:
licenses = License.arel_table
School.where(
License.where(
licenses[:school_id].eq(School.arel_table[:id]).
and(licenses[:start_date].gte(Time.now)).
and(licenses[:end_date].lte(Time.now))).exists)
Alternatively you can build a custom LEFT JOIN and check for NULLs:
licenses = License.arel_table
schools = School.arel_table
custom_join = schools.join(licenses, Arel::Nodes::OuterJoin).
on(licenses[:school_id].eq(schools[:id].
and(licenses[:start_date].gte(Time.now)).
and(licenses[:end_date].lte(Time.now))).join_sources
School.joins(custom_join).where(licenses: {id: nil})
You can also skip Arel completely and build the LEFT JOIN by hand:
School.joins("LEFT JOIN licenses ON licenses.school_id = schools.id AND '#{Time.now.to_s(:db)}' BETWEEN licenses.start_date AND licenses.end_date").
where(licenses: {id: nil})
A LEFT JOIN will typically be less performant than EXISTS, although you might find it simpler to understand.
This isn't very satisfactory (i'd really like to know how to do it with just sql) but i ended up caching (with memcache) the ids of all schools that do have a valid license, and then just testing whether the school's id is in that list.
Like i say i'd still like to see a pure sql solution, particularly one that didn't involve more nested queries. Got a feeling i could do it with COALESCE() but haven't quite been able to figure it out.
How about this?
School.find_by_sql("Select * from schools where id not in (select distinct(lisensable_id) from licenses where licensable_type='school' and licenses.start_date > #{Time.now} and licenses.end_date < #{Time.now})")

How do I create this active_records query

I have a user model and a story model. Users have many stories.
I want to create a scope that returns the twenty-five users records for users who have created the most stories today, along with the amount of stories that they have created.
I know that there are people on SO that are great with active_records queries. I also know that I am not one of those guys:-(. Help would be greatly appreciated and readily accepted!
#UPDATE with the solution
I've been working with #MrYoshiji's suggestion, and here is what i came up with (note, I'm using this query in my active_admin dashboard):
panel "Today's Top Posters" do
time_range = Date.today.beginning_of_day..Date.today.end_of_day
table_for User.joins(:stories)
.select('users.username, count(stories.*) as story_count')
.group('users.id')
.where(:stories => {:created_at => time_range})
.order('story_count DESC')
.limit(25) do
column :username
column "story_count"
end
end
And low and behold, it works!!!!
Note, when I tried a simplified version of MrYoshiji's suggestion:
User.includes(:stories)
.select('users.username, count(stories.*) as story_count')
.order('story_count DESC') #with or without the group statement
.limit(25)
I got the following error:
> User.includes(:stories).select('users.username, count(stories.*) as story_count').order('story_count DESC').limit(25)
User Load (1.6ms) SELECT DISTINCT "users".id, story_count AS alias_0 FROM "users" LEFT OUTER JOIN "stories" ON "stories"."user_id" = "users"."id" ORDER BY story_count DESC LIMIT 25
ActiveRecord::StatementInvalid: PG::Error: ERROR: column "story_count" does not exist
LINE 1: SELECT DISTINCT "users".id, story_count AS alias_0 FROM "us...
It seems like includes don't like any aliasing.
I can't test this right now, running under Windows... Can you try it?
User.includes(:stories)
.select('users.*, count(stories.*) as story_count')
.group('users.id')
.order('story_count DESC')
.where('stories.created_at BETWEEN ? AND ?', Date.today.beginning_of_day, Day.today.end_of_day)
.limit(25)
Using .includes, as suggested by MrYshiji, did not work due to aliasing problems (see the original question for more info on this). Instead, I used .joins as follows to get the query results that I wanted (note, this query is inside my active_admin dashboard):
panel "Today's Top Posters" do
time_range = Date.today.beginning_of_day..Date.today.end_of_day
table_for User.joins(:stories)
.select('users.username, count(stories.*) as story_count')
.group('users.id')
.where(:stories => {:created_at => time_range})
.order('story_count DESC')
.limit(25) do
column :username
column "story_count"
end
end

ActiveRecord: Adding condition to ON clause for includes

I have a model offers and another historical_offers, one offer has_many historical_offers.
Now I would like to eager load the historical_offers of one given day for a set of offers, if it exists. For this, I think I need to pass the day to the ON clause, not the WHERE clause, so that I get all offers, also when there is no historical_offer for the given day.
With
Offer.where(several_complex_conditions).includes(:historical_offers).where("historical_offers.day = ?", Date.today)
I would get
SELECT * FROM offers
LEFT OUTER JOIN historical_offers
ON offers.id = historical_offers.offer_id
WHERE day = '2012-11-09' AND ...
But I want to have the condition in the ON clause, not in the WHERE clause:
SELECT * FROM offers
LEFT OUTER JOIN historical_offers
ON offers.id = historical_offers.offer_id AND day = '2012-11-09'
WHERE ...
I guess I could alter the has_many definition with a lambda condition for a specific date, but how would I pass in a date then?
Alternatively I could write the joins mysqlf like this:
Offer.where(several_complex_conditions)
.joins(["historical_offers ON offers.id = historical_offers.offer_id AND day = ?", Date.today])
But how can I hook this up so that eager loading is done?
After a few hours headscratching and trying all sorts of ways to accomplish eager loading of a constrained set of associated records I came across #dbenhur's answer in this thread which works fine for me - however the condition isn't something I'm passing in (it's a date relative to Date.today). Basically it is creating an association with the conditions I wanted to put into the LEFT JOIN ON clause into the has_many condition.
has_many :prices, order: "rate_date"
has_many :future_valid_prices,
class_name: 'Price',
conditions: ['rate_date > ? and rate is not null', Date.today-7.days]
And then in my controller:
#property = current_agent.properties.includes(:future_valid_prices).find_by_id(params[:id])

Help optimizing ActiveRecord query (voting system)

I have a voting system with two models: Item(id, name) and Vote(id, item_id, user_id).
Here's the code I have so far:
class Item < ActiveRecord::Base
has_many :votes
def self.most_popular
items = Item.all #where can I optimize here?
items.sort {|x,y| x.votes.length <=> y.votes.length}.first #so I don't need to do anything here?
end
end
There's a few things wrong with this, mainly that I retrieve all the Item records, THEN use Ruby to compute popularity. I am almost certain there is a simple solution to this, but I can't quite put my finger on it.
I'd much rather gather records and run the calculations in the initial query. This way, I can add a simple :limit => 1 (or LIMIT 1) to the query.
Any help would be great--either rewrite in all ActiveRecord or even in raw SQl. The latter would actually give me a much clearer picture of the nature of the query I want to execute.
Group the votes by item id, order them by count and then take the item of the first one. In rails 3 the code for this is:
Vote.group(:item_id).order("count(*) DESC").first.item
In rails 2, this should work:
Vote.all(:order => "count(*) DESC", :group => :item_id).first.item
sepp2k has the right idea. In case you're not using Rails 3, the equivalent is:
Vote.first(:group => :item_id, :order => "count(*) DESC", :include => :item).item
Probably there's a better way to do this in ruby, but in SQL (mysql at least) you could try something like this to get a top 10 ranking:
SELECT i.id, i.name, COUNT( v.id ) AS total_votes
FROM Item i
LEFT JOIN Vote v ON ( i.id = v.item_id )
GROUP BY i.id
ORDER BY total_votes DESC
LIMIT 10
One easy way of handling this is to add a vote count field to the Item, and update that each time there is a vote. Rails used to do that automatically for you, but not sure if it's still the case in 2.x and 3.0. It's easy enough for you to do it in any case using an Observer pattern or else just by putting in a "after_save" in the Vote model.
Then your query is very easy, by simply adding a "VOTE_COUNT DESC" order to your query.