How to use IEx to query the database in Phoenix

Start an IEx session connected to your Phoenix app's Ecto repo and run queries interactively for debugging, exploration, and development.

IEx (Interactive Elixir) is the REPL that ships with Elixir. When you start it inside a Phoenix project, you get full access to your application code — including Ecto schemas, repos, and contexts. This makes it one of the most powerful tools for exploring and debugging your database.

Starting IEx with your Phoenix app

The key is to start IEx within your Phoenix project so it loads your application and connects to the database:

# From your Phoenix project root
iex -S mix

# For Phoenix servers (starts the web server too)
iex -S mix phx.server

# With a specific environment
MIX_ENV=test iex -S mix

iex -S mix compiles and loads your app. iex -S mix phx.server does the same but also starts the web server so you can hit your endpoints while exploring data.

Basic querying

Once in IEx, import Ecto.Query and alias your repo and schemas:

iex> import Ecto.Query
Ecto.Query

iex> alias MyApp.Repo
MyApp.Repo

iex> alias MyApp.Blog.Post
MyApp.Blog.Post

iex> alias MyApp.Blog.Comment
MyApp.Blog.Comment

iex> alias MyApp.Accounts.User
MyApp.Accounts.User

Fetch records

# Get all posts
iex> Repo.all(Post)

# Get a single record by ID
iex> Repo.get(Post, 1)

# Get by field (raises if not found with !)
iex> Repo.get_by!(Post, slug: "hello-world")

# Count records
iex> Repo.aggregate(Post, :count)
3

Where clauses

iex> Post |> where(published: true) |> Repo.all()

iex> Post |> where([p], p.views > 100) |> Repo.all()

iex> Post |> where([p], p.inserted_at > ago(7, "day")) |> Repo.all()

iex> Post |> where([p], ilike(p.title, ^"%elixir%")) |> Repo.all()

# Combine multiple conditions
iex> Post |> where(published: true) |> where([p], p.views > 50) |> Repo.all()

Ordering, limiting, and selecting fields

# Order by newest first
iex> Post |> order_by(desc: :inserted_at) |> Repo.all()

# Limit results
iex> Post |> order_by(desc: :inserted_at) |> limit(5) |> Repo.all()

# Select specific fields (returns maps or tuples)
iex> Post |> select([p], %{title: p.title, views: p.views}) |> Repo.all()
[%{title: "Hello", views: 42}, %{title: "World", views: 7}]

iex> Post |> select([p], {p.title, p.views}) |> Repo.all()
[{"Hello", 42}, {"World", 7}]

Joins

# Inner join posts with their authors
iex> Post
...> |> join(:inner, [p], a in assoc(p, :author))
...> |> select([p, a], {p.title, a.name})
...> |> Repo.all()
[{"Hello", "Alice"}, {"World", "Bob"}]

# Left join (includes posts without authors)
iex> Post
...> |> join(:left, [p], a in assoc(p, :author))
...> |> select([p, a], {p.title, a.name})
...> |> Repo.all()

# Join and preload associations
iex> Post
...> |> join(:inner, [p], c in assoc(p, :comments))
...> |> preload([p, c], comments: c)
...> |> Repo.all()

# Join on a specific field
iex> Post
...> |> join(:inner, [p], u in User, on: p.author_id == u.id)
...> |> where([p, u], u.role == "admin")
...> |> select([p, u], {p.title, u.email})
...> |> Repo.all()

Aggregates and group_by

# Count posts per author
iex> Post
...> |> group_by([p], p.author_id)
...> |> select([p], %{author_id: p.author_id, count: count(p.id)})
...> |> Repo.all()

# Average views with grouping
iex> Post
...> |> group_by([p], p.author_id)
...> |> select([p], %{author_id: p.author_id, avg_views: avg(p.views)})
...> |> Repo.all()

# Aggregate with a join
iex> Post
...> |> join(:inner, [p], a in assoc(p, :author))
...> |> group_by([p, a], a.name)
...> |> select([p, a], %{author: a.name, post_count: count(p.id)})
...> |> Repo.all()

Using context functions

Your contexts provide a clean API layer. Use them from IEx the same way your LiveViews and controllers do:

iex> alias MyApp.Blog

iex> Blog.list_posts()
iex> Blog.get_post!(1)
iex> Blog.create_post(%{title: "From IEx", body: "Created from console"})
iex> Blog.update_post(post, %{title: "Updated from IEx"})
iex> Blog.delete_post(post)

Inserting, updating, and deleting

When you need to bypass contexts (quick data seeding, debugging):

# Insert
iex> %Post{title: "Hello", body: "World"} |> Repo.insert!()

# Insert with changeset (runs validations)
iex> Post.changeset(%Post{}, %{title: "Hello", body: "World"}) |> Repo.insert!()

# Update
iex> post = Repo.get!(Post, 1)
iex> Post.changeset(post, %{title: "Updated"}) |> Repo.update!()

# Delete
iex> Repo.delete!(post)

# Bulk insert
iex> Repo.insert_all(Post, [
...>   [title: "First", body: "Body 1", inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()],
...>   [title: "Second", body: "Body 2", inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()]
...> ])

Note: Repo.insert_all skips changeset validations and auto-generated fields. You must provide inserted_at and updated_at manually.

Inspecting the schema

IEx is great for exploring your schemas at runtime:

# Check a schema's fields
iex> Post.__schema__(:fields)
[:id, :title, :body, :slug, :views, :published_at, :author_id, :inserted_at, :updated_at]

# Check associations
iex> Post.__schema__(:associations)
[:author, :comments]

# Check a specific association
iex> Post.__schema__(:association, :comments)
%Ecto.Association.Has{...}

# Inspect a struct
iex> post = Repo.get!(Post, 1)
iex> IO.inspect(post, structs: false)
%{__meta__: ..., id: 1, title: "Hello", ...}

Useful IEx tricks for database work

h — Get documentation

iex> h Ecto.Query.from/2
iex> h Repo.all/1
iex> h Repo.insert/2

t — Get type info

iex> t Ecto.Query

v — Print history

iex> v(-1)   # Re-print the last result
iex> v(-2)   # Re-print the second-to-last result

Flush — Print pending messages

iex> flush()

c — Recompile changed files

When you change a module in your editor and want to use it in IEx:

iex> recompile()

Break out of a stuck query

If a query hangs (e.g., a lock), press Ctrl+\\ to force-kill the IEx process, or use Ctrl+C twice.

Connecting to a running production node

For production debugging, you can attach to a running BEAM node:

# Find the node name (check rel/vm.args or your release config)
# Then connect:
iex --name [email protected] --remsh [email protected] --cookie <secret_cookie>

# Or use remote_console on Heroku / Gigalixir
gigalixir ps:remote_console

Warning: Be cautious when querying on production. Prefer read-only queries. Avoid Repo.delete! and Repo.update! unless you’re certain.

Debugging slow queries

# Ecto logs queries to the console by default.
# To see the SQL without executing:

iex> import Ecto.Query
iex> query = from p in Post, where: p.views > 100
iex> Repo.to_sql(:all, query)
{"SELECT p0.\"id\", p0.\"title\", ... FROM \"posts\" AS p0 WHERE (p0.\"views\" > 100)", []}

# Time a query
iex> :timer.tc(fn -> Repo.all(Post) end)
{1200, [%Post{...}, ...]}  # 1.2ms

Tips

Tip Description
Use ! variants sparingly Repo.get! raises on nil — fine for IEx, risky in app code
Alias your modules Saves typing; alias MyApp.Blog.Post instead of the full path
Use ` >` pipe style
Check Repo.to_sql/2 See the generated SQL before running
recompile() after edits Pick up code changes without restarting IEx
Use contexts in IEx They validate your data just like the web layer does