I have 4 databases in my rails program, Products, Offerings, OfferingProducts, and Orders
Sample Offering: Medium Kit
Medium Kit Contains 2 Products (This is set up in the OfferingProduct db)
1 Small
2 Medium
customer Orders 2 medium kits
this program should account for 2 Small and 4 Medium from this order and many orders similar
I'm trying to set up an inventory system to track how many of each product are sold every week so that I can reorder products. To calculate this I have:
def get_trend
product = Product.first
customer_orders =[]
product.offerings.each do |o|
customer_orders = customer_orders + o.orders
end
o=customer_orders.group_by { |t| t.date.beginning_of_week }
y = []
op=self.offering_products;
o.sort.each do |week, orders|
y << orders.map { |o| o.quantity*op.find_by_offering_id(o.offering).quantity }.sum
end
return y
end
This seems to work, but it takes about 20 seconds to calculate. Is there any way to make this faster? I know most of the time is taken by the
y << orders.map { |o| o.quantity*op.find_by_offering_id(o.offering).quantity }.sum
line which calculates the number of products ordered in a given week. Any thoughts?
The problem is you're using iterations (i.e loops in application code) when you'd be better off using declarations (i.e. joins in the database). Loops will never be as fast as joins, and moving the dataset into the application's memory won't be helping either.
I'm guessing you're using ActiveRecord? If so, this may help with how to query with joins: http://guides.rubyonrails.org/active_record_querying.html
Here is what I ended up doing thanks to the info from Iain
def get_trend
sql = ActiveRecord::Base.connection()
if Rails.env == "production"
d=sql.execute("SELECT SUM(orders.quantity * offering_products.quantity), EXTRACT(ISOYEAR FROM orders.date) AS year, EXTRACT(WEEK FROM orders.date) AS week " +
" FROM orders " +
" INNER JOIN offerings ON offerings.id = orders.offering_id " +
" INNER JOIN offering_products ON offering_products.offering_id = offerings.id " +
" INNER JOIN products ON products.id = offering_products.product_id " +
" WHERE (products.id = #{self.id}) GROUP BY year, week ORDER BY year, week ")
y=d.map { |a| a["sum"].to_i }
else
d=sql.execute("SELECT SUM(orders.quantity * offering_products.quantity), strftime('%G-%V', orders.date) AS year " +
" FROM orders " +
" INNER JOIN offerings ON offerings.id = orders.offering_id " +
" INNER JOIN offering_products ON offering_products.offering_id = offerings.id " +
" INNER JOIN products ON products.id = offering_products.product_id " +
" WHERE (products.id = #{self.id}) GROUP BY year")
y=d.map { |a| a[0] }
end
return y
end
Update: needed to differentiate the code between production and local, because heroku uses postgresql, and the date functions work differently:( which also means the result isnt exactly the same, because the functions treat the first week of the year differently.
Related
I have a Schema like this:
User(:id)
Event(:id, :start_date, :end_date,:duration, :recurrence_pattern)
EventAssignment(:user_id, :event_id, :date)
recurrence_pattern is a rrule string (see https://icalendar.org/rrule-tool.html, https://github.com/square/ruby-rrule)
date is a date formatted like 'YYYY-MM-DD'
start_date and end_date are timestamps like 'YYYY-MM-DDTHH:MM:SS'
I want to find all users that have at least one allocation overlapping with 2 timestamps, say from and to
So I wrote a bit of postgres and arel
assignment_subquery = EventAssignment.joins(:event).where(
'"user_id" = users.id AND
(?) <= "event_assignments".date + (to_char("events".start_date, \'HH24:MI\'))::time + make_interval(mins => "events".duration) AND
(event_assignments.date) + (to_char(events.start_date, \'HH24:MI\'))::time <= (?)',
from, to).arel.exists
User.where(assignment_subquery)
edit: some more postgres(#max)
assignment_subquery = EventAssignment.joins(:event).where(
'"user_id" = users.id AND
("event_assignments".date + (to_char("events".start_date, \'HH24:MI\'))::time + make_interval(mins => "events".duration),
(event_assignments.date) + (to_char(events.start_date, \'HH24:MI\'))::time)
OVERLAPS ((?), (?))'
, from, to).arel.exists
Which works fine
my question is:
Is there a better more rails way to do this?
Noob here, I'm trying to query my SQLite database for entries that have been made in the last 7 days and then return them.
This is the current attempt
user.rb
def featuredfeed
#x = []
#s = []
Recipe.all.each do |y|
#x << "SELECT id FROM recipes WHERE id = #{y.id} AND created_at > datetime('now','-7 days')"
end
Recipe.all.each do |d|
#t = "SELECT id FROM recipes where id = #{d.id}"
#x.each do |p|
if #t = p
#s << d
end
end
end
#s
end
This code returns each recipe 6(total number of objects in the DB) times regardless of how old it is.
#x should only be 3 id's
#x = [13,15,16]
if i run
SELECT id FROM recipes WHERE id = 13 AND created_at > datetime('now','-7 days')
1 Rows returned with id 13 is returned
but if look for an id that is more than 7 days old such as 12
SELECT id FROM recipes WHERE id = 12 AND created_at > datetime('now','-7 days')
0 Rows returned
I'm probably over complicating this but I've spent way too long on it at this point.
the return type has to be Recipe.
To return objects created within last 7 days just use where clause:
Recipe.where('created_at >= ?', 1.week.ago)
Check out docs for more info on querying db.
Edit according to comments:
Since you are using acts_as_votable gem, add the votes caching, so that filtering by votes score is straightforward:
Recipe.where('cached_votes_total >= ?', 10)
Ruby is expressive. I would take the opportunity to use a scope. With Active Record Scopes, this query can be represented in a meaningful way within your code, using syntactic sugar.
scope :from_recent_week, -> { where('created_at >= ?', Time.zone.now - 1.week) }
This allows you to chain your scoped query and enhance readability:
Recipe.from_recent_week.each do
something_more_meaningful_than_a_SQL_query
end
It looks to me that your problem is database abstraction, something Rails does for you. If you are looking for a function that returns the three ids you indicate, I think you would want to do this:
#x = Recipe.from_recent_week.map(&:id)
No need for any of the other fluff, no declarations necessary. I also would encourage you to use a different variable name instead of #x. Please use something more like:
#ids_from_recent_week = Recipe.from_recent_week.map(&:id)
I am learning ActiveRecord. Can I build this query?
#sales_by_product = ActiveRecord::Base.connection.execute("SELECT
it.name,
it.id,
it.seller_id,
pur.volume,
pur.sales
FROM items it
INNER JOIN (SELECT
item_id,
COUNT(*) AS volume,
SUM(price) AS sales,
workflow_state AS state
FROM purchases
WHERE workflow_state = 'payment_successful'
GROUP BY item_id,
workflow_state) pur
ON pur.item_id = it.id
WHERE it.seller_id = '" + current_user.id.to_s + "'")
I would like to use the AR api as much as possible but I have not yet gotten the above to work using just AR.
Thanks!
I don't think it is a good idea to use AR for this query. It seems fun at first, but becomes annoying. And, it will be difficult to change later.
You can create your own query builder:
def query_for current_user
<<-SQL
SELECT
it.name,
it.id,
it.seller_id,
pur.volume,
pur.sales
FROM items it
INNER JOIN (SELECT
item_id,
COUNT(*) AS volume,
SUM(price) AS sales,
workflow_state AS state
FROM purchases
WHERE workflow_state = 'payment_successful'
GROUP BY item_id,
workflow_state) pur
ON pur.item_id = it.id
WHERE it.seller_id = '" + current_user.id.to_s + "'")
SQL
end
#sales_by_product = ActiveRecord::Base.connection.execute( query_for( current_user ))
I need to run sql query like
sql = 'SELECT * FROM users WHERE id != ' + self.id.to_s + ' AND id NOT IN (SELECT artner_id FROM encounters WHERE user_id = ' + self.id.to_s + ')'
sql += ' AND id NOT IN (SELECT user_id FROM encounters WHERE partner_id = ' + self.id.to_s + ' AND predisposition = ' + Encounter::Negative.to_s + ')'
sql += ' AND cfg_sex = ' + self.sex.to_s + ' AND cfg_country = ' + self.country.to_s + ' AND cfg_city = ' + self.city.to_s
sql += ' ORDER BY rand() LIMIT 1'
It can be executed by AR.find_by_sql, but the code before is bad readable.
Are there any query builder, which can build that query?
For example, Kohana (it is PHP framework, I am php developer, but I want to change that kid-language to ruby/rails) have a query builder, which works like this:
$sql = DB::select('*')->from('users');
$sql->where('id', 'NOT_IN', DB::expr('SELECT partner_id FROM encounters WHERE user_id = '.$user->id));
$sql->where('id', 'NOT_IN', DB::expr('SELECT user_id FROM encounters WHERE partner_id = '.$user->id.' AND predisposition = '.Encounter::Negative));
....
etc
...
Query which was builded with query builder like a Kohana query builder is more readable and understandable.
Are there any gem to solve this problem?
You need the squeel gem. It extends AR with blocks and makes very complicated queries with ease.
Just few features:
# not_in == cool! )
Product.where{id.not_in LineItem.select{product_id}}
# SELECT "products".* FROM "products" WHERE "products"."id" NOT IN
# (SELECT "line_items"."product_id" FROM "line_items" )
# outer joins on pure Ruby:
LineItem.joins{product.outer}
# LineItem Load (0.0ms) SELECT "line_items".* FROM "line_items"
# LEFT OUTER JOIN "products" ON "products"."id" = "line_items"."product_id"
# calcs, aliasing:
Product.select{[avg(price).as(middle)]}
# SELECT avg("products"."price") AS middle FROM "products"
# comparison
Product.where{id != 100500}
Product.where{price<10}
# logical OR
Product.where{(price<10) | (title.like '%rails%')}
# SELECT "products".* FROM "products" WHERE (("products"."price" < 10 OR
# "products"."title" LIKE '%rails%'))
# xxx_any feature (also available xxx_all)
Product.where{title.like_any %w[%ruby% %rails%]}
# SELECT "products".* FROM "products" WHERE (("products"."title" LIKE '%ruby%' OR
# "products"."title" LIKE '%rails%'))
Note the using blocks: {...} here aren't hashes. Also note the absence of symbols.
If you decide to pick it, read the section that starts with "This carries with it an important implication"
There's a ruby library that utilizes relational algebra. It is called ARel. If you are using Rails 3.x, then you already have.
ids = Partner.where(user_id: self.id).pluck(:partner_id) << self.id
users = User.where("id NOT IN #{ ids.join(',') }")
Here's the same query cast into rails AREL terms. It's not pretty yet -- it's a complicated query in general.
User.where("id = ? AND "
"id NOT IN (SELECT artner_id FROM encounters WHERE user_id = ?) AND " +
"id NOT IN (SELECT user_id FROM encounters WHERE partner_id = ? AND predisposition = ? ) AND " +
"cfg_sex = ? AND cfg_country = ? AND cfg_city = ?)",
self.id, self.id, self.id, Encounter::Negative,
self.sex, self.country, self.city).order(" rand() ").limit(1)
(I've not tested this, so it's possible there could be typo's in it.)
I'd recommend a couple things:
When you have complex where clauses they can be chained together and AREL will put them back together generally pretty well. This allows you to use scopes in your model classes and chain them together.
For example, you could do this:
class User < ActiveRecord::Base
def self.in_city_state_country(city, state, country)
where("cfg_sex = ? AND cfg_country = ? AND cfg_city = ?", city, state, country)
end
def self.is_of_sex(sex)
where("cfg_sex = ?", sex)
end
end
Then you could rewrite these portions of the query this way:
User.is_of_sex(user.sex).in_city_state_country(user.city, user.state, user.country)
and so on.
Breaking the queries down into smaller parts also makes it easier to test specific pieces of it with your rspecs. It results in more modular, maintainable code.
For more details, check out the Rails Guide - Active Record Query Interface
So lets say I have the following in a Post model, each record has the field "num" with a random value of a number and a user_id.
So I make this:
#posts = Post.where(:user_id => 1)
Now lets say I want to limit my #posts array's records to have a sum of 50 or more in the num value (with only the final record going over the limit). So it would be adding post.num + post2.num + post3.num etc, until it the total reaches at least 50.
Is there a way to do this?
I would say to just grab all of the records like you already are:
#posts = Post.where(:user_id => 1)
and then use Ruby to do the rest:
sum, i = 0, 0
until sum >= 50
post = #posts[i].delete
sum, i = sum+post.num, i+1
end
There's probably a more elegant way but this will work. It deletes posts in order until the sum has exceed or is equal to 50. Then #posts is left with the rest of the records. Hopefully I understood your question.
You need to use the PostgreSQL Window functions
This gives you the rows with the net sum lower than 50
SELECT a.id, sum(a.num) num_sum OVER (ORDER BY a.user_id)
FROM posts a
WHERE a.user_id = 1 AND a.num_sum < 50
But your case is trickier as you want to go over the limit by one row:
SELECT a.id, sum(a.num) num_sum OVER (ORDER BY a.user_id)
FROM posts a
WHERE a.user_id = 1 AND a.num_sum <= (
SELECT MIN(c.num_sum)
FROM (
SELECT sum(b.num) num_sum OVER (ORDER BY b.user_id)
FROM posts b
WHERE b.user_id = 1 AND b.num_sum >= 50
) c )
You have to convert this SQL to Arel.