Active Record vs Raw SQL
Fast recall
Quick revision
Core answer
Whether you know AR is the default (readable/safe) and raw SQL is for complex/perf-critical queries — but must be parameterized.
Using raw SQL with string interpolation (injection) — always bind; or avoiding raw SQL even when N+1 hits.
AR for normal CRUD/simple queries (readable, safe). Raw SQL for complex joins/aggregates/bulk where AR is awkward/slow — but parameterize.
Interview-ready guide
Interview answer
Active Record is the default query boundary in Rails. Relations compose with scopes, associations, authorization, and pagination; value conditions are bound safely; and loaded rows become models with the application's normal type casting and behaviour. That makes ordinary reads and writes understandable across the codebase.
Raw SQL is appropriate when SQL expresses the operation more honestly than a complicated relation. Examples include recursive common-table expressions, window functions, database-specific JSON or full-text operators, carefully designed bulk updates, and performance-sensitive reports whose exact query plan matters.
For example, ranking each sale within its region is naturally one query with ROW_NUMBER() OVER (PARTITION BY region_id ORDER BY total DESC). Recreating that operation by loading every sale and grouping in Ruby transfers more data and loses the database's set-based execution.
Raw SQL removes some Active Record protection. Placeholders or the adapter's bind API must keep values separate from syntax. The caller also needs to know whether the result is models, ActiveRecord::Result, affected-row count, or scalar values, and vendor-specific syntax can reduce portability.
I keep SQL in a repository or query object named after the business result, add tests for its result and edge cases, and review the execution plan with realistic data. The choice is not ORM versus performance: it is the clearest safe boundary that lets the database do the required work and the application understand the result.
Active Record vs Raw SQL
Direct Answer
Use Active Record relations for most Rails queries because they compose with scopes and associations, bind values safely, and return application models. Use carefully isolated SQL when the database operation is clearer or more capable in SQL—for example, a recursive CTE, window function, vendor-specific feature, bulk statement, or measured hot query that the relation API cannot express well. Raw SQL should keep values bound, define its return shape, remain adapter-aware, and be protected by focused tests; it is not a shortcut around understanding the generated query.
Deep Dive & Architecture
Active Record is strongest when the relation is still recognisable as a database operation. Scopes can add predicates, joins, projections, and ordering while preserving a relation for later composition. This also allows cross-cutting rules such as account scope to remain visible instead of being rebuilt inside a SQL string.
The abstraction becomes costly when a database-native operation is broken into many round trips or large Ruby collections. A raw query can express one atomic set operation, but only if its parameters, transaction boundary, and result mapping are explicit. Moving text into a heredoc alone does not make the design safer.
Safety has several dimensions. Bind parameters address injection for values, but identifiers and SQL fragments cannot generally be bound the same way and need a trusted allowlist. Read-only intent, timeouts, transaction locks, and permission boundaries still matter even when the syntax is parameterised.
Performance decisions need evidence. The generated SQL from Active Record may already match the hand-written form, and a missing index will harm both. Compare query plans, rows read, allocations, and end-to-end latency before accepting the maintenance and portability cost of a specialised query.
Code Example
class RegionalSalesRanking
SQL = <<~SQL
SELECT id, region_id, total_cents,
ROW_NUMBER() OVER (
PARTITION BY region_id ORDER BY total_cents DESC
) AS regional_rank
FROM sales
WHERE sold_at >= $1
SQL
def self.since(time)
bind = ActiveRecord::Relation::QueryAttribute.new(
'sold_at', time, ActiveRecord::Type::DateTime.new
)
ActiveRecord::Base.connection.exec_query(SQL, name, [bind])
end
end
Common Mistakes & Pitfalls
Using raw SQL with string interpolation (injection) — always bind; or avoiding raw SQL even when N+1 hits.
Check your understanding
Answer these without looking back at the article.
- Why can values be bound while dynamic table or column names need an allowlist?
- What result type does `exec_query` return?
- Which measurements justify accepting adapter-specific SQL?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.