···11+# Ecto Benchmarks
22+33+Ecto has a benchmark suite to track performance of sensitive operations. Benchmarks
44+are run using the [Benchee](https://github.com/PragTob/benchee) library and
55+need PostgreSQL and MySQL up and running.
66+77+To run the benchmarks tests just type in the console:
88+99+```
1010+# POSIX-compatible shells
1111+$ MIX_ENV=bench mix run bench/bench_helper.exs
1212+```
1313+1414+```
1515+# other shells
1616+$ env MIX_ENV=bench mix run bench/bench_helper.exs
1717+```
1818+1919+Benchmarks are inside the `scripts/` directory and are divided into two
2020+categories:
2121+2222+* `micro benchmarks`: Operations that don't actually interface with the database,
2323+but might need it up and running to start the Ecto agents and processes.
2424+2525+* `macro benchmarks`: Operations that are actually run in the database. This are
2626+more likely to integration tests.
2727+2828+You can also run a benchmark individually by giving the path to the benchmark
2929+script instead of `bench/bench_helper.exs`.
3030+3131+# Docker
3232+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:
3333+3434+```
3535+docker run -p 3306:3306 --name mysql_server -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7
3636+```
+7
bench/bench_helper.exs
···11+# Micro benchmarks
22+Code.require_file("scripts/micro/load_bench.exs", __DIR__)
33+Code.require_file("scripts/micro/to_sql_bench.exs", __DIR__)
44+55+## Macro benchmarks needs postgresql and mysql up and running
66+Code.require_file("scripts/macro/insert_bench.exs", __DIR__)
77+Code.require_file("scripts/macro/all_bench.exs", __DIR__)
+53
bench/scripts/macro/all_bench.exs
···11+# -----------------------------------Goal--------------------------------------
22+# Compare the performance of querying all objects of the different supported
33+# databases
44+55+# -------------------------------Description-----------------------------------
66+# This benchmark tracks performance of querying a set of objects registered in
77+# the database with Repo.all/2 function. The query pass through
88+# the steps of translating the SQL statements, sending them to the database and
99+# load the results into Ecto structures. Both, Ecto Adapters and Database itself
1010+# play a role and can affect the results of this benchmark.
1111+1212+# ----------------------------Factors(don't change)---------------------------
1313+# Different adapters supported by Ecto with the proper database up and running
1414+1515+# ----------------------------Parameters(change)-------------------------------
1616+# There is only a unique parameter in this benchmark, the User objects to be
1717+# fetched.
1818+1919+Code.require_file("../../support/setup.exs", __DIR__)
2020+2121+alias Ecto.Bench.User
2222+2323+limit = 5_000
2424+2525+users =
2626+ 1..limit
2727+ |> Enum.map(fn _ -> User.sample_data() end)
2828+2929+# We need to insert data to fetch
3030+Ecto.Bench.PgRepo.insert_all(User, users)
3131+Ecto.Bench.MyXQLRepo.insert_all(User, users)
3232+3333+jobs = %{
3434+ "Pg Repo.all/2" => fn -> Ecto.Bench.PgRepo.all(User, limit: limit) end,
3535+ "MyXQL Repo.all/2" => fn -> Ecto.Bench.MyXQLRepo.all(User, limit: limit) end
3636+}
3737+3838+path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
3939+file = Path.join(path, "all.json")
4040+4141+Benchee.run(
4242+ jobs,
4343+ formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
4444+ formatter_options: [json: [file: file]],
4545+ time: 10,
4646+ after_each: fn results ->
4747+ ^limit = length(results)
4848+ end
4949+)
5050+5151+# Clean inserted data
5252+Ecto.Bench.PgRepo.delete_all(User)
5353+Ecto.Bench.MyXQLRepo.delete_all(User)
+46
bench/scripts/macro/insert_bench.exs
···11+# -----------------------------------Goal--------------------------------------
22+# Compare the performance of inserting changesets and structs in the different
33+# supported databases
44+55+# -------------------------------Description-----------------------------------
66+# This benchmark tracks performance of inserting changesets and structs in the
77+# database with Repo.insert!/1 function. The query pass through
88+# the steps of translating the SQL statements, sending them to the database and
99+# returning the result of the transaction. Both, Ecto Adapters and Database itself
1010+# play a role and can affect the results of this benchmark.
1111+1212+# ----------------------------Factors(don't change)---------------------------
1313+# Different adapters supported by Ecto with the proper database up and running
1414+1515+# ----------------------------Parameters(change)-------------------------------
1616+# Different inputs to be inserted, aka Changesets and Structs
1717+1818+Code.require_file("../../support/setup.exs", __DIR__)
1919+2020+alias Ecto.Bench.User
2121+2222+inputs = %{
2323+ "Struct" => struct(User, User.sample_data()),
2424+ "Changeset" => User.changeset(User.sample_data())
2525+}
2626+2727+jobs = %{
2828+ "Exqlite Insert" => fn entry -> Ecto.Bench.ExqliteRepo.insert!(entry) end,
2929+ "Pg Insert" => fn entry -> Ecto.Bench.PgRepo.insert!(entry) end,
3030+ "MyXQL Insert" => fn entry -> Ecto.Bench.MyXQLRepo.insert!(entry) end
3131+}
3232+3333+path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
3434+file = Path.join(path, "insert.json")
3535+3636+Benchee.run(
3737+ jobs,
3838+ inputs: inputs,
3939+ formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
4040+ formatter_options: [json: [file: file]]
4141+)
4242+4343+# Clean inserted data
4444+Ecto.Bench.ExqliteRepo.delete_all(User)
4545+Ecto.Bench.PgRepo.delete_all(User)
4646+Ecto.Bench.MyXQLRepo.delete_all(User)
+55
bench/scripts/micro/load_bench.exs
···11+# -----------------------------------Goal--------------------------------------
22+# Compare the implementation of loading raw database data into Ecto structures by
33+# the different database adapters
44+55+# -------------------------------Description-----------------------------------
66+# Repo.load/2 is an important step of a database query.
77+# This benchmark tracks performance of loading "raw" data into ecto structures
88+# Raw data can be in different types (e.g. keyword lists, maps), in this tests
99+# we benchmark against map inputs
1010+1111+# ----------------------------Factors(don't change)---------------------------
1212+# Different adapters supported by Ecto, each one has its own implementation that
1313+# is tested against different inputs
1414+1515+# ----------------------------Parameters(change)-------------------------------
1616+# Different sizes of raw data(small, medium, big) and different attribute types
1717+# such as UUID, Date and Time fetched from the database and needs to be
1818+# loaded into Ecto structures.
1919+2020+Code.require_file("../../support/setup.exs", __DIR__)
2121+2222+alias Ecto.Bench.User
2323+2424+inputs = %{
2525+ "Small 1 Thousand" =>
2626+ 1..1_000 |> Enum.map(fn _ -> %{name: "Alice", email: "email@email.com"} end),
2727+ "Medium 100 Thousand" =>
2828+ 1..100_000 |> Enum.map(fn _ -> %{name: "Alice", email: "email@email.com"} end),
2929+ "Big 1 Million" =>
3030+ 1..1_000_000 |> Enum.map(fn _ -> %{name: "Alice", email: "email@email.com"} end),
3131+ "Time attr" =>
3232+ 1..100_000 |> Enum.map(fn _ -> %{name: "Alice", time_attr: ~T[21:25:04.361140]} end),
3333+ "Date attr" => 1..100_000 |> Enum.map(fn _ -> %{name: "Alice", date_attr: ~D[2018-06-20]} end),
3434+ "NaiveDateTime attr" =>
3535+ 1..100_000
3636+ |> Enum.map(fn _ -> %{name: "Alice", naive_datetime_attr: ~N[2019-06-20 21:32:07.424178]} end),
3737+ "UUID attr" =>
3838+ 1..100_000
3939+ |> Enum.map(fn _ -> %{name: "Alice", uuid: Ecto.UUID.bingenerate()} end)
4040+}
4141+4242+jobs = %{
4343+ "Pg Loader" => fn data -> Enum.map(data, &Ecto.Bench.PgRepo.load(User, &1)) end,
4444+ "MyXQL Loader" => fn data -> Enum.map(data, &Ecto.Bench.MyXQLRepo.load(User, &1)) end
4545+}
4646+4747+path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
4848+file = Path.join(path, "load.json")
4949+5050+Benchee.run(
5151+ jobs,
5252+ inputs: inputs,
5353+ formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
5454+ formatter_options: [json: [file: file]]
5555+)
+64
bench/scripts/micro/to_sql_bench.exs
···11+# -----------------------------------Goal--------------------------------------
22+# Compare the implementation of parsing Ecto.Query objects into SQL queries by
33+# the different database adapters
44+55+# -------------------------------Description-----------------------------------
66+# Repo.to_sql/2 is an important step of a database query.
77+# This benchmark tracks performance of parsing Ecto.Query structures into
88+# "raw" SQL query strings.
99+# Different Ecto.Query objects has multiple combinations and some different attributes
1010+# depending on the query type. In this tests we benchmark against different
1111+# query types and complexity.
1212+1313+# ----------------------------Factors(don't change)---------------------------
1414+# Different adapters supported by Ecto, each one has its own implementation that
1515+# is tested against different query inputs
1616+1717+# ----------------------------Parameters(change)-------------------------------
1818+# Different query objects (select, delete, update) to be translated into pure SQL
1919+# strings.
2020+2121+Code.require_file("../../support/setup.exs", __DIR__)
2222+2323+import Ecto.Query
2424+2525+alias Ecto.Bench.{User, Game}
2626+2727+inputs = %{
2828+ "Ordinary Select All" => {:all, from(User)},
2929+ "Ordinary Delete All" => {:delete_all, from(User)},
3030+ "Ordinary Update All" => {:update_all, from(User, update: [set: [name: "Thor"]])},
3131+ "Ordinary Where" => {:all, from(User, where: [name: "Thanos", email: "blah@blah"])},
3232+ "Fetch First Registry" => {:all, first(User)},
3333+ "Fetch Last Registry" => {:all, last(User)},
3434+ "Ordinary Order By" => {:all, order_by(User, desc: :name)},
3535+ "Complex Query 2 Joins" =>
3636+ {:all,
3737+ from(User, where: [name: "Thanos"])
3838+ |> join(:left, [u], ux in User, on: u.id == ux.id)
3939+ |> join(:right, [j], uj in User, on: j.id == 1 and j.email == "email@email")
4040+ |> select([u, ux], {u.name, ux.email})},
4141+ "Complex Query 4 Joins" =>
4242+ {:all,
4343+ from(User)
4444+ |> join(:left, [u], g in Game, on: g.name == u.name)
4545+ |> join(:right, [g], u in User, on: g.id == 1 and u.email == "email@email")
4646+ |> join(:inner, [u], g in fragment("SELECT * from games where game.id = ?", u.id))
4747+ |> join(:left, [g], u in fragment("SELECT * from users = ?", g.id))
4848+ |> select([u, g], {u.name, g.price})}
4949+}
5050+5151+jobs = %{
5252+ "Pg Query Builder" => fn {type, query} -> Ecto.Bench.PgRepo.to_sql(type, query) end,
5353+ "MyXQL Query Builder" => fn {type, query} -> Ecto.Bench.MyXQLRepo.to_sql(type, query) end
5454+}
5555+5656+path = System.get_env("BENCHMARKS_OUTPUT_PATH") || "bench/results"
5757+file = Path.join(path, "to_sql.json")
5858+5959+Benchee.run(
6060+ jobs,
6161+ inputs: inputs,
6262+ formatters: [Benchee.Formatters.JSON, Benchee.Formatters.Console],
6363+ formatter_options: [json: [file: file]]
6464+)
+15
bench/support/migrations.exs
···11+defmodule Ecto.Bench.CreateUser do
22+ use Ecto.Migration
33+44+ def change do
55+ create table(:users) do
66+ add(:name, :string)
77+ add(:email, :string)
88+ add(:password, :string)
99+ add(:time_attr, :time)
1010+ add(:date_attr, :date)
1111+ add(:naive_datetime_attr, :naive_datetime)
1212+ add(:uuid, :binary_id)
1313+ end
1414+ end
1515+end