backend-db-performance — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited backend-db-performance (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Systematic approach to identifying and fixing database performance issues.
Before ANY optimization, verify current state:
SELECT indexname, indexdef FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';ls -la supabase/migrations/ | grep -i "index\|optim\|perf"SELECT 1 FROM pg_indexes WHERE indexname = 'your_proposed_index';get_advisors MCP tool for performance/securityWhy: Duplicate indexes waste storage and slow writes. Always verify before adding.
Prisma - Enable query logging:
// lib/db.ts
import { PrismaClient } from '@prisma/client'
export const db = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
],
})
db.$on('query', (e) => {
if (e.duration > 100) { // Log queries > 100ms
console.log(`Slow query (${e.duration}ms):`, e.query)
}
})Supabase - Query analysis:
-- Enable query stats
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find slow queries
SELECT
query,
calls,
total_time / calls as avg_time_ms,
rows / calls as avg_rows
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 20;| Issue | Symptom | Solution |
|---|---|---|
| N+1 Queries | Many small queries | Use include / eager load |
| Missing Index | Slow WHERE/JOIN | Add index on filtered columns |
| Full Table Scan | Slow on large tables | Add index, limit results |
| Over-fetching | Slow response | Select only needed fields |
| No Pagination | Memory issues | Add cursor/offset pagination |
Problem: Fetching related data in loop
// Bad - N+1 queries
const posts = await db.post.findMany()
for (const post of posts) {
const author = await db.user.findUnique({ where: { id: post.authorId } })
// 1 query for posts + N queries for authors
}Solution: Eager loading
// Good - 2 queries total
const posts = await db.post.findMany({
include: {
author: true,
},
})
// Or with select for specific fields
const posts = await db.post.findMany({
include: {
author: {
select: { id: true, name: true, avatar: true }
},
},
})Supabase equivalent:
// Single query with join
const { data: posts } = await supabase
.from('posts')
.select(`
*,
author:users(id, name, avatar)
`)Add index when column is used in:
WHERE clauses (filtering)JOIN conditionsORDER BY clausesDon't add index when:
-- Single column index
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- Composite index (order matters!)
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);
-- Unique index
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Partial index (index subset of rows)
CREATE INDEX idx_posts_published ON posts(created_at)
WHERE published = true;
-- GIN index for JSONB/array
CREATE INDEX idx_posts_tags ON posts USING GIN(tags);
-- Full-text search
CREATE INDEX idx_posts_search ON posts
USING GIN(to_tsvector('english', title || ' ' || content));model Post {
id String @id @default(cuid())
userId String
title String
status Status
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
// Single column index
@@index([userId])
// Composite index
@@index([userId, createdAt(sort: Desc)])
// Unique constraint (creates unique index)
@@unique([userId, title])
}// Bad - fetches all columns
const users = await db.user.findMany()
// Good - fetches only needed
const users = await db.user.findMany({
select: {
id: true,
name: true,
email: true,
},
})Offset pagination (simple, but slow at high offsets):
const posts = await db.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
})Cursor pagination (better for large datasets):
const posts = await db.post.findMany({
take: limit,
skip: cursor ? 1 : 0, // Skip cursor itself
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
})
// Return next cursor
const nextCursor = posts.length === limit ? posts[posts.length - 1].id : null// Bad - individual inserts
for (const item of items) {
await db.item.create({ data: item })
}
// Good - batch insert
await db.item.createMany({
data: items,
skipDuplicates: true,
})
// Good - transaction for related data
await db.$transaction([
db.order.create({ data: order }),
db.orderItem.createMany({ data: orderItems }),
db.inventory.updateMany({ where: {...}, data: {...} }),
])// Get count without fetching data
const count = await db.post.count({
where: { published: true },
})
// Combined with pagination
const [posts, count] = await db.$transaction([
db.post.findMany({ where, take: limit, skip: offset }),
db.post.count({ where }),
])Normalize when:
Denormalize when:
-- Normalized (separate table)
CREATE TABLE post_stats (
post_id UUID PRIMARY KEY REFERENCES posts(id),
view_count INT DEFAULT 0,
like_count INT DEFAULT 0
);
-- Denormalized (same table)
ALTER TABLE posts
ADD COLUMN view_count INT DEFAULT 0,
ADD COLUMN like_count INT DEFAULT 0;-- Use appropriate types
id UUID DEFAULT gen_random_uuid() -- vs TEXT for IDs
status VARCHAR(20) -- vs unlimited TEXT
price DECIMAL(10,2) -- vs FLOAT for money
created_at TIMESTAMPTZ -- vs TIMESTAMP (include timezone)
-- Use enums for fixed values
CREATE TYPE status AS ENUM ('draft', 'published', 'archived');model Post {
id String @id
deletedAt DateTime?
@@index([deletedAt]) // Index for filtering
}
// Query pattern
const posts = await db.post.findMany({
where: { deletedAt: null },
})-- Bad: Function call in RLS (slow)
CREATE POLICY "slow_policy" ON posts
FOR SELECT USING (
user_id IN (SELECT user_id FROM team_members WHERE team_id = get_user_team())
);
-- Good: Direct comparison (fast)
CREATE POLICY "fast_policy" ON posts
FOR SELECT USING (user_id = auth.uid());
-- Good: Join-based (when needed)
CREATE POLICY "team_policy" ON posts
FOR SELECT USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_members.team_id = posts.team_id
AND team_members.user_id = auth.uid()
)
);// Move complex aggregations to Edge Functions
// instead of multiple round trips
// supabase/functions/dashboard-stats/index.ts
Deno.serve(async (req) => {
const stats = await supabase.rpc('get_dashboard_stats', {
user_id: userId
})
return new Response(JSON.stringify(stats))
})EXPLAIN ANALYZE
SELECT * FROM posts
WHERE user_id = 'abc123'
ORDER BY created_at DESC
LIMIT 20;
-- Look for:
-- - Seq Scan (bad on large tables)
-- - Index Scan (good)
-- - Nested Loop (check if N+1)
-- - High actual time| Metric | Target | Action if Exceeded |
|---|---|---|
| Query time | < 100ms | Add index, optimize |
| Rows scanned | < 10x returned | Add index |
| Memory usage | < 256MB | Add LIMIT, pagination |
| Connection count | < pool size | Use connection pooling |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.