RailsでModel作るとデフォルトでcreated_at, updated_atを作成してくれて、便利ですね。
実際にサービスを開発・運営していくと、自らの要求だったり、企画者や偉い人からの要請だったりして、例えば「昨日会員登録した人が何人だったのか」のようなデータを出す必要が出てきます。
それをどのように取るかという話。
昨日一日で会員登録した人を取るとして、
失敗例その1
Railsってcreated_atを作ってくれてるからこれで行けるんじゃない?
User.where(created_at: Date.yesterday)
結果
なんも返ってきません。
created_atは時刻を含んでいますが、'2015-03-11'で検索をかけても時刻が入っていないので何とも一致しないわけですね。
実行されているSQLはこんなのです。
SELECT `users`.* FROM `users` WHERE `users`.`created_at` = '2015-03-12'
失敗例その2
では時刻を捨てよう。
User.where("DATE(created_at) = '#{Date.yesterday}'")
結果
返ってきます。勝ったッ!第3部完!
と思うのですが、DATE(created_at)
してしまうとインデックスが効かなくなります。そもそもインデックスを貼っていないというケースもあると思いますが、インデックスを貼っていなかったり効いてなかったりすると、DBの性能にもよりますがユーザーが10万ぐらいになってきたあたりからしんどくなると思います。
100万、1000万Userを目指すようなビジョンがあるのであればやっちゃだめですね。
実行されているSQLはこんなのです。
SELECT `users`.* FROM `users` WHERE (DATE(created_at) = '2015-03-12')
じゃあどうすんのよ
SQLのクエリ的な解決方法で言うと、
where users.created_at between '2015-03-12 0:00:00' and '2015-03-12 23:59:59'
になります。
これをRailsでどう書くか。
User.where("created_at between '#{Date.yesterday} 0:00:00' and '#{Date.yesterday} 23:59:59'")
とかやりたくないな〜、って思ってたらActiveSupportで便利なものがありました。
- Date#beginning_of_day
- Date#end_of_day
これを使って、以下のように書けます。
range = Date.yesterday.beginning_of_day..Date.yesterday.end_of_day
User.where(created_at: range)
実行されるSQLはこれ。
SELECT `users`.* FROM `users` WHERE (`users`.`created_at` BETWEEN '2015-03-12 00:00:00' AND '2015-03-12 23:59:59')
スッキリだし、インデックスも有効です。
追記
@jnchito さんにコメントで all_day
を教えていただきました。
User.where(created_at: 1.day.ago.all_day)
SELECT "users".* FROM "users" WHERE ("users"."created_at" BETWEEN '2015-03-12 15:00:00.000000' AND '2015-03-13 14:59:59.999999')
さらにスッキリしてますね。