Rails complex query first ascending, then descending - sql

so, I need to make a query which would work like this:
first, it would display elements, which have a date attribute "end_date" bigger than the current date, and the "release_date" attribute lower than the current date (example: today is the 15th of September, end_date is the 20th and release is the 12th). These elements are "active".
After displaying those, I would like to display past elements by descending order - past elements are those, who have "end_date" lower than the current date.
I would appreciate any help with building this query. Thanks!
P.S. I'm using PostgreSQL, Ruby 2.2.0 and Rails 4.2.1

One of options is to make two queries and then merge ActiveRecord::Relation objects. Maybe not so efficient (2 queries), but easy to understand and refactor.
#relevant_elements = Element.where('end_date > :current_date AND release_date < :current_date', current_date: Date.today).order('end_date ASC')
#past_elements = Element.where('end_date < :current_date', current_date: Date.today).order('end_date DESC')
#elements = #relevant_elements.merge(#past_elements)

Related

Is there a way to set all dates?

I was wondering, in SQL/dbt is there a way to set all dates to be >= another date?
Say I have a 'createdat' date field and a 'updatedat' date field. I use it multiple times in my query (multiple CTEs) as well as other dates. I want to make sure all dates used are less then the last day of last month (i.e. <= last_day(current_date()-30, month)).
Is there a way to set that in the beginning of the query?
This can definitely be done. You'll want to compare the greatest() of a number of columns with whatever date cut-off you want.
Effectively, it would be:
select *
from {{ ref('some_table') }}
where greatest(created_at,updated_at) < date_trunc('month', current_date)
You can obviously add as many columns to that query as you'd like.
N.B.: On some warehouses, greatest returns null if any of the columns in it are null. In that situation, you'll need to coalesce each date with some date placeholder, like '1970-01-01'.

SQL SELECT date from table, and calculate how many days since that date

I'm looking to calculate how many days have passed since a specific date, retrieved from a table in my database. Based on the info I've found on W3Schools (Here), I have attempted using DATEDIFF, but am coming up against a couple of different errors I can't seem to work around.
I have included my code below, and based on this, what I want to happen is this: Select the "DD" from the "Wave_Data" table, and, based on "sysdate", work out how many days have lapsed since then.
SELECT DATEDIFF(WEEKDAY,:P1_DD,SYSDATE)
FROM WAVE_DATA
WHERE WAVE_NUMBER = :P1_WAVE;
The final calculation would then be inputted into a text field within my ApEx database.
Thank you in advance for any help you may be able to provide,
Dominic
In Oracle you can just subtract one Date from another to get the difference (in days) between them:
SELECT SYSDATE - :p1_dd
FROM Wave_Data
WHERE Wave_Number = :p1_wave;
If you want to know the difference between the dates without any time parts then you can do:
SELECT TRUNC( SYSDATE ) - TRUNC( :p1_dd )
FROM Wave_Data
WHERE Wave_Number = :p1_wave;
or
SELECT FLOOR( SYSDATE - :p1_dd )
FROM Wave_Data
WHERE Wave_Number = :p1_wave;

sqlalchemy select by date column only x newset days

suppose I have a table MyTable with a column some_date (date type of course) and I want to select the newest 3 months data (or x days).
What is the best way to achieve this?
Please notice that the date should not be measured from today but rather from the date range in the table (which might be older then today)
I need to find the maximum date and compare it to each row - if the difference is less than x days, return it.
All of this should be done with sqlalchemy and without loading the entire table.
What is the best way of doing it? must I have a subquery to find the maximum date? How do I select last X days?
Any help is appreciated.
EDIT:
The following query works in Oracle but seems inefficient (is max calculated for each row?) and I don't think that it'll work for all dialects:
select * from my_table where (select max(some_date) from my_table) - some_date < 10
You can do this in a single query and without resorting to creating datediff.
Here is an example I used for getting everything in the past day:
one_day = timedelta(hours=24)
one_day_ago = datetime.now() - one_day
Message.query.filter(Message.created > one_day_ago).all()
You can adapt the timedelta to whatever time range you are interested in.
UPDATE
Upon re-reading your question it looks like I failed to take into account the fact that you want to compare two dates which are in the database rather than today's day. I'm pretty sure that this sort of behavior is going to be database specific. In Postgres, you can use straightforward arithmetic.
Operations with DATEs
1. The difference between two DATES is always an INTEGER, representing the number of DAYS difference
DATE '1999-12-30' - DATE '1999-12-11' = INTEGER 19
You may add or subtract an INTEGER to a DATE to produce another DATE
DATE '1999-12-11' + INTEGER 19 = DATE '1999-12-30'
You're probably using timestamps if you are storing dates in postgres. Doing math with timestamps produces an interval object. Sqlalachemy works with timedeltas as a representation of intervals. So you could do something like:
one_day = timedelta(hours=24)
Model.query.join(ModelB, Model.created - ModelB.created < interval)
I haven't tested this exactly, but I've done things like this and they have worked.
I ended up doing two selects - one to get the max date and another to get the data
using the datediff recipe from this thread I added a datediff function and using the query q = session.query(MyTable).filter(datediff(max_date, some_date) < 10)
I still don't think this is the best way, but untill someone proves me wrong, it will have to do...

How to create "Upcoming birthdays" module in Rails?

I have a table "users" with a column "date_of_birth" (DATE format with day, month, year).
In frontend I need to list 5 upcoming birthdays.
Spent ages trying to work out the logic.. also browsed every possible article in Google with no luck..
Any suggestions how to do this in RoR?
Thanks!
Several answers have suggested calculating/storing day of year and sorting on that, but this alone won't do much good when you're close to the end of the year and need to consider people with birthdays in the beginning of the next year.
I'd go for a solution where you calculate the full date (year, month, day) of each person's next birthday, then sort on that. You can do this in Ruby code, or using a stored procedure in the database. The latter will be faster, but will make your app harder to migrate to a different db platform later.
It would be reasonable to update this list of upcoming birthdays once per day only, which means you can use some form of caching. Thus the speed of the query/code needed is less of an issue, and something like this should work fine as long as you cache the result:
class User
def next_birthday
year = Date.today.year
mmdd = date_of_birth.strftime('%m%d')
year += 1 if mmdd < Date.today.strftime('%m%d')
mmdd = '0301' if mmdd == '0229' && !Date.parse("#{year}0101").leap?
return Date.parse("#{year}#{mmdd}")
end
end
users = User.find(:all, :select => 'id, date_of_birth').sort_by(&:next_birthday).first(5)
Edit: Fixed to work correctly with leap years.
Thanks to this post in my rails 3 app i use:
u = User.where("strftime('%m%d', date_of_birth) = ?", Date.today.strftime('%m%d'))
Update:
To use this with Postgresql:
u = User.where("extract(month from date_of_birth) = ? AND extract(day from date_of_birth) = ?", Date.today.strftime('%m'), Date.today.strftime('%d'))
It's better to use SQL to make this query:
next_month = (Date.today + 1.month).month # or use any other integer
users_having_birhtday_next_month =
User.where("EXTRACT(MONTH FROM date_of_birth) = ?", next_month)
Note: EXTRACT - PostgreSQL function
I'd have a before_save callback that calculates and stores to the day of the year in the database alongside the birthday.
You then have a simple query to pull back the next 5 birthdays. Make sure to handle the boundary condition where you are at the end of the year (I'd check if you don't get 5 results in RoR and then run a new query for the 1st Jan to get some extra birthdays to make it up to 5).
You will probably want to cache the results so you don't keep rerunning the query if it is on a common page.
I too thought that day of year would be the way to go, but the fact that it is different for most of the year depending on whether it is a leap year or not makes it tricky.
Better is to store the month and day as a string: d.strftime('%m%d'). You can then use that as (possibly) two queries (assuming new column is 'monthday')
First,
User.find(:all,
:condition => [:monthday > Date.now.strftime('%m%d')],
:select => "DISTINCT monthday",
:limit => 5)
If you don't get 5 results, do the query again, except use "0101" instead of the date calculation and lower the limit.
This gets you a list of monthday strings that you then have to turn back into dates.
If you want users, remove the :select line.
If you're on Oracle, you can do it without creating a new column. IMO it's a smell to create a column that contains data you already have.
The SQL's a bit ugly - I'm sure there's a more elegant way to do it. Generally in these cases I'd ask my DBA friends for advice.
User.find(:all,
:conditions =>
"TO_NUMBER(TO_CHAR(dob, 'MMDD')) >= TO_NUMBER(TO_CHAR(SYSDATE, 'MMDD'))",
:order => "TO_NUMBER(TO_CHAR(dob, 'MMDD'))",
:limit => 5)
Some people think a duplicate column is faster, but if you have enough user data that speed's an issue, you should benchmark the duplicate column against a table without it that has a functional index on TO_NUMBER(TO_CHAR(dob, 'MMDD')).
Here's how I find today's birthdays:
User.find_by_sql("select * from users where date_format(date_of_birth, '%m%d') = date_format(now(), '%m%d')")
I run this once a day. It takes less than a second from about 100,000 rows. (It doesn't properly handle people born on Feb 29th.)
If your database is mysql, the following is probably faster:
scope :birthday_next_week, (lambda {
where('DayOfYear(date_of_birth) >= 7
And DayOfYear(date_of_birth) - DayOfYear(curdate()) Between 0 and 6) Or
(MOD(YEAR(curDate()),4) = 0) And MOD(YEAR(curDate()),100) != 0
And (DayOfYear(date_of_birth) + 366 - DayOfYear(curdate())) % 366 < 7) Or
(DayOfYear(date_of_birth) + 365 - DayOfYear(curdate())) % 365 < 7').
order('DATE_FORMAT( date_of_birth, "%m-%d" ) ASC')
})
Edit: changed to make it work in the week before new years eve / leap years.
I found this worked for me. I didn't require to filter the most recent five results, so I used month is same as current.
def upcoming_birthdays
#resident = current_business.residents.where("extract(month from date_of_birth) = ?", Date.today.strftime('%m')).paginate(:page => params[:page])
end

Best way in MySQL or Rails to get AVG per day within a specific date range

I'm trying to make a graph in Rails, for example the avg sales amount per day for each day in a given date range
Say I have a products_sold model which has a "sales_price" float attribute. But if a specific day has no sales (e.g none in the model/db), I want to return simply 0.
What's the best way in MySQL/Rails to get this done? I know I can do something like this:
(This SQL query might be the completely wrong way to get what I'm wanting too)
SELECT avg(sales_price) AS avg, DATE_FORMAT(created_at, '%m-%d-%Y') AS date
FROM products_sold WHERE merchant_id = 1 GROUP BY date;
And get results like this:
| avg | date |
23 01-03-2009
50 01-05-2009
34 01-07-2009
... ...
What I'd like to get is this:
| avg | date |
23 01-03-2009
0 01-04-2009
50 01-05-2009
0 01-06-2009
34 01-07-2009
0 01-08-2009
... ...
Can I do this with SQL or will I have to post-process the results to find what dates in the daterange aren't in the SQL result set? Perhaps I need some sub-selects or IF statements?
Thanks for any help everyone.
Is there a reason (other than the date one already mentioned) why you wouldn't use the built-in group function capabilities in ActiveRecord? You seem to be concerned about "post-processing", which I don't think is really something to worry about.
You're in Rails, so you should probably be looking for a Rails solution first[1]. My first thought would be to do something like
Product.average(:sales_price, :group => "DATE(created_at)", :conditions => ["merchant_id=?", 1])
which ActiveRecord turned into pretty much the SQL you described. Assuming there's a declared has_many association between Merchant and Product, then you'd probably be better using that, so something like:
ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
(I'm hoping that your description of the model as "products_sold" is some kind of transcription error, btw - if not, you're somewhat off-message with your class naming!)
After all that, you're back where you started, but you got there in a more conventional Rails way (and Rails really values conventions!). Now we need to fill in the gaps.
I'll assume you know your date range, let's say it's defined as all dates from from_date to to_date.
date_aves = (from_date..to_date).map{|dt| [dt, 0]}
That builds the complete list of dates as an array. We don't need the dates where we got an average:
ave_price_dates = ave_prices.collect{|ave_price| ave_price[0]} # build an array of dates
date_aves.delete_if { |dt| ave_price.dates.index(dt[0]) } # remove zero entries for dates retrieved from DB
date_aves.concat(ave_prices) # add the query results
date_aves.sort_by{|ave| ave[0] } # sort by date
That lot looks a bit cluttered to me: I think it could be terser and cleaner. I'd investigate building a Hash or Struct rather than staying in arrays.
[1] I'm not saying don't use SQL - situations do occur where ActiveRecord can't generate the most efficient query and you fall back on find_by_sql. That's fine, it's supposed to be like that, but I think you should try to use it only as a last resort.
For any such query, you will need to find a mechanism to generate a table with one row for each date that you want to report on. Then you will do an outer join of that table with the data table you are analyzing. You may also have to play with NVL or COALESCE to convert nulls into zeroes.
The hard part is working out how to generate the (temporary) table that contains the list of dates for the range you need to analyze. That is DBMS-specific.
Your idea of mapping date/time values to a single date is spot on, though. You'd need to pull a similar trick - mapping all the dates to an ISO 8601 date format like 2009-W01 for week 01 - if you wanted to analyze weekly sales.
Also, you would do better to map your DATE format to 2009-01-08 notation because then you can sort in date order using a plain character sort.
To dry up a bit:
ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
date_aves = (from_date..to_date).map{|dt| [dt, ave_prices[dt.strftime "%Y-%m-%d"] || 0]}
Does MySQL have set-returning functions? I.e. functions that return different values on each row of a query? As an example from PostgreSQL, you can do:
select 'foo', generate_series(3, 5);
This will produce a result set consisting of 2 columns and 3 rows, where the left column contains 'foo' on each row and the right column contains 3, 4 and 5.
So, assuming you have an equivalent of generate_series() in MySQL, and subqueries: What you need is a LEFT OUTER JOIN from this function to the query that you already have. That will ensure you see each date appear in the output:
SELECT
avg(sales_price) as avg,
DATE_FORMAT(the_date, '%m-%d-%Y') as date
FROM (select cast('2008-JAN-01' as date) + generate_series(0, 364) as the_date) date_range
LEFT OUTER JOIN products_sold on (the_date = created_at)
WHERE merchant_id = 1
GROUP BY date;
You may need to fiddle with this a bit to get the syntax right for MySQL.