4

Assuming there are 2 models called User and Post

Which will be better performance(fast) either "Plan A" or "Plan B"?

"Plan A"

controller

@users = User.find_all_by_country(params[:country])
@posts = Post.find_all_by_category(params[:category])

view

<%= @users.count.to_s %>
<%= @posts.count.to_s %>

"Plan B"

controller

@users = User.find_all_by_country(params[:country])
@posts = Post.find_all_by_category(params[:category])

view

<%= @users.length.to_s %>
<%= @posts.length.to_s %>
Foo
  • 800
  • 1
  • 7
  • 17

2 Answers2

16

In ruby, count, length and size all do pretty much the same thing regarding arrays. See here for more info.

When using ActiveRecord objects, however, count is better than length, and size is even better.

find_all_by_country returns a dumb array so you shouldn't use that method (because it always returns an array). Instead, use where(country: params[:country]).

I'll let Code School's Rails Best Practices slide nº 93 speak for itself (and hope they don't get mad at me for reproducing it here).

enter image description here

Just in case the image gets taken down, basically:

  1. length always pulls all the records and then calls .length on the array - bad

  2. count always does a count query - good

  3. size looks at the cache if you have a cache counter, otherwise does a count query - best

Community
  • 1
  • 1
Ashitaka
  • 19,028
  • 6
  • 54
  • 69
  • Thanks!!!. This is such perfect answer. Thanks very much:) In my application. I sometime delete records on phpMyAdmin. That's why it won't delete related records so that my counter cache will get messed up. That's the reason why I don't wanna use counter cache. In this situation, would you use just length instead? – Foo Feb 10 '13 at 05:14
  • @Foo Note that this answer is **wrong**, as you aren't invoking `length` or `count` on an ActiveResource relation, but on a plain old Ruby array. The length/count/size methods the slide is talking about aren't the same ones you're using. – user229044 Feb 10 '13 at 05:23
  • @meagar In my understanding, once I fetch records by this `@users = User.find_all_by_country(params[:country])`, it won't invoke SQL query even if I do this in view. `<%= @users.length.to_s %>` – Foo Feb 10 '13 at 05:34
  • @Ashitaka That's even more wrong, `find_by` will return a single record which won't respond to `length` *or* `count`. – user229044 Feb 10 '13 at 05:47
  • @Foo That is correct. Using the dynamic `find_all_by_` helpers produces a flat array of objects. Invoking length or count on it is different from invoking it on *any other array*, no SQL is involved. The point is that this isn't the best way to find the number of results, if that's all you need. – user229044 Feb 10 '13 at 05:49
  • @Ashitaka Exactly as I pointed out in my answer, where you somewhat pompously told me "That's not how Rails works". – user229044 Feb 10 '13 at 05:50
  • @meagar Thanks. Now I fully understood what you meant – Foo Feb 10 '13 at 05:56
  • Very clear answer. +1 for this ;) Thanks @Ashitaka very much. – Blue Smith Sep 15 '13 at 17:59
1

Both will be the same, count with no arguments and length are identical as you are invoking them on a Ruby array (returned by the magic find_* method), and not an ActiveRecord object.

That said, both methods are the worst way to do this, if you're simply interested in the number of matching records.

Instead of instantiating the entire result set just to find its length, use .count on an actual ActiveRecord relation:

@num_users = User.where(country: params[:country]).count
@num_posts = Post.where(category: params[:category]).count

This will actually execute as select count(*) from instead of a full select * from, which will be much faster depending on the number of results.

user229044
  • 232,980
  • 40
  • 330
  • 338
  • Thanks how would you make it faster if not using counter_cache – Foo Feb 10 '13 at 03:52
  • 1
    @Foo What makes you think you can make it any faster? `.length` and `.count` are so fast that you probably can't measure them. If you want to just find the number of matching records, see my modified answer. – user229044 Feb 10 '13 at 03:54
  • Thanks for reply. I was just imagining that more than 10000 people load the page at the same time. Then there will be many Posts that has comments for each. Obviously it has the number of the comments each post has. I want to show those number with count. Not counter cache for some reason. If there are 50 posts shown in the page. 10000 people x 50 posts count will be running. So I wanted to make it faster as fast as possible. – Foo Feb 10 '13 at 03:59
  • Thanks for detailed explanation. What if you are trying to fetch @users with the same condition? can you mix them together? or you just have to have 2 lines like `@users = User.find_all_by_country(params[:country])` and `@num_users = User.where(country: params[:country]).count` ? – Foo Feb 10 '13 at 04:06
  • @Ashitaka Sorry, but no, you're wrong and you've missed some pretty important details that I took explicit pains to outline in my answer. He isn't using `.length` or `.count` on an ActiveResource object. He's invoking it on a dumb array, where `length` and `count` are identical. – user229044 Feb 10 '13 at 05:22
  • Your previous answer wasn't that clear. You mentioned AREL and didn't differentiate between dumb arrays and activerecord objects (which I clearly missed, my bad). However, it's obvious that Foo should be using a `find_all_by` method when all he wants to do is a count query. – Ashitaka Feb 10 '13 at 05:47
  • 1
    My "previous answer" was posted **40 minutes** before your comment. Nice try. My answer as it reads now is exactly how it read when you left your comment. Both versions of my answer stated explicitly that he shouldn't be using `find_all_by` and should use `where`, which you've now copied into your answer. – user229044 Feb 10 '13 at 05:52
  • @meagar why would you not think ahead? Then you have to waste time fixing old code because you were to lazy to find the best way to code something. That is bad practice. – ricks Aug 17 '18 at 22:01