Timescale seems pretty popular, but it's not exactly clear how to configure PostgreSQL extensions in Elixir. So here's my take.

Prerequisites

  • PostgreSQL 11
  • TimescaleDB

I've already set up my postgre tables so I'll be continuing from that. For the basics, check out the official Ecto docs. I've pasted my table creation migration at the bottom also.

Load the extension into PostgreSQL

For Postgre to use an extension, it must first be loaded.

TimescaleDB has made a comfy automatic configurator timescaledb-tune. It does a bunch of performance enhancing things, but since I want to know exactly what's going on, I'm adding the extension manually.

Basically just add a line to the postgresql.conf with this command.

echo "shared_preload_libraries = 'timescaledb'" | sudo tee -a /var/lib/postgres/data/postgresql.conf

Now restart postgresql with sudo systemctl restart postgresql.

Create migrations

First create the migrations file in your app.

mix ecto.gen.migration create_extension_timescaledb

And in the new file we attach the TimescaleDB to Postgre:

# priv/repo/migrations/20200327131930_create_extension_timescale.exs

defmodule Bashboard.Repo.Migrations.CreateExtensionTimescale do
  use Ecto.Migration

  def up do
    execute("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE")
    execute("SELECT create_hypertable('messages', 'inserted_at')")
  end

  def down do
    execute("DROP EXTENSION IF EXISTS timescaledb CASCADE")
  end
end

Thanks firehose@github for giving a clear example.

So first it attaches the extension to PostgreSQL, then it creates the tables it needs.

Run migrations

Just run the command

mix ecto.migrate

Now everything should be set up to use Timescale in your Elixir app.

Troubleshooting

cannot create a unique index

My migrations worked, but the SELECT command didn't, got an error:

16:23:49.343 [info]  execute "SELECT create_hypertable('messages', 'inserted_at')"
** (Postgrex.Error) ERROR TS103 (nil) cannot create a unique index without the column "inserted_at" (used in partitioning)
    (ecto_sql) lib/ecto/adapters/sql.ex:612: Ecto.Adapters.SQL.raise_sql_call_error/1
    (elixir) lib/enum.ex:1336: Enum."-map/2-lists^map/1-0-"/2
    (ecto_sql) lib/ecto/adapters/sql.ex:699: Ecto.Adapters.SQL.execute_ddl/4
    (ecto_sql) lib/ecto/migration/runner.ex:343: Ecto.Migration.Runner.log_and_execute_ddl/3
    (ecto_sql) lib/ecto/migration/runner.ex:117: anonymous fn/6 in Ecto.Migration.Runner.flush/0
    (elixir) lib/enum.ex:1948: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto_sql) lib/ecto/migration/runner.ex:116: Ecto.Migration.Runner.flush/0
    (stdlib) timer.erl:166: :timer.tc/1

The solution to this was to make the timestamp() column primary key. Since I'm resetting my db anyway, here's my new table creation migration:

defmodule Bashboard.Repo.Migrations.CreateMessages do
  use Ecto.Migration

  def change do
    create table(:messages) do
      add(:user, :string, null: false)
      add(:dash, :string, null: false)
      add(:widget, :string, null: false)
      add(:body, :map)

      timestamps(
        type: :utc_datetime,
        updated_at: false,
        primary_key: true   # this was missing
      )
    end

    create(index(:messages, [:user, :dash, :widget]))
  end
end

People on the Timescale Slack have also explained further:

Flushing after table creation

I didn't stumble on this issue, but @camoz did, so if you're defining a table and making a hypertable in a single migration, then flush it in between:

def up do
  create table("foos", primary_key: false) do
    add :start_minute, :utc_datetime, primary_key: true
    # some more columns
    timestamps(type: :utc_datetime)
  end
  flush()
  execute("SELECT create_hypertable('foos', 'start_minute')")
end

Attribution

Thanks camonz@elixir-slack for a lot of help!