mini_sql 0.2.4 → 1.1.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/.github/workflows/ci.yml +66 -0
- data/.rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml +355 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG +22 -0
- data/Gemfile +3 -1
- data/Guardfile +2 -0
- data/README.md +125 -1
- data/Rakefile +3 -1
- data/bench/builder_perf.rb +138 -0
- data/bench/decorator_perf.rb +143 -0
- data/bench/mini_sql_methods_perf.rb +80 -0
- data/bench/prepared_perf.rb +59 -0
- data/bench/shared/generate_data.rb +133 -0
- data/bench/timestamp_perf.rb +22 -21
- data/bench/topic_mysql_perf.rb +1 -7
- data/bench/topic_perf.rb +27 -169
- data/bench/topic_wide_perf.rb +92 -0
- data/bin/console +1 -0
- data/lib/mini_sql.rb +20 -8
- data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
- data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
- data/lib/mini_sql/builder.rb +64 -24
- data/lib/mini_sql/connection.rb +15 -3
- data/lib/mini_sql/decoratable.rb +22 -0
- data/lib/mini_sql/deserializer_cache.rb +2 -0
- data/lib/mini_sql/inline_param_encoder.rb +12 -13
- data/lib/mini_sql/mysql/connection.rb +18 -3
- data/lib/mini_sql/mysql/deserializer_cache.rb +14 -16
- data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
- data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
- data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
- data/lib/mini_sql/postgres/coders.rb +2 -0
- data/lib/mini_sql/postgres/connection.rb +89 -0
- data/lib/mini_sql/postgres/deserializer_cache.rb +36 -16
- data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
- data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
- data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
- data/lib/mini_sql/postgres_jdbc/connection.rb +8 -1
- data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +43 -43
- data/lib/mini_sql/result.rb +30 -0
- data/lib/mini_sql/serializer.rb +84 -0
- data/lib/mini_sql/sqlite/connection.rb +20 -2
- data/lib/mini_sql/sqlite/deserializer_cache.rb +14 -16
- data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
- data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
- data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
- data/lib/mini_sql/version.rb +1 -1
- data/mini_sql.gemspec +7 -2
- metadata +75 -11
- data/.travis.yml +0 -26
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'pg', github: 'ged/ruby-pg'
|
8
|
+
gem 'mini_sql', path: '../'
|
9
|
+
gem 'activerecord'
|
10
|
+
gem 'activemodel'
|
11
|
+
gem 'benchmark-ips'
|
12
|
+
gem 'draper'
|
13
|
+
gem 'pry'
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'active_record'
|
17
|
+
require 'benchmark/ips'
|
18
|
+
require 'mini_sql'
|
19
|
+
|
20
|
+
require '../mini_sql/bench/shared/generate_data'
|
21
|
+
|
22
|
+
ar_connection, conn_config = GenerateData.new(count_records: 1_000).call
|
23
|
+
MINI_SQL = MiniSql::Connection.get(ar_connection.raw_connection)
|
24
|
+
|
25
|
+
|
26
|
+
Benchmark.ips do |r|
|
27
|
+
r.report('query_hash') do |n|
|
28
|
+
while n > 0
|
29
|
+
MINI_SQL.query_hash('select id, title from topics order by id limit 1000').each do |hash|
|
30
|
+
[hash['id'], hash['title']]
|
31
|
+
end
|
32
|
+
n -= 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
r.report('query_array') do |n|
|
36
|
+
while n > 0
|
37
|
+
MINI_SQL.query_array('select id, title from topics order by id limit 1000').each do |id, title|
|
38
|
+
[id, title]
|
39
|
+
end
|
40
|
+
n -= 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
r.report('query') do |n|
|
44
|
+
while n > 0
|
45
|
+
MINI_SQL.query('select id, title from topics order by id limit 1000').each do |obj|
|
46
|
+
[obj.id, obj.title]
|
47
|
+
end
|
48
|
+
n -= 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
r.compare!
|
53
|
+
end
|
54
|
+
|
55
|
+
# Comparison:
|
56
|
+
# query_array: 1663.3 i/s
|
57
|
+
# query: 1254.5 i/s - 1.33x (± 0.00) slower
|
58
|
+
# query_hash: 1095.4 i/s - 1.52x (± 0.00) slower
|
59
|
+
|
60
|
+
|
61
|
+
Benchmark.ips do |r|
|
62
|
+
r.report('query_single') do |n|
|
63
|
+
while n > 0
|
64
|
+
MINI_SQL.query_single('select id from topics order by id limit 1000')
|
65
|
+
n -= 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
r.report('query_array') do |n|
|
69
|
+
while n > 0
|
70
|
+
MINI_SQL.query_array('select id from topics order by id limit 1000').flatten
|
71
|
+
n -= 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
r.compare!
|
76
|
+
end
|
77
|
+
|
78
|
+
# Comparison:
|
79
|
+
# query_single: 2445.1 i/s
|
80
|
+
# query_array: 1681.1 i/s - 1.45x (± 0.00) slower
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'mini_sql', path: '../'
|
8
|
+
gem 'pg'
|
9
|
+
gem 'activerecord'
|
10
|
+
gem 'activemodel'
|
11
|
+
gem 'benchmark-ips'
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'active_record'
|
15
|
+
require 'benchmark/ips'
|
16
|
+
require 'mini_sql'
|
17
|
+
|
18
|
+
require '../mini_sql/bench/shared/generate_data'
|
19
|
+
|
20
|
+
ar_connection, _ = GenerateData.new(count_records: 10_000).call
|
21
|
+
MINI_SQL = MiniSql::Connection.get(ar_connection.raw_connection)
|
22
|
+
|
23
|
+
|
24
|
+
sql = <<~SQL
|
25
|
+
select users.first_name, count(distinct topics.id) topics_count
|
26
|
+
from topics
|
27
|
+
inner join users on user_id = users.id
|
28
|
+
inner join categories on category_id = categories.id
|
29
|
+
where users.id = ?
|
30
|
+
group by users.id
|
31
|
+
SQL
|
32
|
+
|
33
|
+
Benchmark.ips do |x|
|
34
|
+
x.report("ps") do |n|
|
35
|
+
while n > 0
|
36
|
+
MINI_SQL.prepared.query(sql, rand(100))
|
37
|
+
n -= 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
x.report("without ps") do |n|
|
41
|
+
while n > 0
|
42
|
+
MINI_SQL.query(sql, rand(100))
|
43
|
+
n -= 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
x.compare!
|
48
|
+
end
|
49
|
+
|
50
|
+
# Warming up --------------------------------------
|
51
|
+
# ps 1.008k i/100ms
|
52
|
+
# without ps 284.000 i/100ms
|
53
|
+
# Calculating -------------------------------------
|
54
|
+
# ps 10.287k (± 4.2%) i/s - 51.408k in 5.006807s
|
55
|
+
# without ps 2.970k (± 5.3%) i/s - 15.052k in 5.083272s
|
56
|
+
#
|
57
|
+
# Comparison:
|
58
|
+
# ps: 10287.2 i/s
|
59
|
+
# without ps: 2970.0 i/s - 3.46x (± 0.00) slower
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class GenerateData
|
4
|
+
class ::Topic < ActiveRecord::Base;
|
5
|
+
belongs_to :user
|
6
|
+
belongs_to :category
|
7
|
+
end
|
8
|
+
class ::User < ActiveRecord::Base; end
|
9
|
+
class ::Category < ActiveRecord::Base; end
|
10
|
+
|
11
|
+
def initialize(count_records:)
|
12
|
+
@count_records = count_records
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
conn_settings = {
|
17
|
+
password: 'postgres',
|
18
|
+
user: 'postgres',
|
19
|
+
host: 'localhost'
|
20
|
+
}
|
21
|
+
|
22
|
+
db_conn = conn_settings.merge(database: "test_db", adapter: "postgresql")
|
23
|
+
|
24
|
+
pg = PG::Connection.new(conn_settings)
|
25
|
+
pg.exec "DROP DATABASE IF EXISTS test_db"
|
26
|
+
pg.exec "CREATE DATABASE test_db"
|
27
|
+
pg.close
|
28
|
+
|
29
|
+
ActiveRecord::Base.establish_connection(db_conn)
|
30
|
+
pg = ActiveRecord::Base.connection.raw_connection
|
31
|
+
|
32
|
+
pg.exec <<~SQL
|
33
|
+
drop table if exists topics;
|
34
|
+
drop table if exists users;
|
35
|
+
drop table if exists categories;
|
36
|
+
CREATE TABLE topics (
|
37
|
+
id integer NOT NULL PRIMARY KEY,
|
38
|
+
title character varying NOT NULL,
|
39
|
+
last_posted_at timestamp without time zone,
|
40
|
+
created_at timestamp without time zone NOT NULL,
|
41
|
+
updated_at timestamp without time zone NOT NULL,
|
42
|
+
views integer DEFAULT 0 NOT NULL,
|
43
|
+
posts_count integer DEFAULT 0 NOT NULL,
|
44
|
+
user_id integer,
|
45
|
+
last_post_user_id integer NOT NULL,
|
46
|
+
reply_count integer DEFAULT 0 NOT NULL,
|
47
|
+
featured_user1_id integer,
|
48
|
+
featured_user2_id integer,
|
49
|
+
featured_user3_id integer,
|
50
|
+
avg_time integer,
|
51
|
+
deleted_at timestamp without time zone,
|
52
|
+
highest_post_number integer DEFAULT 0 NOT NULL,
|
53
|
+
image_url character varying,
|
54
|
+
like_count integer DEFAULT 0 NOT NULL,
|
55
|
+
incoming_link_count integer DEFAULT 0 NOT NULL,
|
56
|
+
category_id integer,
|
57
|
+
visible boolean DEFAULT true NOT NULL,
|
58
|
+
moderator_posts_count integer DEFAULT 0 NOT NULL,
|
59
|
+
closed boolean DEFAULT false NOT NULL,
|
60
|
+
archived boolean DEFAULT false NOT NULL,
|
61
|
+
bumped_at timestamp without time zone NOT NULL,
|
62
|
+
has_summary boolean DEFAULT false NOT NULL,
|
63
|
+
vote_count integer DEFAULT 0 NOT NULL,
|
64
|
+
archetype character varying DEFAULT 'regular'::character varying NOT NULL,
|
65
|
+
featured_user4_id integer,
|
66
|
+
notify_moderators_count integer DEFAULT 0 NOT NULL,
|
67
|
+
spam_count integer DEFAULT 0 NOT NULL,
|
68
|
+
pinned_at timestamp without time zone,
|
69
|
+
score double precision,
|
70
|
+
percent_rank double precision DEFAULT 1.0 NOT NULL,
|
71
|
+
subtype character varying,
|
72
|
+
slug character varying,
|
73
|
+
deleted_by_id integer,
|
74
|
+
participant_count integer DEFAULT 1,
|
75
|
+
word_count integer,
|
76
|
+
excerpt character varying(1000),
|
77
|
+
pinned_globally boolean DEFAULT false NOT NULL,
|
78
|
+
pinned_until timestamp without time zone,
|
79
|
+
fancy_title character varying(400),
|
80
|
+
highest_staff_post_number integer DEFAULT 0 NOT NULL,
|
81
|
+
featured_link character varying
|
82
|
+
);
|
83
|
+
|
84
|
+
CREATE TABLE users (
|
85
|
+
id integer NOT NULL PRIMARY KEY,
|
86
|
+
first_name character varying NOT NULL,
|
87
|
+
last_name character varying NOT NULL
|
88
|
+
);
|
89
|
+
CREATE TABLE categories (
|
90
|
+
id integer NOT NULL PRIMARY KEY,
|
91
|
+
name character varying NOT NULL,
|
92
|
+
title character varying NOT NULL,
|
93
|
+
description character varying NOT NULL
|
94
|
+
);
|
95
|
+
SQL
|
96
|
+
|
97
|
+
generate_table(Topic)
|
98
|
+
generate_table(User)
|
99
|
+
generate_table(Category)
|
100
|
+
|
101
|
+
pg.exec <<~SQL
|
102
|
+
CREATE INDEX user_id ON topics USING btree (user_id);
|
103
|
+
CREATE INDEX category_id ON topics USING btree (category_id);
|
104
|
+
SQL
|
105
|
+
|
106
|
+
pg.exec "vacuum full analyze topics"
|
107
|
+
pg.exec "vacuum full analyze users"
|
108
|
+
pg.exec "vacuum full analyze categories"
|
109
|
+
|
110
|
+
[ActiveRecord::Base.connection, db_conn]
|
111
|
+
end
|
112
|
+
|
113
|
+
def generate_table(klass)
|
114
|
+
data =
|
115
|
+
@count_records.times.map do |id|
|
116
|
+
topic = { id: id }
|
117
|
+
klass.columns.each do |c|
|
118
|
+
topic[c.name.to_sym] = value_from_type(c.type)
|
119
|
+
end
|
120
|
+
topic
|
121
|
+
end
|
122
|
+
klass.insert_all(data)
|
123
|
+
end
|
124
|
+
|
125
|
+
def value_from_type(type)
|
126
|
+
case type
|
127
|
+
when :integer then rand(1000)
|
128
|
+
when :datetime then Time.now
|
129
|
+
when :boolean then false
|
130
|
+
else "HELLO WORLD" * 2
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/bench/timestamp_perf.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/inline'
|
2
4
|
|
3
5
|
gemfile do
|
@@ -21,8 +23,8 @@ require 'benchmark/ips'
|
|
21
23
|
require 'mini_sql'
|
22
24
|
|
23
25
|
ActiveRecord::Base.establish_connection(
|
24
|
-
:
|
25
|
-
:
|
26
|
+
adapter: "postgresql",
|
27
|
+
database: "test_db"
|
26
28
|
)
|
27
29
|
|
28
30
|
Sequel.default_timezone = :utc
|
@@ -47,20 +49,20 @@ SQL
|
|
47
49
|
class Timestamp < ActiveRecord::Base
|
48
50
|
end
|
49
51
|
|
50
|
-
class TimestampSequel< Sequel::Model(:timestamps)
|
52
|
+
class TimestampSequel < Sequel::Model(:timestamps)
|
51
53
|
end
|
52
54
|
|
53
|
-
|
54
55
|
Timestamp.transaction do
|
55
56
|
stamps = {
|
56
57
|
}
|
57
58
|
Timestamp.columns.each do |c|
|
58
|
-
stamps[c.name.to_sym] =
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
stamps[c.name.to_sym] =
|
60
|
+
case c.type
|
61
|
+
when :integer then 1
|
62
|
+
when :datetime then Time.now
|
63
|
+
when :boolean then false
|
64
|
+
else "HELLO WORLD" * 2
|
65
|
+
end
|
64
66
|
end
|
65
67
|
|
66
68
|
1000.times do |id|
|
@@ -71,7 +73,7 @@ end
|
|
71
73
|
|
72
74
|
$conn = ActiveRecord::Base.connection.raw_connection
|
73
75
|
|
74
|
-
def ar_pluck_times(l=1000)
|
76
|
+
def ar_pluck_times(l = 1000)
|
75
77
|
s = +""
|
76
78
|
Timestamp.limit(l).order(:id).pluck(:time1, :time2).each do |time1, time2|
|
77
79
|
s << time1.to_f.to_s
|
@@ -80,7 +82,7 @@ def ar_pluck_times(l=1000)
|
|
80
82
|
s
|
81
83
|
end
|
82
84
|
|
83
|
-
def ar_select_times(l=1000)
|
85
|
+
def ar_select_times(l = 1000)
|
84
86
|
s = +""
|
85
87
|
Timestamp.limit(l).order(:id).select(:time1, :time2).each do |t|
|
86
88
|
s << t.time1.to_f.to_s
|
@@ -91,7 +93,7 @@ end
|
|
91
93
|
|
92
94
|
$mini_sql = MiniSql::Connection.new($conn)
|
93
95
|
|
94
|
-
def pg_times_params(l=1000)
|
96
|
+
def pg_times_params(l = 1000)
|
95
97
|
s = +""
|
96
98
|
# use the safe pattern here
|
97
99
|
r = $conn.async_exec_params(-"select time1, time2 from timestamps order by id limit $1", [l])
|
@@ -110,7 +112,7 @@ def pg_times_params(l=1000)
|
|
110
112
|
s
|
111
113
|
end
|
112
114
|
|
113
|
-
def pg_times(l=1000)
|
115
|
+
def pg_times(l = 1000)
|
114
116
|
s = +""
|
115
117
|
# use the safe pattern here
|
116
118
|
r = $conn.async_exec("select time1, time2 from timestamps order by id limit #{l}")
|
@@ -129,7 +131,7 @@ def pg_times(l=1000)
|
|
129
131
|
s
|
130
132
|
end
|
131
133
|
|
132
|
-
def mini_sql_times(l=1000)
|
134
|
+
def mini_sql_times(l = 1000)
|
133
135
|
s = +""
|
134
136
|
$mini_sql.query(-"select time1, time2 from timestamps order by id limit ?", l).each do |t|
|
135
137
|
s << t.time1.to_f.to_s
|
@@ -138,7 +140,7 @@ def mini_sql_times(l=1000)
|
|
138
140
|
s
|
139
141
|
end
|
140
142
|
|
141
|
-
def sequel_times(l=1000)
|
143
|
+
def sequel_times(l = 1000)
|
142
144
|
s = +""
|
143
145
|
TimestampSequel.limit(l).order(:id).select(:time1, :time2).each do |t|
|
144
146
|
s << t.time1.to_f.to_s
|
@@ -147,7 +149,7 @@ def sequel_times(l=1000)
|
|
147
149
|
s
|
148
150
|
end
|
149
151
|
|
150
|
-
def sequel_pluck_times(l=1000)
|
152
|
+
def sequel_pluck_times(l = 1000)
|
151
153
|
s = +""
|
152
154
|
TimestampSequel.limit(l).order(:id).select_map([:time1, :time2]).each do |t|
|
153
155
|
s << t[0].to_f.to_s
|
@@ -156,7 +158,7 @@ def sequel_pluck_times(l=1000)
|
|
156
158
|
s
|
157
159
|
end
|
158
160
|
|
159
|
-
def sequel_raw_times(l=1000)
|
161
|
+
def sequel_raw_times(l = 1000)
|
160
162
|
s = +""
|
161
163
|
DB[-"select time1, time2 from timestamps order by id limit ?", l].map([:time1, :time2]).each do |time1, time2|
|
162
164
|
s << time1.to_f.to_s
|
@@ -166,13 +168,13 @@ def sequel_raw_times(l=1000)
|
|
166
168
|
end
|
167
169
|
|
168
170
|
# usage is not really recommended but just to compare to pluck lets have it
|
169
|
-
def mini_sql_times_single(l=1000)
|
171
|
+
def mini_sql_times_single(l = 1000)
|
170
172
|
s = +""
|
171
173
|
i = 0
|
172
174
|
r = $mini_sql.query_single(-"select time1, time2 from timestamps order by id limit ?", l)
|
173
175
|
while i < r.length
|
174
176
|
s << r[i].to_f.to_s
|
175
|
-
s << r[i+1].to_f.to_s
|
177
|
+
s << r[i + 1].to_f.to_s
|
176
178
|
i += 2
|
177
179
|
end
|
178
180
|
s
|
@@ -190,7 +192,6 @@ end
|
|
190
192
|
# s
|
191
193
|
# end
|
192
194
|
|
193
|
-
|
194
195
|
results = [
|
195
196
|
ar_select_times,
|
196
197
|
ar_pluck_times,
|
data/bench/topic_mysql_perf.rb
CHANGED
@@ -91,7 +91,6 @@ end
|
|
91
91
|
class TopicSequel < Sequel::Model(:topics)
|
92
92
|
end
|
93
93
|
|
94
|
-
|
95
94
|
Topic.transaction do
|
96
95
|
topic = {
|
97
96
|
}
|
@@ -179,7 +178,7 @@ def mini_sql_title_id_query_single
|
|
179
178
|
r = $mini_sql.query_single(-"select id, title from topics order by id limit 1000")
|
180
179
|
while i < r.length
|
181
180
|
s << r[i].to_s
|
182
|
-
s << r[i+1]
|
181
|
+
s << r[i + 1]
|
183
182
|
i += 2
|
184
183
|
end
|
185
184
|
s
|
@@ -197,7 +196,6 @@ results = [
|
|
197
196
|
|
198
197
|
exit(-1) unless results.uniq.length == 1
|
199
198
|
|
200
|
-
|
201
199
|
Benchmark.ips do |r|
|
202
200
|
r.report("ar select title id") do |n|
|
203
201
|
while n > 0
|
@@ -244,8 +242,6 @@ Benchmark.ips do |r|
|
|
244
242
|
r.compare!
|
245
243
|
end
|
246
244
|
|
247
|
-
|
248
|
-
|
249
245
|
def wide_topic_ar
|
250
246
|
Topic.first
|
251
247
|
end
|
@@ -301,10 +297,8 @@ end
|
|
301
297
|
# ar select title id pluck: 317.1 i/s - 1.53x slower
|
302
298
|
# ar select title id: 102.3 i/s - 4.74x slower
|
303
299
|
|
304
|
-
|
305
300
|
# Comparison:
|
306
301
|
# wide topic mini sql: 6768.7 i/s
|
307
302
|
# wide topic mysql: 6063.9 i/s - same-ish: difference falls within error
|
308
303
|
# wide topic sequel: 4908.6 i/s - same-ish: difference falls within error
|
309
304
|
# wide topic ar: 2630.2 i/s - 2.57x slower
|
310
|
-
|