n1-check
Detect N+1 query anti-patterns specifically — Repo calls inside Enum/for loops, missing preloads on associations. Use when N+1 is explicitly suspected, NOT for unrelated Ecto questions or wider database performance.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/n1-check
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
N+1 Query Detection
Identify and fix N+1 query anti-patterns in Ecto/Phoenix applications.
Iron Laws - Never Violate These
- Never access associations without preload - Always preload before
Enum.map - No Repo calls inside loops - Restructure to batch queries
- Preload at context boundary - Load associations in context, not controllers/views
- Use joins for filtering - Use
join+preloadwhen filtering by association
Detection Patterns
Pattern 1: Enum.map with Repo
# BAD: N+1 queries
users
|> Enum.map(fn user -> Repo.get(Order, user.order_id) end)
# GOOD: Single query with preload
users
|> Repo.preload(:orders)
Pattern 2: Association Access Without Preload
# BAD: Lazy loading triggers N queries
for user <- users do
user.posts # Triggers query for each user!
end
# GOOD: Eager load first
users = Repo.all(User) |> Repo.preload(:posts)
for user <- users do
user.posts # Already loaded
end
Pattern 3: Nested Association Access
# BAD: N+1 for nested associations
user.posts |> Enum.map(fn post -> post.comments end)
# GOOD: Nested preload
Repo.preload(user, posts: :comments)
Quick Detection Commands
Use Grep with context lines (-B 5 -A 5) to find Enum.map near Repo. calls in lib/**/*.ex.
Use Grep to find association access patterns (.posts, .comments, .orders) in lib/**/*.ex.
Use Grep with context (-B 3) to find Repo.get or Repo.one near loop patterns (for, Enum) in lib/**/*.ex.
Analysis Command
Use Grep to find all Repo. calls in a context module, then verify each query has appropriate preloads.
References
For detailed patterns, see:
${CLAUDE_SKILL_DIR}/references/preload-patterns.md- Efficient preloading strategies${CLAUDE_SKILL_DIR}/references/query-optimization.md- Query batching techniques
Files (claude-elixir-phoenix)
-
references
-
preload-patterns.md 2.9 KB
# Preload Patterns Efficient strategies for loading associations in Ecto. ## Basic Preloading ### Single Association ```elixir # In query User |> Repo.all() |> Repo.preload(:posts) # In query itself (single query with join) from u in User, preload: [:posts] |> Repo.all() ``` ### Nested Associations ```elixir # Preload nested: user -> posts -> comments Repo.preload(user, posts: :comments) # Multiple levels Repo.preload(user, posts: [comments: :author]) ``` ### Multiple Associations ```elixir # Multiple associations at same level Repo.preload(user, [:posts, :comments, :profile]) # Mixed nesting Repo.preload(user, [:profile, posts: :comments]) ``` ## Advanced Preloading ### Custom Query Preloads ```elixir # Preload only active posts active_posts_query = from p in Post, where: p.active == true user |> Repo.preload(posts: active_posts_query) ``` ### Preload with Ordering ```elixir ordered_posts = from p in Post, order_by: [desc: p.inserted_at] user |> Repo.preload(posts: ordered_posts) ``` ### Preload with Limit ```elixir # Get only latest 5 posts per user recent_posts = from p in Post, order_by: [desc: p.inserted_at], limit: 5 users |> Repo.preload(posts: recent_posts) ``` ## Join Preloading For filtering by associations, use joins: ```elixir # Find users with published posts (efficient) from u in User, join: p in assoc(u, :posts), where: p.published == true, preload: [posts: p] |> Repo.all() ``` ## Lateral Join for Top-N per Group ```elixir # Get top 3 posts per user (PostgreSQL) from u in User, inner_lateral_join: p in subquery( from p in Post, where: p.user_id == parent_as(:user).id, order_by: [desc: p.inserted_at], limit: 3 ), as: :user, preload: [posts: p] |> Repo.all() ``` ## Context-Level Preloading Always preload at the context boundary: ```elixir # In context module defmodule MyApp.Accounts do def get_user_with_posts!(id) do User |> Repo.get!(id) |> Repo.preload(:posts) end def list_users_with_profiles do User |> Repo.all() |> Repo.preload(:profile) end end ``` ## Preload in Pipelines ```elixir def list_active_users_with_orders do User |> where([u], u.active == true) |> Repo.all() |> Repo.preload(orders: :line_items) end ``` ## Anti-Patterns to Avoid ### Preloading in Views ```elixir # BAD: Preload in template <%= for post <- Repo.preload(@user, :posts).posts do %> # GOOD: Preload in controller/LiveView assigns = %{user: Accounts.get_user_with_posts!(id)} ``` ### Preloading Everything ```elixir # BAD: Over-preloading Repo.preload(user, [:posts, :comments, :likes, :followers, :following]) # GOOD: Preload only what's needed for the view Repo.preload(user, [:profile, posts: :comments]) ``` ### Conditional Preloading ```elixir # Use preload opts for conditional loading def get_user(id, opts \\ []) do user = Repo.get!(User, id) if Keyword.get(opts, :with_posts, false) do Repo.preload(user, :posts) else user end end ``` -
query-optimization.md 3.8 KB
# Query Optimization Techniques Strategies for optimizing Ecto queries and eliminating N+1 patterns. ## Batching Queries ### Replace Loop Queries with IN Clause ```elixir # BAD: N queries Enum.map(user_ids, fn id -> Repo.get(User, id) end) # GOOD: Single query from(u in User, where: u.id in ^user_ids) |> Repo.all() ``` ### Batch Inserts ```elixir # BAD: N inserts Enum.each(items, fn item -> %Item{} |> Item.changeset(item) |> Repo.insert() end) # GOOD: Single insert Repo.insert_all(Item, items) ``` ### Batch Updates ```elixir # BAD: N updates Enum.each(users, fn user -> user |> User.changeset(%{active: false}) |> Repo.update() end) # GOOD: Single update from(u in User, where: u.id in ^user_ids) |> Repo.update_all(set: [active: false]) ``` ## Query Composition ### Composable Query Functions ```elixir defmodule MyApp.Queries.UserQueries do import Ecto.Query def base, do: from(u in User) def active(query \\ base()) do from u in query, where: u.active == true end def with_posts(query \\ base()) do from u in query, preload: [:posts] end def recent(query \\ base(), days \\ 30) do cutoff = DateTime.utc_now() |> DateTime.add(-days, :day) from u in query, where: u.inserted_at > ^cutoff end end # Usage: Compose queries UserQueries.base() |> UserQueries.active() |> UserQueries.with_posts() |> UserQueries.recent(7) |> Repo.all() ``` ## Subqueries for Complex Filtering ### EXISTS Subquery ```elixir # Find users with at least one published post posts_subquery = from p in Post, where: p.user_id == parent_as(:user).id, where: p.published == true from u in User, as: :user, where: exists(posts_subquery) |> Repo.all() ``` ### Count Subquery ```elixir # Get users with post counts post_counts = from p in Post, group_by: p.user_id, select: %{user_id: p.user_id, count: count(p.id)} from u in User, left_join: pc in subquery(post_counts), on: pc.user_id == u.id, select: {u, pc.count} |> Repo.all() ``` ## Window Functions ### Ranking Within Groups ```elixir # Get top post per user from p in Post, windows: [user_window: [partition_by: p.user_id, order_by: [desc: p.likes]]], select: %{ post: p, rank: row_number() |> over(:user_window) } |> Repo.all() |> Enum.filter(fn %{rank: rank} -> rank == 1 end) ``` ## Database-Specific Optimizations ### PostgreSQL DISTINCT ON ```elixir # Latest post per user (PostgreSQL only) from p in Post, distinct: p.user_id, order_by: [asc: p.user_id, desc: p.inserted_at] |> Repo.all() ``` ### Index Hints Ensure indexes exist for: - Foreign keys (`user_id`, `post_id`) - Columns in WHERE clauses - Columns in ORDER BY - Columns used in joins ```elixir # Migration create index(:posts, [:user_id]) create index(:posts, [:published, :inserted_at]) ``` ## Avoiding Common Pitfalls ### Select Only Needed Fields ```elixir # BAD: Select all columns when only needing ids Repo.all(User) |> Enum.map(& &1.id) # GOOD: Select only what's needed from(u in User, select: u.id) |> Repo.all() ``` ### Use Streams for Large Datasets ```elixir # BAD: Load all into memory Repo.all(LargeTable) |> Enum.each(&process/1) # GOOD: Stream processing LargeTable |> Repo.stream() |> Stream.each(&process/1) |> Stream.run() ``` ### Aggregate in Database ```elixir # BAD: Count in Elixir Repo.all(User) |> length() # GOOD: Count in database Repo.aggregate(User, :count) ``` ## Monitoring Queries ### Telemetry for Query Logging ```elixir # In application.ex :telemetry.attach( "ecto-query-logger", [:my_app, :repo, :query], &MyApp.QueryLogger.handle_event/4, nil ) defmodule MyApp.QueryLogger do require Logger def handle_event(_event, measurements, metadata, _config) do if measurements.total_time > 100_000_000 do # > 100ms Logger.warning("Slow query: #{metadata.query}") end end end ```
-
-
SKILL.md 2.1 KB
--- name: n1-check description: "Detect N+1 query anti-patterns specifically — Repo calls inside Enum/for loops, missing preloads on associations. Use when N+1 is explicitly suspected, NOT for unrelated Ecto questions or wider database performance." effort: medium --- # N+1 Query Detection Identify and fix N+1 query anti-patterns in Ecto/Phoenix applications. ## Iron Laws - Never Violate These 1. **Never access associations without preload** - Always preload before `Enum.map` 2. **No Repo calls inside loops** - Restructure to batch queries 3. **Preload at context boundary** - Load associations in context, not controllers/views 4. **Use joins for filtering** - Use `join` + `preload` when filtering by association ## Detection Patterns ### Pattern 1: Enum.map with Repo ```elixir # BAD: N+1 queries users |> Enum.map(fn user -> Repo.get(Order, user.order_id) end) # GOOD: Single query with preload users |> Repo.preload(:orders) ``` ### Pattern 2: Association Access Without Preload ```elixir # BAD: Lazy loading triggers N queries for user <- users do user.posts # Triggers query for each user! end # GOOD: Eager load first users = Repo.all(User) |> Repo.preload(:posts) for user <- users do user.posts # Already loaded end ``` ### Pattern 3: Nested Association Access ```elixir # BAD: N+1 for nested associations user.posts |> Enum.map(fn post -> post.comments end) # GOOD: Nested preload Repo.preload(user, posts: :comments) ``` ## Quick Detection Commands Use Grep with context lines (`-B 5 -A 5`) to find `Enum.map` near `Repo.` calls in `lib/**/*.ex`. Use Grep to find association access patterns (`.posts`, `.comments`, `.orders`) in `lib/**/*.ex`. Use Grep with context (`-B 3`) to find `Repo.get` or `Repo.one` near loop patterns (`for`, `Enum`) in `lib/**/*.ex`. ## Analysis Command Use Grep to find all `Repo.` calls in a context module, then verify each query has appropriate preloads. ## References For detailed patterns, see: - `${CLAUDE_SKILL_DIR}/references/preload-patterns.md` - Efficient preloading strategies - `${CLAUDE_SKILL_DIR}/references/query-optimization.md` - Query batching techniques
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.