Skip to content

Commit e67d395

Browse files
authored
add insert benchmarks comparing myxql/postgrex/exqlite (#111)
1 parent de25b5e commit e67d395

File tree

12 files changed

+410
-1
lines changed

12 files changed

+410
-1
lines changed

bench/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Ecto Benchmarks
2+
3+
Ecto has a benchmark suite to track performance of sensitive operations. Benchmarks
4+
are run using the [Benchee](https://github.com/PragTob/benchee) library and
5+
need PostgreSQL and MySQL up and running.
6+
7+
To run the benchmarks tests just type in the console:
8+
9+
```
10+
# POSIX-compatible shells
11+
$ MIX_ENV=bench mix run bench/bench_helper.exs
12+
```
13+
14+
```
15+
# other shells
16+
$ env MIX_ENV=bench mix run bench/bench_helper.exs
17+
```
18+
19+
Benchmarks are inside the `scripts/` directory and are divided into two
20+
categories:
21+
22+
* `micro benchmarks`: Operations that don't actually interface with the database,
23+
but might need it up and running to start the Ecto agents and processes.
24+
25+
* `macro benchmarks`: Operations that are actually run in the database. This are
26+
more likely to integration tests.
27+
28+
You can also run a benchmark individually by giving the path to the benchmark
29+
script instead of `bench/bench_helper.exs`.
30+
31+
# Docker
32+
I had Postgres already installed and running locally, but needed to get MySQL up and running. The easiest way to do this is with this command:
33+
34+
```
35+
docker run -p 3306:3306 --name mysql_server -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7
36+
```

bench/bench_helper.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Micro benchmarks
2+
Code.require_file("scripts/micro/load_bench.exs", __DIR__)
3+
Code.require_file("scripts/micro/to_sql_bench.exs", __DIR__)
4+
5+
## Macro benchmarks needs postgresql and mysql up and running
6+
Code.require_file("scripts/macro/insert_bench.exs", __DIR__)
7+
Code.require_file("scripts/macro/all_bench.exs", __DIR__)

bench/scripts/macro/all_bench.exs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# -----------------------------------Goal--------------------------------------
2+
# Compare the performance of querying all objects of the different supported
3+
# databases
4+
5+
# -------------------------------Description-----------------------------------
6+
# This benchmark tracks performance of querying a set of objects registered in
7+
# the database with Repo.all/2 function. The query pass through
8+
# the steps of translating the SQL statements, sending them to the database and
9+
# load the results into Ecto structures. Both, Ecto Adapters and Database itself
10+
# play a role and can affect the results of this benchmark.
11+
12+
# ----------------------------Factors(don't change)---------------------------
13+
# Different adapters supported by Ecto with the proper database up and running
14+
15+
# ----------------------------Parameters(change)-------------------------------
16+
# There is only a unique parameter in this benchmark, the User objects to be
17+
# fetched.
18+
19+
Code.require_file("../../support/setup.exs", __DIR__)
20+
21+
alias Ecto.Bench.User
22+
23+
limit = 5_000
24+
25+
users =
26+
1..limit
27+
|> Enum.map(fn _ -> User.sample_data() end)
28+
29+
# We need to insert data to fetch
30+
Ecto.Bench.PgRepo.insert_all(User, users)
31+
Ecto.Bench.MyXQLRepo.insert_all(User, users)
32+
33+
jobs = %{
34+
"Pg Repo.all/2" => fn -> Ecto.Bench.PgRepo.all(User, limit: limit) end,
35+
"MyXQL Repo.all/2" => fn -> Ecto.Bench.MyXQLRepo.all(User, limit: limit) end
36+
}
37+
38+
path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
39+
file = Path.join(path, "all.json")
40+
41+
Benchee.run(
42+
jobs,
43+
formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
44+
formatter_options: [json: [file: file]],
45+
time: 10,
46+
after_each: fn results ->
47+
^limit = length(results)
48+
end
49+
)
50+
51+
# Clean inserted data
52+
Ecto.Bench.PgRepo.delete_all(User)
53+
Ecto.Bench.MyXQLRepo.delete_all(User)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -----------------------------------Goal--------------------------------------
2+
# Compare the performance of inserting changesets and structs in the different
3+
# supported databases
4+
5+
# -------------------------------Description-----------------------------------
6+
# This benchmark tracks performance of inserting changesets and structs in the
7+
# database with Repo.insert!/1 function. The query pass through
8+
# the steps of translating the SQL statements, sending them to the database and
9+
# returning the result of the transaction. Both, Ecto Adapters and Database itself
10+
# play a role and can affect the results of this benchmark.
11+
12+
# ----------------------------Factors(don't change)---------------------------
13+
# Different adapters supported by Ecto with the proper database up and running
14+
15+
# ----------------------------Parameters(change)-------------------------------
16+
# Different inputs to be inserted, aka Changesets and Structs
17+
18+
Code.require_file("../../support/setup.exs", __DIR__)
19+
20+
alias Ecto.Bench.User
21+
22+
inputs = %{
23+
"Struct" => struct(User, User.sample_data()),
24+
"Changeset" => User.changeset(User.sample_data())
25+
}
26+
27+
jobs = %{
28+
"Exqlite Insert" => fn entry -> Ecto.Bench.ExqliteRepo.insert!(entry) end,
29+
"Pg Insert" => fn entry -> Ecto.Bench.PgRepo.insert!(entry) end,
30+
"MyXQL Insert" => fn entry -> Ecto.Bench.MyXQLRepo.insert!(entry) end
31+
}
32+
33+
path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
34+
file = Path.join(path, "insert.json")
35+
36+
Benchee.run(
37+
jobs,
38+
inputs: inputs,
39+
formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
40+
formatter_options: [json: [file: file]]
41+
)
42+
43+
# Clean inserted data
44+
Ecto.Bench.ExqliteRepo.delete_all(User)
45+
Ecto.Bench.PgRepo.delete_all(User)
46+
Ecto.Bench.MyXQLRepo.delete_all(User)

bench/scripts/micro/load_bench.exs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# -----------------------------------Goal--------------------------------------
2+
# Compare the implementation of loading raw database data into Ecto structures by
3+
# the different database adapters
4+
5+
# -------------------------------Description-----------------------------------
6+
# Repo.load/2 is an important step of a database query.
7+
# This benchmark tracks performance of loading "raw" data into ecto structures
8+
# Raw data can be in different types (e.g. keyword lists, maps), in this tests
9+
# we benchmark against map inputs
10+
11+
# ----------------------------Factors(don't change)---------------------------
12+
# Different adapters supported by Ecto, each one has its own implementation that
13+
# is tested against different inputs
14+
15+
# ----------------------------Parameters(change)-------------------------------
16+
# Different sizes of raw data(small, medium, big) and different attribute types
17+
# such as UUID, Date and Time fetched from the database and needs to be
18+
# loaded into Ecto structures.
19+
20+
Code.require_file("../../support/setup.exs", __DIR__)
21+
22+
alias Ecto.Bench.User
23+
24+
inputs = %{
25+
"Small 1 Thousand" =>
26+
1..1_000 |> Enum.map(fn _ -> %{name: "Alice", email: "email@email.com"} end),
27+
"Medium 100 Thousand" =>
28+
1..100_000 |> Enum.map(fn _ -> %{name: "Alice", email: "email@email.com"} end),
29+
"Big 1 Million" =>
30+
1..1_000_000 |> Enum.map(fn _ -> %{name: "Alice", email: "email@email.com"} end),
31+
"Time attr" =>
32+
1..100_000 |> Enum.map(fn _ -> %{name: "Alice", time_attr: ~T[21:25:04.361140]} end),
33+
"Date attr" => 1..100_000 |> Enum.map(fn _ -> %{name: "Alice", date_attr: ~D[2018-06-20]} end),
34+
"NaiveDateTime attr" =>
35+
1..100_000
36+
|> Enum.map(fn _ -> %{name: "Alice", naive_datetime_attr: ~N[2019-06-20 21:32:07.424178]} end),
37+
"UUID attr" =>
38+
1..100_000
39+
|> Enum.map(fn _ -> %{name: "Alice", uuid: Ecto.UUID.bingenerate()} end)
40+
}
41+
42+
jobs = %{
43+
"Pg Loader" => fn data -> Enum.map(data, &Ecto.Bench.PgRepo.load(User, &1)) end,
44+
"MyXQL Loader" => fn data -> Enum.map(data, &Ecto.Bench.MyXQLRepo.load(User, &1)) end
45+
}
46+
47+
path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
48+
file = Path.join(path, "load.json")
49+
50+
Benchee.run(
51+
jobs,
52+
inputs: inputs,
53+
formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
54+
formatter_options: [json: [file: file]]
55+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -----------------------------------Goal--------------------------------------
2+
# Compare the implementation of parsing Ecto.Query objects into SQL queries by
3+
# the different database adapters
4+
5+
# -------------------------------Description-----------------------------------
6+
# Repo.to_sql/2 is an important step of a database query.
7+
# This benchmark tracks performance of parsing Ecto.Query structures into
8+
# "raw" SQL query strings.
9+
# Different Ecto.Query objects has multiple combinations and some different attributes
10+
# depending on the query type. In this tests we benchmark against different
11+
# query types and complexity.
12+
13+
# ----------------------------Factors(don't change)---------------------------
14+
# Different adapters supported by Ecto, each one has its own implementation that
15+
# is tested against different query inputs
16+
17+
# ----------------------------Parameters(change)-------------------------------
18+
# Different query objects (select, delete, update) to be translated into pure SQL
19+
# strings.
20+
21+
Code.require_file("../../support/setup.exs", __DIR__)
22+
23+
import Ecto.Query
24+
25+
alias Ecto.Bench.{User, Game}
26+
27+
inputs = %{
28+
"Ordinary Select All" => {:all, from(User)},
29+
"Ordinary Delete All" => {:delete_all, from(User)},
30+
"Ordinary Update All" => {:update_all, from(User, update: [set: [name: "Thor"]])},
31+
"Ordinary Where" => {:all, from(User, where: [name: "Thanos", email: "blah@blah"])},
32+
"Fetch First Registry" => {:all, first(User)},
33+
"Fetch Last Registry" => {:all, last(User)},
34+
"Ordinary Order By" => {:all, order_by(User, desc: :name)},
35+
"Complex Query 2 Joins" =>
36+
{:all,
37+
from(User, where: [name: "Thanos"])
38+
|> join(:left, [u], ux in User, on: u.id == ux.id)
39+
|> join(:right, [j], uj in User, on: j.id == 1 and j.email == "email@email")
40+
|> select([u, ux], {u.name, ux.email})},
41+
"Complex Query 4 Joins" =>
42+
{:all,
43+
from(User)
44+
|> join(:left, [u], g in Game, on: g.name == u.name)
45+
|> join(:right, [g], u in User, on: g.id == 1 and u.email == "email@email")
46+
|> join(:inner, [u], g in fragment("SELECT * from games where game.id = ?", u.id))
47+
|> join(:left, [g], u in fragment("SELECT * from users = ?", g.id))
48+
|> select([u, g], {u.name, g.price})}
49+
}
50+
51+
jobs = %{
52+
"Pg Query Builder" => fn {type, query} -> Ecto.Bench.PgRepo.to_sql(type, query) end,
53+
"MyXQL Query Builder" => fn {type, query} -> Ecto.Bench.MyXQLRepo.to_sql(type, query) end
54+
}
55+
56+
path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
57+
file = Path.join(path, "to_sql.json")
58+
59+
Benchee.run(
60+
jobs,
61+
inputs: inputs,
62+
formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
63+
formatter_options: [json: [file: file]]
64+
)

bench/support/migrations.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule Ecto.Bench.CreateUser do
2+
use Ecto.Migration
3+
4+
def change do
5+
create table(:users) do
6+
add(:name, :string)
7+
add(:email, :string)
8+
add(:password, :string)
9+
add(:time_attr, :time)
10+
add(:date_attr, :date)
11+
add(:naive_datetime_attr, :naive_datetime)
12+
add(:uuid, :binary_id)
13+
end
14+
end
15+
end

bench/support/repo.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
pg_bench_url = System.get_env("PG_URL") || "postgres:postgres@localhost"
2+
myxql_bench_url = System.get_env("MYXQL_URL") || "root@localhost"
3+
4+
Application.put_env(
5+
:ecto_sql,
6+
Ecto.Bench.PgRepo,
7+
url: "ecto://" <> pg_bench_url <> "/ecto_test",
8+
adapter: Ecto.Adapters.Postgres,
9+
show_sensitive_data_on_connection_error: true
10+
)
11+
12+
Application.put_env(
13+
:ecto_sql,
14+
Ecto.Bench.MyXQLRepo,
15+
url: "ecto://" <> myxql_bench_url <> "/ecto_test_myxql",
16+
adapter: Ecto.Adapters.MyXQL,
17+
protocol: :tcp,
18+
show_sensitive_data_on_connection_error: true
19+
)
20+
21+
Application.put_env(
22+
:ecto_sql,
23+
Ecto.Bench.ExqliteRepo,
24+
adapter: Ecto.Adapters.Exqlite,
25+
database: "/tmp/exqlite_bench.db",
26+
journal_mode: :wal,
27+
cache_size: -64000,
28+
temp_store: :memory,
29+
pool_size: 5,
30+
show_sensitive_data_on_connection_error: true
31+
)
32+
33+
defmodule Ecto.Bench.PgRepo do
34+
use Ecto.Repo, otp_app: :ecto_sql, adapter: Ecto.Adapters.Postgres, log: false
35+
end
36+
37+
defmodule Ecto.Bench.MyXQLRepo do
38+
use Ecto.Repo, otp_app: :ecto_sql, adapter: Ecto.Adapters.MyXQL, log: false
39+
end
40+
41+
defmodule Ecto.Bench.ExqliteRepo do
42+
use Ecto.Repo, otp_app: :ecto_sql, adapter: Ecto.Adapters.Exqlite, log: false
43+
end

bench/support/schemas.exs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule Ecto.Bench.User do
2+
use Ecto.Schema
3+
4+
schema "users" do
5+
field(:name, :string)
6+
field(:email, :string)
7+
field(:password, :string)
8+
field(:time_attr, :time)
9+
field(:date_attr, :date)
10+
field(:naive_datetime_attr, :naive_datetime)
11+
field(:uuid, :binary_id)
12+
end
13+
14+
@required_attrs [
15+
:name,
16+
:email,
17+
:password,
18+
:time_attr,
19+
:date_attr,
20+
:naive_datetime_attr,
21+
:uuid
22+
]
23+
24+
def changeset() do
25+
changeset(sample_data())
26+
end
27+
28+
def changeset(data) do
29+
Ecto.Changeset.cast(%__MODULE__{}, data, @required_attrs)
30+
end
31+
32+
def sample_data do
33+
%{
34+
name: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
35+
email: "foobar@email.com",
36+
password: "mypass",
37+
time_attr: Time.utc_now() |> Time.truncate(:second),
38+
date_attr: Date.utc_today(),
39+
naive_datetime_attr: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second),
40+
uuid: Ecto.UUID.generate()
41+
}
42+
end
43+
end
44+
45+
defmodule Ecto.Bench.Game do
46+
use Ecto.Schema
47+
48+
schema "games" do
49+
field(:name, :string)
50+
field(:price, :float)
51+
end
52+
end

0 commit comments

Comments
 (0)