Skip to content
This repository was archived by the owner on Mar 19, 2021. It is now read-only.

Commit 375d703

Browse files
Alex GolubovConnorRigby
authored andcommitted
Added fine tuning of query via internal result chunking
1 parent 0545182 commit 375d703

File tree

12 files changed

+196
-107
lines changed

12 files changed

+196
-107
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,35 @@ Sqlitex.Server.query(Golf.DB,
6262
ORDER BY g.played_at DESC LIMIT 10")
6363
```
6464

65+
# Configuration
66+
67+
Sqlitex uses the Erlang library [esqlite](https://github.com/mmzeeman/esqlite)
68+
which accepts a timeout parameter for almost all interactions with the database.
69+
The default value for this timeout is 5000 ms. Many functions in Sqlitex accept
70+
a `:db_timeout` option that is passed on to the esqlite calls and also defaults
71+
to 5000 ms. If required, this default value can be overridden globally with the
72+
following in your `config.exs`:
73+
74+
```
75+
config :sqlitex, db_timeout: 10_000 # or other positive integer number of ms
76+
```
77+
78+
Another esqlite parameter is :db_chunk_size.
79+
This is a count of rows to read from native sqlite and send to erlang process in one bulk.
80+
For example, the table `mytable` has 1000 rows. We make the query to get all rows with `db_chunk_size: 500` parameter:
81+
```
82+
Sqlitex.query(db, "select * from mytable", db_chunk_size: 500)
83+
```
84+
in this case all rows will be passed from native sqlite OS thread to the erlang process in two passes.
85+
Each pass will contain 500 rows.
86+
This parameter decrease overhead of transmitting rows from native OS sqlite thread to the erlang process by
87+
chunking list of result rows.
88+
Please, decrease this value if rows are heavy. Default value is 5000.
89+
If you in doubt what to do with this parameter, please, do nothing. Default value is ok.
90+
```
91+
config :sqlitex, db_chunk_size: 500 # if most of the database rows are heavy
92+
```
93+
94+
6595
# Looking for Ecto?
6696
Check out the [SQLite Ecto2 adapter](https://github.com/Sqlite-Ecto/sqlite_ecto2)

lib/sqlitex.ex

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,38 @@ defmodule Sqlitex do
3737
```
3838
config :sqlitex, db_timeout: 10_000 # or other positive integer number of ms
3939
```
40+
41+
Another esqlite parameter is :db_chunk_size.
42+
This is a count of rows to read from native sqlite and send to erlang process in one bulk.
43+
For example, the table `mytable` has 1000 rows. We make the query to get all rows with `db_chunk_size: 500` parameter:
44+
```
45+
Sqlitex.query(db, "select * from mytable", db_chunk_size: 500)
46+
```
47+
in this case all rows will be passed from native sqlite OS thread to the erlang process in two passes.
48+
Each pass will contain 500 rows.
49+
This parameter decrease overhead of transmitting rows from native OS sqlite thread to the erlang process by
50+
chunking list of result rows.
51+
Please, decrease this value if rows are heavy. Default value is 5000.
52+
If you in doubt what to do with this parameter, please, do nothing. Default value is ok.
53+
```
54+
config :sqlitex, db_chunk_size: 500 # if most of the database rows are heavy
55+
```
4056
"""
4157

4258
alias Sqlitex.Config
4359

4460
@spec close(connection) :: :ok
4561
@spec close(connection, Keyword.t) :: :ok
4662
def close(db, opts \\ []) do
47-
timeout = Keyword.get(opts, :db_timeout, Config.db_timeout())
48-
:esqlite3.close(db, timeout)
63+
:esqlite3.close(db, Config.db_timeout(opts))
4964
end
5065

5166
@spec open(charlist | String.t) :: {:ok, connection} | {:error, {atom, charlist}}
5267
@spec open(charlist | String.t, Keyword.t) :: {:ok, connection} | {:error, {atom, charlist}}
5368
def open(path, opts \\ [])
5469
def open(path, opts) when is_binary(path), do: open(string_to_charlist(path), opts)
5570
def open(path, opts) do
56-
timeout = Keyword.get(opts, :db_timeout, Config.db_timeout())
57-
:esqlite3.open(path, timeout)
71+
:esqlite3.open(path, Config.db_timeout(opts))
5872
end
5973

6074
def with_db(path, fun, opts \\ []) do
@@ -78,8 +92,7 @@ defmodule Sqlitex do
7892
"""
7993
@spec set_update_hook(connection, pid, Keyword.t()) :: :ok | {:error, term()}
8094
def set_update_hook(db, pid, opts \\ []) do
81-
timeout = Keyword.get(opts, :db_timeout, Config.db_timeout())
82-
:esqlite3.set_update_hook(pid, db, timeout)
95+
:esqlite3.set_update_hook(pid, db, Config.db_timeout(opts))
8396
end
8497

8598
@doc """
@@ -92,8 +105,7 @@ defmodule Sqlitex do
92105
@spec exec(connection, string_or_charlist) :: :ok | sqlite_error
93106
@spec exec(connection, string_or_charlist, Keyword.t) :: :ok | sqlite_error
94107
def exec(db, sql, opts \\ []) do
95-
timeout = Keyword.get(opts, :db_timeout, Config.db_timeout())
96-
:esqlite3.exec(sql, db, timeout)
108+
:esqlite3.exec(sql, db, Config.db_timeout(opts))
97109
end
98110

99111
@doc "A shortcut to `Sqlitex.Query.query/3`"

lib/sqlitex/config.ex

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
defmodule Sqlitex.Config do
22
@moduledoc false
33

4-
def db_timeout do
5-
Application.get_env(:sqlitex, :db_timeout, 5_000)
4+
@default_call_timeout 5_000
5+
@default_db_timeout 5_000
6+
@default_db_chunk_size 5_000
7+
8+
def call_timeout(opts \\ []) do
9+
Keyword.get(opts, :call_timeout,
10+
Keyword.get(opts, :timeout, # backward compatibility with the :timeout parameter
11+
Application.get_env(:sqlitex, :call_timeout, @default_call_timeout)))
12+
end
13+
14+
def db_timeout(opts \\ []) do
15+
Keyword.get(opts, :db_timeout, Application.get_env(:sqlitex, :db_timeout, @default_db_timeout))
616
end
17+
18+
def db_chunk_size(opts \\ []) do
19+
Keyword.get(opts, :db_chunk_size, Application.get_env(:sqlitex, :db_chunk_size, @default_db_chunk_size))
20+
end
21+
722
end

lib/sqlitex/query.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ defmodule Sqlitex.Query do
2424
* `into` - The collection to put results into. This defaults to a list.
2525
* `db_timeout` - The timeout (in ms) to apply to each of the underlying SQLite operations. Defaults
2626
to `Application.get_env(:sqlitex, :db_timeout)` or `5000` ms if not configured.
27+
* `db_chunk_size` - The number of rows to read from native sqlite and send to erlang process in one bulk.
28+
Defaults to `Application.get_env(:sqlitex, :db_chunk_size)` or `5000` ms if not configured.
2729
2830
## Returns
2931
* {:ok, [results...]} on success
@@ -39,7 +41,7 @@ defmodule Sqlitex.Query do
3941
def query(db, sql, opts \\ []) do
4042
with {:ok, stmt} <- Statement.prepare(db, sql, opts),
4143
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), opts),
42-
{:ok, res} <- Statement.fetch_all(stmt, Keyword.get(opts, :db_timeout, 5_000), Keyword.get(opts, :into, [])),
44+
{:ok, res} <- Statement.fetch_all(stmt, opts),
4345
do: {:ok, res}
4446
end
4547

@@ -73,6 +75,8 @@ defmodule Sqlitex.Query do
7375
to bind as a list.
7476
* `db_timeout` - The timeout (in ms) to apply to each of the underlying SQLite operations. Defaults
7577
to `Application.get_env(:sqlitex, :db_timeout)` or `5000` ms if not configured.
78+
* `db_chunk_size` - The number of rows to read from native sqlite and send to erlang process in one bulk.
79+
Defaults to `Application.get_env(:sqlitex, :db_chunk_size)` or `5000` ms if not configured.
7680
7781
## Returns
7882
* {:ok, %{rows: [[1, 2], [2, 3]], columns: [:a, :b], types: [:INTEGER, :INTEGER]}} on success
@@ -84,7 +88,7 @@ defmodule Sqlitex.Query do
8488
def query_rows(db, sql, opts \\ []) do
8589
with {:ok, stmt} <- Statement.prepare(db, sql, opts),
8690
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), opts),
87-
{:ok, rows} <- Statement.fetch_all(stmt, Keyword.get(opts, :db_timeout, 5_000), :raw_list),
91+
{:ok, rows} <- Statement.fetch_all(stmt, Keyword.put(opts, :into, :raw_list)),
8892
do: {:ok, %{rows: rows, columns: stmt.column_names, types: stmt.column_types}}
8993
end
9094

lib/sqlitex/server.ex

Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -58,87 +58,107 @@ defmodule Sqlitex.Server do
5858
- `stmt_cache_size: (positive_integer)` to override the default limit (20) of statements
5959
that are cached when calling `prepare/3`.
6060
- `db_timeout: (positive_integer)` to override `:esqlite3`'s default timeout of 5000 ms for
61-
interactions with the database. This can also be set in `config.exs` as
62-
`config :sqlitex, db_timeout: 5_000`.
61+
interactions with the database. This can also be set in `config.exs` as `config :sqlitex, db_timeout: 5_000`.
62+
- `db_chunk_size: (positive_integer)` to override `:esqlite3`'s default chunk_size of 5000 rows
63+
to read from native sqlite and send to erlang process in one bulk.
64+
This can also be set in `config.exs` as `config :sqlitex, db_chunk_size: 5_000`.
6365
"""
6466
def start_link(db_path, opts \\ []) do
6567
stmt_cache_size = Keyword.get(opts, :stmt_cache_size, 20)
66-
timeout = Keyword.get(opts, :db_timeout, Config.db_timeout())
67-
GenServer.start_link(__MODULE__, {db_path, stmt_cache_size, timeout}, opts)
68+
config = [
69+
db_timeout: Config.db_timeout(opts),
70+
db_chunk_size: Config.db_chunk_size(opts)
71+
]
72+
GenServer.start_link(__MODULE__, {db_path, stmt_cache_size, config}, opts)
6873
end
6974

7075
## GenServer callbacks
7176

72-
def init({db_path, stmt_cache_size, timeout})
77+
def init({db_path, stmt_cache_size, config})
7378
when is_integer(stmt_cache_size) and stmt_cache_size > 0
7479
do
75-
case Sqlitex.open(db_path, [db_timeout: timeout]) do
76-
{:ok, db} -> {:ok, {db, __MODULE__.StatementCache.new(db, stmt_cache_size), timeout}}
80+
case Sqlitex.open(db_path, config) do
81+
{:ok, db} -> {:ok, {db, __MODULE__.StatementCache.new(db, stmt_cache_size), config}}
7782
{:error, reason} -> {:stop, reason}
7883
end
7984
end
8085

81-
def handle_call({:exec, sql}, _from, {db, stmt_cache, timeout}) do
82-
result = Sqlitex.exec(db, sql, [db_timeout: timeout])
83-
{:reply, result, {db, stmt_cache, timeout}}
86+
def handle_call({:exec, sql}, _from, {db, stmt_cache, config}) do
87+
result = Sqlitex.exec(db, sql, config)
88+
{:reply, result, {db, stmt_cache, config}}
8489
end
8590

86-
def handle_call({:query, sql, opts}, _from, {db, stmt_cache, timeout}) do
87-
case query_impl(sql, opts, stmt_cache, timeout) do
88-
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, timeout}}
89-
err -> {:reply, err, {db, stmt_cache, timeout}}
91+
def handle_call({:query, sql, opts}, _from, {db, stmt_cache, config}) do
92+
case query_impl(sql, stmt_cache, Keyword.merge(config, opts)) do
93+
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, config}}
94+
err -> {:reply, err, {db, stmt_cache, config}}
9095
end
9196
end
9297

93-
def handle_call({:query_rows, sql, opts}, _from, {db, stmt_cache, timeout}) do
94-
case query_rows_impl(sql, opts, stmt_cache, timeout) do
95-
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, timeout}}
96-
err -> {:reply, err, {db, stmt_cache, timeout}}
98+
def handle_call({:query_rows, sql, opts}, _from, {db, stmt_cache, config}) do
99+
case query_rows_impl(sql, stmt_cache, Keyword.merge(config, opts)) do
100+
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, config}}
101+
err -> {:reply, err, {db, stmt_cache, config}}
97102
end
98103
end
99104

100-
def handle_call({:prepare, sql}, _from, {db, stmt_cache, timeout}) do
101-
case prepare_impl(sql, stmt_cache, timeout) do
102-
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, timeout}}
103-
err -> {:reply, err, {db, stmt_cache, timeout}}
105+
def handle_call({:prepare, sql}, _from, {db, stmt_cache, config}) do
106+
case prepare_impl(sql, stmt_cache, config) do
107+
{:ok, result, new_cache} -> {:reply, {:ok, result}, {db, new_cache, config}}
108+
err -> {:reply, err, {db, stmt_cache, config}}
104109
end
105110
end
106111

107-
def handle_call({:create_table, name, table_opts, cols}, _from, {db, stmt_cache, timeout}) do
108-
result = Sqlitex.create_table(db, name, table_opts, cols, [db_timeout: timeout])
109-
{:reply, result, {db, stmt_cache, timeout}}
112+
def handle_call({:create_table, name, table_opts, cols}, _from, {db, stmt_cache, config}) do
113+
result = Sqlitex.create_table(db, name, table_opts, cols, config)
114+
{:reply, result, {db, stmt_cache, config}}
110115
end
111116

112-
def handle_call({:set_update_hook, pid, opts}, _from, {db, stmt_cache, timeout}) do
113-
result = Sqlitex.set_update_hook(db, pid, opts)
114-
{:reply, result, {db, stmt_cache, timeout}}
117+
def handle_call({:set_update_hook, pid, opts}, _from, {db, stmt_cache, config}) do
118+
result = Sqlitex.set_update_hook(db, pid, Keyword.merge(config, opts))
119+
{:reply, result, {db, stmt_cache, config}}
115120
end
116121

117-
def handle_cast(:stop, {db, stmt_cache, timeout}) do
118-
{:stop, :normal, {db, stmt_cache, timeout}}
122+
def handle_cast(:stop, {db, stmt_cache, config}) do
123+
{:stop, :normal, {db, stmt_cache, config}}
119124
end
120125

121-
def terminate(_reason, {db, _stmt_cache, _timeout}) do
122-
Sqlitex.close(db)
126+
def terminate(_reason, {db, _stmt_cache, config}) do
127+
Sqlitex.close(db, config)
123128
:ok
124129
end
125130

126131
## Public API
127132

133+
@doc """
134+
Same as `Sqlitex.exec/3` but using the shared db connections saved in the GenServer state.
135+
136+
Returns the results otherwise.
137+
"""
128138
def exec(pid, sql, opts \\ []) do
129-
GenServer.call(pid, {:exec, sql}, timeout(opts))
139+
GenServer.call(pid, {:exec, sql}, Config.call_timeout(opts))
130140
end
131141

142+
@doc """
143+
Same as `Sqlitex.Query.query/3` but using the shared db connections saved in the GenServer state.
144+
145+
Returns the results otherwise.
146+
"""
132147
def query(pid, sql, opts \\ []) do
133-
GenServer.call(pid, {:query, sql, opts}, timeout(opts))
148+
GenServer.call(pid, {:query, sql, opts}, Config.call_timeout(opts))
134149
end
135150

151+
@doc """
152+
Same as `Sqlitex.Query.query_rows/3` but using the shared db connections saved in the GenServer state.
153+
154+
Returns the results otherwise.
155+
"""
136156
def query_rows(pid, sql, opts \\ []) do
137-
GenServer.call(pid, {:query_rows, sql, opts}, timeout(opts))
157+
GenServer.call(pid, {:query_rows, sql, opts}, Config.call_timeout(opts))
138158
end
139159

140160
def set_update_hook(server_pid, notification_pid, opts \\ []) do
141-
GenServer.call(server_pid, {:set_update_hook, notification_pid, opts}, timeout(opts))
161+
GenServer.call(server_pid, {:set_update_hook, notification_pid, opts}, Config.call_timeout(opts))
142162
end
143163

144164
@doc """
@@ -160,7 +180,7 @@ defmodule Sqlitex.Server do
160180
could not be prepared.
161181
"""
162182
def prepare(pid, sql, opts \\ []) do
163-
GenServer.call(pid, {:prepare, sql}, timeout(opts))
183+
GenServer.call(pid, {:prepare, sql}, Config.call_timeout(opts))
164184
end
165185

166186
def create_table(pid, name, table_opts \\ [], cols) do
@@ -173,30 +193,24 @@ defmodule Sqlitex.Server do
173193

174194
## Helpers
175195

176-
defp query_impl(sql, opts, stmt_cache, db_timeout) do
177-
db_opts = [db_timeout: db_timeout]
178-
179-
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, db_opts),
180-
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), db_opts),
181-
{:ok, rows} <- Statement.fetch_all(stmt, Keyword.get(opts, :db_timeout, 5_000), Keyword.get(opts, :into, [])),
196+
defp query_impl(sql, stmt_cache, opts) do
197+
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, opts),
198+
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), opts),
199+
{:ok, rows} <- Statement.fetch_all(stmt, opts),
182200
do: {:ok, rows, new_cache}
183201
end
184202

185-
defp query_rows_impl(sql, opts, stmt_cache, db_timeout) do
186-
db_opts = [db_timeout: db_timeout]
187-
188-
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, db_opts),
189-
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), db_opts),
190-
{:ok, rows} <- Statement.fetch_all(stmt, Keyword.get(opts, :db_timeout, 5_000), :raw_list),
203+
defp query_rows_impl(sql, stmt_cache, opts) do
204+
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, opts),
205+
{:ok, stmt} <- Statement.bind_values(stmt, Keyword.get(opts, :bind, []), opts),
206+
{:ok, rows} <- Statement.fetch_all(stmt, Keyword.put(opts, :into, :raw_list)),
191207
do: {:ok,
192208
%{rows: rows, columns: stmt.column_names, types: stmt.column_types},
193209
new_cache}
194210
end
195211

196-
defp prepare_impl(sql, stmt_cache, db_timeout) do
197-
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, [db_timeout: db_timeout]),
212+
defp prepare_impl(sql, stmt_cache, opts) do
213+
with {%Cache{} = new_cache, stmt} <- Cache.prepare(stmt_cache, sql, opts),
198214
do: {:ok, %{columns: stmt.column_names, types: stmt.column_types}, new_cache}
199215
end
200-
201-
defp timeout(kwopts), do: Keyword.get(kwopts, :timeout, 5000)
202216
end

0 commit comments

Comments
 (0)