README.md

# EctoQueryExplorer

EctoQueryExplorer is a tool used to gather and analyse Ecto query telemetry.

> [!WARNING]
> EctoQueryExplorer is in its experimental stage. Use it at your own risk.

## Movivation

We've already identified a slow query and now want to be able to:

1. locate code that produced a slow database query,
2. run EXPLAIN for the query in question,
3. do 1 and 2 very fast.

We also want to be better understand a bigger picture: which code produces which queries, how often,
how much time time it takes for those queries to finish.

## Design

EctoQueryExplorer relies on query telemetry emitted by Ecto, specifically on stacktraces included in
telemetry. Stacktraces were added to telemetry in https://github.com/elixir-ecto/ecto/pull/3798.

EctoQueryExplorer uses an ETS table to collect samples, queries, params, code locations and MFAs.
Then EctoQueryExplorer saves collected data to SQLite3 to run analysis on top of.

 looks like this:

[![SQLite3 database schema](ecto-query-explorer.png)](https://hex.pm/packages/ecto)

### Caveats

* EctoQueryExplorer will not attempt to persist data across application restarts,
* usage in clustered environments have not been tested,
* the Readme file you're looking at is the only documentation right now.

## Installing

Installation boils down do installing `ecto_query_explorer` and configuring a new repository
to store the data.

Perform the following steps:

1. include the following line to `deps` list in `mix.exs`:

    ```elixir
    {:ecto_query_explorer, "~> 0.1"}
    ```

2. add a new repository module:

    ```elixir
    defmodule MyApp.EctoQueryExplorerRepo do
      use Ecto.Repo,
        otp_app: :my_app,
        adapter: Ecto.Adapters.SQLite3
    end
    ```

3. include the the following contents to `config/config.exs`:

    ```elixir
    # add EctoQueryExplorerRepo to the list of repositories
    config :my_app,
      ecto_repos: [MyApp.MyRepo, MyApp.EctoQueryExplorerRepo]

    # enable stacktraces in MyApp.MyRepo config
    config :my_app, MyApp.MyRepo,
      stacktrace: true

    # configure EctoQueryExplorerRepo
    config :my_app, MyApp.EctoQueryExplorerRepo,
      database: "/tmp/ecto-query-explorer-#{config_env()}.sqlite3"

    # tell ecto_query_explorer about EctoQueryExplorerRepo
    config :ecto_query_explorer,
      repo: MyApp.EctoQueryExplorerRepo,
      otp_app: :my_app
    ```

4. update `application.ex`, include repository and library setup:

    ```elixir
    # ...
    children = [
      MyApp.EctoQueryExplorerRepo,
      {EctoQueryExplorer, Application.get_all_env(:ecto_query_explorer)},
    ]
    ```

5. create a database migration with the following contents:

    ```elixir
    # priv/ecto_query_explorer_repo/migrations/1_initial.exs
    defmodule EctoQueryExplorer.Repo.Migrations.Initial do
      use Ecto.Migration

      def up do
        EctoQueryExplorer.Migration0.up()
      end

      def down do
        EctoQueryExplorer.Migration0.down()
      end
    end
    ```

6. run migration:

    ```sh
    mix ecto.create --repo MyApp.EctoQueryExplorerRepo + migrate --repo MyApp.EctoQueryExplorerRepo
    ```

## Usage

Start your application, and keep it running to collect some query data. After a while, call:

```elixir
EctoQueryExplorer.Data.dump2sqlite()
```

If you'd like to store and analyse queries emitted by the test suite after it finishes, add the following
code to `test_helper.exs`:

```elixir
ExUnit.after_suite(fn _args ->
  EctoQueryExplorer.Data.dump2sqlite()
end)
```

After the SQLite3 database file is generated, you may use the library in 3 different ways.

### Using convenience functions

The following convenience functions are provided by EctoQueryExplorer:

* `EctoQueryExplorer.Queries.filter_by_query/1` - accepts a string containing a regular
  expression and returns a query with samples, including location data.

  See "[The LIKE, GLOB, REGEXP, MATCH, and extract operators](https://www.sqlite.org/lang_expr.html#the_like_glob_regexp_match_and_extract_operators)"
  for more details about the regular expressions.

  Example:

  ```elixir
  EctoQueryExplorer.Queries.filter_by_query("update%")
  ```

* `EctoQueryExplorer.Queries.filter_by_parameter/1` - use this one if you're looking for a query that was executing with a specific parameter value,

  Example:

  ```elixir
  user_id = "da06de53-34bb-4b6a-9f36-0e8478b05458"
  EctoQueryExplorer.Queries.filter_by_parameter(user_id)
  ```

  Notes:

  1. for a given query, parameters are saved only for 3 slowest samples,
  2. when calling this function, all stored parameters will fetched from the SQLite3 table and decoded using `:erlang.binary_to_term/1` function.

* `EctoQueryExplorer.Queries.filter_by_mfa/3` - return all queries, which were produced by stacktraces that contain given module, function and arity.

  Example:

  ```elixir
  EctoQueryExplorer.Queries.filter_by_mfa(User, :register, 1)
  ```

* `EctoQueryExplorer.Queries.filter_by_location/2` - return all queries, which were produced by stacktraces that contain given file and line number.

  Example:

  ```elixir
  EctoQueryExplorer.Queries.filter_by_location("lib/my_app/path/to/code/module.ex", 12)
  ```

* `EctoQueryExplorer.Queries.explain/1` - accepts query ID, and returns a map containing query plan. Can be used to produce JSON for tools like https://explain.dalibo.com.

  Example:

  ```elixir
  report = EctoQueryExplorer.Queries.explain(123_234_345)
  Jason.encode!(report) # JSON to be used for further analysis or plan visualisation
  ```

  Notes:

  * the query will be wrapped in a transaction, which is reverted after producing the `explain` report.
  * query the database using familiar Ecto queries. Examples:
  * query the file directly using `sqlite3` command line utility. Examples:

### Using Ecto queries

### Using `sqlite3` CLI utility

Consider running the query using `.mode table` and `.width auto` for nicer outputs. Run queries like this:

```sh
sqlite3 -cmd ".mode table" -cmd ".width auto 160" /tmp/ecto-query-explorer-prod.sqlite3 'query string'
```

Here are some examples:

1. get top 5 most popular queries:

    ```sql
    select counter, text from queries order by counter desc limit 5
    ```

2. get top 5 most popular stacktraces:

    ```sql
    select id from stacktraces order by counter desc limit 5
    ```

3. find all queries that have at least 25 parameters in them:

    ```sql
    select id, text from queries where text like '%$25%' order by counter desc limit 5
    ```

4. get size of the database:

    <details>
    <summary><code>sqlite3-stats.sql</code></summary>

    ```sql
    with
    records as (
      select 'queries' name, count(1) total_records from queries
      union
      select 'samples' name, count(1) total_records from samples
      union
      select 'functions' name, count(1) total_records from functions
      union
      select 'locations' name, count(1) total_records from locations
      union
      select 'stacktraces' name, count(1) total_records from stacktraces
      union
      select 'stacktrace_entries' name, count(1) total_records from stacktrace_entries
      union
      select 'params' name, count(1) total_records from params
    ),
    sizes as (
        select SUM(pgsize) bytes,
               name
          from dbstat
         where name in ('queries', 'samples', 'functions', 'locations', 'stacktrace_entries', 'params', 'stacktraces')
      group by name
    )
      select r.name,
             total_records,
             bytes
        from records r
        join sizes s on s.name = r.name
    order by bytes desc
    ```
    </details>

## License

EctoQueryExplorer is licensed under MIT license. See [LICENSE](LICENSE) for more details.