activerecord-precount 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +54 -10
- data/benchmark.rb +6 -3
- data/lib/active_record/associations/preloader/count_loader.rb +4 -8
- data/lib/active_record/precount/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03bcfbd6e4eeb0e73abd3ba62ce3479ce88be195
|
4
|
+
data.tar.gz: ed4a627ba81085af77fc6c0fc0c5036d6aed498b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ec150f0c07963351a74c8657673bb14a3afe9e4476c19658ea74a1e489b89e21f5abc876aa097c703460db396069cacf54bfd7a70ab0e536c9a20c953d5a46f
|
7
|
+
data.tar.gz: 0c50e140ed0cfb414f64c380e3fd95c92083a207e2b2dbf144536d285d24bc1c23ded87944eeb74ecce362aa493c818b5fc0097621e9ea32902b7eb73f8433bf
|
data/README.md
CHANGED
@@ -32,7 +32,19 @@ Tweet.all.precount(:favorites).each do |tweet|
|
|
32
32
|
p tweet.favorites.count
|
33
33
|
end
|
34
34
|
# SELECT `tweets`.* FROM `tweets`
|
35
|
-
# SELECT `tweets`.`
|
35
|
+
# SELECT COUNT(`tweets`.`tweet_id`), tweet_id FROM `tweets` WHERE `tweets`.`tweet_id` IN (1, 2, 3, 4, 5) GROUP BY tweet_id
|
36
|
+
```
|
37
|
+
|
38
|
+
## Benchmark
|
39
|
+
|
40
|
+
With [this benchmark](https://github.com/k0kubun/activerecord-precount/blob/079c8fdaaca4e7f08f542f825e296183a3f19c67/benchmark.rb)
|
41
|
+
([result](https://travis-ci.org/k0kubun/activerecord-precount/jobs/48996451)),
|
42
|
+
precounted query is **7.7x faster** than N+1 count query.
|
43
|
+
|
44
|
+
```rb
|
45
|
+
# Tweet count is 50, and each tweet has 10 favorites
|
46
|
+
Tweet.precount(:favorites).first(50).map(&:favorites_count) # 0.190
|
47
|
+
Tweet.first(50).map{ |t| t.favorites.count } # 1.472
|
36
48
|
```
|
37
49
|
|
38
50
|
## Installation
|
@@ -54,16 +66,48 @@ gem 'activerecord-precount'
|
|
54
66
|
- mysql
|
55
67
|
- postgresql
|
56
68
|
|
57
|
-
##
|
69
|
+
## Advanced Usage
|
70
|
+
|
71
|
+
### Nested eager loading
|
72
|
+
`Foo.precount(:bars)` automatically defines `bars_count` association for `Foo`.
|
73
|
+
Thus you can preload the association and call `foo.bars_count`.
|
74
|
+
|
75
|
+
You can manually define `bars_count` with follwoing code.
|
76
|
+
|
77
|
+
```diff
|
78
|
+
class Foo < ActiveRecord::Base
|
79
|
+
- has_many :bars
|
80
|
+
+ has_many :bars, count_loader: true
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
Then there are two different ways to preload the `bars_count`.
|
85
|
+
|
86
|
+
```rb
|
87
|
+
# the same
|
88
|
+
Foo.preload(:bars_count)
|
89
|
+
Foo.precount(:bars)
|
90
|
+
```
|
91
|
+
|
92
|
+
With this condition, you can eagerly load nested association by preload.
|
93
|
+
|
94
|
+
```rb
|
95
|
+
Hoge.preload(foo: :bars_count)
|
96
|
+
```
|
97
|
+
|
98
|
+
### Performance issue
|
99
|
+
|
100
|
+
With activerecord-precount gem installed, `bars.count` fallbacks to `bars_count` if `bars_count` is defined.
|
101
|
+
Though precounted `bars.count` is faster than not-precounted one, the fallback is currently much slower than just calling `bars_count`.
|
102
|
+
|
103
|
+
```rb
|
104
|
+
# slow
|
105
|
+
Foo.precount(:bars).map { |f| f.bars.count }
|
58
106
|
|
59
|
-
|
60
|
-
|
107
|
+
# fast (recommended)
|
108
|
+
Foo.precount(:bars).map { |f| f.bars_count }
|
61
109
|
```
|
62
110
|
|
63
|
-
##
|
111
|
+
## License
|
64
112
|
|
65
|
-
|
66
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
67
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
68
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
69
|
-
5. Create a new Pull Request
|
113
|
+
MIT License
|
data/benchmark.rb
CHANGED
@@ -8,8 +8,9 @@ require 'models/tweet'
|
|
8
8
|
RBench.run(50) do
|
9
9
|
column :counter_cache, title: 'counter_cache'
|
10
10
|
column :left_join, title: 'LEFT JOIN'
|
11
|
-
column :count_loader, title: 'precount
|
12
|
-
column :
|
11
|
+
column :count_loader, title: 'precount'
|
12
|
+
column :precount, title: 'slow precount'
|
13
|
+
column :has_many, title: 'preload'
|
13
14
|
column :count_query, title: 'N+1 COUNT'
|
14
15
|
|
15
16
|
join_relation = Tweet.joins('LEFT JOIN favorites ON tweets.id = favorites.tweet_id').
|
@@ -26,6 +27,7 @@ RBench.run(50) do
|
|
26
27
|
[10, 5],
|
27
28
|
[20, 20],
|
28
29
|
[30, 100],
|
30
|
+
[50, 10],
|
29
31
|
]
|
30
32
|
|
31
33
|
test_cases.each do |tweets_count, favorites_count|
|
@@ -34,7 +36,8 @@ RBench.run(50) do
|
|
34
36
|
report "N = #{tweets_count}, count = #{favorites_count}" do
|
35
37
|
counter_cache { Tweet.first(tweets_count).map(&:favorites_count_cache) }
|
36
38
|
left_join { join_relation.first(tweets_count).map(&:joined_count) }
|
37
|
-
count_loader { Tweet.
|
39
|
+
count_loader { Tweet.precount(:favorites).first(tweets_count).map(&:favorites_count) }
|
40
|
+
precount { Tweet.precount(:favorites).first(tweets_count).map { |t| t.favorites.count } }
|
38
41
|
has_many { Tweet.preload(:favorites).first(tweets_count).map{ |t| t.favorites.size } }
|
39
42
|
count_query { Tweet.first(tweets_count).map{ |t| t.favorites.count } }
|
40
43
|
end
|
@@ -14,10 +14,7 @@ module ActiveRecord
|
|
14
14
|
|
15
15
|
def preload(preloader)
|
16
16
|
associated_records_by_owner(preloader).each do |owner, associated_records|
|
17
|
-
|
18
|
-
|
19
|
-
association = owner.association(reflection.name)
|
20
|
-
association.target = count
|
17
|
+
owner.association(reflection.name).target = associated_records.first.to_i
|
21
18
|
end
|
22
19
|
end
|
23
20
|
|
@@ -26,14 +23,13 @@ module ActiveRecord
|
|
26
23
|
records_for(slice)
|
27
24
|
}
|
28
25
|
|
29
|
-
@preloaded_records.map { |
|
30
|
-
key
|
31
|
-
[record, key]
|
26
|
+
@preloaded_records.first.map { |key, count|
|
27
|
+
[count, key]
|
32
28
|
}
|
33
29
|
end
|
34
30
|
|
35
31
|
def query_scope(ids)
|
36
|
-
scope.where(association_key.in(ids)).
|
32
|
+
scope.where(association_key.in(ids)).group(association_key_name).count(association_key_name)
|
37
33
|
end
|
38
34
|
end
|
39
35
|
end
|