mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-31 10:19:38 -04:00
Save DB space and egress by removing prompts answered by <= 5 people
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import chalk from 'chalk'
|
||||
import {runScript} from './run-script'
|
||||
|
||||
// Deletes "orphaned" compatibility answers — rows whose `question_id` points at
|
||||
// a compatibility_prompts row that no longer exists. These accumulated because
|
||||
// neither answer table had a foreign key on question_id, so deleting a prompt
|
||||
// left its answers behind. Covers both answer tables:
|
||||
// - compatibility_answers (multiple-choice answers)
|
||||
// - compatibility_answers_free (free-response answers)
|
||||
//
|
||||
// The companion migration
|
||||
// supabase/migrations/20250101005800_add_compatibility_answers_question_fk.sql
|
||||
// adds `question_id -> compatibility_prompts(id) ON DELETE CASCADE` so this
|
||||
// can't recur. That migration also clears orphans inline (a FK can't be added
|
||||
// while violating rows exist), so this script is mainly for a quick ad-hoc
|
||||
// cleanup / dry-run audit before the migration is applied.
|
||||
//
|
||||
// Dry-run by default: set DELETE=1 in the environment to actually delete.
|
||||
// Safe to re-run; idempotent.
|
||||
|
||||
const TABLES = ['compatibility_answers', 'compatibility_answers_free'] as const
|
||||
|
||||
runScript(async ({pg}) => {
|
||||
const dryRun = process.env.DELETE !== '1'
|
||||
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Finding orphaned compatibility answers (question_id with no matching prompt)… ${
|
||||
dryRun ? chalk.yellow('(DRY RUN — set DELETE=1 to delete)') : chalk.red('(DELETING)')
|
||||
}`,
|
||||
),
|
||||
)
|
||||
|
||||
let grandTotal = 0
|
||||
for (const table of TABLES) {
|
||||
const orphanCount = await pg.one(
|
||||
`SELECT count(*)::int AS n
|
||||
FROM ${table} a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM compatibility_prompts p WHERE p.id = a.question_id
|
||||
)`,
|
||||
[],
|
||||
(r: {n: number}) => r.n,
|
||||
)
|
||||
|
||||
console.log(chalk.blue(` ${table}: ${orphanCount} orphaned answer(s).`))
|
||||
grandTotal += orphanCount
|
||||
|
||||
if (orphanCount === 0 || dryRun) continue
|
||||
|
||||
const deleted = await pg.result(
|
||||
`DELETE FROM ${table} a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM compatibility_prompts p WHERE p.id = a.question_id
|
||||
)`,
|
||||
[],
|
||||
(r) => r.rowCount,
|
||||
)
|
||||
console.log(chalk.green(` [DELETED] ${deleted} row(s) from ${table}`))
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
chalk.yellow(`\nDry run complete — ${grandTotal} orphaned row(s) found, none deleted.`),
|
||||
)
|
||||
} else {
|
||||
console.log(chalk.cyan('\n── Deletion complete ───────────────────────────────'))
|
||||
console.log(chalk.green(` Orphans removed : ${grandTotal}`))
|
||||
console.log(chalk.cyan('────────────────────────────────────────────────────'))
|
||||
}
|
||||
process.exit(0)
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import chalk from 'chalk'
|
||||
import {runScript} from './run-script'
|
||||
|
||||
// Deletes every row in `compatibility_prompts` answered by fewer than N
|
||||
// distinct people.
|
||||
//
|
||||
// "Answered by a person" means that person's user id appears as a creator_id in
|
||||
// either answer table for the prompt:
|
||||
// - compatibility_answers (multiple-choice answers)
|
||||
// - compatibility_answers_free (free-response answers)
|
||||
// We count DISTINCT creator_id across both tables (a single person who answered
|
||||
// in both still counts once), then delete prompts whose count is < N. N defaults
|
||||
// to 1 (so the default deletes only prompts nobody answered); set THRESHOLD=k to
|
||||
// delete every prompt with fewer than k distinct answerers.
|
||||
//
|
||||
// Deleting a prompt cascades to compatibility_prompts_translations
|
||||
// (fk_question ON DELETE CASCADE), so its translations are removed too. This
|
||||
// reclaims DB storage and cuts egress from clients fetching dead prompts.
|
||||
//
|
||||
// Dry-run by default: set DELETE=1 in the environment to actually delete.
|
||||
// Safe to re-run; processed in batches to avoid long-running transactions.
|
||||
|
||||
// See compatibility_prompts_rows.sql and compatibility_answers_rows.csv (private) for backup
|
||||
// with all the 2800 prompts and answers in case we need to restore some of them.
|
||||
|
||||
const BATCH_SIZE = 500
|
||||
|
||||
runScript(async ({pg}) => {
|
||||
const dryRun = process.env.DELETE !== '1'
|
||||
|
||||
const threshold = 2
|
||||
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Finding compatibility prompts answered by fewer than ${threshold} people… ${
|
||||
dryRun ? chalk.yellow('(DRY RUN — set DELETE=1 to delete)') : chalk.red('(DELETING)')
|
||||
}`,
|
||||
),
|
||||
)
|
||||
|
||||
const ids = await pg.map(
|
||||
`SELECT p.id, p.question, coalesce(ac.answerers, 0)::int AS answerers
|
||||
FROM compatibility_prompts p
|
||||
LEFT JOIN (
|
||||
SELECT question_id, count(DISTINCT creator_id) AS answerers
|
||||
FROM (
|
||||
-- multiple_choice = -1 means the person skipped the prompt; don't count it
|
||||
SELECT question_id, creator_id FROM compatibility_answers
|
||||
WHERE multiple_choice IS DISTINCT FROM -1
|
||||
UNION
|
||||
SELECT question_id, creator_id FROM compatibility_answers_free
|
||||
WHERE multiple_choice IS DISTINCT FROM -1
|
||||
) u
|
||||
GROUP BY question_id
|
||||
) ac ON ac.question_id = p.id
|
||||
WHERE coalesce(ac.answerers, 0) <= $1
|
||||
ORDER BY p.id`,
|
||||
[threshold],
|
||||
(row: {id: string; question: string; answerers: number}) => row,
|
||||
)
|
||||
|
||||
console.log(
|
||||
chalk.blue(`Found ${ids.length} prompt(s) answered by fewer than ${threshold} people.`),
|
||||
)
|
||||
if (ids.length < 2000) {
|
||||
for (const {id, question, answerers} of ids) {
|
||||
console.log(chalk.gray(` id=${id} (${answerers} answerer(s)) — ${question}`))
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
console.log(chalk.green('Nothing to delete.'))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(chalk.yellow('\nDry run complete — no rows deleted.'))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
let totalDeleted = 0
|
||||
const allIds = ids.map((r) => r.id)
|
||||
for (let i = 0; i < allIds.length; i += BATCH_SIZE) {
|
||||
const batch = allIds.slice(i, i + BATCH_SIZE)
|
||||
const deleted = await pg.result(
|
||||
`DELETE FROM compatibility_prompts WHERE id = ANY($1::bigint[])`,
|
||||
[batch],
|
||||
(r) => r.rowCount,
|
||||
)
|
||||
totalDeleted += deleted
|
||||
console.log(chalk.green(` [DELETED] batch of ${deleted} prompt(s)`))
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('\n── Deletion complete ───────────────────────────────'))
|
||||
console.log(chalk.green(` Deleted : ${totalDeleted}`))
|
||||
console.log(chalk.cyan('────────────────────────────────────────────────────'))
|
||||
process.exit(0)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
-- Get expensive queries (in terms of total rows)
|
||||
-- Run in Supabase -> SQL editor -> Export -> Copy as CSV
|
||||
SELECT query,
|
||||
calls,
|
||||
total_exec_time,
|
||||
@@ -6,16 +7,20 @@ SELECT query,
|
||||
1.0 * total_exec_time / calls AS avg_time
|
||||
FROM pg_stat_statements
|
||||
ORDER BY rows DESC
|
||||
LIMIT 10;
|
||||
LIMIT 20;
|
||||
|
||||
-- Get row size (in bytes)
|
||||
SELECT count(*) as sampled_rows,
|
||||
pg_size_pretty(sum(pg_column_size(t.*))) as total_data_size,
|
||||
pg_size_pretty(avg(pg_column_size(t.*))) as avg_row_size
|
||||
pg_size_pretty(avg(pg_column_size(t.*))) as bytes_per_row
|
||||
FROM (
|
||||
-- Paste your actual query here, but add a LIMIT
|
||||
SELECT *
|
||||
from user_events
|
||||
LIMIT 1000) t;
|
||||
|
||||
-- Multiply row size by the number of rows to get the get total egress size for each query (since pg_stat_statement was last reset)
|
||||
-- Multiply row size by the number of rows to get the get total egress size for each query (since pg_stat_statement was last reset)
|
||||
-- Do this in Supabase Largest Queries.xlsx (private file)
|
||||
|
||||
-- Reset pg_stat_statements (for next time, to have stats starting from today)
|
||||
SELECT pg_stat_statements_reset();
|
||||
@@ -1,23 +1,31 @@
|
||||
CREATE TABLE IF NOT EXISTS compatibility_answers (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
creator_id TEXT NOT NULL,
|
||||
explanation TEXT,
|
||||
importance INTEGER NOT NULL,
|
||||
multiple_choice INTEGER NOT NULL,
|
||||
pref_choices INTEGER[] NOT NULL,
|
||||
question_id BIGINT NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS compatibility_answers
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
creator_id TEXT NOT NULL,
|
||||
explanation TEXT,
|
||||
importance INTEGER NOT NULL,
|
||||
multiple_choice INTEGER NOT NULL,
|
||||
pref_choices INTEGER[] NOT NULL,
|
||||
question_id BIGINT NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE compatibility_answers
|
||||
ADD CONSTRAINT compatibility_answers_creator_id_fkey
|
||||
FOREIGN KEY (creator_id)
|
||||
REFERENCES users(id)
|
||||
REFERENCES users (id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE compatibility_answers
|
||||
ADD CONSTRAINT compatibility_answers_question_id_fkey
|
||||
FOREIGN KEY (question_id)
|
||||
REFERENCES compatibility_prompts (id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE compatibility_answers ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE compatibility_answers
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE compatibility_answers
|
||||
ADD CONSTRAINT unique_question_creator
|
||||
|
||||
@@ -14,6 +14,12 @@ ALTER TABLE compatibility_answers_free
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE compatibility_answers_free
|
||||
ADD CONSTRAINT compatibility_answers_free_question_id_fkey
|
||||
FOREIGN KEY (question_id)
|
||||
REFERENCES compatibility_prompts(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE compatibility_answers_free ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
Reference in New Issue
Block a user