FAQ / Troubleshooting
Practical answers to common questions and failure modes when working with mat_views (definitions → services → jobs → rake tasks), the demo app, and PostgreSQL materialized views.
Quick diagnostics (copy-paste)
Is my MV there? What’s its schema?
SELECT schemaname, matviewname FROM pg_matviews ORDER BY 1,2;
What unique indexes exist on an MV?
SELECT i.relname AS index_name, idx.indisunique, a.attname AS column
FROM pg_class t
JOIN pg_index idx ON idx.indrelid = t.oid
JOIN pg_class i ON i.oid = idx.indexrelid
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(idx.indkey)
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public' AND t.relname = 'mv_user_activity';
Who’s holding a lock on my MV?
SELECT pid, usename, state, query
FROM pg_stat_activity
WHERE query ILIKE '%mv_user_activity%'
ORDER BY query_start;
Latest run status per view (refresh):
SELECT d.name, r.status, r.duration_ms, r.finished_at
FROM mat_view_definitions d
LEFT JOIN LATERAL (
SELECT * FROM mat_view_refresh_runs r
WHERE r.mat_view_definition_id = d.id
ORDER BY r.created_at DESC
LIMIT 1
) r ON true
ORDER BY d.name;
General
“Nothing happens when I run a rake task”
- Ensure Rails environment is loaded (
=> :environment
is present). - Tasks log via
Rails.logger
(notputs
). In development:- Ensure the logger outputs to STDOUT and the log level isn’t too high.
- If the task enqueues jobs, make sure your queue backend is running (or use
config.active_job.queue_adapter = :inline
for immediate execution in dev). - Add
--yes
orYES=1
to bypass confirmation prompts.
“Adapter guesses the wrong backend?”
- It does not guess. The adapter calls the unified API:
MatViews::Jobs::Adapter.enqueue(job, queue:, args:)
It relies on your app’s configured backend (ActiveJob, Sidekiq, Resque). Configure your backend per your app’s needs.
Definitions (schema & SQL)
“Invalid view name format”
- Definitions must use simple identifiers (no dots):
mv_user_activity
✅,public.mv_user_activity
❌ - Schema is resolved via
search_path
and quoted safely by services.
“View exists in DB but there’s no MatViewDefinition”
- Tasks intentionally raise a guardrail error if you pass
public.my_mv
and there’s no matchingMatViewDefinition
. - Fix: create the definition record (preferred) or drop the orphan MV manually if it’s truly obsolete.
Create
“Create failed with SQL error”
- Check your definition’s
sql
. Run it as a plainSELECT
first. - Verify permissions (e.g., on referenced tables).
- For large outputs, consider
WITH NO DATA
→ index → refresh (see Best Practices).
“Create idempotently”
- Use the job/task flow; pass
force
only when you want to rebuild. - Pattern:
CREATE ... WITH NO DATA
, then build indexes concurrently, then refresh concurrently.
Refresh
“cannot refresh materialized view concurrently without a unique index
”
- Concurrent refresh requires a unique index that covers all rows of the MV result.
-
Create it first (often on
id
/user_id
):CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_mv_user_activity_user_id ON "public"."mv_user_activity"(user_id);
“Concurrent refresh fails in a transaction”
REFRESH MATERIALIZED VIEW CONCURRENTLY
cannot run inside a transaction block.- Ensure your orchestration does not wrap it with
BEGIN … COMMIT
.
“Regular refresh locks readers”
- That’s expected for regular refresh. Use concurrent (requires unique index) or the swap strategy to minimize read downtime.
“Refresh fails due to locks”
- Check
pg_stat_activity
to find long transactions or sessions referencing the MV. - Consider retry logic, stagger refresh schedules, or switch to swap if appropriate.
Swap refresh
“Indexes/privileges disappeared after swap”
- The new temp MV must receive the same indexes and grants before you rename it into place.
- Ensure your
SwapRefresh
implementation recreates all required indexes and permissions.
Delete
“Dependencies exist. Use cascade: true”
- Another object (e.g., a view) depends on the MV. Drop dependents first or pass
cascade: true
. - Rake task:
mat_views:delete_by_name[mv_name,true,--yes]
“Delete should be idempotent”
- Use
if_exists: true
(default in the service). Result is:skipped
when the MV isn’t present.
Rake tasks
“How do I skip confirmation?”
-
Pass
--yes
as the last arg or setYES=1
:bin/rake 'mat_views:create_all[,--yes]' YES=1 bin/rake 'mat_views:refresh_all[]'
“Which arguments do tasks accept?”
- Create:
[view_name, force, --yes]
/[definition_id, force, --yes]
/[force, --yes]
-
Refresh: [view_name, row_count_strategy, --yes]
(estimatedexact blank) -
Delete: [view_name, cascade, --yes]
(truefalse)
“Name vs schema-qualified name?”
- You may pass
public.mv_name
. If a physical MV exists but no definition, tasks raise to avoid acting on unknown objects. - Prefer passing definition names (simple identifier).
Jobs & Adapter
“Which queue do jobs use?”
MatViews.configuration.job_queue
(default:default
). Override in your initializer.
“Do jobs record results anywhere?”
- Yes. Each job writes a run row:
- Create →
mat_view_create_runs
- Refresh →
mat_view_refresh_runs
- Delete →
mat_view_delete_runs
- Create →
- They include
status
,started_at
,finished_at
,duration_ms
,error
, andmeta
.
“How do I enqueue?”
MatViews::Jobs::Adapter.enqueue(
MatViews::RefreshViewJob,
queue: MatViews.configuration.job_queue,
args: [definition.id, :estimated] # or :exact
)
- The adapter does not detect your backend; it assumes it’s already configured (ActiveJob/Sidekiq/Resque).
Validation & Benchmarking
“Validator says: no MVs found”
-
Create views first (bootstrap demo or your own definitions):
bin/rake 'mat_views:bootstrap_demo[1,--yes]' # or bin/rake 'mat_views:create_all[,--yes]' && bin/rake 'mat_views:refresh_all[,--yes]'
“Found MVs in DB but no definitions”
- The validator needs definitions to get the baseline SQL.
- Ensure
MatViews::MatViewDefinition
exists for each MV name.
“Row counts differ (baseline vs MV)”
- Your MV definition may have drifted from the baseline SQL or missed filters/joins.
- Recreate/swap the MV with the current definition.
- For stricter checks, extend the validator to compare aggregates or sampled rows.
“Huge variance in timings”
- Run more iterations (e.g., 100–300).
- Ensure a quiet DB; watch
pg_stat_activity
. - Consider a warmup run if you want hot-cache numbers.
Permissions
“Permission denied on MV”
-
Grant read access explicitly:
GRANT SELECT ON "public"."mv_user_activity" TO app_user;
-
Also verify privileges on underlying tables if you run baseline SQL in validation.
Search path & quoting
“My view is in a non-public schema”
- The services resolve schema from
search_path
, including quoted tokens and$user
. If the resolved schema doesn’t exist, they fallback topublic
. - For explicit control, keep
search_path
predictable per environment.
“Why do you quote identifiers?”
- We quote
schema
andrelation
withquote_table_name
/ identifier rules to avoid conflicts with mixed case or reserved words.
Testing & CI
“How do I silence logs in specs?”
-
In RSpec, mock
Rails.logger
with a spy:let(:fake_logger) { instance_spy(Logger, info: ->(*){}, warn: ->(*){}, error: ->(*){}, debug: ->(*){}) } allow(Rails).to receive(:logger).and_return(fake_logger)
“How do I test enqueue calls?”
-
Set up a spy:
allow(MatViews::Jobs::Adapter).to receive(:enqueue) # ... expect(MatViews::Jobs::Adapter).to have_received(:enqueue) .with(MatViews::RefreshViewJob, queue: anything, args: [defn.id, :estimated])
Common error → fix cheatsheet
Error message (snippet) | Likely cause | Fix |
---|---|---|
Invalid view name format | Definition name has a dot or invalid chars | Use simple identifier (e.g., mv_users ) |
cannot refresh ... concurrently without a unique index | Missing unique index | Create unique index; refresh again |
... inside a transaction block | Running concurrent refresh in a transaction | Run outside BEGIN ... COMMIT |
dependencies exist. Use cascade: true | Dropping MV with dependents | Drop dependents or cascade: true |
No MatViews::MatViewDefinition found | Name mismatch / missing definition | Create the definition or rename MV |
“No MVs found” in validator | Nothing in pg_matviews | Create MVs (bootstrap or create tasks) |
Row counts mismatch | MV SQL drifted | Recreate/swap MV; compare SQL carefully |
Jobs don’t run | Backend not running | Start your queue engine or use :inline in dev |
Still stuck?
- Inspect the run rows (create/refresh/delete) for error strings and timing.
- Look at
meta[:sql]
in responses or logs for the exact SQL executed. - Enable more verbose DB logs (Postgres
log_statement
,log_duration
) in a dev/staging environment. - Raise an issue on the project’s GitHub with details about your setup, Postgres version, and any relevant logs.
- We offer Professional support for complex issues or custom integrations. Contact us at sales@codevedas.com for inquiries.