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 mixiex -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.UserFetch 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)
3Where 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_allskips changeset validations and auto-generated fields. You must provideinserted_atandupdated_atmanually.
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/2t — Get type info
iex> t Ecto.Queryv — Print history
iex> v(-1) # Re-print the last result
iex> v(-2) # Re-print the second-to-last resultFlush — 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_consoleWarning: Be cautious when querying on production. Prefer read-only queries. Avoid
Repo.delete!andRepo.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.2msTips
| 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 |