feat: 🎸 benchmarks and seeds
This commit is contained in:
510
scripts/seed/seedCustomerProductShapes.ts
Normal file
510
scripts/seed/seedCustomerProductShapes.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
import {
|
||||
AppEnv,
|
||||
customerEntitlements,
|
||||
customerPrices,
|
||||
customerProducts,
|
||||
type Entitlement,
|
||||
type FreeTrial,
|
||||
type InsertCustomerEntitlement,
|
||||
type InsertCustomerProduct,
|
||||
type Product,
|
||||
rollovers,
|
||||
} from "@autumn/shared";
|
||||
import { initDrizzle } from "@server/db/initDrizzle.js";
|
||||
import { loadLocalEnv } from "@server/utils/envUtils.js";
|
||||
import { generateId } from "@server/utils/genUtils.js";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { TEST_ORG_CONFIG } from "../setupTestUtils/createTestOrg.js";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const CONFIG = {
|
||||
batchSize: 2000,
|
||||
rolloverRatio: 0.15,
|
||||
looseCeRatio: 0.2,
|
||||
shapes: [
|
||||
{ name: "empty", weight: 40 },
|
||||
{ name: "single_main", weight: 30 },
|
||||
{ name: "main_plus_addon", weight: 15 },
|
||||
{ name: "main_with_trial", weight: 10 },
|
||||
{ name: "power_user", weight: 5 },
|
||||
] as const,
|
||||
} as const;
|
||||
|
||||
type ShapeName = (typeof CONFIG.shapes)[number]["name"];
|
||||
|
||||
interface CliArgs {
|
||||
org_slug: string;
|
||||
wipe?: boolean;
|
||||
limit?: number;
|
||||
start_offset?: number;
|
||||
skip_seeded_check?: boolean;
|
||||
resume?: boolean;
|
||||
}
|
||||
|
||||
const parseArgs = (): CliArgs => {
|
||||
const args = process.argv.slice(2);
|
||||
const parsed: CliArgs = { org_slug: TEST_ORG_CONFIG.slug };
|
||||
for (const arg of args) {
|
||||
if (!arg.startsWith("--")) continue;
|
||||
const [key, value] = arg.slice(2).split("=");
|
||||
switch (key) {
|
||||
case "org_slug":
|
||||
parsed.org_slug = value;
|
||||
break;
|
||||
case "wipe":
|
||||
parsed.wipe = true;
|
||||
break;
|
||||
case "limit":
|
||||
parsed.limit = Number.parseInt(value, 10);
|
||||
break;
|
||||
case "start_offset":
|
||||
parsed.start_offset = Number.parseInt(value, 10);
|
||||
break;
|
||||
case "skip_seeded_check":
|
||||
parsed.skip_seeded_check = true;
|
||||
break;
|
||||
case "resume":
|
||||
parsed.resume = true;
|
||||
break;
|
||||
default:
|
||||
console.error(chalk.red(`Unknown flag: --${key}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const weightedPick = <T extends { weight: number; name: string }>(
|
||||
choices: readonly T[],
|
||||
): T => {
|
||||
const total = choices.reduce((s, c) => s + c.weight, 0);
|
||||
let r = Math.random() * total;
|
||||
for (const c of choices) {
|
||||
r -= c.weight;
|
||||
if (r <= 0) return c;
|
||||
}
|
||||
return choices[choices.length - 1];
|
||||
};
|
||||
|
||||
const randInt = (min: number, max: number) =>
|
||||
Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const randFloat = (min: number, max: number) =>
|
||||
Math.random() * (max - min) + min;
|
||||
const pick = <T>(arr: readonly T[]): T =>
|
||||
arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Seed Customer Products / Entitlements ================\n",
|
||||
),
|
||||
);
|
||||
const args = parseArgs();
|
||||
const env = AppEnv.Sandbox;
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
const org = await db.query.organizations.findFirst({
|
||||
where: (orgs, { eq }) => eq(orgs.slug, args.org_slug),
|
||||
});
|
||||
if (!org) throw new Error(`Org '${args.org_slug}' not found`);
|
||||
console.log(chalk.cyan(`Org: ${org.slug} (${org.id})`));
|
||||
|
||||
if (args.wipe) {
|
||||
console.log(chalk.cyan("Wiping existing bench cps/ces/rollovers..."));
|
||||
await db.execute(sql`
|
||||
DELETE FROM rollovers WHERE cus_ent_id IN (
|
||||
SELECT ce.id FROM customer_entitlements ce
|
||||
JOIN customers c ON c.internal_id = ce.internal_customer_id
|
||||
WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%'
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DELETE FROM customer_entitlements ce
|
||||
USING customers c
|
||||
WHERE c.internal_id = ce.internal_customer_id
|
||||
AND c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%'
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DELETE FROM customer_prices cpr
|
||||
USING customer_products cp, customers c
|
||||
WHERE cpr.customer_product_id = cp.id
|
||||
AND cp.internal_customer_id = c.internal_id
|
||||
AND c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%'
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DELETE FROM customer_products cp
|
||||
USING customers c
|
||||
WHERE c.internal_id = cp.internal_customer_id
|
||||
AND c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%'
|
||||
`);
|
||||
console.log(chalk.green("✅ Wipe complete\n"));
|
||||
}
|
||||
|
||||
const products = (await db.execute(sql`
|
||||
SELECT p.internal_id, p.id, p.is_add_on, p.is_default, p.group, p.created_at
|
||||
FROM products p
|
||||
WHERE p.org_id = ${org.id}
|
||||
AND p.archived = false
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM entitlements e WHERE e.internal_product_id = p.internal_id
|
||||
)
|
||||
ORDER BY p.created_at
|
||||
`)) as unknown as Array<
|
||||
Pick<Product, "internal_id" | "id" | "is_add_on" | "is_default" | "group" | "created_at">
|
||||
>;
|
||||
|
||||
const mainProducts = products.filter((p) => !p.is_add_on);
|
||||
const addonProducts = products.filter((p) => p.is_add_on);
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` ${mainProducts.length} main products, ${addonProducts.length} addons (must have at least 1 entitlement)`,
|
||||
),
|
||||
);
|
||||
if (mainProducts.length === 0) throw new Error("No main products to seed with");
|
||||
|
||||
const allEnts = (await db.execute(sql`
|
||||
SELECT e.id, e.internal_product_id, e.internal_feature_id, e.allowance,
|
||||
e.allowance_type, e.interval, e.interval_count, e.usage_limit,
|
||||
e.entity_feature_id, e.rollover, e.feature_id
|
||||
FROM entitlements e
|
||||
JOIN products p ON p.internal_id = e.internal_product_id
|
||||
WHERE p.org_id = ${org.id} AND p.archived = false
|
||||
`)) as unknown as Array<
|
||||
Pick<
|
||||
Entitlement,
|
||||
| "id"
|
||||
| "internal_product_id"
|
||||
| "internal_feature_id"
|
||||
| "allowance"
|
||||
| "allowance_type"
|
||||
| "interval"
|
||||
| "interval_count"
|
||||
| "usage_limit"
|
||||
| "entity_feature_id"
|
||||
| "rollover"
|
||||
| "feature_id"
|
||||
>
|
||||
>;
|
||||
const entsByProduct = new Map<string, typeof allEnts>();
|
||||
for (const e of allEnts) {
|
||||
const list = entsByProduct.get(e.internal_product_id) ?? [];
|
||||
list.push(e);
|
||||
entsByProduct.set(e.internal_product_id, list);
|
||||
}
|
||||
console.log(
|
||||
chalk.gray(` ${allEnts.length} entitlements across products\n`),
|
||||
);
|
||||
|
||||
const freeTrials = (await db.execute(sql`
|
||||
SELECT id, internal_product_id, length, duration FROM free_trials
|
||||
`)) as unknown as Array<Pick<FreeTrial, "id" | "internal_product_id"> & { length: number; duration: string }>;
|
||||
const trialsByProduct = new Map<string, typeof freeTrials>();
|
||||
for (const ft of freeTrials) {
|
||||
const list = trialsByProduct.get(ft.internal_product_id) ?? [];
|
||||
list.push(ft);
|
||||
trialsByProduct.set(ft.internal_product_id, list);
|
||||
}
|
||||
|
||||
const features = (await db.execute(sql`
|
||||
SELECT internal_id, id, type FROM features
|
||||
WHERE org_id = ${org.id} AND archived = false
|
||||
`)) as unknown as Array<{ internal_id: string; id: string; type: string }>;
|
||||
const meteredFeatures = features.filter((f) => f.type !== "boolean");
|
||||
|
||||
const seededHas = await db.execute(sql`
|
||||
SELECT COUNT(*)::int AS n FROM customer_products cp
|
||||
JOIN customers c ON c.internal_id = cp.internal_customer_id
|
||||
WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%'
|
||||
`);
|
||||
const existing = (seededHas as unknown as { n: number }[])[0].n;
|
||||
if (existing > 0 && !args.skip_seeded_check && !args.wipe) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ ${existing.toLocaleString()} bench customer_products already exist. Re-run with --wipe to reset, or --skip_seeded_check to add more.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const customersToSeed = (await db.execute(sql`
|
||||
SELECT c.internal_id, c.id, c.created_at
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%'
|
||||
ORDER BY c.created_at
|
||||
${args.limit ? sql`LIMIT ${args.limit}` : sql``}
|
||||
`)) as unknown as Array<{ internal_id: string; id: string; created_at: number }>;
|
||||
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Seeding cp/ce/rollover/cprice across ${customersToSeed.length.toLocaleString()} cus_bench_* customers`,
|
||||
),
|
||||
);
|
||||
|
||||
const shapeCounts: Record<ShapeName, number> = {
|
||||
empty: 0,
|
||||
single_main: 0,
|
||||
main_plus_addon: 0,
|
||||
main_with_trial: 0,
|
||||
power_user: 0,
|
||||
};
|
||||
const cpRows: InsertCustomerProduct[] = [];
|
||||
const ceRows: InsertCustomerEntitlement[] = [];
|
||||
const rolloverRows: Array<typeof rollovers.$inferInsert>[number][] = [];
|
||||
const cprRows: Array<typeof customerPrices.$inferInsert>[number][] = [];
|
||||
let looseCeCount = 0;
|
||||
|
||||
const startedAt = performance.now();
|
||||
|
||||
for (const cus of customersToSeed) {
|
||||
const shape = weightedPick(CONFIG.shapes).name;
|
||||
shapeCounts[shape]++;
|
||||
if (shape === "empty") continue;
|
||||
|
||||
const mainProd = pick(mainProducts);
|
||||
const startsAt = cus.created_at + randInt(60_000, 86_400_000);
|
||||
const cpsForThisCustomer: Array<{
|
||||
cpId: string;
|
||||
prod: typeof mainProd;
|
||||
isAddon: boolean;
|
||||
trialEndsAt: number | null;
|
||||
freeTrialId: string | null;
|
||||
}> = [];
|
||||
|
||||
cpsForThisCustomer.push({
|
||||
cpId: generateId("cus_prod"),
|
||||
prod: mainProd,
|
||||
isAddon: false,
|
||||
trialEndsAt: null,
|
||||
freeTrialId: null,
|
||||
});
|
||||
|
||||
if (shape === "main_plus_addon" || shape === "power_user") {
|
||||
const addonCount = shape === "power_user" ? randInt(2, 3) : 1;
|
||||
const shuffledAddons = [...addonProducts].sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < Math.min(addonCount, shuffledAddons.length); i++) {
|
||||
cpsForThisCustomer.push({
|
||||
cpId: generateId("cus_prod"),
|
||||
prod: shuffledAddons[i],
|
||||
isAddon: true,
|
||||
trialEndsAt: null,
|
||||
freeTrialId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (shape === "main_with_trial") {
|
||||
const trials = trialsByProduct.get(mainProd.internal_id);
|
||||
if (trials && trials.length > 0) {
|
||||
const trial = pick(trials);
|
||||
const trialEndsAt =
|
||||
startsAt + (trial.length ?? 7) * 24 * 60 * 60 * 1000;
|
||||
cpsForThisCustomer[0].trialEndsAt = trialEndsAt;
|
||||
cpsForThisCustomer[0].freeTrialId = trial.id;
|
||||
}
|
||||
}
|
||||
|
||||
for (const cpEntry of cpsForThisCustomer) {
|
||||
const subId = `sub_${cpEntry.cpId}`;
|
||||
cpRows.push({
|
||||
id: cpEntry.cpId,
|
||||
internal_customer_id: cus.internal_id,
|
||||
internal_product_id: cpEntry.prod.internal_id,
|
||||
product_id: cpEntry.prod.id,
|
||||
customer_id: cus.id,
|
||||
created_at: startsAt,
|
||||
starts_at: startsAt,
|
||||
status: cpEntry.trialEndsAt ? "trialing" : "active",
|
||||
processor: { type: "stripe", id: subId },
|
||||
subscription_ids: [subId],
|
||||
scheduled_ids: [],
|
||||
quantity: cpEntry.isAddon ? randInt(1, 5) : 1,
|
||||
is_custom: false,
|
||||
canceled: false,
|
||||
trial_ends_at: cpEntry.trialEndsAt,
|
||||
free_trial_id: cpEntry.freeTrialId,
|
||||
api_version: 23,
|
||||
api_semver: "2.3.0",
|
||||
billing_version: "v2",
|
||||
collection_method: "charge_automatically",
|
||||
});
|
||||
|
||||
cprRows.push({
|
||||
id: generateId("cus_price"),
|
||||
created_at: startsAt,
|
||||
price_id: null,
|
||||
internal_customer_id: cus.internal_id,
|
||||
customer_product_id: cpEntry.cpId,
|
||||
});
|
||||
|
||||
const prodEnts = entsByProduct.get(cpEntry.prod.internal_id) ?? [];
|
||||
for (const ent of prodEnts) {
|
||||
const isUnlimited = ent.allowance_type === "unlimited";
|
||||
const isBoolean = ent.allowance_type === null;
|
||||
const allowance = Number(ent.allowance ?? 0);
|
||||
const balance = isUnlimited
|
||||
? 0
|
||||
: isBoolean
|
||||
? 0
|
||||
: Math.max(0, allowance - randInt(0, Math.max(1, allowance)));
|
||||
const nextResetAt = ent.interval && ent.interval !== "lifetime"
|
||||
? startsAt + 30 * 24 * 60 * 60 * 1000
|
||||
: null;
|
||||
const ceId = generateId("cus_ent");
|
||||
ceRows.push({
|
||||
id: ceId,
|
||||
customer_product_id: cpEntry.cpId,
|
||||
entitlement_id: ent.id,
|
||||
internal_customer_id: cus.internal_id,
|
||||
internal_feature_id: ent.internal_feature_id,
|
||||
customer_id: cus.id,
|
||||
feature_id: ent.feature_id,
|
||||
unlimited: isUnlimited,
|
||||
balance,
|
||||
additional_balance: 0,
|
||||
adjustment: 0,
|
||||
created_at: startsAt,
|
||||
next_reset_at: nextResetAt,
|
||||
usage_allowed: ent.usage_limit != null,
|
||||
entities: null,
|
||||
cache_version: 0,
|
||||
});
|
||||
|
||||
if (
|
||||
!isBoolean &&
|
||||
!isUnlimited &&
|
||||
allowance > 0 &&
|
||||
Math.random() < CONFIG.rolloverRatio
|
||||
) {
|
||||
rolloverRows.push({
|
||||
id: generateId("rollover"),
|
||||
cus_ent_id: ceId,
|
||||
balance: randFloat(0, allowance * 0.5),
|
||||
expires_at: startsAt + 60 * 24 * 60 * 60 * 1000,
|
||||
entities: {},
|
||||
usage: randFloat(0, allowance * 0.2),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(shape === "power_user" || Math.random() < CONFIG.looseCeRatio) &&
|
||||
meteredFeatures.length > 0
|
||||
) {
|
||||
const looseCount = shape === "power_user" ? randInt(2, 4) : 1;
|
||||
const used = new Set<string>();
|
||||
for (let i = 0; i < looseCount; i++) {
|
||||
const feat = pick(meteredFeatures);
|
||||
if (used.has(feat.internal_id)) continue;
|
||||
used.add(feat.internal_id);
|
||||
const looseAllowance = randInt(50, 500);
|
||||
ceRows.push({
|
||||
id: generateId("cus_ent"),
|
||||
customer_product_id: null,
|
||||
entitlement_id: pick(allEnts).id,
|
||||
internal_customer_id: cus.internal_id,
|
||||
internal_feature_id: feat.internal_id,
|
||||
customer_id: cus.id,
|
||||
feature_id: feat.id,
|
||||
unlimited: false,
|
||||
balance: randInt(0, looseAllowance),
|
||||
additional_balance: 0,
|
||||
adjustment: 0,
|
||||
created_at: startsAt,
|
||||
next_reset_at: null,
|
||||
usage_allowed: false,
|
||||
entities: null,
|
||||
cache_version: 0,
|
||||
});
|
||||
looseCeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cpRows.length >= CONFIG.batchSize) {
|
||||
await flush({ db, cpRows, ceRows, cprRows, rolloverRows });
|
||||
}
|
||||
}
|
||||
|
||||
await flush({ db, cpRows, ceRows, cprRows, rolloverRows });
|
||||
|
||||
const elapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
|
||||
console.log(chalk.green(`\n✅ Done in ${elapsed}s`));
|
||||
console.log(chalk.bold("\nShape distribution:"));
|
||||
for (const [name, count] of Object.entries(shapeCounts)) {
|
||||
const pct = ((count / customersToSeed.length) * 100).toFixed(1);
|
||||
console.log(` ${name.padEnd(20)} ${count.toLocaleString().padStart(8)} (${pct}%)`);
|
||||
}
|
||||
console.log(chalk.gray(` loose ces inserted: ${looseCeCount.toLocaleString()}`));
|
||||
|
||||
const finalStats = await db.execute(sql`
|
||||
SELECT
|
||||
(SELECT COUNT(*)::int FROM customer_products cp JOIN customers c ON c.internal_id = cp.internal_customer_id WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%') AS cps,
|
||||
(SELECT COUNT(*)::int FROM customer_entitlements ce JOIN customers c ON c.internal_id = ce.internal_customer_id WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%') AS ces,
|
||||
(SELECT COUNT(*)::int FROM customer_prices cpr JOIN customer_products cp ON cp.id = cpr.customer_product_id JOIN customers c ON c.internal_id = cp.internal_customer_id WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%') AS cprs,
|
||||
(SELECT COUNT(*)::int FROM rollovers ro JOIN customer_entitlements ce ON ce.id = ro.cus_ent_id JOIN customers c ON c.internal_id = ce.internal_customer_id WHERE c.org_id = ${org.id} AND c.env = ${env} AND c.id LIKE 'cus_bench_%') AS rollovers
|
||||
`);
|
||||
const totals = (finalStats as unknown as Record<string, number>[])[0];
|
||||
console.log(chalk.bold("\nDB totals after seed (cus_bench_* only):"));
|
||||
for (const [k, v] of Object.entries(totals)) {
|
||||
console.log(` ${k.padEnd(20)} ${Number(v).toLocaleString().padStart(8)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(chalk.red("\n❌ Seed failed:"));
|
||||
if (e instanceof Error) {
|
||||
console.error(chalk.red(` ${e.message}`));
|
||||
console.error(chalk.gray(e.stack));
|
||||
} else {
|
||||
console.error(chalk.red(` ${String(e)}`));
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
};
|
||||
|
||||
const chunk = <T>(arr: T[], size: number): T[][] => {
|
||||
const out: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
||||
return out;
|
||||
};
|
||||
|
||||
const flush = async ({
|
||||
db,
|
||||
cpRows,
|
||||
ceRows,
|
||||
cprRows,
|
||||
rolloverRows,
|
||||
}: {
|
||||
db: ReturnType<typeof initDrizzle>["db"];
|
||||
cpRows: InsertCustomerProduct[];
|
||||
ceRows: InsertCustomerEntitlement[];
|
||||
cprRows: Array<typeof customerPrices.$inferInsert>;
|
||||
rolloverRows: Array<typeof rollovers.$inferInsert>;
|
||||
}) => {
|
||||
for (const batch of chunk(cpRows, 1000))
|
||||
await db.insert(customerProducts).values(batch);
|
||||
for (const batch of chunk(cprRows, 1000))
|
||||
await db.insert(customerPrices).values(batch);
|
||||
for (const batch of chunk(ceRows, 1000))
|
||||
await db.insert(customerEntitlements).values(batch);
|
||||
for (const batch of chunk(rolloverRows, 1000))
|
||||
await db.insert(rollovers).values(batch);
|
||||
const total =
|
||||
cpRows.length + ceRows.length + cprRows.length + rolloverRows.length;
|
||||
process.stdout.write(
|
||||
chalk.gray(
|
||||
`\r flushed ${cpRows.length.toLocaleString()} cp, ${ceRows.length.toLocaleString()} ce, ${cprRows.length.toLocaleString()} cpr, ${rolloverRows.length.toLocaleString()} ro (total ${total.toLocaleString()})`,
|
||||
),
|
||||
);
|
||||
cpRows.length = 0;
|
||||
ceRows.length = 0;
|
||||
cprRows.length = 0;
|
||||
rolloverRows.length = 0;
|
||||
};
|
||||
|
||||
await main();
|
||||
process.exit(0);
|
||||
@@ -25,6 +25,7 @@ interface CliArgs {
|
||||
skip_entities?: boolean;
|
||||
wipe?: boolean;
|
||||
count?: number;
|
||||
start_offset?: number;
|
||||
}
|
||||
|
||||
const parseArgs = (): CliArgs => {
|
||||
@@ -48,11 +49,14 @@ const parseArgs = (): CliArgs => {
|
||||
case "count":
|
||||
parsed.count = Number.parseInt(value, 10);
|
||||
break;
|
||||
case "start_offset":
|
||||
parsed.start_offset = Number.parseInt(value, 10);
|
||||
break;
|
||||
default:
|
||||
console.error(chalk.red(`Unknown flag: --${key}`));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"\nUsage: bun run scripts/seed/seedPaginationBenchmark.ts [--count=<n>] [--skip_entities] [--wipe] [--org_slug=<slug>]",
|
||||
"\nUsage: bun run scripts/seed/seedPaginationBenchmark.ts [--count=<n>] [--start_offset=<n>] [--skip_entities] [--wipe] [--org_slug=<slug>]",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -175,27 +179,30 @@ const seedCustomers = async ({
|
||||
orgId,
|
||||
env,
|
||||
count,
|
||||
startOffset = 0,
|
||||
}: {
|
||||
db: ReturnType<typeof initDrizzle>["db"];
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
count: number;
|
||||
startOffset?: number;
|
||||
}) => {
|
||||
const now = Date.now();
|
||||
const rangeMs = CONFIG.timeRangeDays * 24 * 60 * 60 * 1000;
|
||||
const startMs = now - rangeMs;
|
||||
|
||||
const endIndex = startOffset + count;
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Seeding ${count.toLocaleString()} customers (${CONFIG.timeRangeDays}-day range, batch=${CONFIG.batchSize})...`,
|
||||
`Seeding indices ${startOffset.toLocaleString()}..${(endIndex - 1).toLocaleString()} (${count.toLocaleString()} customers, ${CONFIG.timeRangeDays}-day range, batch=${CONFIG.batchSize})...`,
|
||||
),
|
||||
);
|
||||
|
||||
const startedAt = performance.now();
|
||||
let inserted = 0;
|
||||
|
||||
for (let batchStart = 0; batchStart < count; batchStart += CONFIG.batchSize) {
|
||||
const batchEnd = Math.min(batchStart + CONFIG.batchSize, count);
|
||||
for (let batchStart = startOffset; batchStart < endIndex; batchStart += CONFIG.batchSize) {
|
||||
const batchEnd = Math.min(batchStart + CONFIG.batchSize, endIndex);
|
||||
const rows: CustomerRow[] = [];
|
||||
|
||||
for (let i = batchStart; i < batchEnd; i++) {
|
||||
@@ -342,20 +349,39 @@ const main = async () => {
|
||||
);
|
||||
|
||||
const existingCount = existing[0]?.count ?? 0;
|
||||
let startOffset = args.start_offset ?? 0;
|
||||
let seedCount = customerCount;
|
||||
|
||||
if (existingCount > 0 && !args.wipe) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Found ${existingCount.toLocaleString()} existing bench customers. Re-run with --wipe to reset, or skip seeding.`,
|
||||
),
|
||||
);
|
||||
if (existingCount >= customerCount) {
|
||||
console.log(chalk.green("✅ Seed already satisfies target count — exiting."));
|
||||
return;
|
||||
if (args.start_offset === undefined) {
|
||||
if (existingCount >= customerCount) {
|
||||
console.log(
|
||||
chalk.green("✅ Seed already satisfies target count — exiting."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
startOffset = existingCount;
|
||||
seedCount = customerCount - existingCount;
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Continuing seed: indices ${startOffset.toLocaleString()}..${(startOffset + seedCount - 1).toLocaleString()} (${seedCount.toLocaleString()} customers).`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log(chalk.yellow(`Continuing seed to fill remaining ${(customerCount - existingCount).toLocaleString()} customers.`));
|
||||
}
|
||||
|
||||
await seedCustomers({ db, orgId: org.id, env, count: customerCount });
|
||||
await seedCustomers({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
count: seedCount,
|
||||
startOffset,
|
||||
});
|
||||
|
||||
if (!args.skip_entities) {
|
||||
await seedEntities({
|
||||
|
||||
153
server/experiments/benchDashboardMerge.ts
Normal file
153
server/experiments/benchDashboardMerge.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { AppEnv, RELEVANT_STATUSES } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { CusBatchService } from "../src/internal/customers/CusBatchService";
|
||||
import { CusSearchService } from "../src/internal/customers/CusSearchService";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import {
|
||||
type FlattenedCustomerRow,
|
||||
reassembleFlattenedCustomer,
|
||||
} from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV = AppEnv.Sandbox;
|
||||
const REPEATS = 5;
|
||||
const LIMIT = parseInt(process.env.LIMIT ?? "500", 10);
|
||||
const OFFSET_PCT = parseInt(process.env.OFFSET_PCT ?? "95", 10);
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Dashboard Merge Bench ================\n",
|
||||
),
|
||||
);
|
||||
const { db, client } = initDrizzle();
|
||||
const ctx = {
|
||||
db,
|
||||
client,
|
||||
org: { id: ORG_ID, slug: "unit-test-org" },
|
||||
env: ENV,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
},
|
||||
} as any;
|
||||
|
||||
try {
|
||||
const total = (
|
||||
(await db.execute(
|
||||
sql`SELECT COUNT(*)::int AS n FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV}`,
|
||||
)) as unknown as { n: number }[]
|
||||
)[0].n;
|
||||
const offset = Math.floor((total * OFFSET_PCT) / 100);
|
||||
const row = (await db.execute(
|
||||
sql`SELECT created_at, id FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV} ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET ${offset}`,
|
||||
)) as unknown as { created_at: number; id: string }[];
|
||||
const cursor = { t: Number(row[0].created_at), id: row[0].id };
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` total=${total.toLocaleString()} cursor at ${OFFSET_PCT}%: ${cursor.id} limit=${LIMIT}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const measure = async (label: string, fn: () => Promise<void>) => {
|
||||
for (let i = 0; i < 2; i++) await fn();
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const t0 = performance.now();
|
||||
await fn();
|
||||
samples.push(performance.now() - t0);
|
||||
}
|
||||
samples.sort((a, b) => a - b);
|
||||
const p50 = samples[Math.floor(samples.length / 2)];
|
||||
const min = samples[0];
|
||||
const max = samples[samples.length - 1];
|
||||
console.log(
|
||||
` ${chalk.cyan(label.padEnd(20))} p50=${p50.toFixed(0).padStart(4)}ms min=${min.toFixed(0).padStart(4)}ms max=${max.toFixed(0).padStart(4)}ms`,
|
||||
);
|
||||
return p50;
|
||||
};
|
||||
|
||||
const oldPath = async () => {
|
||||
const { internalIds } =
|
||||
await CusSearchService.resolveInternalIdsByCursor({
|
||||
db,
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
search: "",
|
||||
cursor,
|
||||
limit: LIMIT,
|
||||
});
|
||||
if (internalIds.length === 0) return;
|
||||
const query = getCursorPaginatedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: internalIds.length,
|
||||
internalCustomerIds: internalIds,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
const rows = (await db.execute(query)) as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
const flat = (rows[0] ?? {}) as unknown as FlattenedCustomerRow;
|
||||
reassembleFlattenedCustomer(flat);
|
||||
};
|
||||
|
||||
const newPath = async () => {
|
||||
await CusBatchService.getDashboardCursorPage({
|
||||
ctx,
|
||||
search: "",
|
||||
filters: undefined,
|
||||
cursor,
|
||||
limit: LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const oldWithStatus = async () => {
|
||||
// Call old path manually (resolve + fetch) with status filter for parity
|
||||
await oldPath();
|
||||
};
|
||||
|
||||
const newWithStatus = async () => {
|
||||
await CusBatchService.getDashboardCursorPage({
|
||||
ctx,
|
||||
search: "",
|
||||
filters: { status: ["active"] },
|
||||
cursor,
|
||||
limit: LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const oldP50 = await measure("OLD (resolve+fetch)", oldPath);
|
||||
const newP50 = await measure("NEW (no filters)", newPath);
|
||||
const newStatusP50 = await measure(
|
||||
"NEW (status=active)",
|
||||
newWithStatus,
|
||||
);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.magentaBright("================ Summary ================"));
|
||||
const delta = (((newP50 - oldP50) / oldP50) * 100).toFixed(0);
|
||||
console.log(
|
||||
` old → new (no filters): ${oldP50.toFixed(0)}ms → ${newP50.toFixed(0)}ms (${delta}%)`,
|
||||
);
|
||||
console.log(
|
||||
` new with status=active: ${newStatusP50.toFixed(0)}ms (uses resolve+fetch fallback)`,
|
||||
);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
24
server/experiments/checkRevcatIndex.ts
Normal file
24
server/experiments/checkRevcatIndex.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const rows = (await db.execute(sql`
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = 'customer_products'
|
||||
AND (indexname LIKE '%revenuecat%' OR indexdef LIKE '%revenuecat%')
|
||||
`)) as any[];
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
47
server/experiments/debugOptimizedQuery.ts
Normal file
47
server/experiments/debugOptimizedQuery.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
import { getOptimizedFullCusQuery } from "./optimizedFullCusQuery";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const query = getOptimizedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: AppEnv.Sandbox,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: false,
|
||||
limit: 1000,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
|
||||
console.log(chalk.cyan("Running optimized first page..."));
|
||||
try {
|
||||
const r = await db.execute(query);
|
||||
console.log(chalk.green(`OK: ${(r as any).length} row(s)`));
|
||||
const first = (r as any)[0];
|
||||
if (first) {
|
||||
for (const [k, v] of Object.entries(first)) {
|
||||
const arr = Array.isArray(v) ? v : v != null && typeof v === "object" && "length" in (v as any) ? (v as any[]) : null;
|
||||
console.log(` ${k}: ${arr ? `${arr.length} items` : typeof v}`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(chalk.red(`SQL ERROR: ${e.message}`));
|
||||
console.error(chalk.gray(`code: ${e.code}, where: ${e.where ?? "n/a"}, detail: ${e.detail ?? "n/a"}, hint: ${e.hint ?? "n/a"}`));
|
||||
if (e.position) console.error(chalk.gray(`position: ${e.position}`));
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
305
server/experiments/diffDashboardMerge.ts
Normal file
305
server/experiments/diffDashboardMerge.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import {
|
||||
AppEnv,
|
||||
type FullCustomer,
|
||||
RELEVANT_STATUSES,
|
||||
StandardCursor,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { CusBatchService } from "../src/internal/customers/CusBatchService";
|
||||
import { CusSearchService } from "../src/internal/customers/CusSearchService";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import {
|
||||
type FlattenedCustomerRow,
|
||||
reassembleFlattenedCustomer,
|
||||
} from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV = AppEnv.Sandbox;
|
||||
|
||||
type DashboardFilters = {
|
||||
status?: string[];
|
||||
version?: string[];
|
||||
none?: boolean;
|
||||
processor?: string[];
|
||||
};
|
||||
|
||||
const runOldPath = async ({
|
||||
ctx,
|
||||
search,
|
||||
filters,
|
||||
cursor,
|
||||
limit,
|
||||
}: {
|
||||
ctx: any;
|
||||
search: string;
|
||||
filters?: DashboardFilters;
|
||||
cursor: { t: number; id: string } | null;
|
||||
limit: number;
|
||||
}): Promise<{
|
||||
fullCustomers: FullCustomer[];
|
||||
next_cursor: string | null;
|
||||
}> => {
|
||||
const { internalIds, peek } =
|
||||
await CusSearchService.resolveInternalIdsByCursor({
|
||||
db: ctx.db,
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
search,
|
||||
filters,
|
||||
cursor,
|
||||
limit,
|
||||
});
|
||||
if (internalIds.length === 0) {
|
||||
return { fullCustomers: [], next_cursor: null };
|
||||
}
|
||||
const query = getCursorPaginatedFullCusQuery({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: internalIds.length,
|
||||
internalCustomerIds: internalIds,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
const rows = (await ctx.db.execute(query)) as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
const flat = (rows[0] ?? {
|
||||
customers: [],
|
||||
customer_products: [],
|
||||
customer_entitlements: [],
|
||||
extra_customer_entitlements: [],
|
||||
customer_prices: [],
|
||||
entitlements: [],
|
||||
rollovers: [],
|
||||
replaceables: [],
|
||||
free_trials: [],
|
||||
subscriptions: [],
|
||||
}) as unknown as FlattenedCustomerRow;
|
||||
const fullCustomers = reassembleFlattenedCustomer(flat);
|
||||
const next_cursor = peek ? StandardCursor.encode(peek) : null;
|
||||
return { fullCustomers, next_cursor };
|
||||
};
|
||||
|
||||
const minimalCtx = () => {
|
||||
const { db, client } = initDrizzle();
|
||||
return {
|
||||
db,
|
||||
client,
|
||||
org: { id: ORG_ID, slug: "unit-test-org" },
|
||||
env: ENV,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
const sortById = <T extends { internal_id?: string; id?: string | null }>(
|
||||
arr: T[],
|
||||
): T[] =>
|
||||
[...arr].sort((a, b) =>
|
||||
(a.internal_id ?? a.id ?? "").localeCompare(b.internal_id ?? b.id ?? ""),
|
||||
);
|
||||
|
||||
const sortDeep = (obj: unknown): unknown => {
|
||||
if (Array.isArray(obj)) {
|
||||
const sorted = obj.map(sortDeep);
|
||||
if (sorted.length > 0 && typeof sorted[0] === "object" && sorted[0] !== null) {
|
||||
(sorted as any[]).sort((a, b) => {
|
||||
const ka = (a as any).id ?? (a as any).internal_id ?? "";
|
||||
const kb = (b as any).id ?? (b as any).internal_id ?? "";
|
||||
return String(ka).localeCompare(String(kb));
|
||||
});
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
if (obj && typeof obj === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj as Record<string, unknown>)
|
||||
.map(([k, v]) => [k, sortDeep(v)] as const)
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const diffCustomers = (
|
||||
a: FullCustomer[],
|
||||
b: FullCustomer[],
|
||||
label: string,
|
||||
) => {
|
||||
if (a.length !== b.length) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
` ${label} ✗ length mismatch: old=${a.length} new=${b.length}`,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const sa = sortById(a);
|
||||
const sb = sortById(b);
|
||||
let mismatches = 0;
|
||||
for (let i = 0; i < sa.length; i++) {
|
||||
const ja = JSON.stringify(sortDeep(sa[i]));
|
||||
const jb = JSON.stringify(sortDeep(sb[i]));
|
||||
if (ja !== jb) {
|
||||
if (mismatches < 2) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
` ${label} ✗ customer #${i} (${(sa[i] as any).id}) differs`,
|
||||
),
|
||||
);
|
||||
const keysA = Object.keys(sortDeep(sa[i]) as object);
|
||||
for (const k of keysA) {
|
||||
const va = JSON.stringify((sortDeep(sa[i]) as any)[k]);
|
||||
const vb = JSON.stringify((sortDeep(sb[i]) as any)[k]);
|
||||
if (va !== vb) {
|
||||
console.log(
|
||||
chalk.gray(` KEY ${k}\n old=${va?.slice(0, 160)}\n new=${vb?.slice(0, 160)}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
if (mismatches > 0) {
|
||||
console.log(
|
||||
chalk.red(` ${label} ✗ ${mismatches}/${sa.length} customers differ`),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.green(` ${label} ✓ ${sa.length} customers identical`));
|
||||
return true;
|
||||
};
|
||||
|
||||
type Case = {
|
||||
name: string;
|
||||
search?: string;
|
||||
filters?: DashboardFilters;
|
||||
cursor?: { t: number; id: string } | null;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Dashboard Merge Parity ================\n",
|
||||
),
|
||||
);
|
||||
|
||||
const ctx = minimalCtx();
|
||||
|
||||
const total = (
|
||||
(await ctx.db.execute(
|
||||
sql`SELECT COUNT(*)::int AS n FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV}`,
|
||||
)) as unknown as { n: number }[]
|
||||
)[0].n;
|
||||
const deepOffset = Math.floor(total * 0.95);
|
||||
const deepRow = (await ctx.db.execute(
|
||||
sql`SELECT created_at, id FROM customers WHERE org_id = ${ORG_ID} AND env = ${ENV} ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET ${deepOffset}`,
|
||||
)) as unknown as { created_at: number; id: string }[];
|
||||
const deepCursor = {
|
||||
t: Number(deepRow[0].created_at),
|
||||
id: deepRow[0].id,
|
||||
};
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` total=${total.toLocaleString()} deep_cursor=${deepCursor.id} (offset ~95%)\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const cases: Case[] = [
|
||||
{ name: "baseline", limit: 200 },
|
||||
{ name: "search 'bench_000450'", search: "bench_000450", limit: 100 },
|
||||
{ name: "status=active", filters: { status: ["active"] }, limit: 200 },
|
||||
{ name: "status=past_due", filters: { status: ["past_due"] }, limit: 200 },
|
||||
{ name: "status=canceled", filters: { status: ["canceled"] }, limit: 200 },
|
||||
{
|
||||
name: "status=free_trial",
|
||||
filters: { status: ["free_trial"] },
|
||||
limit: 200,
|
||||
},
|
||||
{ name: "status=expired", filters: { status: ["expired"] }, limit: 200 },
|
||||
{
|
||||
name: "status=active,past_due",
|
||||
filters: { status: ["active", "past_due"] },
|
||||
limit: 200,
|
||||
},
|
||||
{ name: "none=true", filters: { none: true }, limit: 200 },
|
||||
{
|
||||
name: "processor=stripe",
|
||||
filters: { processor: ["stripe"] },
|
||||
limit: 200,
|
||||
},
|
||||
{ name: "deep cursor (95%)", cursor: deepCursor, limit: 200 },
|
||||
{
|
||||
name: "deep cursor + status=active",
|
||||
cursor: deepCursor,
|
||||
filters: { status: ["active"] },
|
||||
limit: 200,
|
||||
},
|
||||
{
|
||||
name: "search no-match",
|
||||
search: "no_such_customer_zzz",
|
||||
limit: 50,
|
||||
},
|
||||
];
|
||||
|
||||
let pass = 0;
|
||||
let fail = 0;
|
||||
try {
|
||||
for (const tc of cases) {
|
||||
const args = {
|
||||
ctx,
|
||||
search: tc.search ?? "",
|
||||
filters: tc.filters,
|
||||
cursor: tc.cursor ?? null,
|
||||
limit: tc.limit ?? 200,
|
||||
};
|
||||
const oldRes = await runOldPath(args);
|
||||
const newRes = await CusBatchService.getDashboardCursorPage(args);
|
||||
const ok = diffCustomers(
|
||||
oldRes.fullCustomers,
|
||||
newRes.fullCustomers,
|
||||
tc.name,
|
||||
);
|
||||
const oldDecoded = StandardCursor.decode(oldRes.next_cursor ?? "");
|
||||
const newDecoded = StandardCursor.decode(newRes.next_cursor ?? "");
|
||||
const cursorMatches =
|
||||
(oldDecoded?.t ?? null) === (newDecoded?.t ?? null) &&
|
||||
(oldDecoded?.id ?? null) === (newDecoded?.id ?? null);
|
||||
if (!cursorMatches) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` cursor differs: old=${JSON.stringify(oldDecoded)} new=${JSON.stringify(newDecoded)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ok && cursorMatches) pass++;
|
||||
else fail++;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.bold(
|
||||
`Result: ${chalk.green(`${pass} pass`)} / ${chalk.red(`${fail} fail`)} of ${cases.length}`,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await ctx.client.end();
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
180
server/experiments/diffOptimizedVsCurrent.ts
Normal file
180
server/experiments/diffOptimizedVsCurrent.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import type { FlattenedCustomerRow } from "../src/internal/customers/reassembleFlattenedCustomer/index.js";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { reassembleFlattenedCustomer } from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { getOptimizedFullCusQuery } from "./optimizedFullCusQuery";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
|
||||
type Args = {
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
inStatuses: typeof RELEVANT_STATUSES;
|
||||
withSubs: boolean;
|
||||
limit: number;
|
||||
cusProductLimit: number;
|
||||
};
|
||||
|
||||
const sharedArgs: Args & {
|
||||
cursor?: { v: 0; t: number; id: string };
|
||||
} = {
|
||||
orgId: ORG_ID,
|
||||
env: AppEnv.Sandbox,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: parseInt(process.env.LIMIT ?? "50", 10),
|
||||
cusProductLimit: 15,
|
||||
};
|
||||
|
||||
type AnyRecord = Record<string, unknown>;
|
||||
|
||||
const fetchFlat = async (
|
||||
db: ReturnType<typeof initDrizzle>["db"],
|
||||
q: any,
|
||||
): Promise<FlattenedCustomerRow> => {
|
||||
const rows = await db.execute(q);
|
||||
const flat = (rows as unknown as AnyRecord[])[0] as unknown as FlattenedCustomerRow;
|
||||
return flat;
|
||||
};
|
||||
|
||||
const sortById = (a: { id?: string }, b: { id?: string }) =>
|
||||
(a.id ?? "").localeCompare(b.id ?? "");
|
||||
|
||||
const sortDeep = (obj: unknown): unknown => {
|
||||
if (Array.isArray(obj)) {
|
||||
const sorted = obj.map(sortDeep);
|
||||
if (sorted.length > 0 && typeof sorted[0] === "object" && sorted[0] !== null) {
|
||||
(sorted as AnyRecord[]).sort((a, b) => {
|
||||
const ka = (a as AnyRecord).id ?? (a as AnyRecord).internal_id ?? "";
|
||||
const kb = (b as AnyRecord).id ?? (b as AnyRecord).internal_id ?? "";
|
||||
return String(ka).localeCompare(String(kb));
|
||||
});
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
if (obj && typeof obj === "object") {
|
||||
const entries = Object.entries(obj as AnyRecord)
|
||||
.map(([k, v]) => [k, sortDeep(v)] as const)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const offsetPct = parseInt(process.env.OFFSET_PCT ?? "0", 10);
|
||||
if (offsetPct > 0) {
|
||||
const total = (await db.execute(
|
||||
sql`SELECT COUNT(*)::int AS n FROM customers WHERE org_id = ${ORG_ID} AND env = 'sandbox'`,
|
||||
)) as unknown as { n: number }[];
|
||||
const offset = Math.floor((total[0].n * offsetPct) / 100);
|
||||
const row = (await db.execute(
|
||||
sql`SELECT created_at, id FROM customers WHERE org_id = ${ORG_ID} AND env = 'sandbox' ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET ${offset}`,
|
||||
)) as unknown as { created_at: number; id: string }[];
|
||||
sharedArgs.cursor = { v: 0, t: row[0].created_at, id: row[0].id };
|
||||
console.log(chalk.cyan(`Using deep cursor at ${offsetPct}%: ${row[0].id}`));
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`Running CURRENT query (limit=${sharedArgs.limit}, cursor=${sharedArgs.cursor ? "yes" : "no"})...`));
|
||||
const flatCurrent = await fetchFlat(
|
||||
db,
|
||||
getCursorPaginatedFullCusQuery(sharedArgs),
|
||||
);
|
||||
|
||||
console.log(chalk.cyan(`Running OPTIMIZED query...`));
|
||||
const flatOptim = await fetchFlat(
|
||||
db,
|
||||
getOptimizedFullCusQuery(sharedArgs),
|
||||
);
|
||||
|
||||
// Compare top-level array lengths
|
||||
const keys = [
|
||||
"customers",
|
||||
"customer_products",
|
||||
"customer_entitlements",
|
||||
"extra_customer_entitlements",
|
||||
"customer_prices",
|
||||
"entitlements",
|
||||
"rollovers",
|
||||
"replaceables",
|
||||
"free_trials",
|
||||
"subscriptions",
|
||||
] as const;
|
||||
|
||||
console.log(chalk.bold("\nTop-level counts:"));
|
||||
let lenMismatch = false;
|
||||
for (const k of keys) {
|
||||
const a = (flatCurrent[k] as unknown[] | undefined)?.length ?? 0;
|
||||
const b = (flatOptim[k] as unknown[] | undefined)?.length ?? 0;
|
||||
const match = a === b;
|
||||
if (!match) lenMismatch = true;
|
||||
console.log(
|
||||
` ${k.padEnd(32)} current=${String(a).padStart(5)} optim=${String(b).padStart(5)} ${match ? chalk.green("✓") : chalk.red("✗")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (lenMismatch) {
|
||||
console.log(chalk.red("\n❌ Length mismatch — aborting deep compare"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Reassemble both and deep compare the FullCustomer arrays
|
||||
const customersA = reassembleFlattenedCustomer(flatCurrent);
|
||||
const customersB = reassembleFlattenedCustomer(flatOptim);
|
||||
|
||||
console.log(chalk.cyan(`\nReassembled ${customersA.length} customers (both queries)`));
|
||||
|
||||
if (customersA.length !== customersB.length) {
|
||||
console.log(chalk.red(`Mismatch in reassembled customer counts: ${customersA.length} vs ${customersB.length}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Sort by id for stable diff (same cursor order should already be aligned)
|
||||
customersA.sort((a: any, b: any) => (a.internal_id ?? "").localeCompare(b.internal_id ?? ""));
|
||||
customersB.sort((a: any, b: any) => (a.internal_id ?? "").localeCompare(b.internal_id ?? ""));
|
||||
|
||||
let diffs = 0;
|
||||
for (let i = 0; i < customersA.length; i++) {
|
||||
const a = sortDeep(customersA[i]);
|
||||
const b = sortDeep(customersB[i]);
|
||||
const ja = JSON.stringify(a);
|
||||
const jb = JSON.stringify(b);
|
||||
if (ja !== jb) {
|
||||
diffs++;
|
||||
if (diffs <= 3) {
|
||||
console.log(chalk.red(`\nDIFF for customer #${i} (${(customersA[i] as any).id}):`));
|
||||
// find first differing key
|
||||
for (const k of Object.keys(a as AnyRecord)) {
|
||||
const va = JSON.stringify((a as AnyRecord)[k]);
|
||||
const vb = JSON.stringify((b as AnyRecord)[k]);
|
||||
if (va !== vb) {
|
||||
console.log(` KEY ${k}:`);
|
||||
console.log(chalk.gray(` current: ${va?.slice(0, 200)}`));
|
||||
console.log(chalk.gray(` optim: ${vb?.slice(0, 200)}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (diffs === 0) {
|
||||
console.log(chalk.green(`\n✅ All ${customersA.length} customers match byte-for-byte`));
|
||||
} else {
|
||||
console.log(chalk.red(`\n❌ ${diffs}/${customersA.length} customers differ`));
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
426
server/experiments/diffV07VsV22.ts
Normal file
426
server/experiments/diffV07VsV22.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
type CusProductStatus,
|
||||
type FullCustomer,
|
||||
type ListCustomersV2Params,
|
||||
RELEVANT_STATUSES,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { getPaginatedFullCusQuery } from "../src/internal/customers/getFullCusQuery";
|
||||
import {
|
||||
type FlattenedCustomerRow,
|
||||
reassembleFlattenedCustomer,
|
||||
} from "../src/internal/customers/reassembleFlattenedCustomer";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORGS: { id: string; env: AppEnv; label: string }[] = [
|
||||
{ id: "biu9vSF7vghBLSKW1UTDwxHBAivjnPaK", env: "live" as AppEnv, label: "firecrawl" },
|
||||
{ id: "r7pwHiekGsqt32qGqcVku6thWFh5aHh8", env: "live" as AppEnv, label: "runable" },
|
||||
];
|
||||
const LIMIT = parseInt(process.env.DIFF_LIMIT ?? "50", 10);
|
||||
const DEEP_OFFSET = parseInt(process.env.DIFF_DEEP_OFFSET ?? "500", 10);
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
const STATEMENT_TIMEOUT_MS = 30_000;
|
||||
|
||||
type ResolvedParams = Partial<ListCustomersV2Params> & {
|
||||
offset?: number;
|
||||
cursor?: { t: number; id: string };
|
||||
};
|
||||
|
||||
type CaseContext = { db: DB; orgId: string; env: AppEnv };
|
||||
type Case = {
|
||||
name: string;
|
||||
build: (ctx: CaseContext) => Promise<ResolvedParams | null>;
|
||||
};
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const runReadOnly = async <T,>(db: DB, fn: (tx: any) => Promise<T>): Promise<T> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
return fn(tx);
|
||||
});
|
||||
};
|
||||
|
||||
const v22NormalizeTimestamp = (value: any): number => {
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? Date.now() : parsed;
|
||||
}
|
||||
return typeof value === "number" ? value : Date.now();
|
||||
};
|
||||
|
||||
const v22Normalize = (raw: any): any => {
|
||||
const out = {
|
||||
...raw,
|
||||
created_at: v22NormalizeTimestamp(raw.created_at),
|
||||
};
|
||||
if (raw.customer_products && Array.isArray(raw.customer_products)) {
|
||||
out.customer_products = raw.customer_products.map((cp: any) => ({
|
||||
...cp,
|
||||
created_at: v22NormalizeTimestamp(cp.created_at),
|
||||
starts_at: cp.starts_at
|
||||
? v22NormalizeTimestamp(cp.starts_at)
|
||||
: v22NormalizeTimestamp(cp.created_at),
|
||||
canceled_at: cp.canceled_at ? v22NormalizeTimestamp(cp.canceled_at) : null,
|
||||
ended_at: cp.ended_at ? v22NormalizeTimestamp(cp.ended_at) : null,
|
||||
trial_ends_at: cp.trial_ends_at
|
||||
? v22NormalizeTimestamp(cp.trial_ends_at)
|
||||
: null,
|
||||
quantity: cp.quantity ? parseInt(cp.quantity, 10) || 1 : 1,
|
||||
options: cp.options || [],
|
||||
collection_method: cp.collection_method || "charge_automatically",
|
||||
subscription_ids: cp.subscription_ids || [],
|
||||
scheduled_ids: cp.scheduled_ids || [],
|
||||
customer_entitlements: (cp.customer_entitlements || []).map((ce: any) => ({
|
||||
...ce,
|
||||
created_at: v22NormalizeTimestamp(ce.created_at),
|
||||
next_reset_at: ce.next_reset_at
|
||||
? v22NormalizeTimestamp(ce.next_reset_at)
|
||||
: null,
|
||||
balance: ce.balance ? parseFloat(ce.balance) || 0 : 0,
|
||||
adjustment: ce.adjustment ? parseFloat(ce.adjustment) || 0 : 0,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const fetchV22 = async (
|
||||
db: DB,
|
||||
orgId: string,
|
||||
env: AppEnv,
|
||||
params: ResolvedParams,
|
||||
) => {
|
||||
const { subscription_status, plans, search, processors, offset = 0 } = params;
|
||||
const query = getPaginatedFullCusQuery({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses: subscription_status
|
||||
? [subscription_status as unknown as CusProductStatus]
|
||||
: RELEVANT_STATUSES,
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: true,
|
||||
limit: LIMIT,
|
||||
offset,
|
||||
search,
|
||||
plans,
|
||||
processors,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
});
|
||||
const rows = await runReadOnly(db, (tx) => tx.execute(query));
|
||||
return (rows as any[]).map(v22Normalize) as FullCustomer[];
|
||||
};
|
||||
|
||||
const fetchV23 = async (
|
||||
db: DB,
|
||||
orgId: string,
|
||||
env: AppEnv,
|
||||
params: ResolvedParams,
|
||||
) => {
|
||||
const { subscription_status, plans, search, processors, cursor } = params;
|
||||
const query = getCursorPaginatedFullCusQuery({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses: subscription_status
|
||||
? [subscription_status as unknown as CusProductStatus]
|
||||
: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: LIMIT,
|
||||
search,
|
||||
plans,
|
||||
processors,
|
||||
cursor: cursor ? { v: 0 as const, t: cursor.t, id: cursor.id } : undefined,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
});
|
||||
const rows = (await runReadOnly(db, (tx) => tx.execute(query))) as any[];
|
||||
const flat = (rows[0] ?? {}) as unknown as FlattenedCustomerRow;
|
||||
const all = reassembleFlattenedCustomer(flat);
|
||||
return all.slice(0, LIMIT);
|
||||
};
|
||||
|
||||
const resolveCursorAtOffset = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
offset,
|
||||
inStatuses,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
offset: number;
|
||||
inStatuses?: CusProductStatus[];
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
search?: string;
|
||||
}): Promise<{ t: number; id: string } | null> => {
|
||||
const query = getPaginatedFullCusQuery({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses: inStatuses ?? RELEVANT_STATUSES,
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: false,
|
||||
limit: 1,
|
||||
offset: offset - 1,
|
||||
search,
|
||||
plans,
|
||||
processors,
|
||||
cusProductLimit: 1,
|
||||
});
|
||||
const rows = (await runReadOnly(db, (tx) => tx.execute(query))) as any[];
|
||||
if (rows.length === 0) return null;
|
||||
const row = rows[0] as { id: string; created_at: number | string };
|
||||
return { t: v22NormalizeTimestamp(row.created_at), id: row.id };
|
||||
};
|
||||
|
||||
const resolveFirstPlanId = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}): Promise<string | null> => {
|
||||
const rows = (await runReadOnly(
|
||||
db,
|
||||
(tx) => tx.execute(sql`
|
||||
SELECT p.id
|
||||
FROM products p
|
||||
JOIN customer_products cp ON cp.internal_product_id = p.internal_id
|
||||
WHERE p.org_id = ${orgId} AND p.env = ${env}
|
||||
GROUP BY p.id
|
||||
ORDER BY COUNT(*) DESC
|
||||
LIMIT 1
|
||||
`),
|
||||
)) as any[];
|
||||
return rows[0]?.id ?? null;
|
||||
};
|
||||
|
||||
const resolveSearchSubstring = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}): Promise<string | null> => {
|
||||
const rows = (await runReadOnly(
|
||||
db,
|
||||
(tx) => tx.execute(sql`
|
||||
SELECT c.email
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId} AND c.env = ${env}
|
||||
AND c.email IS NOT NULL
|
||||
AND length(c.email) >= 5
|
||||
LIMIT 1
|
||||
`),
|
||||
)) as any[];
|
||||
const email = rows[0]?.email as string | undefined;
|
||||
if (!email) return null;
|
||||
const at = email.indexOf("@");
|
||||
if (at < 3) return email.slice(0, 3);
|
||||
return email.slice(0, Math.min(at, 4));
|
||||
};
|
||||
|
||||
const CASES: Case[] = [
|
||||
{
|
||||
name: "page 1 / no filters",
|
||||
build: async () => ({}),
|
||||
},
|
||||
{
|
||||
name: "page 1 / subscription_status=active",
|
||||
build: async () => ({ subscription_status: "active" }),
|
||||
},
|
||||
{
|
||||
name: "page 1 / processors=revenuecat",
|
||||
build: async () => ({ processors: ["revenuecat"] }),
|
||||
},
|
||||
{
|
||||
name: "page 1 / search",
|
||||
build: async (ctx) => {
|
||||
const search = await resolveSearchSubstring(ctx);
|
||||
if (!search) return null;
|
||||
return { search };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "page 1 / plans",
|
||||
build: async (ctx) => {
|
||||
const planId = await resolveFirstPlanId(ctx);
|
||||
if (!planId) return null;
|
||||
return { plans: [{ id: planId }] };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `deep / offset=${DEEP_OFFSET} / no filters`,
|
||||
build: async (ctx) => {
|
||||
const cursor = await resolveCursorAtOffset({
|
||||
db: ctx.db,
|
||||
orgId: ctx.orgId,
|
||||
env: ctx.env,
|
||||
offset: DEEP_OFFSET,
|
||||
});
|
||||
if (!cursor) return null;
|
||||
return { offset: DEEP_OFFSET, cursor };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const findDiff = (a: any, b: any, path = "$"): string | null => {
|
||||
if (a === b) return null;
|
||||
if (a === null || b === null || a === undefined || b === undefined) {
|
||||
return `${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
|
||||
}
|
||||
if (typeof a !== typeof b) {
|
||||
return `${path}: type ${typeof a} vs ${typeof b}`;
|
||||
}
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) {
|
||||
return `${path}: array vs non-array`;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return `${path}.length: ${a.length} vs ${b.length}`;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const d = findDiff(a[i], b[i], `${path}[${i}]`);
|
||||
if (d) return d;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (typeof a === "object") {
|
||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||
for (const k of keys) {
|
||||
const d = findDiff(a[k], b[k], `${path}.${k}`);
|
||||
if (d) return d;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return `${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
|
||||
};
|
||||
|
||||
const summarize = (label: string, list: FullCustomer[]) => {
|
||||
const cps = list.reduce((n, c) => n + (c.customer_products?.length ?? 0), 0);
|
||||
const subs = list.reduce(
|
||||
(n, c) => n + (c.subscriptions?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
const extras = list.reduce(
|
||||
(n, c) => n + (c.extra_customer_entitlements?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
return `${label}: ${list.length} cus / ${cps} cps / ${subs} subs / ${extras} loose_ces`;
|
||||
};
|
||||
|
||||
const runOrg = async ({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
label,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
label: string;
|
||||
}) => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n========= ${label} (${orgId}) env=${env} limit=${LIMIT} =========\n`,
|
||||
),
|
||||
);
|
||||
let total = 0;
|
||||
let passes = 0;
|
||||
for (const c of CASES) {
|
||||
total++;
|
||||
console.log(chalk.cyan(`\n--- ${c.name} ---`));
|
||||
const params = await c.build({ db, orgId, env });
|
||||
if (!params) {
|
||||
console.log(chalk.yellow(" ⊘ skipped (no data to build params)"));
|
||||
passes++;
|
||||
continue;
|
||||
}
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` params: ${JSON.stringify(params, (_k, v) => (typeof v === "bigint" ? String(v) : v))}`,
|
||||
),
|
||||
);
|
||||
const [v22, v23] = await Promise.all([
|
||||
fetchV22(db, orgId, env, params),
|
||||
fetchV23(db, orgId, env, params),
|
||||
]);
|
||||
console.log(chalk.gray(` ${summarize("V2.2", v22)}`));
|
||||
console.log(chalk.gray(` ${summarize("V2.3", v23)}`));
|
||||
const diff = findDiff(v22, v23);
|
||||
if (diff) {
|
||||
console.log(chalk.red(` ❌ DIFF at ${diff}`));
|
||||
const idx = diff.match(/\$\[(\d+)\]/);
|
||||
if (idx) {
|
||||
const i = parseInt(idx[1]!, 10);
|
||||
console.log(chalk.gray(` --- V2.2 [${i}] ---`));
|
||||
console.log(chalk.gray(JSON.stringify(v22[i], null, 2).slice(0, 4000)));
|
||||
console.log(chalk.gray(` --- V2.3 [${i}] ---`));
|
||||
console.log(chalk.gray(JSON.stringify(v23[i], null, 2).slice(0, 4000)));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.green(` ✓ MATCH`));
|
||||
passes++;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
chalk.magentaBright(`\n ${label}: ${passes}/${total} cases match`),
|
||||
);
|
||||
return { total, passes };
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
let grandTotal = 0;
|
||||
let grandPasses = 0;
|
||||
for (const org of ORGS) {
|
||||
try {
|
||||
const { total, passes } = await runOrg({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env: org.env,
|
||||
label: org.label,
|
||||
});
|
||||
grandTotal += total;
|
||||
grandPasses += passes;
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`\n❌ Org ${org.label} failed:`), err);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n========= TOTAL: ${grandPasses}/${grandTotal} cases match =========\n`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Diff failed:"));
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
52
server/experiments/inspectIndexes.ts
Normal file
52
server/experiments/inspectIndexes.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const tables = [
|
||||
"customers",
|
||||
"customer_products",
|
||||
"customer_entitlements",
|
||||
"customer_prices",
|
||||
"subscriptions",
|
||||
];
|
||||
for (const t of tables) {
|
||||
console.log(chalk.bold(`\n=== ${t} indexes ===`));
|
||||
const r = (await db.execute(sql.raw(`
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = '${t}' AND schemaname = 'public'
|
||||
ORDER BY indexname
|
||||
`))) as unknown as { indexname: string; indexdef: string }[];
|
||||
for (const idx of r) {
|
||||
console.log(` ${idx.indexname}`);
|
||||
console.log(` ${idx.indexdef}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get total rows in customer_products across all orgs
|
||||
console.log(chalk.bold("\n=== Global customer_products stats ==="));
|
||||
const stats = (await db.execute(sql.raw(`
|
||||
SELECT
|
||||
reltuples::bigint AS estimated_rows,
|
||||
pg_size_pretty(pg_relation_size('customer_products')) AS table_size,
|
||||
pg_size_pretty(pg_indexes_size('customer_products')) AS indexes_size
|
||||
FROM pg_class WHERE relname = 'customer_products'
|
||||
`))) as unknown as { estimated_rows: string; table_size: string; indexes_size: string }[];
|
||||
console.log(` estimated_rows=${stats[0].estimated_rows} table=${stats[0].table_size} indexes=${stats[0].indexes_size}`);
|
||||
|
||||
// Actual count globally
|
||||
const real = (await db.execute(sql.raw(`SELECT COUNT(*)::int AS n FROM customer_products`))) as unknown as { n: number }[];
|
||||
console.log(` actual_total_rows=${real[0].n.toLocaleString()}`);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
90
server/experiments/inspectUnitTestOrg.ts
Normal file
90
server/experiments/inspectUnitTestOrg.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import chalk from "chalk";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
|
||||
const main = async () => {
|
||||
const { db, client } = initDrizzle();
|
||||
try {
|
||||
const tables = [
|
||||
"customers",
|
||||
"customer_products",
|
||||
"customer_entitlements",
|
||||
"customer_prices",
|
||||
"subscriptions",
|
||||
"entitlements",
|
||||
"products",
|
||||
"rollovers",
|
||||
"replaceables",
|
||||
"free_trials",
|
||||
];
|
||||
|
||||
console.log(chalk.bold(`\nRow counts for org=${ORG_ID}:\n`));
|
||||
for (const t of tables) {
|
||||
const hasOrg = ["customers", "subscriptions", "entitlements", "products", "features"];
|
||||
const col = hasOrg.includes(t)
|
||||
? `WHERE org_id = '${ORG_ID}'`
|
||||
: t === "customer_products" ||
|
||||
t === "customer_entitlements" ||
|
||||
t === "customer_prices"
|
||||
? `WHERE internal_customer_id IN (SELECT internal_id FROM customers WHERE org_id = '${ORG_ID}')`
|
||||
: "";
|
||||
const start = performance.now();
|
||||
const r = (await db.execute(
|
||||
sql.raw(`SELECT COUNT(*)::int AS n FROM ${t} ${col}`),
|
||||
)) as unknown as { n: number }[];
|
||||
const n = r[0]?.n ?? 0;
|
||||
console.log(
|
||||
` ${t.padEnd(28)} ${n.toLocaleString().padStart(10)} (${(performance.now() - start).toFixed(0)}ms)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Sample a customer to see if it has any products
|
||||
const sample = (await db.execute(sql.raw(`
|
||||
SELECT c.internal_id, c.id,
|
||||
(SELECT COUNT(*)::int FROM customer_products cp WHERE cp.internal_customer_id = c.internal_id) AS cp_count,
|
||||
(SELECT COUNT(*)::int FROM customer_entitlements ce WHERE ce.internal_customer_id = c.internal_id) AS ce_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = '${ORG_ID}'
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT 10
|
||||
`))) as unknown as Record<string, unknown>[];
|
||||
console.log(chalk.bold(`\nFirst 10 customers by created_at DESC:\n`));
|
||||
for (const row of sample) {
|
||||
console.log(
|
||||
` ${(row.id as string).padEnd(30)} cp=${row.cp_count} ce=${row.ce_count}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Histogram: how many customers have N products?
|
||||
const hist = (await db.execute(sql.raw(`
|
||||
SELECT cp_bucket, COUNT(*)::int AS customer_count FROM (
|
||||
SELECT c.internal_id,
|
||||
LEAST(
|
||||
(SELECT COUNT(*)::int FROM customer_products cp WHERE cp.internal_customer_id = c.internal_id),
|
||||
10
|
||||
) AS cp_bucket
|
||||
FROM customers c
|
||||
WHERE c.org_id = '${ORG_ID}'
|
||||
) t
|
||||
GROUP BY cp_bucket
|
||||
ORDER BY cp_bucket
|
||||
`))) as unknown as { cp_bucket: number; customer_count: number }[];
|
||||
console.log(chalk.bold(`\nCustomer × #products histogram (capped at 10):\n`));
|
||||
for (const row of hist) {
|
||||
const bar = "█".repeat(Math.min(60, Math.floor(row.customer_count / 5000)));
|
||||
console.log(
|
||||
` ${row.cp_bucket.toString().padStart(2)}: ${row.customer_count.toLocaleString().padStart(8)} ${bar}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
174
server/experiments/list-customers-sql/00-baseline-deep.sql
Normal file
174
server/experiments/list-customers-sql/00-baseline-deep.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
-- ============================================================
|
||||
-- Variant 00: baseline (current getCursorPaginatedFullCusQuery)
|
||||
-- Deep page (cursor ≈ 45% of 1.22M customers), limit=1000
|
||||
-- Target org: Firecrawl
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH customer_records AS (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
AND (c.created_at, c.id) < (1774237983361, '772c9569-fc97-4c30-9fd8-c8a585b66755')
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
customer_products_with_prices AS (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product,
|
||||
cpr_data.customer_prices,
|
||||
ce_data.customer_entitlements,
|
||||
ft_data.free_trial
|
||||
FROM customer_records cr
|
||||
JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
|
||||
) FILTER (WHERE cpr.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_prices
|
||||
FROM customer_prices cpr
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
WHERE cpr.customer_product_id = cp.id
|
||||
) cpr_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST) FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_entitlements
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cp.id
|
||||
) ce_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT row_to_json(ft) AS free_trial
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) ft_data ON true
|
||||
),
|
||||
|
||||
customer_products_aggregated AS (
|
||||
SELECT
|
||||
cpwp.internal_customer_id,
|
||||
json_agg(row_to_json(cpwp) ORDER BY cpwp.created_at DESC) AS customer_products
|
||||
FROM customer_products_with_prices cpwp
|
||||
GROUP BY cpwp.internal_customer_id
|
||||
),
|
||||
|
||||
customer_subscriptions AS (
|
||||
SELECT
|
||||
s.internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(s)) FILTER (WHERE s.stripe_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS subscriptions
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
cpwp.internal_customer_id,
|
||||
s.*
|
||||
FROM customer_products_with_prices cpwp
|
||||
JOIN LATERAL unnest(cpwp.subscription_ids) AS cpwp_sub(stripe_id) ON true
|
||||
JOIN subscriptions s ON s.stripe_id = cpwp_sub.stripe_id
|
||||
) s
|
||||
GROUP BY s.internal_customer_id
|
||||
),
|
||||
|
||||
extra_customer_entitlements AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST)
|
||||
FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
ORDER BY ce.id DESC
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
GROUP BY cr.internal_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
cr.*,
|
||||
COALESCE(cpa.customer_products, '[]'::json) AS customer_products,
|
||||
COALESCE(cs.subscriptions, '[]'::json) AS subscriptions,
|
||||
COALESCE(ece.extra_customer_entitlements, '[]'::json) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN customer_products_aggregated cpa ON cpa.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN customer_subscriptions cs ON cs.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN extra_customer_entitlements ece ON ece.internal_customer_id = cr.internal_id
|
||||
ORDER BY cr.created_at DESC, cr.id DESC;
|
||||
173
server/experiments/list-customers-sql/00-baseline-page1.sql
Normal file
173
server/experiments/list-customers-sql/00-baseline-page1.sql
Normal file
@@ -0,0 +1,173 @@
|
||||
-- ============================================================
|
||||
-- Variant 00: baseline (current getCursorPaginatedFullCusQuery)
|
||||
-- Page 1, limit=1000, withSubs=true, full CTE pipeline
|
||||
-- Target org: Firecrawl
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH customer_records AS (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
customer_products_with_prices AS (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product,
|
||||
cpr_data.customer_prices,
|
||||
ce_data.customer_entitlements,
|
||||
ft_data.free_trial
|
||||
FROM customer_records cr
|
||||
JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
|
||||
) FILTER (WHERE cpr.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_prices
|
||||
FROM customer_prices cpr
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
WHERE cpr.customer_product_id = cp.id
|
||||
) cpr_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST) FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_entitlements
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cp.id
|
||||
) ce_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT row_to_json(ft) AS free_trial
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) ft_data ON true
|
||||
),
|
||||
|
||||
customer_products_aggregated AS (
|
||||
SELECT
|
||||
cpwp.internal_customer_id,
|
||||
json_agg(row_to_json(cpwp) ORDER BY cpwp.created_at DESC) AS customer_products
|
||||
FROM customer_products_with_prices cpwp
|
||||
GROUP BY cpwp.internal_customer_id
|
||||
),
|
||||
|
||||
customer_subscriptions AS (
|
||||
SELECT
|
||||
s.internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(s)) FILTER (WHERE s.stripe_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS subscriptions
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
cpwp.internal_customer_id,
|
||||
s.*
|
||||
FROM customer_products_with_prices cpwp
|
||||
JOIN LATERAL unnest(cpwp.subscription_ids) AS cpwp_sub(stripe_id) ON true
|
||||
JOIN subscriptions s ON s.stripe_id = cpwp_sub.stripe_id
|
||||
) s
|
||||
GROUP BY s.internal_customer_id
|
||||
),
|
||||
|
||||
extra_customer_entitlements AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST)
|
||||
FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
ORDER BY ce.id DESC
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
GROUP BY cr.internal_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
cr.*,
|
||||
COALESCE(cpa.customer_products, '[]'::json) AS customer_products,
|
||||
COALESCE(cs.subscriptions, '[]'::json) AS subscriptions,
|
||||
COALESCE(ece.extra_customer_entitlements, '[]'::json) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN customer_products_aggregated cpa ON cpa.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN customer_subscriptions cs ON cs.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN extra_customer_entitlements ece ON ece.internal_customer_id = cr.internal_id
|
||||
ORDER BY cr.created_at DESC, cr.id DESC;
|
||||
@@ -0,0 +1,131 @@
|
||||
-- ============================================================
|
||||
-- Variant 01: inline correlated subqueries (no cpwp CTE)
|
||||
-- Page 1, limit=1000, withSubs=true
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Hypothesis: eliminating the materialize-then-group-then-sort
|
||||
-- of customer_products_with_prices saves the 180ms sort that
|
||||
-- dominated the baseline. Per-row LATERAL keeps json_agg local.
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH customer_records AS (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
)
|
||||
|
||||
SELECT
|
||||
cr.*,
|
||||
|
||||
COALESCE((
|
||||
SELECT json_agg(row_to_json(cp_full) ORDER BY cp_full.created_at DESC)
|
||||
FROM (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product,
|
||||
COALESCE((
|
||||
SELECT json_agg(
|
||||
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
|
||||
) FILTER (WHERE cpr.id IS NOT NULL)
|
||||
FROM customer_prices cpr
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
WHERE cpr.customer_product_id = cp.id
|
||||
), '[]'::json) AS customer_prices,
|
||||
COALESCE((
|
||||
SELECT json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', COALESCE((
|
||||
SELECT json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
), '[]'::json),
|
||||
'rollovers', COALESCE((
|
||||
SELECT json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST)
|
||||
FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
), '[]'::json)
|
||||
)
|
||||
) FILTER (WHERE ce.id IS NOT NULL)
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cp.id
|
||||
), '[]'::json) AS customer_entitlements,
|
||||
(
|
||||
SELECT row_to_json(ft)
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) AS free_trial
|
||||
FROM customer_products cp
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY prod.is_add_on ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp_full
|
||||
), '[]'::json) AS customer_products,
|
||||
|
||||
COALESCE((
|
||||
SELECT json_agg(row_to_json(s_distinct)) FILTER (WHERE s_distinct.stripe_id IS NOT NULL)
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
cp_sub.internal_customer_id,
|
||||
s.*
|
||||
FROM customer_products cp_sub
|
||||
CROSS JOIN LATERAL unnest(cp_sub.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN subscriptions s ON s.stripe_id = sub_id_t.sub_id
|
||||
WHERE cp_sub.internal_customer_id = cr.internal_id
|
||||
AND cp_sub.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
) s_distinct
|
||||
), '[]'::json) AS subscriptions,
|
||||
|
||||
COALESCE((
|
||||
SELECT json_agg(ce_full.ce_obj ORDER BY ce_full.ce_id DESC)
|
||||
FROM (
|
||||
SELECT
|
||||
ce.id AS ce_id,
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', COALESCE((
|
||||
SELECT json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
), '[]'::json),
|
||||
'rollovers', COALESCE((
|
||||
SELECT json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST)
|
||||
FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
), '[]'::json)
|
||||
) AS ce_obj
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce_full
|
||||
), '[]'::json) AS extra_customer_entitlements
|
||||
|
||||
FROM customer_records cr
|
||||
ORDER BY cr.created_at DESC, cr.id DESC;
|
||||
@@ -0,0 +1,127 @@
|
||||
-- ============================================================
|
||||
-- Variant 02: flat bulk fetch (assemble in code)
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Hypothesis: one SQL round-trip, multiple JSON columns each
|
||||
-- with a bulk-IN-list fetch per relation. No per-row JSON
|
||||
-- construction, no nested SubPlan per ce, no joins beyond what
|
||||
-- the planner pulls into hash joins. Application assembles the
|
||||
-- nested response.
|
||||
--
|
||||
-- Includes entity-attached cps (cps where internal_entity_id
|
||||
-- matches an entity owned by a page-customer). Entities are
|
||||
-- returned as a separate relation.
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
ents AS MATERIALIZED (
|
||||
SELECT e.*
|
||||
FROM entities e
|
||||
WHERE e.internal_customer_id IN (SELECT internal_id FROM cr)
|
||||
),
|
||||
|
||||
cps_ranked AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY cp.internal_customer_id
|
||||
ORDER BY p.is_add_on ASC, cp.created_at DESC
|
||||
) AS rn
|
||||
FROM customer_products cp
|
||||
JOIN products p ON p.internal_id = cp.internal_product_id
|
||||
WHERE cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
AND (
|
||||
cp.internal_customer_id IN (SELECT internal_id FROM cr)
|
||||
OR cp.internal_entity_id IN (SELECT internal_id FROM ents)
|
||||
)
|
||||
),
|
||||
|
||||
cps AS MATERIALIZED (
|
||||
SELECT * FROM cps_ranked WHERE rn <= 15
|
||||
),
|
||||
|
||||
ces AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id IN (SELECT id FROM cps)
|
||||
OR (
|
||||
ce.internal_customer_id IN (SELECT internal_id FROM cr)
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
)
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.created_at DESC, x.id DESC), '[]'::jsonb)
|
||||
FROM cr x) AS customers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x)), '[]'::jsonb)
|
||||
FROM ents x) AS entities,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x)), '[]'::jsonb)
|
||||
FROM cps x) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(p)), '[]'::jsonb)
|
||||
FROM products p
|
||||
WHERE p.internal_id IN (SELECT internal_product_id FROM cps)) AS products,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x)), '[]'::jsonb)
|
||||
FROM ces x) AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(e)), '[]'::jsonb)
|
||||
FROM entitlements e
|
||||
WHERE e.id IN (SELECT entitlement_id FROM ces)) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(f)), '[]'::jsonb)
|
||||
FROM features f
|
||||
WHERE f.internal_id IN (
|
||||
SELECT e.internal_feature_id::text
|
||||
FROM entitlements e
|
||||
WHERE e.id IN (SELECT entitlement_id FROM ces)
|
||||
)) AS features,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(cpr)), '[]'::jsonb)
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id IN (SELECT id FROM cps)) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(pr)), '[]'::jsonb)
|
||||
FROM prices pr
|
||||
WHERE pr.id IN (
|
||||
SELECT price_id
|
||||
FROM customer_prices
|
||||
WHERE customer_product_id IN (SELECT id FROM cps)
|
||||
)) AS prices,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ro)), '[]'::jsonb)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id IN (SELECT id FROM ces)
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(r)), '[]'::jsonb)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id IN (SELECT id FROM ces)) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ft)), '[]'::jsonb)
|
||||
FROM free_trials ft
|
||||
WHERE ft.id IN (
|
||||
SELECT free_trial_id FROM cps WHERE free_trial_id IS NOT NULL
|
||||
)) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(s)), '[]'::jsonb)
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id IN (
|
||||
SELECT DISTINCT sub_id
|
||||
FROM cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
)) AS subscriptions;
|
||||
@@ -0,0 +1,121 @@
|
||||
-- ============================================================
|
||||
-- Variant 03: LATERAL per-row for parents, bulk for leaves
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Hypothesis: variant 02 died because OR predicates + bulk-IN
|
||||
-- forced seq scans. Fix: keep LATERAL per-row for cps and ces
|
||||
-- (forces per-key index lookups), then bulk-fetch leaves
|
||||
-- (entitlements / features / rollovers / replaceables) using
|
||||
-- a small ce_ids relation as the driver. No JSON build inside
|
||||
-- inner subplans — flat rows only, app assembles.
|
||||
--
|
||||
-- NOTE: skipping entity-attached cps in this variant. The OR
|
||||
-- on internal_customer_id / internal_entity_id was the killer
|
||||
-- in variant 02. We can layer entities back in once the base
|
||||
-- shape is fast.
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (
|
||||
SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id
|
||||
) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_bound AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ces_loose AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ce_keys AS MATERIALIZED (
|
||||
SELECT id, entitlement_id FROM ces_bound
|
||||
UNION ALL
|
||||
SELECT id, entitlement_id FROM ces_loose
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*) ORDER BY x.created_at DESC, x.id DESC), '[]'::jsonb)
|
||||
FROM cr x) AS customers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb)
|
||||
FROM cps_flat x) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb)
|
||||
FROM ces_bound x) AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb)
|
||||
FROM ces_loose x) AS extra_customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))), '[]'::jsonb)
|
||||
FROM cps_flat cps
|
||||
JOIN customer_prices cpr ON cpr.customer_product_id = cps.id
|
||||
LEFT JOIN prices p ON p.id = cpr.price_id) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(e.*) || jsonb_build_object('feature', to_jsonb(f.*))), '[]'::jsonb)
|
||||
FROM ce_keys
|
||||
JOIN entitlements e ON e.id = ce_keys.entitlement_id
|
||||
JOIN features f ON f.internal_id = e.internal_feature_id) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ro.*)), '[]'::jsonb)
|
||||
FROM ce_keys
|
||||
JOIN rollovers ro ON ro.cus_ent_id = ce_keys.id
|
||||
WHERE ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(r.*)), '[]'::jsonb)
|
||||
FROM ce_keys
|
||||
JOIN replaceables r ON r.cus_ent_id = ce_keys.id) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ft.*)), '[]'::jsonb)
|
||||
FROM cps_flat cps
|
||||
JOIN free_trials ft ON ft.id = cps.free_trial_id
|
||||
WHERE cps.free_trial_id IS NOT NULL) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(s.*)), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN subscriptions s ON s.stripe_id = sub_id_t.sub_id
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions;
|
||||
@@ -0,0 +1,150 @@
|
||||
-- ============================================================
|
||||
-- Variant 04: LATERAL for leaves too
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Hypothesis: variant 03's killer was the planner choosing a
|
||||
-- Merge Join over the full entitlements table because the
|
||||
-- ce_keys CTE row estimate was 100x off (206k est, 2.8k real).
|
||||
-- Same in features (53k seq scan + 17MB hash).
|
||||
--
|
||||
-- Fix: wrap each leaf bulk fetch in CROSS JOIN LATERAL so the
|
||||
-- inner subquery executes per-driver-row — forces nested loop
|
||||
-- index scan regardless of CTE cardinality estimates.
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (
|
||||
SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id
|
||||
) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_bound AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ces_loose AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ce_keys AS MATERIALIZED (
|
||||
SELECT id, entitlement_id FROM ces_bound
|
||||
UNION ALL
|
||||
SELECT id, entitlement_id FROM ces_loose
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*) ORDER BY x.created_at DESC, x.id DESC), '[]'::jsonb)
|
||||
FROM cr x) AS customers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb)
|
||||
FROM cps_flat x) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb)
|
||||
FROM ces_bound x) AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb)
|
||||
FROM ces_loose x) AS extra_customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))), '[]'::jsonb)
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT cpr.*
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id = cps.id
|
||||
) cpr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p.*
|
||||
FROM prices p
|
||||
WHERE p.id = cpr.price_id
|
||||
) p ON true) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(e.*) || jsonb_build_object('feature', to_jsonb(f.*))), '[]'::jsonb)
|
||||
FROM ce_keys ck
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT e.*
|
||||
FROM entitlements e
|
||||
WHERE e.id = ck.entitlement_id
|
||||
) e
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT f.*
|
||||
FROM features f
|
||||
WHERE f.internal_id = e.internal_feature_id
|
||||
) f) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ro.*)), '[]'::jsonb)
|
||||
FROM ce_keys ck
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT ro.*
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ck.id
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
) ro) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(r.*)), '[]'::jsonb)
|
||||
FROM ce_keys ck
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT r.*
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ck.id
|
||||
) r) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ft.*)), '[]'::jsonb)
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT ft.*
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cps.free_trial_id
|
||||
) ft
|
||||
WHERE cps.free_trial_id IS NOT NULL) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(s.*)), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT s.*
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id = sub_id_t.sub_id
|
||||
) s
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions;
|
||||
163
server/experiments/list-customers-sql/05-array-fence-page1.sql
Normal file
163
server/experiments/list-customers-sql/05-array-fence-page1.sql
Normal file
@@ -0,0 +1,163 @@
|
||||
-- ============================================================
|
||||
-- Variant 05: unnest(array_agg) fence to defeat merge join
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Variants 03/04 failed because Postgres pulls up LATERAL +
|
||||
-- single-equality predicates and re-plans as a merge join over
|
||||
-- the full entitlements table (2M rows scanned). The bad CTE
|
||||
-- row estimate (206k vs 2.8k actual) made merge join "look"
|
||||
-- cheaper.
|
||||
--
|
||||
-- Fix: turn ce_keys into a runtime array via `array_agg`,
|
||||
-- then `unnest` + JOIN LATERAL. The array is a single scalar
|
||||
-- value to the planner — no row-estimate pullup, no merge join.
|
||||
-- The unnest output drives nested-loop index lookups one per
|
||||
-- element.
|
||||
--
|
||||
-- Bonus: drop ORDER BY in `jsonb_agg(customers)` since cr is
|
||||
-- already sorted by created_at DESC, id DESC.
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (
|
||||
SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id
|
||||
) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_bound AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ces_loose AS MATERIALIZED (
|
||||
SELECT ce.*
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ce_arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
array_agg(id) AS all_ids,
|
||||
array_agg(DISTINCT entitlement_id) AS distinct_entitlement_ids
|
||||
FROM (
|
||||
SELECT id, entitlement_id FROM ces_bound
|
||||
UNION ALL
|
||||
SELECT id, entitlement_id FROM ces_loose
|
||||
) _
|
||||
),
|
||||
|
||||
cps_arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
array_agg(id) AS cp_ids,
|
||||
array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) AS free_trial_ids
|
||||
FROM cps_flat
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb) FROM cr x) AS customers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb) FROM cps_flat x) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb) FROM ces_bound x) AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(x.*)), '[]'::jsonb) FROM ces_loose x) AS extra_customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))), '[]'::jsonb)
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (
|
||||
SELECT cpr.*
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id = cps.id
|
||||
) cpr ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p.*
|
||||
FROM prices p
|
||||
WHERE p.id = cpr.price_id
|
||||
) p ON true) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(e.*) || jsonb_build_object('feature', to_jsonb(f.*))), '[]'::jsonb)
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM ce_arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (
|
||||
SELECT e.*
|
||||
FROM entitlements e
|
||||
WHERE e.id = u.entitlement_id
|
||||
) e ON true
|
||||
JOIN LATERAL (
|
||||
SELECT f.*
|
||||
FROM features f
|
||||
WHERE f.internal_id = e.internal_feature_id
|
||||
) f ON true) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ro.*)), '[]'::jsonb)
|
||||
FROM unnest((SELECT all_ids FROM ce_arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ro.*
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = u.ce_id
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
) ro ON true) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(r.*)), '[]'::jsonb)
|
||||
FROM unnest((SELECT all_ids FROM ce_arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT r.*
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = u.ce_id
|
||||
) r ON true) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ft.*)), '[]'::jsonb)
|
||||
FROM unnest((SELECT free_trial_ids FROM cps_arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ft.*
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = u.ft_id
|
||||
) ft ON true) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(s.*)), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (
|
||||
SELECT s.*
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id = sub_id_t.sub_id
|
||||
) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions;
|
||||
170
server/experiments/list-customers-sql/06-prebuilt-json-page1.sql
Normal file
170
server/experiments/list-customers-sql/06-prebuilt-json-page1.sql
Normal file
@@ -0,0 +1,170 @@
|
||||
-- ============================================================
|
||||
-- Variant 06: precompute jsonb at CTE build
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Each CTE stores its row as: (fk columns needed by downstream
|
||||
-- + a pre-built jsonb_blob column). The final jsonb_agg
|
||||
-- just collects pre-built blobs — no per-row to_jsonb work
|
||||
-- during the aggregation scan.
|
||||
--
|
||||
-- Expected savings: variant 05 spent ~190ms in jsonb_agg
|
||||
-- across 4 wide-row CTEs. With pre-built blobs, aggregation
|
||||
-- becomes a memory walk + array build (~30-50ms).
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
to_jsonb(c.*) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
to_jsonb(cp.*) || jsonb_build_object('product', to_jsonb(prod.*)) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (
|
||||
SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id
|
||||
) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_bound AS MATERIALIZED (
|
||||
SELECT
|
||||
ce.id,
|
||||
ce.entitlement_id,
|
||||
to_jsonb(ce.*) AS row_json
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ces_loose AS MATERIALIZED (
|
||||
SELECT
|
||||
ce.id,
|
||||
ce.entitlement_id,
|
||||
to_jsonb(ce.*) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
ce_arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
array_agg(id) AS all_ids,
|
||||
array_agg(DISTINCT entitlement_id) AS distinct_entitlement_ids
|
||||
FROM (
|
||||
SELECT id, entitlement_id FROM ces_bound
|
||||
UNION ALL
|
||||
SELECT id, entitlement_id FROM ces_loose
|
||||
) _
|
||||
),
|
||||
|
||||
cps_arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) AS free_trial_ids
|
||||
FROM cps_flat
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(jsonb_agg(row_json), '[]'::jsonb) FROM cr) AS customers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(row_json), '[]'::jsonb) FROM cps_flat) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(row_json), '[]'::jsonb) FROM ces_bound) AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(row_json), '[]'::jsonb) FROM ces_loose) AS extra_customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))), '[]'::jsonb)
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (
|
||||
SELECT cpr.*
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id = cps.id
|
||||
) cpr ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p.*
|
||||
FROM prices p
|
||||
WHERE p.id = cpr.price_id
|
||||
) p ON true) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(e.*) || jsonb_build_object('feature', to_jsonb(f.*))), '[]'::jsonb)
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM ce_arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (
|
||||
SELECT e.*
|
||||
FROM entitlements e
|
||||
WHERE e.id = u.entitlement_id
|
||||
) e ON true
|
||||
JOIN LATERAL (
|
||||
SELECT f.*
|
||||
FROM features f
|
||||
WHERE f.internal_id = e.internal_feature_id
|
||||
) f ON true) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ro.*)), '[]'::jsonb)
|
||||
FROM unnest((SELECT all_ids FROM ce_arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ro.*
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = u.ce_id
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
) ro ON true) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(r.*)), '[]'::jsonb)
|
||||
FROM unnest((SELECT all_ids FROM ce_arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT r.*
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = u.ce_id
|
||||
) r ON true) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(ft.*)), '[]'::jsonb)
|
||||
FROM unnest((SELECT free_trial_ids FROM cps_arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ft.*
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = u.ft_id
|
||||
) ft ON true) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(jsonb_agg(to_jsonb(s.*)), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (
|
||||
SELECT s.*
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id = sub_id_t.sub_id
|
||||
) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions;
|
||||
@@ -0,0 +1,151 @@
|
||||
-- ============================================================
|
||||
-- Variant 07: switch to json/json_agg + merge ces CTEs
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Three tweaks vs variant 06:
|
||||
-- 1. Use `row_to_json` + `json_agg` instead of `to_jsonb` +
|
||||
-- `jsonb_agg`. text format, no binary conversion overhead.
|
||||
-- 2. Merge ces_bound + ces_loose into one CTE with a kind
|
||||
-- column. Saves one materialization pass + one scan.
|
||||
-- 3. Drop the inner Sort Key in cps_flat LATERAL — the
|
||||
-- SubPlan(is_add_on) lookup per cp + sort costs ~10ms.
|
||||
-- Most customers have ≤2 cps so the ordering is moot;
|
||||
-- we still respect ORDER BY cp.created_at DESC.
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::json AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_combined AS MATERIALIZED (
|
||||
SELECT 'bound'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
UNION ALL
|
||||
SELECT 'loose'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
(SELECT array_agg(id) FROM ces_combined) AS all_ce_ids,
|
||||
(SELECT array_agg(DISTINCT entitlement_id) FROM ces_combined) AS distinct_entitlement_ids,
|
||||
(SELECT array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) FROM cps_flat) AS free_trial_ids
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_flat) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'bound') AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'loose') AS extra_customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))), '[]'::json)
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (
|
||||
SELECT cpr.*
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id = cps.id
|
||||
) cpr ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p.*
|
||||
FROM prices p
|
||||
WHERE p.id = cpr.price_id
|
||||
) p ON true) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f))), '[]'::json)
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (
|
||||
SELECT e.*
|
||||
FROM entitlements e
|
||||
WHERE e.id = u.entitlement_id
|
||||
) e ON true
|
||||
JOIN LATERAL (
|
||||
SELECT f.*
|
||||
FROM features f
|
||||
WHERE f.internal_id = e.internal_feature_id
|
||||
) f ON true) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(ro)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ro.*
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = u.ce_id
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
) ro ON true) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(r)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT r.*
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = u.ce_id
|
||||
) r ON true) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(ft)), '[]'::json)
|
||||
FROM unnest((SELECT free_trial_ids FROM arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ft.*
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = u.ft_id
|
||||
) ft ON true) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(s)), '[]'::json)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (
|
||||
SELECT s.*
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id = sub_id_t.sub_id
|
||||
) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions;
|
||||
328
server/experiments/list-customers-sql/08-old-vs-new-deep.sql
Normal file
328
server/experiments/list-customers-sql/08-old-vs-new-deep.sql
Normal file
@@ -0,0 +1,328 @@
|
||||
-- ============================================================
|
||||
-- Deep page comparison: OLD (limit/offset) vs NEW (cursor v07)
|
||||
-- Target: page at offset ~550k (45% of 1.22M customers)
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Both queries return the same shape of data (page of 1000
|
||||
-- customers with full hydration). The OLD path is the current
|
||||
-- v2.2 behavior (limit + offset, full CTE pipeline). The NEW
|
||||
-- path is variant 07 (cursor + array fence + json_agg).
|
||||
--
|
||||
-- Run each EXPLAIN block separately to get clean plans.
|
||||
-- ============================================================
|
||||
|
||||
-- ╔══════════════════════════════════════════════════════════╗
|
||||
-- ║ OLD: limit + offset, current getPaginatedFullCusQuery ║
|
||||
-- ║ LIMIT 1000 OFFSET 550000 ║
|
||||
-- ╚══════════════════════════════════════════════════════════╝
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH customer_records AS (
|
||||
SELECT c.*
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT 1000 OFFSET 550000
|
||||
),
|
||||
|
||||
customer_products_with_prices AS (
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product,
|
||||
cpr_data.customer_prices,
|
||||
ce_data.customer_entitlements,
|
||||
ft_data.free_trial
|
||||
FROM customer_records cr
|
||||
JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
|
||||
) FILTER (WHERE cpr.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_prices
|
||||
FROM customer_prices cpr
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
WHERE cpr.customer_product_id = cp.id
|
||||
) cpr_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST) FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_entitlements
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cp.id
|
||||
) ce_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT row_to_json(ft) AS free_trial
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) ft_data ON true
|
||||
),
|
||||
|
||||
customer_products_aggregated AS (
|
||||
SELECT
|
||||
cpwp.internal_customer_id,
|
||||
json_agg(row_to_json(cpwp) ORDER BY cpwp.created_at DESC) AS customer_products
|
||||
FROM customer_products_with_prices cpwp
|
||||
GROUP BY cpwp.internal_customer_id
|
||||
),
|
||||
|
||||
customer_subscriptions AS (
|
||||
SELECT
|
||||
s.internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(s)) FILTER (WHERE s.stripe_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS subscriptions
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
cpwp.internal_customer_id,
|
||||
s.*
|
||||
FROM customer_products_with_prices cpwp
|
||||
JOIN LATERAL unnest(cpwp.subscription_ids) AS cpwp_sub(stripe_id) ON true
|
||||
JOIN subscriptions s ON s.stripe_id = cpwp_sub.stripe_id
|
||||
) s
|
||||
GROUP BY s.internal_customer_id
|
||||
),
|
||||
|
||||
extra_customer_entitlements AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
),
|
||||
'rollovers', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(ro) ORDER BY ro.expires_at ASC NULLS LAST)
|
||||
FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000 OR ro.expires_at IS NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
ORDER BY ce.id DESC
|
||||
) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
GROUP BY cr.internal_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
cr.*,
|
||||
COALESCE(cpa.customer_products, '[]'::json) AS customer_products,
|
||||
COALESCE(cs.subscriptions, '[]'::json) AS subscriptions,
|
||||
COALESCE(ece.extra_customer_entitlements, '[]'::json) AS extra_customer_entitlements
|
||||
FROM customer_records cr
|
||||
LEFT JOIN customer_products_aggregated cpa ON cpa.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN customer_subscriptions cs ON cs.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN extra_customer_entitlements ece ON ece.internal_customer_id = cr.internal_id
|
||||
ORDER BY cr.created_at DESC;
|
||||
|
||||
|
||||
-- ╔══════════════════════════════════════════════════════════╗
|
||||
-- ║ NEW: variant 07 — cursor + array fence + json_agg ║
|
||||
-- ║ Deep cursor at the same boundary as OFFSET 550000 ║
|
||||
-- ╚══════════════════════════════════════════════════════════╝
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
AND (c.created_at, c.id) < (1774237983361, '772c9569-fc97-4c30-9fd8-c8a585b66755')
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::json AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_combined AS MATERIALIZED (
|
||||
SELECT 'bound'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
UNION ALL
|
||||
SELECT 'loose'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
(SELECT array_agg(id) FROM ces_combined) AS all_ce_ids,
|
||||
(SELECT array_agg(DISTINCT entitlement_id) FROM ces_combined) AS distinct_entitlement_ids,
|
||||
(SELECT array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) FROM cps_flat) AS free_trial_ids
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_flat) AS customer_products,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'bound') AS customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'loose') AS extra_customer_entitlements,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))), '[]'::json)
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (
|
||||
SELECT cpr.*
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id = cps.id
|
||||
) cpr ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p.*
|
||||
FROM prices p
|
||||
WHERE p.id = cpr.price_id
|
||||
) p ON true) AS customer_prices,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f))), '[]'::json)
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (
|
||||
SELECT e.*
|
||||
FROM entitlements e
|
||||
WHERE e.id = u.entitlement_id
|
||||
) e ON true
|
||||
JOIN LATERAL (
|
||||
SELECT f.*
|
||||
FROM features f
|
||||
WHERE f.internal_id = e.internal_feature_id
|
||||
) f ON true) AS entitlements,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(ro)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ro.*
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = u.ce_id
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
) ro ON true) AS rollovers,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(r)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT r.*
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = u.ce_id
|
||||
) r ON true) AS replaceables,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(ft)), '[]'::json)
|
||||
FROM unnest((SELECT free_trial_ids FROM arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ft.*
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = u.ft_id
|
||||
) ft ON true) AS free_trials,
|
||||
|
||||
(SELECT COALESCE(json_agg(row_to_json(s)), '[]'::json)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (
|
||||
SELECT s.*
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id = sub_id_t.sub_id
|
||||
) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions;
|
||||
@@ -0,0 +1,244 @@
|
||||
-- ============================================================
|
||||
-- Variant 09: variant 07 + SQL-side reassembly into nested shape
|
||||
-- Page 1, limit=1000
|
||||
-- Target org: Firecrawl
|
||||
--
|
||||
-- Variant 07 returns 10 flat top-level arrays (app must rebuild
|
||||
-- nested hierarchy). This variant adds GROUP BY reassembly so
|
||||
-- the final result is one row { list: [...customers with full
|
||||
-- nested hierarchy...] } — same shape as the current handler.
|
||||
--
|
||||
-- Cost: extra GROUP BY passes (~6 of them) over the materialized
|
||||
-- CTEs. Should pick HashAggregate (not GroupAggregate with the
|
||||
-- COLLATE-C sort that killed the baseline). Expected: +30-60ms
|
||||
-- on top of variant 07's 128ms.
|
||||
--
|
||||
-- Key additions vs variant 07:
|
||||
-- - ces_combined CTE now also stores internal_customer_id and
|
||||
-- customer_product_id for GROUP BY
|
||||
-- - Six "by_<parent>" CTEs that hash-group leaves under their
|
||||
-- parent
|
||||
-- - Final SELECT joins cr × cps_by_cus × subs_by_cus ×
|
||||
-- loose_ces_by_cus and emits one customer object per row
|
||||
-- ============================================================
|
||||
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, FORMAT TEXT)
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = 'biu9vSF7vghBLSKW1UTDwxHBAivjnPaK'
|
||||
AND c.env = 'live'
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1001
|
||||
),
|
||||
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::jsonb AS row_jsonb
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
|
||||
ces_combined AS MATERIALIZED (
|
||||
SELECT
|
||||
'bound'::text AS kind,
|
||||
ce.id,
|
||||
ce.entitlement_id,
|
||||
ce.internal_customer_id,
|
||||
ce.customer_product_id,
|
||||
row_to_json(ce)::jsonb AS row_jsonb
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
UNION ALL
|
||||
SELECT
|
||||
'loose'::text AS kind,
|
||||
ce.id,
|
||||
ce.entitlement_id,
|
||||
ce.internal_customer_id,
|
||||
ce.customer_product_id,
|
||||
row_to_json(ce)::jsonb AS row_jsonb
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
|
||||
arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
(SELECT array_agg(id) FROM ces_combined) AS all_ce_ids,
|
||||
(SELECT array_agg(DISTINCT entitlement_id) FROM ces_combined) AS distinct_entitlement_ids,
|
||||
(SELECT array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) FROM cps_flat) AS free_trial_ids
|
||||
),
|
||||
|
||||
-- ─── leaf lookup tables (one row per leaf, indexed by parent) ─
|
||||
|
||||
ents_by_id AS MATERIALIZED (
|
||||
SELECT
|
||||
e.id AS entitlement_id,
|
||||
(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f)))::jsonb AS ent_obj
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (SELECT * FROM entitlements e WHERE e.id = u.entitlement_id) e ON true
|
||||
JOIN LATERAL (SELECT * FROM features f WHERE f.internal_id = e.internal_feature_id) f ON true
|
||||
),
|
||||
|
||||
rollovers_by_ce AS MATERIALIZED (
|
||||
SELECT
|
||||
ro.cus_ent_id AS ce_id,
|
||||
jsonb_agg(row_to_json(ro)::jsonb) AS ros_arr
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT ro.*
|
||||
FROM rollovers ro
|
||||
WHERE ro.cus_ent_id = u.ce_id
|
||||
AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
) ro ON true
|
||||
GROUP BY ro.cus_ent_id
|
||||
),
|
||||
|
||||
replaceables_by_ce AS MATERIALIZED (
|
||||
SELECT
|
||||
r.cus_ent_id AS ce_id,
|
||||
jsonb_agg(row_to_json(r)::jsonb) AS reps_arr
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (
|
||||
SELECT r.*
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = u.ce_id
|
||||
) r ON true
|
||||
GROUP BY r.cus_ent_id
|
||||
),
|
||||
|
||||
ces_hydrated AS MATERIALIZED (
|
||||
SELECT
|
||||
ce.kind,
|
||||
ce.internal_customer_id,
|
||||
ce.customer_product_id,
|
||||
ce.id AS ce_id,
|
||||
ce.row_jsonb
|
||||
|| jsonb_build_object('entitlement', e.ent_obj)
|
||||
|| jsonb_build_object('rollovers', COALESCE(r.ros_arr, '[]'::jsonb))
|
||||
|| jsonb_build_object('replaceables', COALESCE(rep.reps_arr, '[]'::jsonb))
|
||||
AS ce_obj
|
||||
FROM ces_combined ce
|
||||
LEFT JOIN ents_by_id e ON e.entitlement_id = ce.entitlement_id
|
||||
LEFT JOIN rollovers_by_ce r ON r.ce_id = ce.id
|
||||
LEFT JOIN replaceables_by_ce rep ON rep.ce_id = ce.id
|
||||
),
|
||||
|
||||
ces_by_cp AS MATERIALIZED (
|
||||
SELECT
|
||||
customer_product_id AS cp_id,
|
||||
jsonb_agg(ce_obj) AS ces_arr
|
||||
FROM ces_hydrated
|
||||
WHERE kind = 'bound'
|
||||
GROUP BY customer_product_id
|
||||
),
|
||||
|
||||
loose_ces_by_cus AS MATERIALIZED (
|
||||
SELECT
|
||||
internal_customer_id,
|
||||
jsonb_agg(ce_obj ORDER BY ce_id DESC) AS loose_arr
|
||||
FROM ces_hydrated
|
||||
WHERE kind = 'loose'
|
||||
GROUP BY internal_customer_id
|
||||
),
|
||||
|
||||
cprs_by_cp AS MATERIALIZED (
|
||||
SELECT
|
||||
cpr.customer_product_id AS cp_id,
|
||||
jsonb_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))) AS cprs_arr
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (
|
||||
SELECT cpr.*
|
||||
FROM customer_prices cpr
|
||||
WHERE cpr.customer_product_id = cps.id
|
||||
) cpr ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p.*
|
||||
FROM prices p
|
||||
WHERE p.id = cpr.price_id
|
||||
) p ON true
|
||||
GROUP BY cpr.customer_product_id
|
||||
),
|
||||
|
||||
fts_by_id AS MATERIALIZED (
|
||||
SELECT ft.id AS ft_id, row_to_json(ft)::jsonb AS ft_obj
|
||||
FROM unnest((SELECT free_trial_ids FROM arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (SELECT * FROM free_trials ft WHERE ft.id = u.ft_id) ft ON true
|
||||
),
|
||||
|
||||
cps_hydrated AS MATERIALIZED (
|
||||
SELECT
|
||||
cps.internal_customer_id,
|
||||
cps.row_jsonb
|
||||
|| jsonb_build_object('customer_prices', COALESCE(cprs.cprs_arr, '[]'::jsonb))
|
||||
|| jsonb_build_object('customer_entitlements', COALESCE(ces.ces_arr, '[]'::jsonb))
|
||||
|| jsonb_build_object('free_trial', ft.ft_obj)
|
||||
AS cp_obj
|
||||
FROM cps_flat cps
|
||||
LEFT JOIN cprs_by_cp cprs ON cprs.cp_id = cps.id
|
||||
LEFT JOIN ces_by_cp ces ON ces.cp_id = cps.id
|
||||
LEFT JOIN fts_by_id ft ON ft.ft_id = cps.free_trial_id
|
||||
),
|
||||
|
||||
cps_by_cus AS MATERIALIZED (
|
||||
SELECT
|
||||
internal_customer_id,
|
||||
jsonb_agg(cp_obj) AS cps_arr
|
||||
FROM cps_hydrated
|
||||
GROUP BY internal_customer_id
|
||||
),
|
||||
|
||||
subs_by_cus AS MATERIALIZED (
|
||||
SELECT
|
||||
cps.internal_customer_id,
|
||||
jsonb_agg(DISTINCT row_to_json(s)::jsonb) AS subs_arr
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (
|
||||
SELECT s.*
|
||||
FROM subscriptions s
|
||||
WHERE s.stripe_id = sub_id_t.sub_id
|
||||
) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
GROUP BY cps.internal_customer_id
|
||||
)
|
||||
|
||||
SELECT jsonb_agg(
|
||||
cr.row_json::jsonb
|
||||
|| jsonb_build_object('customer_products', COALESCE(cps.cps_arr, '[]'::jsonb))
|
||||
|| jsonb_build_object('subscriptions', COALESCE(s.subs_arr, '[]'::jsonb))
|
||||
|| jsonb_build_object('extra_customer_entitlements', COALESCE(lc.loose_arr, '[]'::jsonb))
|
||||
ORDER BY cr.created_at DESC, cr.id DESC
|
||||
) AS list
|
||||
FROM cr
|
||||
LEFT JOIN cps_by_cus cps ON cps.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN subs_by_cus s ON s.internal_customer_id = cr.internal_id
|
||||
LEFT JOIN loose_ces_by_cus lc ON lc.internal_customer_id = cr.internal_id;
|
||||
29
server/experiments/list-customers-sql/README.md
Normal file
29
server/experiments/list-customers-sql/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# List Customers SQL Optimization
|
||||
|
||||
Hand-rolled SQL variants for iterating toward 100ms per page on the list customers cursor query.
|
||||
|
||||
## Test fixture
|
||||
|
||||
- **Org**: Firecrawl (`biu9vSF7vghBLSKW1UTDwxHBAivjnPaK`)
|
||||
- **Env**: `live`
|
||||
- **Limit**: 1000
|
||||
- **Deep cursor**: `{ t: 1774237983361, id: "772c9569-fc97-4c30-9fd8-c8a585b66755" }` (~45% into ~1.22M customers)
|
||||
|
||||
## Variants
|
||||
|
||||
| # | File | What changed | Median ms (page 1 / deep) |
|
||||
|---|------|--------------|---------------------------|
|
||||
| 00 | `00-baseline-{page1,deep}.sql` | Current `getCursorPaginatedFullCusQuery`, full CTE pipeline, withSubs=true | TBD / TBD |
|
||||
|
||||
## How to run
|
||||
|
||||
```sh
|
||||
cat 00-baseline-page1.sql | pbcopy # paste into Table+
|
||||
cat 00-baseline-deep.sql | pbcopy
|
||||
```
|
||||
|
||||
Or directly via psql with prod DATABASE_URL set.
|
||||
|
||||
## Goal
|
||||
|
||||
100ms per page (both first and deep). Currently ~2480ms on Firecrawl per the prior benchmark.
|
||||
173
server/experiments/optimizedFullCusQuery.ts
Normal file
173
server/experiments/optimizedFullCusQuery.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type {
|
||||
AppEnv,
|
||||
CusProductStatus,
|
||||
ListCustomersV2Params,
|
||||
StandardCursorFields,
|
||||
} from "@autumn/shared";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { getCustomerListFilterSql } from "../src/internal/customers/getFullCusQuery";
|
||||
|
||||
export type OptimizedQueryArgs = {
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
inStatuses?: CusProductStatus[];
|
||||
withSubs?: boolean;
|
||||
limit: number;
|
||||
cursor?: StandardCursorFields;
|
||||
internalCustomerIds?: string[];
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
search?: string;
|
||||
cusProductLimit: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set-based replacement for the per-customer LATERAL nested loops.
|
||||
* Key change: each child fetch is a single hash/merge join over the cr CTE
|
||||
* instead of 1001 sequential scans (original Seq Scan-per-loop pattern).
|
||||
*/
|
||||
export const getOptimizedFullCusQuery = ({
|
||||
orgId,
|
||||
env,
|
||||
inStatuses,
|
||||
withSubs = true,
|
||||
limit,
|
||||
cursor,
|
||||
internalCustomerIds,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
cusProductLimit,
|
||||
}: OptimizedQueryArgs) => {
|
||||
const cpStatusFilter = inStatuses?.length
|
||||
? sql`AND cp.status = ANY(ARRAY[${sql.join(
|
||||
inStatuses.map((status) => sql`${status}`),
|
||||
sql`, `,
|
||||
)}])`
|
||||
: sql``;
|
||||
|
||||
const customerListFilterSql = getCustomerListFilterSql({
|
||||
internalCustomerIds,
|
||||
inStatuses,
|
||||
plans,
|
||||
processors,
|
||||
search,
|
||||
});
|
||||
|
||||
const cursorPredicate = cursor
|
||||
? sql`AND (c.created_at, c.id) < (${cursor.t}, ${cursor.id})`
|
||||
: sql``;
|
||||
|
||||
const fetchLimit = limit + 1;
|
||||
|
||||
const subscriptionsSelect = withSubs
|
||||
? sql`(
|
||||
SELECT COALESCE(json_agg(row_to_json(s)), '[]'::json)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_ranked cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN subscriptions s ON s.stripe_id = sub_id_t.sub_id
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s
|
||||
) AS subscriptions`
|
||||
: sql`'[]'::json AS subscriptions`;
|
||||
|
||||
return sql`
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${env}
|
||||
${customerListFilterSql}
|
||||
${cursorPredicate}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${fetchLimit}
|
||||
),
|
||||
cp_ranked_ids AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY cp.internal_customer_id
|
||||
ORDER BY prod.is_add_on ASC, cp.created_at DESC
|
||||
) AS rn
|
||||
FROM cr
|
||||
JOIN customer_products cp ON cp.internal_customer_id = cr.internal_id
|
||||
JOIN products prod ON prod.internal_id = cp.internal_product_id
|
||||
WHERE TRUE ${cpStatusFilter}
|
||||
),
|
||||
cps_ranked AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::json AS row_json
|
||||
FROM cp_ranked_ids r
|
||||
JOIN customer_products cp ON cp.id = r.id
|
||||
JOIN products prod ON prod.internal_id = r.internal_product_id
|
||||
WHERE r.rn <= ${cusProductLimit}
|
||||
),
|
||||
ces_bound AS MATERIALIZED (
|
||||
-- Single hash join instead of 1001 LATERAL scans.
|
||||
SELECT ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cps_ranked
|
||||
JOIN customer_entitlements ce ON ce.customer_product_id = cps_ranked.id
|
||||
),
|
||||
ces_loose AS MATERIALIZED (
|
||||
-- LATERAL with LIMIT 30 stays; existing partial index is well-suited.
|
||||
SELECT ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
ces_all AS MATERIALIZED (
|
||||
SELECT id, entitlement_id FROM ces_bound
|
||||
UNION ALL
|
||||
SELECT id, entitlement_id FROM ces_loose
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_ranked) AS customer_products,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_bound) AS customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_json ORDER BY id DESC), '[]'::json) FROM ces_loose) AS extra_customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))), '[]'::json)
|
||||
FROM cps_ranked
|
||||
JOIN customer_prices cpr ON cpr.customer_product_id = cps_ranked.id
|
||||
LEFT JOIN prices p ON p.id = cpr.price_id
|
||||
) AS customer_prices,
|
||||
(SELECT COALESCE(json_agg(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f))), '[]'::json)
|
||||
FROM (SELECT DISTINCT entitlement_id FROM ces_all) ce
|
||||
JOIN entitlements e ON e.id = ce.entitlement_id
|
||||
JOIN features f ON f.internal_id = e.internal_feature_id
|
||||
) AS entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ro)), '[]'::json)
|
||||
FROM ces_all
|
||||
JOIN rollovers ro ON ro.cus_ent_id = ces_all.id
|
||||
WHERE ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000
|
||||
) AS rollovers,
|
||||
(SELECT COALESCE(json_agg(row_to_json(r)), '[]'::json)
|
||||
FROM ces_all
|
||||
JOIN replaceables r ON r.cus_ent_id = ces_all.id
|
||||
) AS replaceables,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ft)), '[]'::json)
|
||||
FROM (SELECT DISTINCT free_trial_id FROM cps_ranked WHERE free_trial_id IS NOT NULL) cps
|
||||
JOIN free_trials ft ON ft.id = cps.free_trial_id
|
||||
) AS free_trials,
|
||||
${subscriptionsSelect}
|
||||
`;
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
||||
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
import { getCursorPaginatedFullCusQuery } from "./cursorPaginatedFullCusQuery";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
@@ -158,7 +158,7 @@ const resolveRevenuecatDeepCursor = async ({
|
||||
}: {
|
||||
db: DB;
|
||||
deepOffset: number;
|
||||
}): Promise<{ createdAt: number; id: string }> => {
|
||||
}): Promise<{ v: 0; t: number; id: string }> => {
|
||||
const result = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
@@ -182,13 +182,13 @@ const resolveRevenuecatDeepCursor = async ({
|
||||
`Could not resolve revenuecat deep cursor at offset ${deepOffset}. Not enough revenuecat customers.`,
|
||||
);
|
||||
}
|
||||
return { createdAt: row.created_at, id: row.id };
|
||||
return { v: 0, t: row.created_at, id: row.id };
|
||||
};
|
||||
|
||||
const buildCells = ({
|
||||
deepCursor,
|
||||
}: {
|
||||
deepCursor: { createdAt: number; id: string };
|
||||
deepCursor: { v: 0; t: number; id: string };
|
||||
}): Cell[] => {
|
||||
return [
|
||||
{
|
||||
@@ -227,7 +227,7 @@ const renderResults = ({
|
||||
}: {
|
||||
totalCount: number;
|
||||
deepOffset: number;
|
||||
deepCursor: { createdAt: number; id: string };
|
||||
deepCursor: { v: 0; t: number; id: string };
|
||||
results: CellResult[];
|
||||
}): string => {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
@@ -244,7 +244,7 @@ const renderResults = ({
|
||||
`- deep_offset (${DEEP_OFFSET_PCT}% within revcat subset): \`${deepOffset.toLocaleString()}\``,
|
||||
);
|
||||
lines.push(
|
||||
`- deep_cursor (revcat-aware): \`{ t: ${deepCursor.createdAt}, id: ${deepCursor.id} }\``,
|
||||
`- deep_cursor (revcat-aware): \`{ t: ${deepCursor.t}, id: ${deepCursor.id} }\``,
|
||||
);
|
||||
lines.push(`- limit: \`${BASE_LIMIT}\``);
|
||||
lines.push(`- repeats per cell: ${REPEATS}`);
|
||||
@@ -308,7 +308,7 @@ const main = async () => {
|
||||
);
|
||||
const deepCursor = await resolveRevenuecatDeepCursor({ db, deepOffset });
|
||||
console.log(
|
||||
chalk.gray(` cursor = { t: ${deepCursor.createdAt}, id: ${deepCursor.id} }`),
|
||||
chalk.gray(` cursor = { t: ${deepCursor.t}, id: ${deepCursor.id} }`),
|
||||
);
|
||||
console.log();
|
||||
|
||||
|
||||
678
server/experiments/paginationBenchmarkDbStats.ts
Normal file
678
server/experiments/paginationBenchmarkDbStats.ts
Normal file
@@ -0,0 +1,678 @@
|
||||
import { AppEnv, CusProductStatus, type ListCustomersV2Params } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { getPaginatedFullCusQuery } from "../src/internal/customers/getFullCusQuery";
|
||||
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
// Runs against whatever DATABASE_URL points at. Intentionally bypasses
|
||||
// the prod-safety guard — laptop → prod is the entire point.
|
||||
|
||||
const FIRECRAWL_ORG_ID = "biu9vSF7vghBLSKW1UTDwxHBAivjnPaK";
|
||||
const RUNABLE_ORG_ID = "r7pwHiekGsqt32qGqcVku6thWFh5aHh8";
|
||||
const ENV: AppEnv = AppEnv.Live;
|
||||
|
||||
const BASE_LIMIT = 1000;
|
||||
const REPEATS = 5;
|
||||
const STATEMENT_TIMEOUT_MS = 60_000;
|
||||
const READ_ONLY = true;
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
const DEEP_OFFSET_PCT = 45;
|
||||
const FIRECRAWL_BASELINE_DEEP_OFFSET = 950_000;
|
||||
|
||||
const PLAN_MASSIVE = "free";
|
||||
const PLAN_MID = "hobby";
|
||||
const PLAN_RARE = "scale_monthly";
|
||||
const SEARCH_GMAIL = "@gmail";
|
||||
|
||||
const RESULTS_DIR = join(import.meta.dir, "results");
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
type Cursor = { v: 0; t: number; id: string };
|
||||
|
||||
type FilterOverrides = {
|
||||
inStatuses?: CusProductStatus[];
|
||||
search?: string;
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
processors?: ListCustomersV2Params["processors"];
|
||||
internalCustomerIds?: string[];
|
||||
};
|
||||
|
||||
type Scenario = {
|
||||
key: string;
|
||||
label: string;
|
||||
org: "firecrawl" | "runable";
|
||||
orgId: string;
|
||||
filter: FilterOverrides;
|
||||
withDeep: boolean;
|
||||
deepOffset?: number;
|
||||
deepCursor?: Cursor;
|
||||
};
|
||||
|
||||
type Cell = {
|
||||
scenarioKey: string;
|
||||
scenarioLabel: string;
|
||||
org: "firecrawl" | "runable";
|
||||
queryShape: "offset" | "cursor";
|
||||
depth: "page1" | "deep";
|
||||
build: () => SQL;
|
||||
};
|
||||
|
||||
type CellResult = Cell & {
|
||||
medianMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
rowCount: number;
|
||||
samples: number[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const sharedFullCusOpts = {
|
||||
includeInvoices: false,
|
||||
withEntities: false,
|
||||
withTrialsUsed: false,
|
||||
withSubs: true,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
};
|
||||
|
||||
const normalizeRows = (r: unknown): Record<string, unknown>[] => {
|
||||
if (Array.isArray(r)) return r as Record<string, unknown>[];
|
||||
if (r && typeof r === "object" && "rows" in r) {
|
||||
return (r as { rows: Record<string, unknown>[] }).rows;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const runQueryInTxn = async ({
|
||||
db,
|
||||
query,
|
||||
}: { db: DB; query: SQL }): Promise<Record<string, unknown>[]> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
if (READ_ONLY) {
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
}
|
||||
return normalizeRows(await tx.execute(query));
|
||||
});
|
||||
};
|
||||
|
||||
const measureCell = async ({
|
||||
db,
|
||||
cell,
|
||||
}: { db: DB; cell: Cell }): Promise<CellResult> => {
|
||||
const samples: number[] = [];
|
||||
let rowCount = 0;
|
||||
let error: string | undefined;
|
||||
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const query = cell.build();
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const result = await runQueryInTxn({ db, query });
|
||||
const elapsed = performance.now() - startedAt;
|
||||
samples.push(elapsed);
|
||||
rowCount = result.length;
|
||||
} catch (err) {
|
||||
const elapsed = performance.now() - startedAt;
|
||||
samples.push(elapsed);
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
||||
const p95 =
|
||||
sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
||||
const min = sorted[0] ?? 0;
|
||||
const max = sorted[sorted.length - 1] ?? 0;
|
||||
|
||||
return {
|
||||
...cell,
|
||||
medianMs: median,
|
||||
p95Ms: p95,
|
||||
minMs: min,
|
||||
maxMs: max,
|
||||
rowCount,
|
||||
samples,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Per-filter predicate matching getCustomerListFilterSql, used by the
|
||||
// resolver helpers (count + deep cursor) so deep cells are filter-aware.
|
||||
const filterPredicate = (f: FilterOverrides): SQL => {
|
||||
const parts: SQL[] = [];
|
||||
|
||||
if (f.internalCustomerIds?.length) {
|
||||
parts.push(
|
||||
sql`AND c.internal_id IN (${sql.join(
|
||||
f.internalCustomerIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (f.plans?.length) {
|
||||
const planConditions = f.plans.map((plan) => {
|
||||
if (plan.versions?.length) {
|
||||
return sql`(p_filter.id = ${plan.id} AND p_filter.version IN (${sql.join(
|
||||
plan.versions.map((v) => sql`${v}`),
|
||||
sql`, `,
|
||||
)}))`;
|
||||
}
|
||||
return sql`p_filter.id = ${plan.id}`;
|
||||
});
|
||||
parts.push(sql`AND EXISTS (
|
||||
SELECT 1
|
||||
FROM customer_products cp_filter
|
||||
JOIN products p_filter ON cp_filter.internal_product_id = p_filter.internal_id
|
||||
WHERE cp_filter.internal_customer_id = c.internal_id
|
||||
${
|
||||
f.inStatuses?.length
|
||||
? sql`AND cp_filter.status = ANY(ARRAY[${sql.join(
|
||||
f.inStatuses.map((s) => sql`${s}`),
|
||||
sql`, `,
|
||||
)}])`
|
||||
: sql``
|
||||
}
|
||||
AND (${sql.join(planConditions, sql` OR `)})
|
||||
)`);
|
||||
}
|
||||
|
||||
const trimmedSearch = f.search?.trim();
|
||||
if (trimmedSearch) {
|
||||
const pattern = `%${trimmedSearch}%`;
|
||||
parts.push(sql`AND (
|
||||
c.id ILIKE ${pattern}
|
||||
OR c.name ILIKE ${pattern}
|
||||
OR c.email ILIKE ${pattern}
|
||||
)`);
|
||||
}
|
||||
|
||||
if (f.processors?.length) {
|
||||
const procConditions = f.processors
|
||||
.map((proc) => {
|
||||
if (proc === "stripe") return sql`(c.processor->>'id' IS NOT NULL)`;
|
||||
if (proc === "revenuecat")
|
||||
return sql`EXISTS (
|
||||
SELECT 1
|
||||
FROM customer_products cp_processor
|
||||
WHERE cp_processor.internal_customer_id = c.internal_id
|
||||
AND cp_processor.processor->>'type' = 'revenuecat'
|
||||
)`;
|
||||
if (proc === "vercel") return sql`(c.processors->>'vercel' IS NOT NULL)`;
|
||||
return null;
|
||||
})
|
||||
.filter((c): c is SQL => c !== null);
|
||||
|
||||
if (procConditions.length) {
|
||||
parts.push(sql`AND (${sql.join(procConditions, sql` OR `)})`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length ? sql.join(parts, sql` `) : sql``;
|
||||
};
|
||||
|
||||
const resolveCount = async ({
|
||||
db,
|
||||
orgId,
|
||||
filter,
|
||||
}: { db: DB; orgId: string; filter: FilterOverrides }): Promise<number> => {
|
||||
const predicate = filterPredicate(filter);
|
||||
const rows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT COUNT(*)::int AS total_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${ENV}
|
||||
${predicate}
|
||||
`,
|
||||
});
|
||||
return (rows[0] as { total_count: number }).total_count;
|
||||
};
|
||||
|
||||
const resolveDeepCursor = async ({
|
||||
db,
|
||||
orgId,
|
||||
filter,
|
||||
deepOffset,
|
||||
}: {
|
||||
db: DB;
|
||||
orgId: string;
|
||||
filter: FilterOverrides;
|
||||
deepOffset: number;
|
||||
}): Promise<Cursor> => {
|
||||
const predicate = filterPredicate(filter);
|
||||
const rows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.created_at, c.id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${ENV}
|
||||
${predicate}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1 OFFSET ${deepOffset}
|
||||
`,
|
||||
});
|
||||
const row = rows[0] as { created_at: number; id: string } | undefined;
|
||||
if (!row) {
|
||||
throw new Error(
|
||||
`No row at deep offset ${deepOffset} for filter ${JSON.stringify(filter)}`,
|
||||
);
|
||||
}
|
||||
return { v: 0, t: row.created_at, id: row.id };
|
||||
};
|
||||
|
||||
const resolveFirecrawlInternalIds = async ({
|
||||
db,
|
||||
}: { db: DB }): Promise<string[]> => {
|
||||
const rows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT internal_id
|
||||
FROM customers
|
||||
WHERE org_id = ${FIRECRAWL_ORG_ID}
|
||||
AND env = ${ENV}
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
});
|
||||
return rows.map((r) => (r as { internal_id: string }).internal_id);
|
||||
};
|
||||
|
||||
const buildScenarios = ({
|
||||
firecrawlInternalIds,
|
||||
}: { firecrawlInternalIds: string[] }): Scenario[] => {
|
||||
const fc = (
|
||||
key: string,
|
||||
label: string,
|
||||
filter: FilterOverrides,
|
||||
withDeep: boolean,
|
||||
): Scenario => ({
|
||||
key,
|
||||
label,
|
||||
org: "firecrawl",
|
||||
orgId: FIRECRAWL_ORG_ID,
|
||||
filter,
|
||||
withDeep,
|
||||
});
|
||||
|
||||
return [
|
||||
fc("01-baseline", "none (baseline)", {}, true),
|
||||
fc("02-search-gmail", `search '${SEARCH_GMAIL}'`, { search: SEARCH_GMAIL }, true),
|
||||
fc(
|
||||
"03-status-active",
|
||||
"inStatuses=['active']",
|
||||
{ inStatuses: [CusProductStatus.Active] },
|
||||
true,
|
||||
),
|
||||
fc("04-plan-massive", `plans=['${PLAN_MASSIVE}']`, { plans: [{ id: PLAN_MASSIVE }] }, true),
|
||||
fc("05-plan-mid", `plans=['${PLAN_MID}']`, { plans: [{ id: PLAN_MID }] }, true),
|
||||
fc("06-plan-rare", `plans=['${PLAN_RARE}']`, { plans: [{ id: PLAN_RARE }] }, false),
|
||||
fc("07-processor-stripe", "processors=['stripe']", { processors: ["stripe"] }, true),
|
||||
fc(
|
||||
"08-internal-ids",
|
||||
"internalCustomerIds=[10]",
|
||||
{ internalCustomerIds: firecrawlInternalIds },
|
||||
false,
|
||||
),
|
||||
{
|
||||
key: "09-processor-revenuecat",
|
||||
label: "processors=['revenuecat']",
|
||||
org: "runable",
|
||||
orgId: RUNABLE_ORG_ID,
|
||||
filter: { processors: ["revenuecat"] },
|
||||
withDeep: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildCellsForScenario = (s: Scenario): Cell[] => {
|
||||
const inStatuses = s.filter.inStatuses ?? RELEVANT_STATUSES;
|
||||
const filterArgs = {
|
||||
inStatuses,
|
||||
search: s.filter.search,
|
||||
plans: s.filter.plans,
|
||||
processors: s.filter.processors,
|
||||
internalCustomerIds: s.filter.internalCustomerIds,
|
||||
};
|
||||
|
||||
const cells: Cell[] = [
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "offset",
|
||||
depth: "page1",
|
||||
build: () =>
|
||||
getPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
offset: 0,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "cursor",
|
||||
depth: "page1",
|
||||
build: () =>
|
||||
getCursorPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (s.withDeep && s.deepOffset !== undefined && s.deepCursor) {
|
||||
const deepOffset = s.deepOffset;
|
||||
const deepCursor = s.deepCursor;
|
||||
cells.push(
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "offset",
|
||||
depth: "deep",
|
||||
build: () =>
|
||||
getPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
offset: deepOffset,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
{
|
||||
scenarioKey: s.key,
|
||||
scenarioLabel: s.label,
|
||||
org: s.org,
|
||||
queryShape: "cursor",
|
||||
depth: "deep",
|
||||
build: () =>
|
||||
getCursorPaginatedFullCusQuery({
|
||||
orgId: s.orgId,
|
||||
env: ENV,
|
||||
limit: BASE_LIMIT,
|
||||
cursor: deepCursor,
|
||||
...sharedFullCusOpts,
|
||||
...filterArgs,
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
const pairResults = (
|
||||
results: CellResult[],
|
||||
): {
|
||||
scenarioKey: string;
|
||||
scenarioLabel: string;
|
||||
org: string;
|
||||
depth: "page1" | "deep";
|
||||
offset?: CellResult;
|
||||
cursor?: CellResult;
|
||||
}[] => {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
scenarioKey: string;
|
||||
scenarioLabel: string;
|
||||
org: string;
|
||||
depth: "page1" | "deep";
|
||||
offset?: CellResult;
|
||||
cursor?: CellResult;
|
||||
}
|
||||
>();
|
||||
for (const r of results) {
|
||||
const key = `${r.scenarioKey}|${r.depth}`;
|
||||
const group = groups.get(key) ?? {
|
||||
scenarioKey: r.scenarioKey,
|
||||
scenarioLabel: r.scenarioLabel,
|
||||
org: r.org,
|
||||
depth: r.depth,
|
||||
};
|
||||
if (r.queryShape === "offset") group.offset = r;
|
||||
else group.cursor = r;
|
||||
groups.set(key, group);
|
||||
}
|
||||
return Array.from(groups.values());
|
||||
};
|
||||
|
||||
const fmt = (n: number | undefined): string =>
|
||||
n === undefined ? "—" : n.toFixed(0);
|
||||
|
||||
const renderReport = ({
|
||||
scenarios,
|
||||
pairs,
|
||||
totalWallMs,
|
||||
dbHost,
|
||||
}: {
|
||||
scenarios: Scenario[];
|
||||
pairs: ReturnType<typeof pairResults>;
|
||||
totalWallMs: number;
|
||||
dbHost: string;
|
||||
}): string => {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Pagination Benchmark — offset vs cursor — ${date}`);
|
||||
lines.push("");
|
||||
lines.push("## Config");
|
||||
lines.push("");
|
||||
lines.push(`- db host: \`${dbHost}\``);
|
||||
lines.push(`- env: \`${ENV}\``);
|
||||
lines.push(`- limit: ${BASE_LIMIT}`);
|
||||
lines.push(`- repeats per cell: ${REPEATS}`);
|
||||
lines.push(`- statement_timeout_ms: ${STATEMENT_TIMEOUT_MS}`);
|
||||
lines.push(`- read_only: ${READ_ONLY}`);
|
||||
lines.push(`- shared opts: withSubs=true, includeInvoices=false, withEntities=false, withTrialsUsed=false, cusProductLimit=${CUS_PRODUCT_LIMIT}`);
|
||||
lines.push(`- timings are wall-clock from this laptop around drizzle execute (includes network round-trip + result transfer + deserialize)`);
|
||||
lines.push(`- total benchmark wall time: ${(totalWallMs / 1000).toFixed(1)}s`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Scenarios resolved");
|
||||
lines.push("");
|
||||
lines.push("| # | Org | Filter | Filtered count | Deep offset | Deep cursor |");
|
||||
lines.push("|---|-----|--------|----------------|-------------|-------------|");
|
||||
for (const s of scenarios) {
|
||||
const count = (s as Scenario & { resolvedCount?: number }).resolvedCount;
|
||||
lines.push(
|
||||
`| ${s.key} | ${s.org} | \`${s.label}\` | ${count?.toLocaleString() ?? "—"} | ${s.deepOffset?.toLocaleString() ?? "—"} | ${s.deepCursor ? `\`{ t: ${s.deepCursor.t}, id: ${s.deepCursor.id.slice(0, 8)}… }\`` : "—"} |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Results");
|
||||
lines.push("");
|
||||
lines.push(
|
||||
"| # | Org | Filter | Depth | offset median ms | cursor median ms | Δ ms | offset p95 | cursor p95 | offset rows | cursor rows | offset error | cursor error |",
|
||||
);
|
||||
lines.push(
|
||||
"|---|-----|--------|-------|------------------|------------------|------|------------|------------|-------------|-------------|--------------|--------------|",
|
||||
);
|
||||
for (const p of pairs) {
|
||||
const off = p.offset;
|
||||
const cur = p.cursor;
|
||||
const delta =
|
||||
off && cur && !off.error && !cur.error
|
||||
? cur.medianMs - off.medianMs
|
||||
: undefined;
|
||||
const deltaStr =
|
||||
delta === undefined
|
||||
? "—"
|
||||
: `${delta >= 0 ? "+" : ""}${delta.toFixed(0)}`;
|
||||
lines.push(
|
||||
`| ${p.scenarioKey} | ${p.org} | \`${p.scenarioLabel}\` | ${p.depth} | ${fmt(off?.medianMs)} | ${fmt(cur?.medianMs)} | ${deltaStr} | ${fmt(off?.p95Ms)} | ${fmt(cur?.p95Ms)} | ${off?.rowCount ?? "—"} | ${cur?.rowCount ?? "—"} | ${off?.error ? `\`${off.error.slice(0, 50)}\`` : ""} | ${cur?.error ? `\`${cur.error.slice(0, 50)}\`` : ""} |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Raw samples (per cell)");
|
||||
lines.push("");
|
||||
for (const p of pairs) {
|
||||
lines.push(`### ${p.scenarioKey} / ${p.depth}`);
|
||||
if (p.offset) {
|
||||
lines.push(
|
||||
`- offset: [${p.offset.samples.map((s) => s.toFixed(0)).join(", ")}]ms${p.offset.error ? ` (error: ${p.offset.error})` : ""}`,
|
||||
);
|
||||
}
|
||||
if (p.cursor) {
|
||||
lines.push(
|
||||
`- cursor: [${p.cursor.samples.map((s) => s.toFixed(0)).join(", ")}]ms${p.cursor.error ? ` (error: ${p.cursor.error})` : ""}`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const redactHost = (url: string | undefined): string => {
|
||||
if (!url) return "<unset>";
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return `${u.hostname}:${u.port || "5432"}`;
|
||||
} catch {
|
||||
return "<unparseable>";
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const startedAt = performance.now();
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Pagination Benchmark — offset vs cursor (prod DB) ================\n",
|
||||
),
|
||||
);
|
||||
|
||||
const dbHost = redactHost(process.env.DATABASE_URL);
|
||||
console.log(chalk.cyan(`db host: ${dbHost}`));
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan("Resolving Firecrawl internal_ids for IN-list cell..."));
|
||||
const firecrawlInternalIds = await resolveFirecrawlInternalIds({ db });
|
||||
console.log(chalk.gray(` got ${firecrawlInternalIds.length} ids`));
|
||||
|
||||
const scenarios = buildScenarios({ firecrawlInternalIds });
|
||||
|
||||
// Resolve counts and deep cursors for each scenario before running cells.
|
||||
for (const s of scenarios) {
|
||||
process.stdout.write(chalk.gray(`Resolving count for ${s.key} (${s.label})... `));
|
||||
const count = await resolveCount({ db, orgId: s.orgId, filter: s.filter });
|
||||
(s as Scenario & { resolvedCount?: number }).resolvedCount = count;
|
||||
console.log(chalk.gray(`${count.toLocaleString()} customers`));
|
||||
|
||||
if (s.withDeep) {
|
||||
const deepOffset =
|
||||
s.key === "01-baseline"
|
||||
? FIRECRAWL_BASELINE_DEEP_OFFSET
|
||||
: Math.floor((count * DEEP_OFFSET_PCT) / 100);
|
||||
s.deepOffset = deepOffset;
|
||||
process.stdout.write(
|
||||
chalk.gray(` resolving deep cursor at offset ${deepOffset}... `),
|
||||
);
|
||||
try {
|
||||
s.deepCursor = await resolveDeepCursor({
|
||||
db,
|
||||
orgId: s.orgId,
|
||||
filter: s.filter,
|
||||
deepOffset,
|
||||
});
|
||||
console.log(
|
||||
chalk.gray(
|
||||
`{ t: ${s.deepCursor.t}, id: ${s.deepCursor.id.slice(0, 8)}… }`,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
),
|
||||
);
|
||||
s.withDeep = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cells = scenarios.flatMap(buildCellsForScenario);
|
||||
console.log();
|
||||
console.log(chalk.cyan(`Running ${cells.length} cells × ${REPEATS} repeats...`));
|
||||
console.log();
|
||||
|
||||
const results: CellResult[] = [];
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
process.stdout.write(
|
||||
chalk.gray(
|
||||
`[${(i + 1).toString().padStart(2)}/${cells.length}] ${cell.scenarioKey} ${cell.queryShape} ${cell.depth} ... `,
|
||||
),
|
||||
);
|
||||
const result = await measureCell({ db, cell });
|
||||
results.push(result);
|
||||
if (result.error) {
|
||||
console.log(chalk.red(`error: ${result.error.slice(0, 80)}`));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`median=${result.medianMs.toFixed(0)}ms p95=${result.p95Ms.toFixed(0)}ms rows=${result.rowCount}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const pairs = pairResults(results);
|
||||
const totalWallMs = performance.now() - startedAt;
|
||||
const report = renderReport({ scenarios, pairs, totalWallMs, dbHost });
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const outPath = join(RESULTS_DIR, `${date}-offset-vs-cursor.md`);
|
||||
writeFileSync(outPath, report);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green(`✅ Report written to ${outPath}`));
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Benchmark Complete (${(totalWallMs / 1000).toFixed(1)}s) ================\n`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Benchmark failed:"));
|
||||
if (error instanceof Error) {
|
||||
console.error(chalk.red(` ${error.message}`));
|
||||
console.error(chalk.gray(error.stack));
|
||||
} else {
|
||||
console.error(chalk.red(` ${String(error)}`));
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
109
server/experiments/results/2026-05-13-offset-vs-cursor.md
Normal file
109
server/experiments/results/2026-05-13-offset-vs-cursor.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Pagination Benchmark — offset vs cursor — 2026-05-13
|
||||
|
||||
## Config
|
||||
|
||||
- db host: `aws-us-east-2-2.pg.psdb.cloud:6432`
|
||||
- env: `live`
|
||||
- limit: 1000
|
||||
- repeats per cell: 1
|
||||
- statement_timeout_ms: 60000
|
||||
- read_only: true
|
||||
- shared opts: withSubs=true, includeInvoices=false, withEntities=false, withTrialsUsed=false, cusProductLimit=15
|
||||
- timings are wall-clock from this laptop around drizzle execute (includes network round-trip + result transfer + deserialize)
|
||||
- total benchmark wall time: 218.3s
|
||||
|
||||
## Scenarios resolved
|
||||
|
||||
| # | Org | Filter | Filtered count | Deep offset | Deep cursor |
|
||||
|---|-----|--------|----------------|-------------|-------------|
|
||||
| 01-baseline | firecrawl | `none (baseline)` | 1,225,211 | 950,000 | `{ t: 1774210364093, id: 5ee7a1eb… }` |
|
||||
| 02-search-gmail | firecrawl | `search '@gmail'` | 184,264 | 82,918 | `{ t: 1776593058654, id: c0e7d692… }` |
|
||||
| 03-status-active | firecrawl | `inStatuses=['active']` | 1,225,211 | 551,344 | `{ t: 1774237999733, id: 95fdf38e… }` |
|
||||
| 04-plan-massive | firecrawl | `plans=['free']` | 1,206,690 | 543,010 | `{ t: 1774238574871, id: 281784e4… }` |
|
||||
| 05-plan-mid | firecrawl | `plans=['hobby']` | 27,421 | 12,339 | `{ t: 1774200253978, id: 5845392c… }` |
|
||||
| 06-plan-rare | firecrawl | `plans=['scale_monthly']` | 204 | — | — |
|
||||
| 07-processor-stripe | firecrawl | `processors=['stripe']` | 38,933 | 17,519 | `{ t: 1773503945904, id: 7e4ae8d2… }` |
|
||||
| 08-internal-ids | firecrawl | `internalCustomerIds=[10]` | 10 | — | — |
|
||||
| 09-processor-revenuecat | runable | `processors=['revenuecat']` | 1,274 | — | — |
|
||||
|
||||
## Results
|
||||
|
||||
| # | Org | Filter | Depth | offset median ms | cursor median ms | Δ ms | offset p95 | cursor p95 | offset rows | cursor rows | offset error | cursor error |
|
||||
|---|-----|--------|-------|------------------|------------------|------|------------|------------|-------------|-------------|--------------|--------------|
|
||||
| 01-baseline | firecrawl | `none (baseline)` | page1 | 3440 | 3393 | -47 | 3440 | 3393 | 1000 | 1001 | | |
|
||||
| 01-baseline | firecrawl | `none (baseline)` | deep | 4126 | 2956 | -1169 | 4126 | 2956 | 1000 | 1001 | | |
|
||||
| 02-search-gmail | firecrawl | `search '@gmail'` | page1 | 2924 | 2964 | +41 | 2924 | 2964 | 1000 | 1001 | | |
|
||||
| 02-search-gmail | firecrawl | `search '@gmail'` | deep | 3393 | 3751 | +358 | 3393 | 3751 | 1000 | 1001 | | |
|
||||
| 03-status-active | firecrawl | `inStatuses=['active']` | page1 | 3939 | 4185 | +246 | 3939 | 4185 | 1000 | 1001 | | |
|
||||
| 03-status-active | firecrawl | `inStatuses=['active']` | deep | 7306 | 3746 | -3560 | 7306 | 3746 | 1000 | 1001 | | |
|
||||
| 04-plan-massive | firecrawl | `plans=['free']` | page1 | 2676 | 2903 | +227 | 2676 | 2903 | 1000 | 1001 | | |
|
||||
| 04-plan-massive | firecrawl | `plans=['free']` | deep | 36372 | 1784 | -34588 | 36372 | 1784 | 1000 | 1001 | | |
|
||||
| 05-plan-mid | firecrawl | `plans=['hobby']` | page1 | 5539 | 7062 | +1523 | 5539 | 7062 | 1000 | 1001 | | |
|
||||
| 05-plan-mid | firecrawl | `plans=['hobby']` | deep | 38772 | 4632 | -34140 | 38772 | 4632 | 1000 | 1001 | | |
|
||||
| 06-plan-rare | firecrawl | `plans=['scale_monthly']` | page1 | 899 | 877 | -23 | 899 | 877 | 139 | 139 | | |
|
||||
| 07-processor-stripe | firecrawl | `processors=['stripe']` | page1 | 2850 | 3017 | +167 | 2850 | 3017 | 1000 | 1001 | | |
|
||||
| 07-processor-stripe | firecrawl | `processors=['stripe']` | deep | 8627 | 4797 | -3829 | 8627 | 4797 | 1000 | 1001 | | |
|
||||
| 08-internal-ids | firecrawl | `internalCustomerIds=[10]` | page1 | 467 | 472 | +5 | 467 | 472 | 10 | 10 | | |
|
||||
| 09-processor-revenuecat | runable | `processors=['revenuecat']` | page1 | 6261 | 5212 | -1049 | 6261 | 5212 | 1000 | 1001 | | |
|
||||
|
||||
## Raw samples (per cell)
|
||||
|
||||
### 01-baseline / page1
|
||||
- offset: [3440]ms
|
||||
- cursor: [3393]ms
|
||||
|
||||
### 01-baseline / deep
|
||||
- offset: [4126]ms
|
||||
- cursor: [2956]ms
|
||||
|
||||
### 02-search-gmail / page1
|
||||
- offset: [2924]ms
|
||||
- cursor: [2964]ms
|
||||
|
||||
### 02-search-gmail / deep
|
||||
- offset: [3393]ms
|
||||
- cursor: [3751]ms
|
||||
|
||||
### 03-status-active / page1
|
||||
- offset: [3939]ms
|
||||
- cursor: [4185]ms
|
||||
|
||||
### 03-status-active / deep
|
||||
- offset: [7306]ms
|
||||
- cursor: [3746]ms
|
||||
|
||||
### 04-plan-massive / page1
|
||||
- offset: [2676]ms
|
||||
- cursor: [2903]ms
|
||||
|
||||
### 04-plan-massive / deep
|
||||
- offset: [36372]ms
|
||||
- cursor: [1784]ms
|
||||
|
||||
### 05-plan-mid / page1
|
||||
- offset: [5539]ms
|
||||
- cursor: [7062]ms
|
||||
|
||||
### 05-plan-mid / deep
|
||||
- offset: [38772]ms
|
||||
- cursor: [4632]ms
|
||||
|
||||
### 06-plan-rare / page1
|
||||
- offset: [899]ms
|
||||
- cursor: [877]ms
|
||||
|
||||
### 07-processor-stripe / page1
|
||||
- offset: [2850]ms
|
||||
- cursor: [3017]ms
|
||||
|
||||
### 07-processor-stripe / deep
|
||||
- offset: [8627]ms
|
||||
- cursor: [4797]ms
|
||||
|
||||
### 08-internal-ids / page1
|
||||
- offset: [467]ms
|
||||
- cursor: [472]ms
|
||||
|
||||
### 09-processor-revenuecat / page1
|
||||
- offset: [6261]ms
|
||||
- cursor: [5212]ms
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
304
server/experiments/sqlBenchUnitTestOrg.ts
Normal file
304
server/experiments/sqlBenchUnitTestOrg.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
|
||||
import { getOptimizedFullCusQuery } from "./optimizedFullCusQuery";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
|
||||
const ENV: AppEnv = AppEnv.Sandbox;
|
||||
const REPEATS = 5;
|
||||
const STATEMENT_TIMEOUT_MS = 60_000;
|
||||
const DEEP_OFFSET_PCT = parseInt(process.env.OFFSET_PCT ?? "60", 10);
|
||||
const LIMIT = parseInt(process.env.LIMIT ?? "1000", 10);
|
||||
const CUS_PRODUCT_LIMIT = 15;
|
||||
const SHOW_EXPLAIN = process.env.EXPLAIN !== "false";
|
||||
const SKIP_CURRENT = process.env.SKIP_CURRENT === "true";
|
||||
const SKIP_OPTIMIZED = process.env.SKIP_OPTIMIZED === "true";
|
||||
const DASHBOARD_MODE = process.env.DASHBOARD === "true";
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const normalizeRows = (r: unknown): Record<string, unknown>[] => {
|
||||
if (Array.isArray(r)) return r as Record<string, unknown>[];
|
||||
if (r && typeof r === "object" && "rows" in r) {
|
||||
return (r as { rows: Record<string, unknown>[] }).rows;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const runInTxn = async ({ db, query }: { db: DB; query: SQL }) => {
|
||||
return db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
return normalizeRows(await tx.execute(query));
|
||||
});
|
||||
};
|
||||
|
||||
const resolveTotalCount = async ({ db }: { db: DB }) => {
|
||||
const result = await runInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT COUNT(*)::int AS total_count
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
`,
|
||||
});
|
||||
return (result[0] as { total_count: number }).total_count;
|
||||
};
|
||||
|
||||
const resolveDeepCursor = async ({
|
||||
db,
|
||||
offset,
|
||||
}: {
|
||||
db: DB;
|
||||
offset: number;
|
||||
}) => {
|
||||
const result = await runInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.created_at, c.id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT 1 OFFSET ${offset}
|
||||
`,
|
||||
});
|
||||
const row = result[0] as { created_at: number; id: string } | undefined;
|
||||
if (!row) throw new Error(`No customer at offset ${offset}`);
|
||||
return { v: 0 as const, t: row.created_at, id: row.id };
|
||||
};
|
||||
|
||||
const buildQuery = (
|
||||
cursor?: { v: 0; t: number; id: string },
|
||||
variant: "current" | "optimized" = "current",
|
||||
internalCustomerIds?: string[],
|
||||
): SQL => {
|
||||
const builder =
|
||||
variant === "optimized"
|
||||
? getOptimizedFullCusQuery
|
||||
: getCursorPaginatedFullCusQuery;
|
||||
return builder({
|
||||
orgId: ORG_ID,
|
||||
env: ENV,
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
withSubs: true,
|
||||
limit: internalCustomerIds?.length ?? LIMIT,
|
||||
cursor,
|
||||
internalCustomerIds,
|
||||
cusProductLimit: CUS_PRODUCT_LIMIT,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveInternalIds = async ({
|
||||
db,
|
||||
offset,
|
||||
limit,
|
||||
}: {
|
||||
db: DB;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}): Promise<string[]> => {
|
||||
const result = await runInTxn({
|
||||
db,
|
||||
query: sql`
|
||||
SELECT c.internal_id
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID} AND c.env = ${ENV}
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`,
|
||||
});
|
||||
return (result as unknown as { internal_id: string }[]).map(
|
||||
(r) => r.internal_id,
|
||||
);
|
||||
};
|
||||
|
||||
const measureRun = async ({
|
||||
db,
|
||||
cursor,
|
||||
label,
|
||||
variant = "current",
|
||||
internalCustomerIds,
|
||||
}: {
|
||||
db: DB;
|
||||
cursor?: { v: 0; t: number; id: string };
|
||||
label: string;
|
||||
variant?: "current" | "optimized";
|
||||
internalCustomerIds?: string[];
|
||||
}) => {
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < REPEATS; i++) {
|
||||
const t0 = performance.now();
|
||||
await runInTxn({ db, query: buildQuery(cursor, variant, internalCustomerIds) });
|
||||
const ms = performance.now() - t0;
|
||||
samples.push(ms);
|
||||
process.stdout.write(chalk.gray(` run ${i + 1}/${REPEATS}: ${ms.toFixed(0)}ms\n`));
|
||||
}
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
||||
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
||||
const min = sorted[0] ?? 0;
|
||||
const max = sorted[sorted.length - 1] ?? 0;
|
||||
console.log(
|
||||
chalk.bold(
|
||||
` ${label}: median=${median.toFixed(0)}ms p95=${p95.toFixed(0)}ms min=${min.toFixed(0)}ms max=${max.toFixed(0)}ms`,
|
||||
),
|
||||
);
|
||||
return { median, p95, min, max, samples };
|
||||
};
|
||||
|
||||
const runExplain = async ({
|
||||
db,
|
||||
cursor,
|
||||
variant = "current",
|
||||
internalCustomerIds,
|
||||
}: {
|
||||
db: DB;
|
||||
cursor?: { v: 0; t: number; id: string };
|
||||
variant?: "current" | "optimized";
|
||||
internalCustomerIds?: string[];
|
||||
}) => {
|
||||
const explain = await runInTxn({
|
||||
db,
|
||||
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${buildQuery(cursor, variant, internalCustomerIds)}`,
|
||||
});
|
||||
return explain.map((r) => r["QUERY PLAN"]).join("\n");
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ SQL Bench — unit-test-org ================\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`Org: ${ORG_ID} Env: ${ENV} Limit: ${LIMIT}`));
|
||||
console.log(chalk.cyan("Resolving total customer count..."));
|
||||
const total = await resolveTotalCount({ db });
|
||||
const deepOffset = Math.floor((total * DEEP_OFFSET_PCT) / 100);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` total=${total.toLocaleString()} deep_offset(${DEEP_OFFSET_PCT}%)=${deepOffset.toLocaleString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.cyan(`Resolving deep cursor at offset ${deepOffset}...`));
|
||||
const deepCursor = await resolveDeepCursor({ db, offset: deepOffset });
|
||||
console.log(
|
||||
chalk.gray(` cursor = { t: ${deepCursor.t}, id: ${deepCursor.id} }\n`),
|
||||
);
|
||||
|
||||
let curFirst: { median: number } | null = null;
|
||||
let curDeep: { median: number } | null = null;
|
||||
let optFirst: { median: number } | null = null;
|
||||
let optDeep: { median: number } | null = null;
|
||||
|
||||
let dashIds: string[] | undefined;
|
||||
if (DASHBOARD_MODE) {
|
||||
const offset = Math.floor((total * DEEP_OFFSET_PCT) / 100);
|
||||
console.log(
|
||||
chalk.cyan(`Dashboard mode: resolving ${LIMIT} internal_ids at offset ${offset}...`),
|
||||
);
|
||||
dashIds = await resolveInternalIds({ db, offset, limit: LIMIT });
|
||||
console.log(chalk.gray(` got ${dashIds.length} ids\n`));
|
||||
}
|
||||
|
||||
if (!SKIP_CURRENT) {
|
||||
console.log(chalk.bold("→ CURRENT first page:"));
|
||||
curFirst = await measureRun({ db, label: "current first" });
|
||||
console.log(chalk.bold(`→ CURRENT deep page${DASHBOARD_MODE ? " (internalCustomerIds)" : ""}:`));
|
||||
curDeep = await measureRun({
|
||||
db,
|
||||
cursor: DASHBOARD_MODE ? undefined : deepCursor,
|
||||
internalCustomerIds: DASHBOARD_MODE ? dashIds : undefined,
|
||||
label: "current deep",
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.gray("(SKIP_CURRENT=true — skipping current variant)"));
|
||||
}
|
||||
|
||||
if (!SKIP_OPTIMIZED) {
|
||||
console.log();
|
||||
console.log(chalk.bold("→ OPTIMIZED first page:"));
|
||||
optFirst = await measureRun({
|
||||
db,
|
||||
label: "optim first",
|
||||
variant: "optimized",
|
||||
});
|
||||
console.log(chalk.bold(`→ OPTIMIZED deep page${DASHBOARD_MODE ? " (internalCustomerIds)" : ""}:`));
|
||||
optDeep = await measureRun({
|
||||
db,
|
||||
cursor: DASHBOARD_MODE ? undefined : deepCursor,
|
||||
internalCustomerIds: DASHBOARD_MODE ? dashIds : undefined,
|
||||
label: "optim deep",
|
||||
variant: "optimized",
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.gray("(SKIP_OPTIMIZED=true — skipping optimized variant)"));
|
||||
}
|
||||
|
||||
if (SHOW_EXPLAIN) {
|
||||
if (!SKIP_CURRENT) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.magenta("================ EXPLAIN: CURRENT deep ================"),
|
||||
);
|
||||
console.log(await runExplain({ db, cursor: deepCursor }));
|
||||
}
|
||||
if (!SKIP_OPTIMIZED) {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.magenta(
|
||||
"================ EXPLAIN: OPTIMIZED deep ================",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
await runExplain({ db, cursor: deepCursor, variant: "optimized" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.magentaBright("================ Summary ================"));
|
||||
const fmtDelta = (oldMs: number, newMs: number) => {
|
||||
const delta = ((newMs - oldMs) / oldMs) * 100;
|
||||
const arrow = delta < 0 ? "↓" : "↑";
|
||||
return chalk[delta < 0 ? "green" : "red"](
|
||||
`${arrow}${Math.abs(delta).toFixed(0)}%`,
|
||||
);
|
||||
};
|
||||
if (curFirst && optFirst) {
|
||||
console.log(
|
||||
` first page current=${curFirst.median.toFixed(0)}ms optimized=${optFirst.median.toFixed(0)}ms ${fmtDelta(curFirst.median, optFirst.median)}`,
|
||||
);
|
||||
console.log(
|
||||
` deep page current=${curDeep!.median.toFixed(0)}ms optimized=${optDeep!.median.toFixed(0)}ms ${fmtDelta(curDeep!.median, optDeep!.median)}`,
|
||||
);
|
||||
} else if (curFirst) {
|
||||
console.log(` first page current=${curFirst.median.toFixed(0)}ms`);
|
||||
console.log(` deep page current=${curDeep!.median.toFixed(0)}ms`);
|
||||
} else if (optFirst) {
|
||||
console.log(` first page optimized=${optFirst.median.toFixed(0)}ms`);
|
||||
console.log(` deep page optimized=${optDeep!.median.toFixed(0)}ms`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`\n❌ ${err instanceof Error ? err.message : err}`));
|
||||
if (err instanceof Error && err.stack) console.error(chalk.gray(err.stack));
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
463
server/experiments/v07ReassemblyBenchmark.ts
Normal file
463
server/experiments/v07ReassemblyBenchmark.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
import chalk from "chalk";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const ORG_ID = "biu9vSF7vghBLSKW1UTDwxHBAivjnPaK";
|
||||
const ENV = "live";
|
||||
const LABEL = "firecrawl-deep";
|
||||
const LIMIT = 1000;
|
||||
const FETCH_LIMIT = LIMIT + 1;
|
||||
const SQL_REPEATS = 1;
|
||||
const REASSEMBLY_REPEATS = 20;
|
||||
const STATEMENT_TIMEOUT_MS = 30_000;
|
||||
const DEEP_CURSOR = {
|
||||
t: 1774237983361,
|
||||
id: "772c9569-fc97-4c30-9fd8-c8a585b66755",
|
||||
};
|
||||
|
||||
const RESULTS_DIR = join(import.meta.dir, "results");
|
||||
|
||||
type DB = ReturnType<typeof initDrizzle>["db"];
|
||||
|
||||
const buildVariant07Sql = (): SQL => sql`
|
||||
WITH cr AS MATERIALIZED (
|
||||
SELECT
|
||||
c.internal_id,
|
||||
c.id,
|
||||
c.created_at,
|
||||
row_to_json(c) AS row_json
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${ORG_ID}
|
||||
AND c.env = ${ENV}
|
||||
AND (c.created_at, c.id) < (${DEEP_CURSOR.t}, ${DEEP_CURSOR.id})
|
||||
ORDER BY c.created_at DESC, c.id DESC
|
||||
LIMIT ${FETCH_LIMIT}
|
||||
),
|
||||
cps_flat AS MATERIALIZED (
|
||||
SELECT
|
||||
cp.id,
|
||||
cp.internal_customer_id,
|
||||
cp.internal_product_id,
|
||||
cp.free_trial_id,
|
||||
cp.subscription_ids,
|
||||
(row_to_json(cp)::jsonb || jsonb_build_object('product', row_to_json(prod)))::json AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT cp.*
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
AND cp.status = ANY(ARRAY['active', 'past_due', 'scheduled'])
|
||||
ORDER BY cp.created_at DESC
|
||||
LIMIT 15
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
),
|
||||
ces_combined AS MATERIALIZED (
|
||||
SELECT 'bound'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cps_flat
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.customer_product_id = cps_flat.id
|
||||
) ce ON true
|
||||
UNION ALL
|
||||
SELECT 'loose'::text AS kind, ce.id, ce.entitlement_id, row_to_json(ce) AS row_json
|
||||
FROM cr
|
||||
JOIN LATERAL (
|
||||
SELECT ce.*
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = cr.internal_id
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
ORDER BY ce.id DESC
|
||||
LIMIT 30
|
||||
) ce ON true
|
||||
),
|
||||
arrays AS MATERIALIZED (
|
||||
SELECT
|
||||
(SELECT array_agg(id) FROM ces_combined) AS all_ce_ids,
|
||||
(SELECT array_agg(DISTINCT entitlement_id) FROM ces_combined) AS distinct_entitlement_ids,
|
||||
(SELECT array_agg(DISTINCT free_trial_id) FILTER (WHERE free_trial_id IS NOT NULL) FROM cps_flat) AS free_trial_ids
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_flat) AS customer_products,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'bound') AS customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM ces_combined WHERE kind = 'loose') AS extra_customer_entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(cpr)::jsonb || jsonb_build_object('price', row_to_json(p))), '[]'::json)
|
||||
FROM cps_flat cps
|
||||
JOIN LATERAL (SELECT cpr.* FROM customer_prices cpr WHERE cpr.customer_product_id = cps.id) cpr ON true
|
||||
LEFT JOIN LATERAL (SELECT p.* FROM prices p WHERE p.id = cpr.price_id) p ON true) AS customer_prices,
|
||||
(SELECT COALESCE(json_agg(row_to_json(e)::jsonb || jsonb_build_object('feature', row_to_json(f))), '[]'::json)
|
||||
FROM unnest((SELECT distinct_entitlement_ids FROM arrays)) AS u(entitlement_id)
|
||||
JOIN LATERAL (SELECT e.* FROM entitlements e WHERE e.id = u.entitlement_id) e ON true
|
||||
JOIN LATERAL (SELECT f.* FROM features f WHERE f.internal_id = e.internal_feature_id) f ON true) AS entitlements,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ro)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (SELECT ro.* FROM rollovers ro WHERE ro.cus_ent_id = u.ce_id AND (ro.expires_at IS NULL OR ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000)) ro ON true) AS rollovers,
|
||||
(SELECT COALESCE(json_agg(row_to_json(r)), '[]'::json)
|
||||
FROM unnest((SELECT all_ce_ids FROM arrays)) AS u(ce_id)
|
||||
JOIN LATERAL (SELECT r.* FROM replaceables r WHERE r.cus_ent_id = u.ce_id) r ON true) AS replaceables,
|
||||
(SELECT COALESCE(json_agg(row_to_json(ft)), '[]'::json)
|
||||
FROM unnest((SELECT free_trial_ids FROM arrays)) AS u(ft_id)
|
||||
JOIN LATERAL (SELECT ft.* FROM free_trials ft WHERE ft.id = u.ft_id) ft ON true) AS free_trials,
|
||||
(SELECT COALESCE(json_agg(row_to_json(s)), '[]'::json)
|
||||
FROM (
|
||||
SELECT DISTINCT s.*
|
||||
FROM cps_flat cps
|
||||
CROSS JOIN LATERAL unnest(cps.subscription_ids) AS sub_id_t(sub_id)
|
||||
JOIN LATERAL (SELECT s.* FROM subscriptions s WHERE s.stripe_id = sub_id_t.sub_id) s ON true
|
||||
WHERE cps.subscription_ids IS NOT NULL
|
||||
) s) AS subscriptions
|
||||
`;
|
||||
|
||||
type V07Row = {
|
||||
customers: any[];
|
||||
customer_products: any[];
|
||||
customer_entitlements: any[];
|
||||
extra_customer_entitlements: any[];
|
||||
customer_prices: any[];
|
||||
entitlements: any[];
|
||||
rollovers: any[];
|
||||
replaceables: any[];
|
||||
free_trials: any[];
|
||||
subscriptions: any[];
|
||||
};
|
||||
|
||||
const reassemble = (flat: V07Row) => {
|
||||
const {
|
||||
customers,
|
||||
customer_products,
|
||||
customer_entitlements,
|
||||
extra_customer_entitlements,
|
||||
customer_prices,
|
||||
entitlements,
|
||||
rollovers,
|
||||
replaceables,
|
||||
free_trials,
|
||||
subscriptions,
|
||||
} = flat;
|
||||
|
||||
const entById = new Map<string, any>();
|
||||
for (const e of entitlements) entById.set(e.id, e);
|
||||
|
||||
const ftById = new Map<string, any>();
|
||||
for (const ft of free_trials) ftById.set(ft.id, ft);
|
||||
|
||||
const subByStripeId = new Map<string, any>();
|
||||
for (const s of subscriptions) subByStripeId.set(s.stripe_id, s);
|
||||
|
||||
const rolloversByCeId = new Map<string, any[]>();
|
||||
for (const ro of rollovers) {
|
||||
const list = rolloversByCeId.get(ro.cus_ent_id);
|
||||
if (list) list.push(ro);
|
||||
else rolloversByCeId.set(ro.cus_ent_id, [ro]);
|
||||
}
|
||||
|
||||
const replaceablesByCeId = new Map<string, any[]>();
|
||||
for (const r of replaceables) {
|
||||
const list = replaceablesByCeId.get(r.cus_ent_id);
|
||||
if (list) list.push(r);
|
||||
else replaceablesByCeId.set(r.cus_ent_id, [r]);
|
||||
}
|
||||
|
||||
const cpricesByCpId = new Map<string, any[]>();
|
||||
for (const cpr of customer_prices) {
|
||||
const list = cpricesByCpId.get(cpr.customer_product_id);
|
||||
if (list) list.push(cpr);
|
||||
else cpricesByCpId.set(cpr.customer_product_id, [cpr]);
|
||||
}
|
||||
|
||||
const hydrateCe = (ce: any) => ({
|
||||
...ce,
|
||||
entitlement: entById.get(ce.entitlement_id) ?? null,
|
||||
rollovers: rolloversByCeId.get(ce.id) ?? [],
|
||||
replaceables: replaceablesByCeId.get(ce.id) ?? [],
|
||||
});
|
||||
|
||||
const cesByCpId = new Map<string, any[]>();
|
||||
for (const ce of customer_entitlements) {
|
||||
const hydrated = hydrateCe(ce);
|
||||
const list = cesByCpId.get(ce.customer_product_id);
|
||||
if (list) list.push(hydrated);
|
||||
else cesByCpId.set(ce.customer_product_id, [hydrated]);
|
||||
}
|
||||
|
||||
const looseCesByCusId = new Map<string, any[]>();
|
||||
for (const ce of extra_customer_entitlements) {
|
||||
const hydrated = hydrateCe(ce);
|
||||
const list = looseCesByCusId.get(ce.internal_customer_id);
|
||||
if (list) list.push(hydrated);
|
||||
else looseCesByCusId.set(ce.internal_customer_id, [hydrated]);
|
||||
}
|
||||
|
||||
const cpsByCusId = new Map<string, any[]>();
|
||||
for (const cp of customer_products) {
|
||||
const hydrated = {
|
||||
...cp,
|
||||
customer_prices: cpricesByCpId.get(cp.id) ?? [],
|
||||
customer_entitlements: cesByCpId.get(cp.id) ?? [],
|
||||
free_trial: cp.free_trial_id ? (ftById.get(cp.free_trial_id) ?? null) : null,
|
||||
};
|
||||
const list = cpsByCusId.get(cp.internal_customer_id);
|
||||
if (list) list.push(hydrated);
|
||||
else cpsByCusId.set(cp.internal_customer_id, [hydrated]);
|
||||
}
|
||||
|
||||
const subsByCusId = new Map<string, any[]>();
|
||||
for (const cp of customer_products) {
|
||||
if (!cp.subscription_ids?.length) continue;
|
||||
const existing = subsByCusId.get(cp.internal_customer_id) ?? [];
|
||||
const seen = new Set(existing.map((s: any) => s.stripe_id));
|
||||
for (const subId of cp.subscription_ids) {
|
||||
if (seen.has(subId)) continue;
|
||||
const sub = subByStripeId.get(subId);
|
||||
if (sub) {
|
||||
existing.push(sub);
|
||||
seen.add(subId);
|
||||
}
|
||||
}
|
||||
if (existing.length) subsByCusId.set(cp.internal_customer_id, existing);
|
||||
}
|
||||
|
||||
const result: any[] = [];
|
||||
for (const c of customers) {
|
||||
result.push({
|
||||
...c,
|
||||
customer_products: cpsByCusId.get(c.internal_id) ?? [],
|
||||
extra_customer_entitlements: looseCesByCusId.get(c.internal_id) ?? [],
|
||||
subscriptions: subsByCusId.get(c.internal_id) ?? [],
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const runQueryInTxn = async ({
|
||||
db,
|
||||
query,
|
||||
}: {
|
||||
db: DB;
|
||||
query: SQL;
|
||||
}): Promise<Record<string, unknown>[]> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.execute(
|
||||
sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`),
|
||||
);
|
||||
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
|
||||
const result = await tx.execute(query);
|
||||
return result as unknown as Record<string, unknown>[];
|
||||
});
|
||||
};
|
||||
|
||||
const summarise = (samples: number[]) => {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
return {
|
||||
min: sorted[0] ?? 0,
|
||||
median: sorted[Math.floor(sorted.length / 2)] ?? 0,
|
||||
p95: sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0,
|
||||
max: sorted[sorted.length - 1] ?? 0,
|
||||
samples: sorted,
|
||||
};
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Variant 07 + Reassembly Benchmark — ${LABEL} ================\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`Org: ${ORG_ID} (${ENV})`));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Deep cursor: t=${DEEP_CURSOR.t} id=${DEEP_CURSOR.id} limit=${LIMIT}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
// Step 1 — EXPLAIN ANALYZE pass for server-side SQL execution time
|
||||
console.log(chalk.cyan("[1/3] EXPLAIN (ANALYZE, FORMAT JSON) — server-side ms"));
|
||||
const explainSamples: number[] = [];
|
||||
let lastExplainPlan: any = null;
|
||||
for (let i = 0; i < SQL_REPEATS; i++) {
|
||||
const explainRows = await runQueryInTxn({
|
||||
db,
|
||||
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${buildVariant07Sql()}`,
|
||||
});
|
||||
const plan = (explainRows[0] as { "QUERY PLAN": any })["QUERY PLAN"];
|
||||
const planObj = Array.isArray(plan) ? plan[0] : plan;
|
||||
const execTime = planObj["Execution Time"] as number;
|
||||
explainSamples.push(execTime);
|
||||
lastExplainPlan = planObj;
|
||||
process.stdout.write(chalk.gray(` run ${i + 1}: ${execTime.toFixed(2)}ms\n`));
|
||||
}
|
||||
const sqlSummary = summarise(explainSamples);
|
||||
|
||||
// Step 2 — actual query to capture rows for reassembly
|
||||
console.log(chalk.cyan("\n[2/3] Executing variant 07 to capture row payload"));
|
||||
const queryStart = performance.now();
|
||||
const rows = await runQueryInTxn({ db, query: buildVariant07Sql() });
|
||||
const queryWallMs = performance.now() - queryStart;
|
||||
const flatRow = rows[0] as unknown as V07Row;
|
||||
|
||||
const counts = {
|
||||
customers: flatRow.customers?.length ?? 0,
|
||||
customer_products: flatRow.customer_products?.length ?? 0,
|
||||
customer_entitlements: flatRow.customer_entitlements?.length ?? 0,
|
||||
extra_customer_entitlements: flatRow.extra_customer_entitlements?.length ?? 0,
|
||||
customer_prices: flatRow.customer_prices?.length ?? 0,
|
||||
entitlements: flatRow.entitlements?.length ?? 0,
|
||||
rollovers: flatRow.rollovers?.length ?? 0,
|
||||
replaceables: flatRow.replaceables?.length ?? 0,
|
||||
free_trials: flatRow.free_trials?.length ?? 0,
|
||||
subscriptions: flatRow.subscriptions?.length ?? 0,
|
||||
};
|
||||
console.log(
|
||||
chalk.gray(` client-side wall (incl. UK→US network): ${queryWallMs.toFixed(2)}ms`),
|
||||
);
|
||||
console.log(chalk.gray(` row counts: ${JSON.stringify(counts)}`));
|
||||
|
||||
const payloadBytes = Buffer.byteLength(JSON.stringify(flatRow), "utf8");
|
||||
console.log(
|
||||
chalk.gray(` payload size: ${(payloadBytes / 1024 / 1024).toFixed(2)} MB`),
|
||||
);
|
||||
|
||||
// Step 3 — reassembly benchmark (pure JS, in-memory)
|
||||
console.log(
|
||||
chalk.cyan(`\n[3/3] Reassembly benchmark — ${REASSEMBLY_REPEATS} runs`),
|
||||
);
|
||||
// Warm up the JIT
|
||||
for (let i = 0; i < 3; i++) reassemble(flatRow);
|
||||
const reassemblySamples: number[] = [];
|
||||
let lastReassembledLen = 0;
|
||||
for (let i = 0; i < REASSEMBLY_REPEATS; i++) {
|
||||
const start = performance.now();
|
||||
const reassembled = reassemble(flatRow);
|
||||
const elapsed = performance.now() - start;
|
||||
reassemblySamples.push(elapsed);
|
||||
lastReassembledLen = reassembled.length;
|
||||
}
|
||||
const reassemblySummary = summarise(reassemblySamples);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` reassembled ${lastReassembledLen} FullCustomers (${REASSEMBLY_REPEATS} runs)`,
|
||||
),
|
||||
);
|
||||
|
||||
// Report
|
||||
console.log(chalk.magentaBright("\n================ RESULTS ================\n"));
|
||||
console.log(chalk.bold("SQL (server-side, EXPLAIN ANALYZE Execution Time):"));
|
||||
console.log(
|
||||
` min=${sqlSummary.min.toFixed(2)}ms median=${sqlSummary.median.toFixed(2)}ms p95=${sqlSummary.p95.toFixed(2)}ms max=${sqlSummary.max.toFixed(2)}ms`,
|
||||
);
|
||||
console.log(chalk.gray(` samples: ${sqlSummary.samples.map((s) => s.toFixed(1)).join(", ")}`));
|
||||
console.log();
|
||||
console.log(chalk.bold("Reassembly (JS, in-memory, network-free):"));
|
||||
console.log(
|
||||
` min=${reassemblySummary.min.toFixed(2)}ms median=${reassemblySummary.median.toFixed(2)}ms p95=${reassemblySummary.p95.toFixed(2)}ms max=${reassemblySummary.max.toFixed(2)}ms`,
|
||||
);
|
||||
console.log(chalk.gray(` samples: ${reassemblySummary.samples.map((s) => s.toFixed(2)).join(", ")}`));
|
||||
console.log();
|
||||
console.log(chalk.bold.green("Total (network-free) median:"));
|
||||
console.log(
|
||||
chalk.bold.green(
|
||||
` ${(sqlSummary.median + reassemblySummary.median).toFixed(2)}ms = ${sqlSummary.median.toFixed(2)}ms SQL + ${reassemblySummary.median.toFixed(2)}ms reassembly`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
console.log(chalk.bold("Reference (UK→US client wall, includes ~100ms network):"));
|
||||
console.log(` one-shot db.execute: ${queryWallMs.toFixed(2)}ms`);
|
||||
console.log();
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const reportPath = join(RESULTS_DIR, `${date}-v07-reassembly-${LABEL}.md`);
|
||||
const fixturePath = join(RESULTS_DIR, `${date}-v07-fixture-${LABEL}.json`);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`# Variant 07 + JS Reassembly — ${date} — ${LABEL}`);
|
||||
lines.push("");
|
||||
lines.push("## Config");
|
||||
lines.push(`- org_id: \`${ORG_ID}\``);
|
||||
lines.push(`- env: \`${ENV}\``);
|
||||
lines.push(`- limit: ${LIMIT} (fetch ${FETCH_LIMIT})`);
|
||||
lines.push(`- deep cursor: \`{ t: ${DEEP_CURSOR.t}, id: ${DEEP_CURSOR.id} }\``);
|
||||
lines.push(`- SQL repeats: ${SQL_REPEATS}`);
|
||||
lines.push(`- reassembly repeats: ${REASSEMBLY_REPEATS} (after 3 warmup runs)`);
|
||||
lines.push("");
|
||||
lines.push("## SQL (server-side, EXPLAIN ANALYZE Execution Time, network-free)");
|
||||
lines.push("");
|
||||
lines.push(`| min | median | p95 | max |`);
|
||||
lines.push(`|---|---|---|---|`);
|
||||
lines.push(
|
||||
`| ${sqlSummary.min.toFixed(2)}ms | ${sqlSummary.median.toFixed(2)}ms | ${sqlSummary.p95.toFixed(2)}ms | ${sqlSummary.max.toFixed(2)}ms |`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`Samples: ${sqlSummary.samples.map((s) => `${s.toFixed(1)}ms`).join(", ")}`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Reassembly (JS, in-memory)");
|
||||
lines.push("");
|
||||
lines.push(`| min | median | p95 | max |`);
|
||||
lines.push(`|---|---|---|---|`);
|
||||
lines.push(
|
||||
`| ${reassemblySummary.min.toFixed(2)}ms | ${reassemblySummary.median.toFixed(2)}ms | ${reassemblySummary.p95.toFixed(2)}ms | ${reassemblySummary.max.toFixed(2)}ms |`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`Samples: ${reassemblySummary.samples.map((s) => `${s.toFixed(2)}ms`).join(", ")}`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Total network-free");
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`**${(sqlSummary.median + reassemblySummary.median).toFixed(2)}ms** = ${sqlSummary.median.toFixed(2)}ms SQL + ${reassemblySummary.median.toFixed(2)}ms reassembly`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Row counts");
|
||||
lines.push("");
|
||||
lines.push("```json");
|
||||
lines.push(JSON.stringify(counts, null, 2));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
lines.push(`Payload size: ${(payloadBytes / 1024 / 1024).toFixed(2)} MB`);
|
||||
lines.push("");
|
||||
lines.push("## Reference: UK→US client wall");
|
||||
lines.push("");
|
||||
lines.push(`One-shot \`db.execute\`: ${queryWallMs.toFixed(2)}ms`);
|
||||
lines.push("");
|
||||
lines.push("## Last EXPLAIN plan (JSON)");
|
||||
lines.push("");
|
||||
lines.push("```json");
|
||||
lines.push(JSON.stringify(lastExplainPlan, null, 2));
|
||||
lines.push("```");
|
||||
|
||||
writeFileSync(reportPath, lines.join("\n"));
|
||||
writeFileSync(fixturePath, JSON.stringify(flatRow));
|
||||
|
||||
console.log(chalk.green(`✅ Report written to ${reportPath}`));
|
||||
console.log(chalk.green(`✅ Fixture written to ${fixturePath}`));
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Benchmark Complete ================\n",
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red("\n❌ Benchmark failed:"));
|
||||
console.error(error);
|
||||
console.error(chalk.gray(JSON.stringify(error, Object.getOwnPropertyNames(error || {}))));
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
547
server/experiments/versionPipelineBench.ts
Normal file
547
server/experiments/versionPipelineBench.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
AllowanceType,
|
||||
type ApiCustomerV5,
|
||||
ApiVersion,
|
||||
ApiVersionClass,
|
||||
applyResponseVersionChanges,
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
CollectionMethod,
|
||||
type CustomerLegacyData,
|
||||
CusProductStatus,
|
||||
EntInterval,
|
||||
FeatureType,
|
||||
type FullCustomer,
|
||||
type FullCustomerEntitlement,
|
||||
type FullCusProduct
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { getApiCustomerBase } from "../src/internal/customers/cusUtils/apiCusUtils/getApiCustomerBase";
|
||||
|
||||
const CUSTOMER_COUNT = 1000;
|
||||
const ITERATIONS = 5;
|
||||
const WARMUP_ITERATIONS = 2;
|
||||
|
||||
const ORG_ID = "org_bench";
|
||||
const FEATURE_IDS = [
|
||||
"messages",
|
||||
"credits",
|
||||
"users",
|
||||
"projects",
|
||||
"api_calls",
|
||||
"storage_gb",
|
||||
"connectors",
|
||||
"webhooks",
|
||||
"exports",
|
||||
"dashboards",
|
||||
];
|
||||
const ADDON_FEATURE_IDS = [
|
||||
"extra_messages",
|
||||
"extra_credits",
|
||||
"premium_support",
|
||||
"sso",
|
||||
"audit_log",
|
||||
];
|
||||
const LOOSE_FEATURE_IDS = [
|
||||
"loose_a",
|
||||
"loose_b",
|
||||
"loose_c",
|
||||
"loose_d",
|
||||
"loose_e",
|
||||
];
|
||||
|
||||
const featureFromId = (id: string) => ({
|
||||
internal_id: `fe_${id}`,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1000,
|
||||
env: AppEnv.Sandbox,
|
||||
id,
|
||||
name: id,
|
||||
type: FeatureType.Metered,
|
||||
config: { usage_type: "single_use" },
|
||||
display: { singular: id, plural: `${id}s` },
|
||||
archived: false,
|
||||
event_names: [id],
|
||||
});
|
||||
|
||||
const allFeatures = [
|
||||
...FEATURE_IDS.map(featureFromId),
|
||||
...ADDON_FEATURE_IDS.map(featureFromId),
|
||||
...LOOSE_FEATURE_IDS.map(featureFromId),
|
||||
];
|
||||
|
||||
const makeEntitlement = ({
|
||||
cusEntId,
|
||||
productInternalId,
|
||||
productCusId,
|
||||
featureId,
|
||||
internalCustomerId,
|
||||
customerId,
|
||||
}: {
|
||||
cusEntId: string;
|
||||
productInternalId: string;
|
||||
productCusId: string | null;
|
||||
featureId: string;
|
||||
internalCustomerId: string;
|
||||
customerId: string;
|
||||
}): FullCustomerEntitlement => ({
|
||||
id: cusEntId,
|
||||
internal_customer_id: internalCustomerId,
|
||||
internal_entity_id: null,
|
||||
internal_feature_id: `fe_${featureId}`,
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
customer_product_id: productCusId,
|
||||
entitlement_id: `ent_${featureId}_${productInternalId}`,
|
||||
created_at: 1000,
|
||||
unlimited: false,
|
||||
balance: 100,
|
||||
additional_balance: 0,
|
||||
usage_allowed: null,
|
||||
next_reset_at: 2000,
|
||||
adjustment: 0,
|
||||
expires_at: null,
|
||||
cache_version: 0,
|
||||
entities: null,
|
||||
external_id: null,
|
||||
entitlement: {
|
||||
id: `ent_${featureId}_${productInternalId}`,
|
||||
created_at: 1000,
|
||||
internal_feature_id: `fe_${featureId}`,
|
||||
internal_product_id: productInternalId,
|
||||
is_custom: false,
|
||||
allowance_type: AllowanceType.Fixed,
|
||||
allowance: 100,
|
||||
interval: EntInterval.Month,
|
||||
interval_count: 1,
|
||||
entity_feature_id: null,
|
||||
feature_id: featureId,
|
||||
usage_limit: null,
|
||||
feature: featureFromId(featureId),
|
||||
},
|
||||
replaceables: [],
|
||||
rollovers: [],
|
||||
});
|
||||
|
||||
const makeCusProduct = ({
|
||||
productCusId,
|
||||
productInternalId,
|
||||
productId,
|
||||
productName,
|
||||
isAddOn,
|
||||
internalCustomerId,
|
||||
customerId,
|
||||
featureIds,
|
||||
cusEntPrefix,
|
||||
internalEntityId,
|
||||
entityId,
|
||||
}: {
|
||||
productCusId: string;
|
||||
productInternalId: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
isAddOn: boolean;
|
||||
internalCustomerId: string;
|
||||
customerId: string;
|
||||
featureIds: string[];
|
||||
cusEntPrefix: string;
|
||||
internalEntityId?: string | null;
|
||||
entityId?: string | null;
|
||||
}): FullCusProduct => ({
|
||||
id: productCusId,
|
||||
internal_product_id: productInternalId,
|
||||
product_id: productId,
|
||||
internal_customer_id: internalCustomerId,
|
||||
customer_id: customerId,
|
||||
internal_entity_id: internalEntityId ?? null,
|
||||
entity_id: entityId ?? null,
|
||||
created_at: 1500,
|
||||
updated_at: null,
|
||||
status: CusProductStatus.Active,
|
||||
canceled: false,
|
||||
starts_at: 1500,
|
||||
access_starts_at: null,
|
||||
trial_ends_at: null,
|
||||
billing_cycle_anchor_resets_at: null,
|
||||
canceled_at: null,
|
||||
ended_at: null,
|
||||
options: [],
|
||||
free_trial_id: null,
|
||||
collection_method: CollectionMethod.ChargeAutomatically,
|
||||
subscription_ids: [`sub_${productCusId}`],
|
||||
scheduled_ids: [],
|
||||
processor: { type: "stripe" as any },
|
||||
quantity: 1,
|
||||
api_semver: null,
|
||||
is_custom: false,
|
||||
billing_version: "v2" as any,
|
||||
external_id: null,
|
||||
stripe_checkout_session_id: null,
|
||||
customer_prices: [
|
||||
{
|
||||
id: `cus_price_${productCusId}`,
|
||||
internal_customer_id: internalCustomerId,
|
||||
customer_product_id: productCusId,
|
||||
created_at: 1500,
|
||||
price_id: `price_${productInternalId}`,
|
||||
price: {
|
||||
id: `price_${productInternalId}`,
|
||||
internal_product_id: productInternalId,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1500,
|
||||
billing_type: null,
|
||||
tier_behavior: null,
|
||||
is_custom: false,
|
||||
config: {
|
||||
type: "fixed",
|
||||
amount: 99,
|
||||
interval: BillingInterval.Month,
|
||||
} as any,
|
||||
entitlement_id: null,
|
||||
proration_config: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
customer_entitlements: featureIds.map((fid, i) =>
|
||||
makeEntitlement({
|
||||
cusEntId: `${cusEntPrefix}_${i}`,
|
||||
productInternalId,
|
||||
productCusId,
|
||||
featureId: fid,
|
||||
internalCustomerId,
|
||||
customerId,
|
||||
}),
|
||||
),
|
||||
product: {
|
||||
id: productId,
|
||||
name: productName,
|
||||
description: null,
|
||||
is_add_on: isAddOn,
|
||||
is_default: false,
|
||||
version: 1,
|
||||
group: "",
|
||||
env: AppEnv.Sandbox,
|
||||
internal_id: productInternalId,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1000,
|
||||
processor: { type: "stripe", id: `prod_${productId}` },
|
||||
base_variant_id: null,
|
||||
archived: false,
|
||||
config: { ignore_past_due: false },
|
||||
},
|
||||
free_trial: null,
|
||||
});
|
||||
|
||||
const makeFullCustomer = (idx: number): FullCustomer => {
|
||||
const internalId = `cus_bench_${idx.toString().padStart(8, "0")}`;
|
||||
const customerId = `bench_${idx}`;
|
||||
|
||||
const customerProducts: FullCusProduct[] = [
|
||||
makeCusProduct({
|
||||
productCusId: `cus_prod_main_${idx}`,
|
||||
productInternalId: "prod_main",
|
||||
productId: "pro",
|
||||
productName: "Pro",
|
||||
isAddOn: false,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
featureIds: FEATURE_IDS,
|
||||
cusEntPrefix: `cus_ent_main_${idx}`,
|
||||
}),
|
||||
makeCusProduct({
|
||||
productCusId: `cus_prod_addon_${idx}`,
|
||||
productInternalId: "prod_addon",
|
||||
productId: "pro_addon",
|
||||
productName: "Pro Add-on",
|
||||
isAddOn: true,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
featureIds: ADDON_FEATURE_IDS,
|
||||
cusEntPrefix: `cus_ent_addon_${idx}`,
|
||||
}),
|
||||
];
|
||||
|
||||
const entities = Array.from({ length: 5 }, (_, i) => ({
|
||||
id: `ent_${idx}_${i}`,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1100,
|
||||
internal_id: `ent_int_${idx}_${i}`,
|
||||
internal_customer_id: internalId,
|
||||
env: AppEnv.Sandbox,
|
||||
name: `Entity ${i}`,
|
||||
deleted: false,
|
||||
feature_id: "seats",
|
||||
internal_feature_id: "fe_seats",
|
||||
}));
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ent = entities[i]!;
|
||||
customerProducts.push(
|
||||
makeCusProduct({
|
||||
productCusId: `cus_prod_main_${idx}_e${i}`,
|
||||
productInternalId: "prod_main",
|
||||
productId: "pro",
|
||||
productName: "Pro (Entity-scoped)",
|
||||
isAddOn: false,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
featureIds: FEATURE_IDS,
|
||||
cusEntPrefix: `cus_ent_main_${idx}_e${i}`,
|
||||
internalEntityId: ent.internal_id,
|
||||
entityId: ent.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const looseEnts: FullCustomerEntitlement[] = LOOSE_FEATURE_IDS.map((fid, i) =>
|
||||
makeEntitlement({
|
||||
cusEntId: `cus_ent_loose_${idx}_${i}`,
|
||||
productInternalId: "prod_loose",
|
||||
productCusId: null,
|
||||
featureId: fid,
|
||||
internalCustomerId: internalId,
|
||||
customerId,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
id: customerId,
|
||||
name: `Bench Customer ${idx}`,
|
||||
email: `bench+${idx}@autumn.dev`,
|
||||
fingerprint: null,
|
||||
internal_id: internalId,
|
||||
org_id: ORG_ID,
|
||||
created_at: 1000,
|
||||
env: AppEnv.Sandbox,
|
||||
processor: { id: `cus_stripe_${idx}`, type: "stripe" },
|
||||
processors: null,
|
||||
metadata: {},
|
||||
send_email_receipts: false,
|
||||
auto_topups: null,
|
||||
spend_limits: null,
|
||||
usage_alerts: null,
|
||||
overage_allowed: null,
|
||||
config: null,
|
||||
customer_products: customerProducts,
|
||||
entities,
|
||||
extra_customer_entitlements: looseEnts,
|
||||
subscriptions: customerProducts
|
||||
.filter((cp) => cp.subscription_ids?.length)
|
||||
.flatMap((cp) =>
|
||||
(cp.subscription_ids ?? []).map((sid) => ({
|
||||
id: sid,
|
||||
stripe_id: sid,
|
||||
stripe_schedule_id: null,
|
||||
created_at: 1500,
|
||||
usage_features: [],
|
||||
metadata: {},
|
||||
org_id: ORG_ID,
|
||||
env: AppEnv.Sandbox,
|
||||
current_period_start: 1500,
|
||||
current_period_end: 4000,
|
||||
})),
|
||||
),
|
||||
} as unknown as FullCustomer;
|
||||
};
|
||||
|
||||
const makeCtx = () =>
|
||||
({
|
||||
org: {
|
||||
id: ORG_ID,
|
||||
slug: "bench",
|
||||
created_at: 1000,
|
||||
default_currency: "usd",
|
||||
config: { in_statuses: [CusProductStatus.Active] },
|
||||
stripe_connected: false,
|
||||
},
|
||||
env: AppEnv.Sandbox,
|
||||
features: allFeatures,
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
trace: () => {},
|
||||
child: () => makeCtx().logger,
|
||||
},
|
||||
expand: [],
|
||||
apiVersion: new ApiVersionClass(ApiVersion.V2_3),
|
||||
}) as any;
|
||||
|
||||
const summarize = (samples: number[]) => {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
return {
|
||||
min: sorted[0] ?? 0,
|
||||
p50: sorted[Math.floor(sorted.length / 2)] ?? 0,
|
||||
p95: sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0,
|
||||
max: sorted[sorted.length - 1] ?? 0,
|
||||
mean: samples.reduce((a, b) => a + b, 0) / samples.length,
|
||||
};
|
||||
};
|
||||
|
||||
const fmt = (n: number) => `${n.toFixed(2)}ms`.padStart(9, " ");
|
||||
|
||||
const runSerial = async (customers: FullCustomer[], ctx: any) => {
|
||||
const finals: ApiCustomerV5[] = [];
|
||||
for (const fullCus of customers) {
|
||||
const { apiCustomer: baseCustomer, legacyData } = await getApiCustomerBase({
|
||||
ctx,
|
||||
fullCus,
|
||||
withAutumnId: false,
|
||||
});
|
||||
const versioned = applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: baseCustomer,
|
||||
legacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Customer,
|
||||
ctx,
|
||||
});
|
||||
finals.push(versioned);
|
||||
}
|
||||
return finals;
|
||||
};
|
||||
|
||||
const runPromiseAll = async (customers: FullCustomer[], ctx: any) => {
|
||||
const finals = await Promise.all(
|
||||
customers.map(async (fullCus) => {
|
||||
const { apiCustomer: baseCustomer, legacyData } = await getApiCustomerBase({
|
||||
ctx,
|
||||
fullCus,
|
||||
withAutumnId: false,
|
||||
});
|
||||
return applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: baseCustomer,
|
||||
legacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Customer,
|
||||
ctx,
|
||||
});
|
||||
}),
|
||||
);
|
||||
return finals;
|
||||
};
|
||||
|
||||
// "sync-equivalent" path: keeps the async function calls but uses a tight
|
||||
// promise-chained sequence with no extra microtask churn between customers.
|
||||
// Since getApiCustomerBase is async, true sync isn't possible without rewriting it.
|
||||
const runSyncEquivalent = (customers: FullCustomer[], ctx: any) => {
|
||||
let p: Promise<ApiCustomerV5[]> = Promise.resolve([] as ApiCustomerV5[]);
|
||||
for (const fullCus of customers) {
|
||||
p = p.then(async (acc) => {
|
||||
const { apiCustomer: baseCustomer, legacyData } = await getApiCustomerBase({
|
||||
ctx,
|
||||
fullCus,
|
||||
withAutumnId: false,
|
||||
});
|
||||
const versioned = applyResponseVersionChanges<ApiCustomerV5, CustomerLegacyData>({
|
||||
input: baseCustomer,
|
||||
legacyData,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.Customer,
|
||||
ctx,
|
||||
});
|
||||
acc.push(versioned);
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
const benchVariant = async ({
|
||||
name,
|
||||
customers,
|
||||
ctx,
|
||||
run,
|
||||
}: {
|
||||
name: string;
|
||||
customers: FullCustomer[];
|
||||
ctx: any;
|
||||
run: (customers: FullCustomer[], ctx: any) => Promise<ApiCustomerV5[]>;
|
||||
}) => {
|
||||
for (let i = 0; i < WARMUP_ITERATIONS; i++) await run(customers, ctx);
|
||||
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const t0 = performance.now();
|
||||
const result = await run(customers, ctx);
|
||||
const t1 = performance.now();
|
||||
if (result.length !== customers.length) {
|
||||
throw new Error(`${name}: result length mismatch ${result.length} vs ${customers.length}`);
|
||||
}
|
||||
samples.push(t1 - t0);
|
||||
}
|
||||
|
||||
const s = summarize(samples);
|
||||
const perCustomer = s.p50 / customers.length;
|
||||
console.log(
|
||||
` ${chalk.cyan(name.padEnd(20))} | total p50 ${fmt(s.p50)} (per cus ${perCustomer.toFixed(3)}ms) | mean ${fmt(s.mean)} | p95 ${fmt(s.p95)} | min ${fmt(s.min)} | max ${fmt(s.max)}`,
|
||||
);
|
||||
return s;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
`\n================ Version Pipeline Bench ================`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` customers per iter: ${CUSTOMER_COUNT.toLocaleString()}, iterations: ${ITERATIONS} (+${WARMUP_ITERATIONS} warmup)\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const ctx = makeCtx();
|
||||
const customers = Array.from({ length: CUSTOMER_COUNT }, (_, i) =>
|
||||
makeFullCustomer(i),
|
||||
);
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` shape per customer: ${customers[0]!.customer_products.length} cps · ${customers[0]!.customer_products.reduce((n, cp) => n + cp.customer_entitlements.length, 0)} bound ces · ${customers[0]!.extra_customer_entitlements.length} loose ces · ${customers[0]!.entities.length} entities`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
|
||||
console.log(chalk.bold("Baseline (serial for-await):"));
|
||||
const serial = await benchVariant({ name: "serial", customers, ctx, run: runSerial });
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold("Variant A (Promise.all):"));
|
||||
const promiseAll = await benchVariant({
|
||||
name: "Promise.all",
|
||||
customers,
|
||||
ctx,
|
||||
run: runPromiseAll,
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold("Variant B (sync-equivalent promise chain):"));
|
||||
const syncish = await benchVariant({
|
||||
name: "sync-equivalent",
|
||||
customers,
|
||||
ctx,
|
||||
run: runSyncEquivalent,
|
||||
});
|
||||
|
||||
const fmtDelta = (other: number, baseline: number) => {
|
||||
const delta = ((other - baseline) / baseline) * 100;
|
||||
const arrow = delta < 0 ? "↓" : "↑";
|
||||
return `${arrow}${Math.abs(delta).toFixed(1)}%`;
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log(chalk.magentaBright("================ Summary ================"));
|
||||
console.log(
|
||||
` serial: ${fmt(serial.p50)} ${chalk.gray("(baseline)")}`,
|
||||
);
|
||||
console.log(
|
||||
` Promise.all: ${fmt(promiseAll.p50)} (${fmtDelta(promiseAll.p50, serial.p50)})`,
|
||||
);
|
||||
console.log(
|
||||
` sync-equivalent: ${fmt(syncish.p50)} (${fmtDelta(syncish.p50, serial.p50)})`,
|
||||
);
|
||||
};
|
||||
|
||||
await main();
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user