quick_count 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +20 -55
- data/lib/quick_count/active_record.rb +5 -5
- data/lib/quick_count/adapters/base.rb +38 -0
- data/lib/quick_count/adapters/factory.rb +32 -0
- data/lib/quick_count/adapters/mysql.rb +87 -0
- data/lib/quick_count/adapters/postgresql.rb +85 -0
- data/lib/quick_count/railtie.rb +0 -7
- data/lib/quick_count/version.rb +1 -1
- data/lib/quick_count.rb +15 -56
- metadata +18 -33
- data/lib/count_estimate/active_record.rb +0 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 669ba9aac319e6ac283367c7c11e5f2ab072df5ca4b3ed7feab47dd08b5924b2
|
|
4
|
+
data.tar.gz: 4c0dd5bf2e1396deeed732e4c7161ff1e4545feaee472062f3a52077926dfb0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea9041fa37e956fc824227301d59bb77472cf840c7d5897937b064a3124205c254a38b623c133f14e62fe4e7dd891fb988e66ce4eaf0067b970c77c1a6b05735
|
|
7
|
+
data.tar.gz: '0099de6a90ca27ec4e9d157fe32e7ab8830298fedc996617dd29d410c9e5c1504ab3981bf7f06e2e20348cb18030a0718e0de661468bbd7a398eebd9b4d4caff'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# QuickCount
|
|
2
2
|
|
|
3
|
+
## 0.2.0 _(March 9th 2026)_
|
|
4
|
+
- **Breaking:** Dropped support for Rails 4.x
|
|
5
|
+
- Expanded support for Rails 5.0 through 8.0
|
|
6
|
+
- Zero-config: removed SQL function install/uninstall infrastructure
|
|
7
|
+
- Refactored to adapter pattern architecture
|
|
8
|
+
- Fixed SQL injection vulnerability in table name interpolation
|
|
9
|
+
- Enhanced estimation using `pg_stat_user_tables` with `pg_class` fallback
|
|
10
|
+
- Added support for partitioned tables
|
|
11
|
+
|
|
3
12
|
## 0.1.2 _(January 7th 2020)_
|
|
4
13
|
- Allows for multiple database support
|
|
5
14
|
|
data/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
[](https://rubygems.org/gems/quick_count)
|
|
2
|
-
[](https://codeclimate.com/github/TwilightCoders/quick_count/test_coverage)
|
|
2
|
+
[](https://github.com/TwilightCoders/quick_count/actions)
|
|
3
|
+
[](https://github.com/TwilightCoders/quick_count)
|
|
5
4
|
|
|
6
5
|
# QuickCount
|
|
7
6
|
|
|
@@ -12,18 +11,16 @@ Luckily, there are [some tricks](https://www.citusdata.com/blog/2016/10/12/count
|
|
|
12
11
|
**Supports:**
|
|
13
12
|
- PostgreSQL
|
|
14
13
|
- [Multi-table Inheritance](https://github.com/TwilightCoders/active_record-mti)
|
|
14
|
+
- MySQL
|
|
15
15
|
|
|
16
|
-
| SQL |
|
|
17
|
-
| --- | --- | --- | --- |
|
|
18
|
-
| `SELECT count(*) FROM small_table;` |
|
|
19
|
-
| `
|
|
20
|
-
| `SELECT
|
|
21
|
-
| `
|
|
22
|
-
| `SELECT
|
|
23
|
-
| `
|
|
24
|
-
| `SELECT count(*) FROM large_table;` | -- | `455270802` | `100.0000000%` | `310.6s` |
|
|
25
|
-
| `SELECT quick_count('large_table');` | `v0.0.5` | `448170751` | `98.44047741%` | `0.047s` |
|
|
26
|
-
| `SELECT quick_count('large_table');` | `v0.0.6` | `454448393` | `99.81935828%` | `0.046s` |
|
|
16
|
+
| SQL | Result | Accuracy | Time |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| `SELECT count(*) FROM small_table;` | `2037104` | `100.0000000%` | `4.900s` |
|
|
19
|
+
| `Post.quick_count` | `2036407` | `99.96578476%` | `0.050s` |
|
|
20
|
+
| `SELECT count(*) FROM medium_table;` | `81716243` | `100.0000000%` | `257.5s` |
|
|
21
|
+
| `Post.quick_count` | `81600513` | `99.85837577%` | `0.048s` |
|
|
22
|
+
| `SELECT count(*) FROM large_table;` | `455270802` | `100.0000000%` | `310.6s` |
|
|
23
|
+
| `Post.quick_count` | `454448393` | `99.81935828%` | `0.046s` |
|
|
27
24
|
|
|
28
25
|
_These metrics were pulled from real databases being used in a production environment._
|
|
29
26
|
|
|
@@ -39,53 +36,26 @@ And then execute:
|
|
|
39
36
|
|
|
40
37
|
$ bundle
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
$ gem install quick_count
|
|
45
|
-
|
|
46
|
-
To finish the install, in rails console:
|
|
47
|
-
|
|
48
|
-
$ QuickCount.install # Install with default (500000) threshold
|
|
39
|
+
That's it. QuickCount automatically integrates with ActiveRecord via a Railtie — no setup step required.
|
|
49
40
|
|
|
50
41
|
## Usage
|
|
51
42
|
|
|
52
43
|
```ruby
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
QuickCount.install # Install with default (500000) threshold
|
|
56
|
-
|
|
57
|
-
# Change the threshold for when `quick_count` kicks in...
|
|
58
|
-
QuickCount.install(threshold: 500000)
|
|
59
|
-
|
|
60
|
-
class User < ActiveRecord::Base
|
|
61
|
-
|
|
62
|
-
end
|
|
63
|
-
|
|
44
|
+
# Fast estimated count for large tables
|
|
64
45
|
User.quick_count
|
|
65
46
|
|
|
66
|
-
# Override the default threshold on a case-by-case basis
|
|
67
|
-
User.quick_count(threshold:
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Uninstallation
|
|
72
|
-
|
|
73
|
-
Remove this line to your application's Gemfile:
|
|
47
|
+
# Override the default threshold (500,000) on a case-by-case basis
|
|
48
|
+
User.quick_count(threshold: 1_000_000)
|
|
74
49
|
|
|
75
|
-
|
|
76
|
-
|
|
50
|
+
# Estimate row count for an arbitrary query
|
|
51
|
+
User.where(active: true).count_estimate
|
|
77
52
|
```
|
|
78
53
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
$ bundle
|
|
82
|
-
|
|
83
|
-
And in a rails console:
|
|
84
|
-
|
|
85
|
-
$ QuickCount.uninstall
|
|
54
|
+
If the estimated count is below the threshold, `quick_count` falls back to an exact `SELECT COUNT(*)`. For tables above the threshold, it returns the estimate directly.
|
|
86
55
|
|
|
87
56
|
## License
|
|
88
|
-
|
|
57
|
+
|
|
58
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
89
59
|
|
|
90
60
|
## Development
|
|
91
61
|
|
|
@@ -96,8 +66,3 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
96
66
|
## Contributing
|
|
97
67
|
|
|
98
68
|
Bug reports and pull requests are welcome on GitHub at https://github.com/TwilightCoders/quick_count.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
## License
|
|
102
|
-
|
|
103
|
-
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
@@ -5,14 +5,14 @@ module QuickCount
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
module ClassMethods
|
|
8
|
-
|
|
9
8
|
def quick_count(threshold: nil)
|
|
10
|
-
threshold
|
|
11
|
-
result = connection.execute("SELECT quick_count('#{table_name}'#{threshold})")
|
|
12
|
-
result[0]["quick_count"].to_i
|
|
9
|
+
QuickCount.quick_count(table_name, threshold: threshold, connection: connection)
|
|
13
10
|
end
|
|
14
|
-
|
|
15
11
|
end
|
|
16
12
|
|
|
13
|
+
# Instance method for ActiveRecord::Relation
|
|
14
|
+
def count_estimate
|
|
15
|
+
QuickCount.count_estimate(to_sql, connection: connection)
|
|
16
|
+
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module QuickCount
|
|
2
|
+
module Adapters
|
|
3
|
+
class Base
|
|
4
|
+
attr_reader :connection
|
|
5
|
+
|
|
6
|
+
def initialize(connection:)
|
|
7
|
+
@connection = connection
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Abstract methods that must be implemented by adapters
|
|
11
|
+
def quick_count(table_name, threshold: nil)
|
|
12
|
+
raise NotImplementedError, "#{self.class} must implement #quick_count"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def count_estimate(query)
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #count_estimate"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def supported?
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #supported?"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def execute_sql(sql)
|
|
26
|
+
connection.execute(sql)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def quote_identifier(identifier)
|
|
30
|
+
connection.quote_column_name(identifier)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def quote_value(value)
|
|
34
|
+
connection.quote(value)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative 'postgresql'
|
|
2
|
+
require_relative 'mysql'
|
|
3
|
+
|
|
4
|
+
module QuickCount
|
|
5
|
+
module Adapters
|
|
6
|
+
class Factory
|
|
7
|
+
ADAPTERS = [Postgresql, Mysql].freeze
|
|
8
|
+
|
|
9
|
+
def self.create(connection:)
|
|
10
|
+
adapter_class = detect_adapter(connection)
|
|
11
|
+
|
|
12
|
+
unless adapter_class
|
|
13
|
+
raise UnsupportedDatabaseError, "Unsupported database: #{connection.adapter_name}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
adapter_class.new(connection: connection)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.detect_adapter(connection)
|
|
20
|
+
ADAPTERS.find { |adapter_class| adapter_class.new(connection: connection).supported? }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.supported_databases
|
|
24
|
+
ADAPTERS.map do |adapter_class|
|
|
25
|
+
adapter_class.name.split('::').last.downcase
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class UnsupportedDatabaseError < StandardError; end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module QuickCount
|
|
4
|
+
module Adapters
|
|
5
|
+
class Mysql < Base
|
|
6
|
+
def supported?
|
|
7
|
+
connection.adapter_name.downcase.match?(/mysql/)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def quick_count(table_name, threshold: nil)
|
|
11
|
+
threshold ||= 500_000
|
|
12
|
+
|
|
13
|
+
# Get estimated count from INFORMATION_SCHEMA
|
|
14
|
+
estimated_count = get_table_rows_estimate(table_name)
|
|
15
|
+
|
|
16
|
+
if estimated_count < threshold
|
|
17
|
+
# Use exact count for small tables
|
|
18
|
+
result = execute_sql("SELECT COUNT(*) as count FROM #{quote_identifier(table_name)}")
|
|
19
|
+
result[0]['count'].to_i
|
|
20
|
+
else
|
|
21
|
+
estimated_count
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def count_estimate(query)
|
|
26
|
+
# Use EXPLAIN to get row estimate
|
|
27
|
+
result = execute_sql("EXPLAIN #{query}")
|
|
28
|
+
|
|
29
|
+
# Parse the rows from EXPLAIN output
|
|
30
|
+
# MySQL EXPLAIN returns different formats, try to extract rows estimate
|
|
31
|
+
if result.respond_to?(:each)
|
|
32
|
+
result.each do |row|
|
|
33
|
+
if row['rows']
|
|
34
|
+
return row['rows'].to_i
|
|
35
|
+
elsif row['Extra'] && row['Extra'].match(/rows=(\d+)/)
|
|
36
|
+
return $1.to_i
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fallback: try to extract from query if it's a simple SELECT
|
|
42
|
+
if query.match(/FROM\s+(\w+)/i)
|
|
43
|
+
table_name = $1
|
|
44
|
+
return get_table_rows_estimate(table_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Last resort - return 0
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def current_database
|
|
54
|
+
connection.current_database
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def get_table_rows_estimate(table_name)
|
|
58
|
+
database_name = current_database
|
|
59
|
+
|
|
60
|
+
# Try INNODB_TABLESTATS first (more accurate for InnoDB)
|
|
61
|
+
result = execute_sql(<<~SQL)
|
|
62
|
+
SELECT NUM_ROWS
|
|
63
|
+
FROM INFORMATION_SCHEMA.INNODB_TABLESTATS
|
|
64
|
+
WHERE NAME = '#{database_name}/#{table_name}'
|
|
65
|
+
SQL
|
|
66
|
+
|
|
67
|
+
if result && result.first && result.first['NUM_ROWS']
|
|
68
|
+
return result.first['NUM_ROWS'].to_i
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fallback to INFORMATION_SCHEMA.TABLES
|
|
72
|
+
result = execute_sql(<<~SQL)
|
|
73
|
+
SELECT TABLE_ROWS
|
|
74
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
75
|
+
WHERE TABLE_SCHEMA = '#{database_name}'
|
|
76
|
+
AND TABLE_NAME = '#{table_name}'
|
|
77
|
+
SQL
|
|
78
|
+
|
|
79
|
+
if result && result.first && result.first['TABLE_ROWS']
|
|
80
|
+
result.first['TABLE_ROWS'].to_i
|
|
81
|
+
else
|
|
82
|
+
0
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module QuickCount
|
|
4
|
+
module Adapters
|
|
5
|
+
class Postgresql < Base
|
|
6
|
+
def supported?
|
|
7
|
+
connection.adapter_name.downcase.match?(/postg/)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def quick_count(table_name, threshold: nil)
|
|
11
|
+
threshold ||= 500_000
|
|
12
|
+
|
|
13
|
+
# Use direct SQL estimation without needing functions
|
|
14
|
+
estimated_count = get_table_estimate(table_name)
|
|
15
|
+
|
|
16
|
+
if estimated_count < threshold
|
|
17
|
+
# Use exact count for small tables
|
|
18
|
+
result = execute_sql("SELECT COUNT(*) as count FROM #{quote_identifier(table_name)}")
|
|
19
|
+
result[0]['count'].to_i
|
|
20
|
+
else
|
|
21
|
+
estimated_count
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def count_estimate(query)
|
|
26
|
+
# Use EXPLAIN to get row estimate directly
|
|
27
|
+
result = execute_sql("EXPLAIN #{query}")
|
|
28
|
+
|
|
29
|
+
# Parse the rows from EXPLAIN output
|
|
30
|
+
if result.respond_to?(:each)
|
|
31
|
+
result.each do |row|
|
|
32
|
+
if row['QUERY PLAN'] && row['QUERY PLAN'].match(/rows=(\d+)/)
|
|
33
|
+
return $1.to_i
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Fallback: return 0 if we can't parse
|
|
39
|
+
0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def get_table_estimate(table_name)
|
|
45
|
+
quoted_table = quote_value(table_name.to_s)
|
|
46
|
+
|
|
47
|
+
# Enhanced estimation using both reltuples and live stats
|
|
48
|
+
result = execute_sql(<<~SQL)
|
|
49
|
+
SELECT COALESCE(
|
|
50
|
+
-- Prefer live statistics when available
|
|
51
|
+
pg_stat.n_live_tup,
|
|
52
|
+
-- Fall back to enhanced planner-style estimation
|
|
53
|
+
CASE
|
|
54
|
+
WHEN pg_class.reltuples < 0 THEN NULL -- never vacuumed
|
|
55
|
+
WHEN pg_class.relpages = 0 THEN 0 -- empty table
|
|
56
|
+
ELSE (pg_class.reltuples / GREATEST(pg_class.relpages, 1)) *
|
|
57
|
+
(pg_relation_size(pg_class.oid) / current_setting('block_size')::int)
|
|
58
|
+
END
|
|
59
|
+
)::bigint AS estimated_count
|
|
60
|
+
FROM pg_class
|
|
61
|
+
LEFT JOIN pg_stat_user_tables pg_stat ON pg_stat.relid = pg_class.oid
|
|
62
|
+
WHERE pg_class.oid = #{quoted_table}::regclass
|
|
63
|
+
|
|
64
|
+
-- Handle partitioned tables
|
|
65
|
+
UNION ALL
|
|
66
|
+
SELECT sum(
|
|
67
|
+
COALESCE(
|
|
68
|
+
pg_stat.n_live_tup,
|
|
69
|
+
(pg_class.reltuples/GREATEST(pg_class.relpages,1)) *
|
|
70
|
+
(pg_relation_size(pg_class.oid)/current_setting('block_size')::int)
|
|
71
|
+
)
|
|
72
|
+
)::bigint as estimated_count
|
|
73
|
+
FROM pg_inherits
|
|
74
|
+
JOIN pg_class ON pg_inherits.inhrelid = pg_class.oid
|
|
75
|
+
LEFT JOIN pg_stat_user_tables pg_stat ON pg_stat.relid = pg_class.oid
|
|
76
|
+
WHERE pg_inherits.inhparent = #{quoted_table}::regclass
|
|
77
|
+
SQL
|
|
78
|
+
|
|
79
|
+
# Get the maximum estimate (handles both regular and partitioned tables)
|
|
80
|
+
estimates = result.map { |row| row['estimated_count']&.to_i || 0 }
|
|
81
|
+
estimates.max || 0
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/quick_count/railtie.rb
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
require 'rails/railtie'
|
|
2
2
|
require 'quick_count/active_record'
|
|
3
|
-
require 'count_estimate/active_record'
|
|
4
3
|
|
|
5
4
|
module QuickCount
|
|
6
5
|
class Railtie < Rails::Railtie
|
|
7
|
-
|
|
8
|
-
# rake_tasks do
|
|
9
|
-
# load "../tasks/quick_count_tasks.rake"
|
|
10
|
-
# end
|
|
11
|
-
|
|
12
6
|
initializer 'quick_count.load' do |app|
|
|
13
7
|
ActiveSupport.on_load(:active_record) do
|
|
14
8
|
QuickCount.load
|
|
15
9
|
end
|
|
16
10
|
end
|
|
17
|
-
|
|
18
11
|
end
|
|
19
12
|
end
|
data/lib/quick_count/version.rb
CHANGED
data/lib/quick_count.rb
CHANGED
|
@@ -1,76 +1,35 @@
|
|
|
1
1
|
require 'quick_count/version'
|
|
2
2
|
require 'quick_count/railtie'
|
|
3
|
+
require 'quick_count/adapters/factory'
|
|
3
4
|
require 'active_record'
|
|
4
5
|
|
|
5
6
|
module QuickCount
|
|
6
|
-
|
|
7
7
|
def self.root
|
|
8
8
|
@root ||= Pathname.new(File.dirname(File.expand_path(File.dirname(__FILE__), '/../')))
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def self.load
|
|
12
|
-
::ActiveRecord::Base.
|
|
13
|
-
::ActiveRecord::Relation.
|
|
12
|
+
::ActiveRecord::Base.include QuickCount::ActiveRecord
|
|
13
|
+
::ActiveRecord::Relation.include QuickCount::ActiveRecord
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def self.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
def self.quick_count(table_name, threshold: nil, connection: ::ActiveRecord::Base.connection)
|
|
17
|
+
adapter = create_adapter(connection: connection)
|
|
18
|
+
adapter.quick_count(table_name, threshold: threshold)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def self.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
connection.execute("DROP FUNCTION IF EXISTS #{schema}.count_estimate(text);")
|
|
21
|
+
def self.count_estimate(query, connection: ::ActiveRecord::Base.connection)
|
|
22
|
+
adapter = create_adapter(connection: connection)
|
|
23
|
+
adapter.count_estimate(query)
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def self.quick_count_sql(threshold: 500000, schema: 'public')
|
|
30
|
-
<<~SQL
|
|
31
|
-
CREATE OR REPLACE FUNCTION #{schema}.quick_count(table_name text, threshold bigint default #{threshold}) RETURNS bigint AS
|
|
32
|
-
$func$
|
|
33
|
-
DECLARE count bigint;
|
|
34
|
-
BEGIN
|
|
35
|
-
EXECUTE 'SELECT
|
|
36
|
-
CASE
|
|
37
|
-
WHEN SUM(estimate)::integer < '|| threshold ||' THEN
|
|
38
|
-
(SELECT COUNT(*) FROM "'|| table_name ||'")
|
|
39
|
-
ELSE
|
|
40
|
-
SUM(estimate)::integer
|
|
41
|
-
END AS count
|
|
42
|
-
FROM (
|
|
43
|
-
SELECT
|
|
44
|
-
((SUM(child.reltuples::float)/greatest(SUM(child.relpages::float),1))) * (SUM(pg_relation_size(child.oid))::float / (current_setting(''block_size'')::float)) AS estimate
|
|
45
|
-
FROM pg_inherits
|
|
46
|
-
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
|
|
47
|
-
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
|
|
48
|
-
WHERE parent.relname = '''|| table_name ||'''
|
|
49
|
-
UNION SELECT (reltuples::float/greatest(relpages::float, 1)) * (pg_relation_size(pg_class.oid)::float / (current_setting(''block_size'')::float)) AS estimate FROM pg_class where relname='''|| table_name ||'''
|
|
50
|
-
) AS tables' INTO count;
|
|
51
|
-
RETURN count;
|
|
52
|
-
END
|
|
53
|
-
$func$ LANGUAGE plpgsql;
|
|
54
|
-
SQL
|
|
26
|
+
def self.supported_databases
|
|
27
|
+
Adapters::Factory.supported_databases
|
|
55
28
|
end
|
|
56
29
|
|
|
57
|
-
|
|
58
|
-
<<~SQL
|
|
59
|
-
CREATE OR REPLACE FUNCTION #{schema}.count_estimate(query text) RETURNS integer AS
|
|
60
|
-
$func$
|
|
61
|
-
DECLARE
|
|
62
|
-
rec record;
|
|
63
|
-
rows integer;
|
|
64
|
-
BEGIN
|
|
65
|
-
FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP
|
|
66
|
-
rows := substring(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');
|
|
67
|
-
EXIT WHEN rows IS NOT NULL;
|
|
68
|
-
END LOOP;
|
|
69
|
-
RETURN rows;
|
|
70
|
-
END
|
|
71
|
-
$func$ LANGUAGE plpgsql;
|
|
72
|
-
SQL
|
|
73
|
-
end
|
|
30
|
+
private
|
|
74
31
|
|
|
32
|
+
def self.create_adapter(connection:)
|
|
33
|
+
Adapters::Factory.create(connection: connection)
|
|
34
|
+
end
|
|
75
35
|
end
|
|
76
|
-
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quick_count
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dale Stevens
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: pg
|
|
@@ -30,54 +29,40 @@ dependencies:
|
|
|
30
29
|
requirements:
|
|
31
30
|
- - ">="
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
32
|
+
version: '5.0'
|
|
34
33
|
- - "<"
|
|
35
34
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: '
|
|
35
|
+
version: '9'
|
|
37
36
|
type: :runtime
|
|
38
37
|
prerelease: false
|
|
39
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
40
39
|
requirements:
|
|
41
40
|
- - ">="
|
|
42
41
|
- !ruby/object:Gem::Version
|
|
43
|
-
version: '
|
|
42
|
+
version: '5.0'
|
|
44
43
|
- - "<"
|
|
45
44
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
45
|
+
version: '9'
|
|
47
46
|
- !ruby/object:Gem::Dependency
|
|
48
47
|
name: railties
|
|
49
48
|
requirement: !ruby/object:Gem::Requirement
|
|
50
49
|
requirements:
|
|
51
50
|
- - ">="
|
|
52
51
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
52
|
+
version: '5.0'
|
|
54
53
|
- - "<"
|
|
55
54
|
- !ruby/object:Gem::Version
|
|
56
|
-
version: '
|
|
55
|
+
version: '9'
|
|
57
56
|
type: :runtime
|
|
58
57
|
prerelease: false
|
|
59
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
60
59
|
requirements:
|
|
61
60
|
- - ">="
|
|
62
61
|
- !ruby/object:Gem::Version
|
|
63
|
-
version: '
|
|
62
|
+
version: '5.0'
|
|
64
63
|
- - "<"
|
|
65
64
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
67
|
-
- !ruby/object:Gem::Dependency
|
|
68
|
-
name: pry-byebug
|
|
69
|
-
requirement: !ruby/object:Gem::Requirement
|
|
70
|
-
requirements:
|
|
71
|
-
- - ">="
|
|
72
|
-
- !ruby/object:Gem::Version
|
|
73
|
-
version: '0'
|
|
74
|
-
type: :development
|
|
75
|
-
prerelease: false
|
|
76
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
-
requirements:
|
|
78
|
-
- - ">="
|
|
79
|
-
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0'
|
|
65
|
+
version: '9'
|
|
81
66
|
- !ruby/object:Gem::Dependency
|
|
82
67
|
name: bundler
|
|
83
68
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -120,8 +105,8 @@ dependencies:
|
|
|
120
105
|
- - ">="
|
|
121
106
|
- !ruby/object:Gem::Version
|
|
122
107
|
version: '0'
|
|
123
|
-
description:
|
|
124
|
-
|
|
108
|
+
description: Fast approximate row counts for large PostgreSQL tables using database
|
|
109
|
+
statistics instead of COUNT(*)
|
|
125
110
|
email:
|
|
126
111
|
- dale@twilightcoders.net
|
|
127
112
|
executables: []
|
|
@@ -131,9 +116,12 @@ files:
|
|
|
131
116
|
- CHANGELOG.md
|
|
132
117
|
- LICENSE
|
|
133
118
|
- README.md
|
|
134
|
-
- lib/count_estimate/active_record.rb
|
|
135
119
|
- lib/quick_count.rb
|
|
136
120
|
- lib/quick_count/active_record.rb
|
|
121
|
+
- lib/quick_count/adapters/base.rb
|
|
122
|
+
- lib/quick_count/adapters/factory.rb
|
|
123
|
+
- lib/quick_count/adapters/mysql.rb
|
|
124
|
+
- lib/quick_count/adapters/postgresql.rb
|
|
137
125
|
- lib/quick_count/railtie.rb
|
|
138
126
|
- lib/quick_count/version.rb
|
|
139
127
|
homepage: https://github.com/TwilightCoders/quick_count
|
|
@@ -141,24 +129,21 @@ licenses:
|
|
|
141
129
|
- MIT
|
|
142
130
|
metadata:
|
|
143
131
|
allowed_push_host: https://rubygems.org
|
|
144
|
-
post_install_message:
|
|
145
132
|
rdoc_options: []
|
|
146
133
|
require_paths:
|
|
147
134
|
- lib
|
|
148
|
-
- spec
|
|
149
135
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
150
136
|
requirements:
|
|
151
137
|
- - ">="
|
|
152
138
|
- !ruby/object:Gem::Version
|
|
153
|
-
version: '2.
|
|
139
|
+
version: '2.5'
|
|
154
140
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
155
141
|
requirements:
|
|
156
142
|
- - ">="
|
|
157
143
|
- !ruby/object:Gem::Version
|
|
158
144
|
version: '0'
|
|
159
145
|
requirements: []
|
|
160
|
-
rubygems_version:
|
|
161
|
-
signing_key:
|
|
146
|
+
rubygems_version: 4.0.5
|
|
162
147
|
specification_version: 4
|
|
163
148
|
summary: Quickly get an accurate count estimation for large tables.
|
|
164
149
|
test_files: []
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
require 'active_support/concern'
|
|
2
|
-
require 'pry'
|
|
3
|
-
|
|
4
|
-
module CountEstimate
|
|
5
|
-
module ActiveRecord
|
|
6
|
-
|
|
7
|
-
def count_estimate
|
|
8
|
-
my_statement = connection.quote(to_sql)
|
|
9
|
-
result = connection.execute("SELECT count_estimate(#{my_statement})")
|
|
10
|
-
result[0]["count_estimate"].to_i
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
end
|
|
14
|
-
end
|