feat: 🎸 pagination benchmarks

This commit is contained in:
amianthus
2026-05-13 13:59:10 +01:00
parent 3508b56315
commit c3525a4686
6 changed files with 3131 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
import { AppEnv, customers, entities, type ExternalProcessors } 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 { and, eq, sql } from "drizzle-orm";
import { TEST_ORG_CONFIG } from "../setupTestUtils/createTestOrg.js";
loadLocalEnv();
const CONFIG = {
totalCustomers: 1_000_000,
totalEntities: 50_000,
timeRangeDays: 730,
batchSize: 5_000,
burstClusterSize: 100,
burstEveryNCustomers: 1_000,
revenuecatRatio: 0.1,
defaultEnv: AppEnv.Sandbox,
seedMarkerName: "Pagination Benchmark Org",
} as const;
interface CliArgs {
org_slug: string;
skip_entities?: boolean;
wipe?: boolean;
count?: number;
}
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 "skip_entities":
parsed.skip_entities = true;
break;
case "wipe":
parsed.wipe = true;
break;
case "count":
parsed.count = 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>]",
),
);
process.exit(1);
}
}
return parsed;
};
const resolveOrg = async ({
db,
slug,
}: {
db: ReturnType<typeof initDrizzle>["db"];
slug: string;
}) => {
const org = await db.query.organizations.findFirst({
where: (orgs, { eq }) => eq(orgs.slug, slug),
});
if (!org) {
throw new Error(
`Org '${slug}' not found. Create it manually first (we do not auto-create orgs to avoid polluting prod accidentally).`,
);
}
return org;
};
const generateCreatedAt = ({
index,
rangeMs,
startMs,
}: {
index: number;
rangeMs: number;
startMs: number;
}): number => {
const burstIndex = Math.floor(index / CONFIG.burstEveryNCustomers);
const burstOffset = index % CONFIG.burstEveryNCustomers;
if (burstOffset < CONFIG.burstClusterSize) {
const burstSlot = burstIndex / (CONFIG.totalCustomers / CONFIG.burstEveryNCustomers);
return Math.floor(startMs + burstSlot * rangeMs);
}
const monotonicSlot = index / CONFIG.totalCustomers;
const jitter = (Math.random() - 0.5) * (rangeMs / CONFIG.totalCustomers) * 50;
return Math.floor(startMs + monotonicSlot * rangeMs + jitter);
};
type CustomerRow = typeof customers.$inferInsert;
const generateCustomerRow = ({
index,
orgId,
env,
createdAt,
}: {
index: number;
orgId: string;
env: AppEnv;
createdAt: number;
}): CustomerRow => {
const internalId = generateId("cus_int");
const externalId = `cus_bench_${index.toString().padStart(8, "0")}`;
const hasRevenuecat = Math.random() < CONFIG.revenuecatRatio;
const processors = hasRevenuecat
? ({
revenuecat: {
id: `rc_${index}`,
app_user_id: `app_${index}`,
},
} as unknown as ExternalProcessors)
: ({} as ExternalProcessors);
return {
internal_id: internalId,
org_id: orgId,
created_at: createdAt,
name: `Customer ${index}`,
id: externalId,
email: `bench+${index}@autumn-test.dev`,
env,
processors,
};
};
const wipeExistingSeed = async ({
db,
orgId,
env,
}: {
db: ReturnType<typeof initDrizzle>["db"];
orgId: string;
env: AppEnv;
}) => {
console.log(chalk.cyan("Wiping existing benchmark seed for this org..."));
await db.execute(sql`
DELETE FROM entities
WHERE org_id = ${orgId}
AND env = ${env}
AND id LIKE 'ent_bench_%'
`);
await db.execute(sql`
DELETE FROM customers
WHERE org_id = ${orgId}
AND env = ${env}
AND id LIKE 'cus_bench_%'
`);
console.log(chalk.green("✅ Wipe complete"));
};
const seedCustomers = async ({
db,
orgId,
env,
count,
}: {
db: ReturnType<typeof initDrizzle>["db"];
orgId: string;
env: AppEnv;
count: number;
}) => {
const now = Date.now();
const rangeMs = CONFIG.timeRangeDays * 24 * 60 * 60 * 1000;
const startMs = now - rangeMs;
console.log(
chalk.cyan(
`Seeding ${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);
const rows: CustomerRow[] = [];
for (let i = batchStart; i < batchEnd; i++) {
rows.push(
generateCustomerRow({
index: i,
orgId,
env,
createdAt: generateCreatedAt({ index: i, rangeMs, startMs }),
}),
);
}
await db.insert(customers).values(rows);
inserted += rows.length;
const pct = ((inserted / count) * 100).toFixed(1);
const elapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
process.stdout.write(
`\r ${chalk.gray(`${inserted.toLocaleString()} / ${count.toLocaleString()} (${pct}%) — ${elapsed}s`)}`,
);
}
const totalElapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
console.log(chalk.green(`\n✅ Inserted ${inserted.toLocaleString()} customers in ${totalElapsed}s`));
};
type EntityRow = typeof entities.$inferInsert;
const seedEntities = async ({
db,
orgId,
env,
customerCount,
entityCount,
}: {
db: ReturnType<typeof initDrizzle>["db"];
orgId: string;
env: AppEnv;
customerCount: number;
entityCount: number;
}) => {
console.log(
chalk.cyan(
`Seeding ${entityCount.toLocaleString()} entities across ${customerCount.toLocaleString()} customers...`,
),
);
const sample = await db
.select({ internal_id: customers.internal_id })
.from(customers)
.where(and(eq(customers.org_id, orgId), eq(customers.env, env)))
.limit(customerCount);
if (sample.length === 0) {
console.log(chalk.yellow("⚠️ No customers found, skipping entity seed"));
return;
}
const now = Date.now();
const rangeMs = CONFIG.timeRangeDays * 24 * 60 * 60 * 1000;
const startMs = now - rangeMs;
const startedAt = performance.now();
let inserted = 0;
for (let batchStart = 0; batchStart < entityCount; batchStart += CONFIG.batchSize) {
const batchEnd = Math.min(batchStart + CONFIG.batchSize, entityCount);
const rows: EntityRow[] = [];
for (let i = batchStart; i < batchEnd; i++) {
const customer = sample[i % sample.length];
rows.push({
internal_id: generateId("ent_int"),
id: `ent_bench_${i.toString().padStart(8, "0")}`,
org_id: orgId,
env,
internal_customer_id: customer.internal_id,
created_at: Math.floor(startMs + (i / entityCount) * rangeMs),
name: `Entity ${i}`,
deleted: false,
});
}
await db.insert(entities).values(rows);
inserted += rows.length;
const pct = ((inserted / entityCount) * 100).toFixed(1);
const elapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
process.stdout.write(
`\r ${chalk.gray(`${inserted.toLocaleString()} / ${entityCount.toLocaleString()} (${pct}%) — ${elapsed}s`)}`,
);
}
const totalElapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
console.log(
chalk.green(`\n✅ Inserted ${inserted.toLocaleString()} entities in ${totalElapsed}s`),
);
};
const main = async () => {
console.log(
chalk.magentaBright(
"\n================ Pagination Benchmark Seed ================\n",
),
);
const args = parseArgs();
const env = CONFIG.defaultEnv;
const customerCount = args.count ?? CONFIG.totalCustomers;
const entityCount = Math.min(
CONFIG.totalEntities,
Math.floor(customerCount * 0.05),
);
const { db, client } = initDrizzle();
try {
const org = await resolveOrg({ db, slug: args.org_slug });
console.log(chalk.cyan("Target:"));
console.log(chalk.gray(` Org: ${org.slug} (${org.id})`));
console.log(chalk.gray(` Env: ${env}`));
console.log(chalk.gray(` Customers: ${customerCount.toLocaleString()}`));
if (!args.skip_entities) {
console.log(chalk.gray(` Entities: ${entityCount.toLocaleString()}`));
}
console.log();
if (args.wipe) {
await wipeExistingSeed({ db, orgId: org.id, env });
console.log();
}
const existing = await db
.select({ count: sql<number>`count(*)::int` })
.from(customers)
.where(
and(
eq(customers.org_id, org.id),
eq(customers.env, env),
sql`${customers.id} LIKE 'cus_bench_%'`,
),
);
const existingCount = existing[0]?.count ?? 0;
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;
}
console.log(chalk.yellow(`Continuing seed to fill remaining ${(customerCount - existingCount).toLocaleString()} customers.`));
}
await seedCustomers({ db, orgId: org.id, env, count: customerCount });
if (!args.skip_entities) {
await seedEntities({
db,
orgId: org.id,
env,
customerCount,
entityCount,
});
}
console.log(
chalk.magentaBright(
"\n================ Seed Complete ================\n",
),
);
} catch (error) {
console.error(chalk.red("\n❌ Seed 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();
}
};
await main();
process.exit(0);

View File

@@ -0,0 +1,338 @@
import type { AppEnv, CusProductStatus, ListCustomersV2Params } from "@autumn/shared";
import { sql } from "drizzle-orm";
import { getCustomerListFilterSql } from "../src/internal/customers/getFullCusQuery";
export type CursorPaginatedFullCusQueryArgs = {
orgId: string;
env: AppEnv;
inStatuses?: CusProductStatus[];
includeInvoices: boolean;
withEntities: boolean;
withTrialsUsed: boolean;
withSubs: boolean;
limit: number;
cursor?: { createdAt: number; id: string };
withEvents?: boolean;
internalCustomerIds?: string[];
plans?: ListCustomersV2Params["plans"];
processors?: ListCustomersV2Params["processors"];
search?: string;
cusProductLimit: number;
};
export const getCursorPaginatedFullCusQuery = ({
orgId,
env,
inStatuses,
includeInvoices,
withEntities,
withTrialsUsed,
withSubs,
limit,
cursor,
withEvents = false,
internalCustomerIds,
plans,
processors,
search,
cusProductLimit,
}: CursorPaginatedFullCusQueryArgs) => {
const withStatusFilter = () => {
return 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.createdAt}, ${cursor.id})`
: sql``;
const fetchLimit = limit + 1;
const extraEntitlementsCTE = sql`, 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
)`;
return sql`
WITH customer_records AS (
SELECT c.*
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}
),
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
${withStatusFilter()}
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
LIMIT ${cusProductLimit}
) 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
)
${
withSubs
? sql`, 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
)`
: sql``
}
${
withEvents
? sql`, customer_events AS (
SELECT
e.internal_customer_id,
COALESCE(
json_agg(
json_build_object(
'id', e.id,
'event_name', e.event_name,
'value', e.value,
'timestamp', e.timestamp,
'properties', e.properties
)
ORDER BY e.timestamp DESC, e.id DESC
) FILTER (WHERE e.id IS NOT NULL),
'[]'::json
) AS events
FROM events e
WHERE e.internal_customer_id IN (SELECT internal_id FROM customer_records)
AND e.set_usage = false
GROUP BY e.internal_customer_id
)`
: sql``
}
${
withEntities
? sql`, customer_entities AS (
SELECT
cr.internal_id AS internal_customer_id,
COALESCE(
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.internal_id IS NOT NULL),
'[]'::json
) AS entities
FROM customer_records cr
LEFT JOIN LATERAL (
SELECT *
FROM entities e
WHERE e.internal_customer_id = cr.internal_id
ORDER BY e.internal_id DESC
LIMIT 300
) e ON true
GROUP BY cr.internal_id
)`
: sql``
}
${
includeInvoices
? sql`, customer_invoices AS (
SELECT
cr.internal_id AS internal_customer_id,
COALESCE(
json_agg(row_to_json(i) ORDER BY i.created_at DESC, i.id DESC) FILTER (WHERE i.id IS NOT NULL),
'[]'::json
) AS invoices
FROM customer_records cr
LEFT JOIN LATERAL (
SELECT *
FROM invoices i
WHERE i.internal_customer_id = cr.internal_id
ORDER BY i.created_at DESC, i.id DESC
LIMIT 10
) i ON true
GROUP BY cr.internal_id
)`
: sql``
}
${
withTrialsUsed
? sql`, customer_trials_used AS (
SELECT
cp.internal_customer_id,
json_agg(json_build_object(
'product_id', p.id,
'fingerprint', c.fingerprint,
'customer_id', c.id
)) AS trials_used
FROM customer_products cp
JOIN products p ON cp.internal_product_id = p.internal_id
JOIN customers c ON cp.internal_customer_id = c.internal_id
WHERE cp.internal_customer_id IN (SELECT internal_id FROM customer_records)
AND cp.free_trial_id IS NOT NULL
GROUP BY cp.internal_customer_id
)`
: sql``
}
${extraEntitlementsCTE}
SELECT
cr.*,
COALESCE(cpa.customer_products, '[]'::json) AS customer_products
${withSubs ? sql`, COALESCE(cs.subscriptions, '[]'::json) AS subscriptions` : sql``}
${withEntities ? sql`, COALESCE(ce.entities, '[]'::json) AS entities` : sql``}
${includeInvoices ? sql`, COALESCE(ci.invoices, '[]'::json) AS invoices` : sql``}
${withTrialsUsed ? sql`, COALESCE(ctu.trials_used, '[]'::json) AS trials_used` : sql``}
${withEvents ? sql`, COALESCE(cev.events, '[]'::json) AS events` : sql``}
, 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
${withSubs ? sql`LEFT JOIN customer_subscriptions cs ON cs.internal_customer_id = cr.internal_id` : sql``}
${withEntities ? sql`LEFT JOIN customer_entities ce ON ce.internal_customer_id = cr.internal_id` : sql``}
${includeInvoices ? sql`LEFT JOIN customer_invoices ci ON ci.internal_customer_id = cr.internal_id` : sql``}
${withTrialsUsed ? sql`LEFT JOIN customer_trials_used ctu ON ctu.internal_customer_id = cr.internal_id` : sql``}
${withEvents ? sql`LEFT JOIN customer_events cev ON cev.internal_customer_id = cr.internal_id` : sql``}
LEFT JOIN extra_customer_entitlements ece ON ece.internal_customer_id = cr.internal_id
ORDER BY cr.created_at DESC, cr.id DESC
`;
};

View File

@@ -0,0 +1,368 @@
import { AppEnv } 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 { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
import { initDrizzle } from "../src/db/initDrizzle";
import { loadLocalEnv } from "../src/utils/envUtils";
import { getCursorPaginatedFullCusQuery } from "./cursorPaginatedFullCusQuery";
loadLocalEnv();
const ORG_ID = "r7pwHiekGsqt32qGqcVku6thWFh5aHh8";
const ENV: AppEnv = AppEnv.Live;
const LABEL = "runable";
const REPEATS = 5;
const STATEMENT_TIMEOUT_MS = 30_000;
const DEEP_OFFSET_PCT = 45;
const BASE_LIMIT = 1000;
const READ_ONLY = true;
const CUS_PRODUCT_LIMIT = 15;
const RESULTS_DIR = join(import.meta.dir, "results");
type DB = ReturnType<typeof initDrizzle>["db"];
const sharedFullCusOpts = {
inStatuses: RELEVANT_STATUSES,
includeInvoices: false,
withEntities: false,
withTrialsUsed: false,
withSubs: true,
cusProductLimit: CUS_PRODUCT_LIMIT,
};
type Cell = {
name: string;
build: () => SQL;
includeExplain?: boolean;
};
type CellResult = {
name: string;
medianMs: number;
p95Ms: number;
minMs: number;
maxMs: number;
rowCount: number;
samples: number[];
explainPlan?: string;
error?: string;
};
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;
}
}
let explainPlan: string | undefined;
if (cell.includeExplain && !error) {
try {
const query = cell.build();
const explainRows = await runQueryInTxn({
db,
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`,
});
explainPlan = explainRows.map((row) => row["QUERY PLAN"]).join("\n");
} catch (err) {
explainPlan = `EXPLAIN failed: ${err instanceof Error ? err.message : String(err)}`;
}
}
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 {
name: cell.name,
medianMs: median,
p95Ms: p95,
minMs: min,
maxMs: max,
rowCount,
samples,
explainPlan,
error,
};
};
const revenuecatCountQuery = (): SQL => sql`
SELECT COUNT(*)::int AS total_count
FROM customers c
WHERE c.org_id = ${ORG_ID}
AND c.env = ${ENV}
AND EXISTS (
SELECT 1 FROM customer_products cp
WHERE cp.internal_customer_id = c.internal_id
AND cp.processor->>'type' = 'revenuecat'
)
`;
const resolveRevenuecatCount = async ({ db }: { db: DB }) => {
const result = await runQueryInTxn({ db, query: revenuecatCountQuery() });
return (result[0] as { total_count: number }).total_count;
};
const resolveRevenuecatDeepCursor = async ({
db,
deepOffset,
}: {
db: DB;
deepOffset: number;
}): Promise<{ createdAt: number; id: string }> => {
const result = await runQueryInTxn({
db,
query: sql`
SELECT c.created_at, c.id
FROM customers c
WHERE c.org_id = ${ORG_ID}
AND c.env = ${ENV}
AND EXISTS (
SELECT 1 FROM customer_products cp
WHERE cp.internal_customer_id = c.internal_id
AND cp.processor->>'type' = 'revenuecat'
)
ORDER BY c.created_at DESC, c.id DESC
LIMIT 1 OFFSET ${deepOffset}
`,
});
const row = result[0] as { created_at: number; id: string } | undefined;
if (!row) {
throw new Error(
`Could not resolve revenuecat deep cursor at offset ${deepOffset}. Not enough revenuecat customers.`,
);
}
return { createdAt: row.created_at, id: row.id };
};
const buildCells = ({
deepCursor,
}: {
deepCursor: { createdAt: number; id: string };
}): Cell[] => {
return [
{
name: `10 cursor / revcat=true / first page / limit ${BASE_LIMIT}`,
build: () =>
getCursorPaginatedFullCusQuery({
orgId: ORG_ID,
env: ENV,
limit: BASE_LIMIT,
processors: ["revenuecat"],
...sharedFullCusOpts,
}),
includeExplain: true,
},
{
name: `11 cursor / revcat=true / deep / limit ${BASE_LIMIT}`,
build: () =>
getCursorPaginatedFullCusQuery({
orgId: ORG_ID,
env: ENV,
limit: BASE_LIMIT,
cursor: deepCursor,
processors: ["revenuecat"],
...sharedFullCusOpts,
}),
includeExplain: true,
},
];
};
const renderResults = ({
totalCount,
deepOffset,
deepCursor,
results,
}: {
totalCount: number;
deepOffset: number;
deepCursor: { createdAt: number; id: string };
results: CellResult[];
}): string => {
const date = new Date().toISOString().slice(0, 10);
const lines: string[] = [];
lines.push(`# Pagination Benchmark — ${date} — prod / ${LABEL}`);
lines.push("");
lines.push("## Config");
lines.push("");
lines.push(`- org_id: \`${ORG_ID}\``);
lines.push(`- env: \`${ENV}\``);
lines.push(`- revenuecat_customers: \`${totalCount.toLocaleString()}\``);
lines.push(
`- deep_offset (${DEEP_OFFSET_PCT}% within revcat subset): \`${deepOffset.toLocaleString()}\``,
);
lines.push(
`- deep_cursor (revcat-aware): \`{ t: ${deepCursor.createdAt}, id: ${deepCursor.id} }\``,
);
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(`- focus: revenuecat filter (single SQL call, cursor + EXISTS combined)`);
lines.push(`- partial index in place: idx_customer_products_revenuecat_processor`);
lines.push("");
lines.push("## Results");
lines.push("");
lines.push("| # | Cell | Rows | median ms | p95 ms | min ms | max ms | Error |");
lines.push("|---|------|------|-----------|--------|--------|--------|-------|");
for (const r of results) {
const idx = r.name.split(" ")[0];
const desc = r.name.split(" ").slice(1).join(" ");
const errCol = r.error ? `\`${r.error.slice(0, 80)}\`` : "";
lines.push(
`| ${idx} | ${desc} | ${r.rowCount} | ${r.medianMs.toFixed(2)} | ${r.p95Ms.toFixed(2)} | ${r.minMs.toFixed(2)} | ${r.maxMs.toFixed(2)} | ${errCol} |`,
);
}
lines.push("");
lines.push("## EXPLAIN ANALYZE");
lines.push("");
for (const r of results) {
if (!r.explainPlan) continue;
lines.push(`### ${r.name}`);
lines.push("");
lines.push("```");
lines.push(r.explainPlan);
lines.push("```");
lines.push("");
}
return lines.join("\n");
};
const main = async () => {
console.log(
chalk.magentaBright(
`\n================ Pagination Benchmark — prod / ${LABEL} ================\n`,
),
);
const { db, client } = initDrizzle();
try {
console.log(chalk.cyan(`Org: ${ORG_ID} (${ENV})`));
console.log(chalk.cyan("Resolving revenuecat customer count..."));
const totalCount = await resolveRevenuecatCount({ db });
const deepOffset = Math.floor((totalCount * DEEP_OFFSET_PCT) / 100);
console.log(
chalk.gray(
` revenuecat_customers = ${totalCount.toLocaleString()}, deep_offset (${DEEP_OFFSET_PCT}% within revcat) = ${deepOffset.toLocaleString()}`,
),
);
console.log(
chalk.cyan(`Resolving revcat-aware deep cursor at offset ${deepOffset}...`),
);
const deepCursor = await resolveRevenuecatDeepCursor({ db, deepOffset });
console.log(
chalk.gray(` cursor = { t: ${deepCursor.createdAt}, id: ${deepCursor.id} }`),
);
console.log();
const cells = buildCells({ deepCursor });
const results: CellResult[] = [];
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
process.stdout.write(
chalk.gray(`[${i + 1}/${cells.length}] ${cell.name} ... `),
);
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(2)}ms p95=${result.p95Ms.toFixed(2)}ms rows=${result.rowCount}`,
),
);
}
}
const report = renderResults({
totalCount,
deepOffset,
deepCursor,
results,
});
const date = new Date().toISOString().slice(0, 10);
const outPath = join(RESULTS_DIR, `${date}-prod-${LABEL}.md`);
writeFileSync(outPath, report);
console.log();
console.log(chalk.green(`✅ Report written to ${outPath}`));
console.log(
chalk.magentaBright(
`\n================ Benchmark Complete ================\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();

View File

@@ -0,0 +1,165 @@
# Pagination Benchmark — 2026-05-12
## Config
- org_id: `org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt`
- env: `sandbox`
- base limit: `50`
- deep offset: `95000`
- deep cursor: `{ t: 1715891201011, id: cus_bench_00005637 }`
- search term: `Customer 50000`
- repeats per cell: 10
## Results
| # | Cell | Rows | median ms | p95 ms | min ms | max ms |
|---|------|------|-----------|--------|--------|--------|
| 01 | offset / no filter / page 0 / limit 50 | 50 | 8.38 | 9.93 | 6.83 | 9.93 |
| 02 | offset / no filter / page 0 / limit 50 + total_count | 1 | 23.10 | 24.72 | 21.36 | 24.72 |
| 03 | offset / no filter / deep (offset 95000) / limit 50 | 50 | 30.60 | 33.61 | 29.01 | 33.61 |
| 04 | offset / search "Customer 50000" / deep (offset 95000) / limit 50 | 0 | 176.28 | 205.53 | 166.42 | 205.53 |
| 05 | offset / search count / "Customer 50000" | 1 | 173.95 | 183.80 | 167.27 | 183.80 |
| 06 | offset / revenuecat=true / page 0 / limit 50 | 50 | 8.49 | 9.06 | 7.59 | 9.06 |
| 07 | cursor / no filter / first page / limit 50 | 51 | 7.89 | 8.23 | 7.63 | 8.23 |
| 08 | cursor / no filter / deep (via constructed cursor) / limit 50 | 51 | 7.96 | 10.06 | 7.76 | 10.06 |
| 09 | cursor / search "Customer 50000" / deep / limit 50 | 0 | 16.82 | 18.31 | 16.24 | 18.31 |
| 10 | cursor / revenuecat=true / first page / limit 50 | 51 | 8.24 | 9.58 | 6.57 | 9.58 |
| 11 | offset / no filter / page 0 / limit 1000 | 1000 | 21.37 | 25.70 | 19.67 | 25.70 |
| 12 | offset / no filter / deep / limit 1000 | 1000 | 40.02 | 43.46 | 37.42 | 43.46 |
| 13 | cursor / no filter / first page / limit 1000 | 1001 | 16.97 | 18.61 | 15.74 | 18.61 |
| 14 | cursor / no filter / deep / limit 1000 | 1001 | 14.74 | 18.50 | 14.13 | 18.50 |
## EXPLAIN ANALYZE
### 01 offset / no filter / page 0 / limit 50
```
Limit (cost=0.42..3.74 rows=50 width=105) (actual time=0.017..0.037 rows=50 loops=1)
Buffers: shared hit=45
-> Index Scan using idx_customers_org_id_env_created_at on customers c (cost=0.42..6209.82 rows=93365 width=105) (actual time=0.016..0.033 rows=50 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text))
Buffers: shared hit=45
Planning Time: 0.080 ms
Execution Time: 0.079 ms
```
### 02 offset / no filter / page 0 / limit 50 + total_count
```
Aggregate (cost=4391.50..4391.52 rows=1 width=4) (actual time=19.979..19.980 rows=1 loops=1)
Buffers: shared hit=2468 read=160
-> Seq Scan on customers c (cost=0.00..4158.09 rows=93365 width=0) (actual time=0.006..15.661 rows=100619 loops=1)
Filter: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text))
Rows Removed by Filter: 3475
Buffers: shared hit=2468 read=160
Planning Time: 0.087 ms
Execution Time: 20.023 ms
```
### 03 offset / no filter / deep (offset 95000) / limit 50
```
Limit (cost=6209.82..6209.88 rows=1 width=105) (actual time=25.752..25.768 rows=50 loops=1)
Buffers: shared hit=38099
-> Index Scan using idx_customers_org_id_env_created_at on customers c (cost=0.42..6209.82 rows=93365 width=105) (actual time=0.018..23.191 rows=95050 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text))
Buffers: shared hit=38099
Planning Time: 0.103 ms
Execution Time: 25.826 ms
```
### 04 offset / search "Customer 50000" / deep (offset 95000) / limit 50
```
Limit (cost=4923.88..4923.88 rows=1 width=105) (actual time=168.954..168.956 rows=0 loops=1)
Buffers: shared hit=2628
-> Sort (cost=4923.81..4923.88 rows=28 width=105) (actual time=168.952..168.953 rows=1 loops=1)
Sort Key: created_at DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=2628
-> Seq Scan on customers c (cost=0.00..4923.14 rows=28 width=105) (actual time=82.227..168.945 rows=1 loops=1)
Filter: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text) AND ((id ~~* '%Customer 50000%'::text) OR (name ~~* '%Customer 50000%'::text) OR (email ~~* '%Customer 50000%'::text)))
Rows Removed by Filter: 104093
Buffers: shared hit=2628
Planning Time: 0.279 ms
Execution Time: 169.025 ms
```
### 05 offset / search count / "Customer 50000"
```
Aggregate (cost=4923.20..4923.22 rows=1 width=4) (actual time=161.501..161.502 rows=1 loops=1)
Buffers: shared hit=2628
-> Seq Scan on customers c (cost=0.00..4923.14 rows=28 width=0) (actual time=82.466..161.495 rows=1 loops=1)
Filter: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text) AND ((id ~~* '%Customer 50000%'::text) OR (name ~~* '%Customer 50000%'::text) OR (email ~~* '%Customer 50000%'::text)))
Rows Removed by Filter: 104093
Buffers: shared hit=2628
Planning Time: 0.267 ms
Execution Time: 161.591 ms
```
### 06 offset / revenuecat=true / page 0 / limit 50
```
Limit (cost=0.42..37.76 rows=50 width=64) (actual time=0.206..0.354 rows=50 loops=1)
Buffers: shared hit=696
-> Index Scan using idx_customers_org_id_env_created_at on customers c (cost=0.42..6443.23 rows=8627 width=64) (actual time=0.206..0.350 rows=50 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text))
Filter: ((processors ->> 'revenuecat'::text) IS NOT NULL)
Rows Removed by Filter: 1062
Buffers: shared hit=696
Planning Time: 0.080 ms
Execution Time: 0.388 ms
```
### 07 cursor / no filter / first page / limit 50
```
Limit (cost=0.42..4.08 rows=51 width=105) (actual time=0.013..0.032 rows=51 loops=1)
Buffers: shared hit=46
-> Index Scan using idx_customers_cursor on customers c (cost=0.42..6703.72 rows=93365 width=105) (actual time=0.012..0.029 rows=51 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text))
Buffers: shared hit=46
Planning Time: 0.076 ms
Execution Time: 0.066 ms
```
### 08 cursor / no filter / deep (via constructed cursor) / limit 50
```
Limit (cost=0.42..9.41 rows=51 width=105) (actual time=0.011..0.031 rows=51 loops=1)
Buffers: shared hit=32
-> Index Scan using idx_customers_cursor on customers c (cost=0.42..2107.49 rows=11952 width=105) (actual time=0.011..0.028 rows=51 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text) AND (ROW(created_at, id) < ROW('1715891201011'::numeric, 'cus_bench_00005637'::text)))
Buffers: shared hit=32
Planning Time: 0.076 ms
Execution Time: 0.062 ms
```
### 09 cursor / search "Customer 50000" / deep / limit 50
```
Limit (cost=0.42..2197.13 rows=4 width=105) (actual time=9.838..9.839 rows=0 loops=1)
Buffers: shared hit=2298
-> Index Scan using idx_customers_cursor on customers c (cost=0.42..2197.13 rows=4 width=105) (actual time=9.837..9.837 rows=0 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text) AND (ROW(created_at, id) < ROW('1715891201011'::numeric, 'cus_bench_00005637'::text)))
Filter: ((id ~~* '%Customer 50000%'::text) OR (name ~~* '%Customer 50000%'::text) OR (email ~~* '%Customer 50000%'::text))
Rows Removed by Filter: 5618
Buffers: shared hit=2298
Planning Time: 0.272 ms
Execution Time: 9.880 ms
```
### 10 cursor / revenuecat=true / first page / limit 50
```
Limit (cost=0.42..41.43 rows=51 width=64) (actual time=0.184..0.320 rows=51 loops=1)
Buffers: shared hit=701
-> Index Scan using idx_customers_cursor on customers c (cost=0.42..6937.13 rows=8627 width=64) (actual time=0.183..0.316 rows=51 loops=1)
Index Cond: ((org_id = 'org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt'::text) AND (env = 'sandbox'::text))
Filter: ((processors ->> 'revenuecat'::text) IS NOT NULL)
Rows Removed by Filter: 1074
Buffers: shared hit=701
Planning Time: 0.070 ms
Execution Time: 0.352 ms
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,488 @@
# Pagination Benchmark — 2026-05-13 — prod / runable
## Config
- org_id: `r7pwHiekGsqt32qGqcVku6thWFh5aHh8`
- env: `live`
- revenuecat_customers: `1,261`
- deep_offset (45% within revcat subset): `567`
- deep_cursor (revcat-aware): `{ t: 1777225699690, id: PL6dMZw5mewXCvW74aPOOaZ28TVh5K8T }`
- limit: `1000`
- repeats per cell: 5
- statement_timeout_ms: 30000
- read_only: true
- focus: revenuecat filter (single SQL call, cursor + EXISTS combined)
- partial index in place: idx_customer_products_revenuecat_processor
## Results
| # | Cell | Rows | median ms | p95 ms | min ms | max ms | Error |
|---|------|------|-----------|--------|--------|--------|-------|
| 10 | cursor / revcat=true / first page / limit 1000 | 1001 | 3055.60 | 3146.11 | 2291.71 | 3146.11 | |
| 11 | cursor / revcat=true / deep / limit 1000 | 693 | 2228.11 | 2701.16 | 2191.98 | 2701.16 | |
## EXPLAIN ANALYZE
### 10 cursor / revcat=true / first page / limit 1000
```
Sort (cost=109707.61..109710.11 rows=1001 width=609) (actual time=399.883..613.996 rows=1001.00 loops=1)
Sort Key: cr.created_at DESC, cr.id DESC
Sort Method: quicksort Memory: 5988kB
Buffers: shared hit=1382434
CTE customer_records
-> Limit (cost=1000.89..37454.69 rows=1001 width=573) (actual time=120.191..351.949 rows=1001.00 loops=1)
Buffers: shared hit=1327851
-> Gather Merge (cost=1000.89..365247.54 rows=10002 width=573) (actual time=120.190..351.839 rows=1001.00 loops=1)
Workers Planned: 4
Workers Launched: 4
Buffers: shared hit=1327851
-> Nested Loop Semi Join (cost=0.83..363056.15 rows=2500 width=573) (actual time=1.776..379.424 rows=249.40 loops=5)
Buffers: shared hit=1327807
-> Parallel Index Scan using idx_customers_cursor on customers c (cost=0.56..247810.82 rows=240186 width=573) (actual time=0.038..224.128 rows=92305.60 loops=5)
Index Cond: ((org_id = 'r7pwHiekGsqt32qGqcVku6thWFh5aHh8'::text) AND (env = 'live'::text))
Index Searches: 1
Buffers: shared hit=403105
-> Index Only Scan using idx_customer_products_revenuecat_processor on customer_products cp_processor (cost=0.28..0.48 rows=1 width=32) (actual time=0.001..0.001 rows=0.00 loops=461528)
Index Cond: (internal_customer_id = c.internal_id)
Heap Fetches: 1057
Index Searches: 461528
Buffers: shared hit=924702
CTE customer_products_with_prices
-> Nested Loop Left Join (cost=1016.52..12367.59 rows=2002 width=678) (actual time=0.492..248.636 rows=1045.00 loops=1)
Buffers: shared hit=52735
-> Nested Loop Left Join (cost=1016.22..12301.53 rows=2002 width=864) (actual time=0.480..243.772 rows=1045.00 loops=1)
Buffers: shared hit=52735
-> Nested Loop Left Join (cost=16.29..9252.69 rows=2002 width=832) (actual time=0.191..81.148 rows=1045.00 loops=1)
Buffers: shared hit=19822
-> Nested Loop (cost=9.48..9182.21 rows=2002 width=800) (actual time=0.085..40.398 rows=1045.00 loops=1)
Buffers: shared hit=11482
-> Nested Loop (cost=9.05..9125.77 rows=2002 width=550) (actual time=0.055..39.523 rows=1045.00 loops=1)
Buffers: shared hit=11442
-> CTE Scan on customer_records cr_2 (cost=0.00..20.02 rows=1001 width=32) (actual time=0.000..18.146 rows=1001.00 loops=1)
Storage: Memory Maximum Storage: 270kB
Buffers: shared hit=1199
-> Limit (cost=9.05..9.06 rows=2 width=551) (actual time=0.020..0.021 rows=1.04 loops=1001)
Buffers: shared hit=10243
-> Sort (cost=9.05..9.06 rows=2 width=551) (actual time=0.020..0.020 rows=1.04 loops=1001)
Sort Key: ((SubPlan 2)), cp.created_at DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=10243
-> Index Scan using customer_products_internal_customer_id_idx on customer_products cp (cost=0.56..9.04 rows=2 width=551) (actual time=0.016..0.017 rows=1.04 loops=1001)
Index Cond: (internal_customer_id = cr_2.internal_id)
Filter: (status = ANY ('{active,past_due,scheduled}'::text[]))
Rows Removed by Filter: 1
Index Searches: 1001
Buffers: shared hit=10243
SubPlan 2
-> Index Scan using plans_pkey on products p (cost=0.41..2.13 rows=1 width=1) (actual time=0.005..0.006 rows=1.00 loops=1045)
Index Cond: (internal_id = cp.internal_product_id)
Index Searches: 1045
Buffers: shared hit=4180
-> Memoize (cost=0.42..2.14 rows=1 width=282) (actual time=0.000..0.000 rows=1.00 loops=1045)
Cache Key: cp.internal_product_id
Cache Mode: logical
Hits: 1035 Misses: 10 Evictions: 0 Overflows: 0 Memory Usage: 4kB
Buffers: shared hit=40
-> Index Scan using plans_pkey on products prod (cost=0.41..2.13 rows=1 width=282) (actual time=0.008..0.008 rows=1.00 loops=10)
Index Cond: (internal_id = cp.internal_product_id)
Index Searches: 10
Buffers: shared hit=40
-> Memoize (cost=6.81..6.82 rows=1 width=32) (actual time=0.038..0.038 rows=1.00 loops=1045)
Cache Key: cp.id
Cache Mode: binary
Hits: 0 Misses: 1045 Evictions: 0 Overflows: 0 Memory Usage: 915kB
Buffers: shared hit=8340
-> Aggregate (cost=6.80..6.81 rows=1 width=32) (actual time=0.037..0.037 rows=1.00 loops=1045)
Buffers: shared hit=8340
-> Nested Loop Left Join (cost=0.84..6.78 rows=2 width=871) (actual time=0.016..0.016 rows=1.00 loops=1045)
Buffers: shared hit=8340
-> Index Scan using idx_customer_prices_product_id on customer_prices cpr (cost=0.42..2.51 rows=2 width=269) (actual time=0.012..0.013 rows=1.00 loops=1045)
Index Cond: (customer_product_id = cp.id)
Index Searches: 1045
Buffers: shared hit=4176
-> Index Scan using prices_pkey on prices p_1 (cost=0.42..2.13 rows=1 width=664) (actual time=0.003..0.003 rows=1.00 loops=1041)
Index Cond: (id = cpr.price_id)
Index Searches: 1041
Buffers: shared hit=4164
-> Memoize (cost=999.93..999.94 rows=1 width=32) (actual time=0.155..0.155 rows=1.00 loops=1045)
Cache Key: cp.id
Cache Mode: binary
Hits: 0 Misses: 1045 Evictions: 0 Overflows: 0 Memory Usage: 3760kB
Buffers: shared hit=32913
-> Aggregate (cost=999.92..999.93 rows=1 width=32) (actual time=0.153..0.153 rows=1.00 loops=1045)
Buffers: shared hit=32913
-> Index Scan using idx_customer_entitlements_product_id on customer_entitlements ce_2 (cost=0.56..78.98 rows=106 width=537) (actual time=0.015..0.018 rows=1.97 loops=1045)
Index Cond: (customer_product_id = cp.id)
Index Searches: 1045
Buffers: shared hit=6094
SubPlan 3
-> Nested Loop (cost=0.84..4.29 rows=1 width=32) (actual time=0.015..0.016 rows=1.00 loops=2063)
Buffers: shared hit=16504
-> Index Scan using entitlements_pkey on entitlements e (cost=0.43..2.15 rows=1 width=359) (actual time=0.002..0.003 rows=1.00 loops=2063)
Index Cond: (id = ce_2.entitlement_id)
Index Searches: 2063
Buffers: shared hit=8252
-> Index Scan using features_pkey on features f (cost=0.41..2.13 rows=1 width=349) (actual time=0.003..0.003 rows=1.00 loops=2063)
Index Cond: (internal_id = (e.internal_feature_id)::text)
Index Searches: 2063
Buffers: shared hit=8252
SubPlan 4
-> Aggregate (cost=1.14..1.15 rows=1 width=32) (actual time=0.003..0.003 rows=1.00 loops=2063)
Buffers: shared hit=2063
-> Seq Scan on replaceables r (cost=0.00..1.12 rows=2 width=154) (actual time=0.002..0.002 rows=0.00 loops=2063)
Filter: (cus_ent_id = ce_2.id)
Rows Removed by Filter: 21
Buffers: shared hit=2063
SubPlan 5
-> Aggregate (cost=3.24..3.25 rows=1 width=32) (actual time=0.010..0.010 rows=1.00 loops=2063)
Buffers: shared hit=8252
-> Index Scan using idx_rollovers_cus_ent_id on rollovers ro (cost=0.56..3.17 rows=4 width=125) (actual time=0.008..0.008 rows=0.00 loops=2063)
Index Cond: (cus_ent_id = ce_2.id)
Index Searches: 2063
Buffers: shared hit=8252
-> Memoize (cost=0.30..2.01 rows=1 width=139) (actual time=0.000..0.000 rows=0.00 loops=1045)
Cache Key: cp.free_trial_id
Cache Mode: logical
Hits: 1044 Misses: 1 Evictions: 0 Overflows: 0 Memory Usage: 1kB
-> Index Scan using free_trials_pkey on free_trials ft (cost=0.29..2.00 rows=1 width=139) (actual time=0.001..0.001 rows=0.00 loops=1)
Index Cond: (id = cp.free_trial_id)
Index Searches: 0
-> Hash Right Join (cost=7588.29..59835.44 rows=1001 width=609) (actual time=396.315..398.203 rows=1001.00 loops=1)
Hash Cond: (cr_1.internal_id = cr.internal_id)
Buffers: shared hit=56512
-> GroupAggregate (cost=7113.76..59337.13 rows=1001 width=64) (actual time=5.530..6.762 rows=1001.00 loops=1)
Group Key: cr_1.internal_id
Buffers: shared hit=3003
-> Sort (cost=7113.76..7128.78 rows=6006 width=569) (actual time=5.521..5.619 rows=1001.00 loops=1)
Sort Key: cr_1.internal_id COLLATE "C"
Sort Method: quicksort Memory: 71kB
Buffers: shared hit=3003
-> Nested Loop Left Join (cost=6.58..6736.82 rows=6006 width=569) (actual time=0.028..5.076 rows=1001.00 loops=1)
Buffers: shared hit=3003
-> CTE Scan on customer_records cr_1 (cost=0.00..20.02 rows=1001 width=32) (actual time=0.001..0.184 rows=1001.00 loops=1)
Storage: Memory Maximum Storage: 270kB
-> Subquery Scan on ce (cost=6.58..6.65 rows=6 width=537) (actual time=0.005..0.005 rows=0.00 loops=1001)
Buffers: shared hit=3003
-> Limit (cost=6.58..6.59 rows=6 width=445) (actual time=0.004..0.004 rows=0.00 loops=1001)
Buffers: shared hit=3003
-> Sort (cost=6.58..6.59 rows=6 width=445) (actual time=0.004..0.004 rows=0.00 loops=1001)
Sort Key: ce_1.id DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=3003
-> Index Scan using idx_customer_entitlements_loose_customer_expires on customer_entitlements ce_1 (cost=0.43..6.50 rows=6 width=445) (actual time=0.003..0.003 rows=0.00 loops=1001)
Index Cond: (internal_customer_id = cr_1.internal_id)
Filter: ((expires_at IS NULL) OR (expires_at > (EXTRACT(epoch FROM now()) * '1000'::numeric)))
Index Searches: 1001
Buffers: shared hit=3003
SubPlan 7
-> Nested Loop (cost=0.84..4.29 rows=1 width=32) (never executed)
-> Index Scan using entitlements_pkey on entitlements e_1 (cost=0.43..2.15 rows=1 width=359) (never executed)
Index Cond: (id = ce.entitlement_id)
Index Searches: 0
-> Index Scan using features_pkey on features f_1 (cost=0.41..2.13 rows=1 width=349) (never executed)
Index Cond: (internal_id = (e_1.internal_feature_id)::text)
Index Searches: 0
SubPlan 8
-> Aggregate (cost=1.14..1.15 rows=1 width=32) (never executed)
-> Seq Scan on replaceables r_1 (cost=0.00..1.12 rows=2 width=154) (never executed)
Filter: (cus_ent_id = ce.id)
SubPlan 9
-> Aggregate (cost=3.24..3.25 rows=1 width=32) (never executed)
-> Index Scan using idx_rollovers_cus_ent_id on rollovers ro_1 (cost=0.56..3.17 rows=4 width=125) (never executed)
Index Cond: (cus_ent_id = ce.id)
Index Searches: 0
-> Hash (cost=462.02..462.02 rows=1001 width=577) (actual time=390.778..390.788 rows=1001.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 5979kB
Buffers: shared hit=53509
-> Hash Left Join (cost=436.72..462.02 rows=1001 width=577) (actual time=387.992..389.081 rows=1001.00 loops=1)
Hash Cond: (cr.internal_id = cs.internal_customer_id)
Buffers: shared hit=53509
-> Hash Left Join (cost=183.56..206.22 rows=1001 width=545) (actual time=385.907..386.673 rows=1001.00 loops=1)
Hash Cond: (cr.internal_id = cpa.internal_customer_id)
Buffers: shared hit=53465
-> CTE Scan on customer_records cr (cost=0.00..20.02 rows=1001 width=513) (actual time=120.195..120.317 rows=1001.00 loops=1)
Storage: Memory Maximum Storage: 270kB
Buffers: shared hit=730
-> Hash (cost=178.66..178.66 rows=392 width=64) (actual time=265.704..265.706 rows=998.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 5778kB
Buffers: shared hit=52735
-> Subquery Scan on cpa (cost=149.82..178.66 rows=392 width=64) (actual time=253.574..264.956 rows=998.00 loops=1)
Buffers: shared hit=52735
-> GroupAggregate (cost=149.82..174.74 rows=392 width=64) (actual time=253.572..264.783 rows=998.00 loops=1)
Group Key: cpwp.internal_customer_id
Buffers: shared hit=52735
-> Sort (cost=149.82..154.83 rows=2002 width=152) (actual time=253.550..253.776 rows=1045.00 loops=1)
Sort Key: cpwp.internal_customer_id COLLATE "C", cpwp.created_at DESC
Sort Method: quicksort Memory: 5253kB
Buffers: shared hit=52735
-> CTE Scan on customer_products_with_prices cpwp (cost=0.00..40.04 rows=2002 width=152) (actual time=0.506..252.288 rows=1045.00 loops=1)
Storage: Memory Maximum Storage: 5161kB
Buffers: shared hit=52735
-> Hash (cost=250.66..250.66 rows=200 width=64) (actual time=2.081..2.087 rows=11.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 13kB
Buffers: shared hit=44
-> Subquery Scan on cs (cost=246.16..250.66 rows=200 width=64) (actual time=2.071..2.080 rows=11.00 loops=1)
Buffers: shared hit=44
-> HashAggregate (cost=246.16..248.66 rows=200 width=64) (actual time=2.070..2.077 rows=11.00 loops=1)
Group Key: s.internal_customer_id
Batches: 1 Memory Usage: 40kB
Buffers: shared hit=44
-> Subquery Scan on s (cost=190.28..230.92 rows=2032 width=298) (actual time=1.993..2.021 rows=11.00 loops=1)
Buffers: shared hit=44
-> HashAggregate (cost=190.28..210.60 rows=2032 width=213) (actual time=1.987..2.008 rows=11.00 loops=1)
Group Key: cpwp_1.internal_customer_id, s_1.id, s_1.stripe_id, s_1.stripe_schedule_id, s_1.created_at, s_1.usage_features, s_1.metadata, s_1.org_id, s_1.env, s_1.current_period_start, s_1.current_period_end
Batches: 1 Memory Usage: 105kB
Buffers: shared hit=44
-> Nested Loop (cost=0.43..134.40 rows=2032 width=213) (actual time=0.308..1.964 rows=11.00 loops=1)
Buffers: shared hit=44
-> Nested Loop (cost=0.00..80.08 rows=2002 width=64) (actual time=0.277..1.821 rows=11.00 loops=1)
-> CTE Scan on customer_products_with_prices cpwp_1 (cost=0.00..40.04 rows=2002 width=64) (actual time=0.001..0.812 rows=1045.00 loops=1)
Storage: Memory Maximum Storage: 5161kB
-> Function Scan on unnest cpwp_sub (cost=0.00..0.01 rows=1 width=32) (actual time=0.001..0.001 rows=0.01 loops=1045)
-> Memoize (cost=0.43..2.15 rows=1 width=181) (actual time=0.012..0.012 rows=1.00 loops=11)
Cache Key: cpwp_sub.stripe_id
Cache Mode: logical
Hits: 0 Misses: 11 Evictions: 0 Overflows: 0 Memory Usage: 3kB
Buffers: shared hit=44
-> Index Scan using idx_subscriptions_stripe_id on subscriptions s_1 (cost=0.42..2.14 rows=1 width=181) (actual time=0.011..0.011 rows=1.00 loops=11)
Index Cond: (stripe_id = cpwp_sub.stripe_id)
Index Searches: 11
Buffers: shared hit=44
Planning:
Buffers: shared hit=60
Planning Time: 3.327 ms
Execution Time: 614.472 ms
```
### 11 cursor / revcat=true / deep / limit 1000
```
Sort (cost=112019.84..112022.34 rows=1001 width=609) (actual time=2205.369..2320.704 rows=693.00 loops=1)
Sort Key: cr.created_at DESC, cr.id DESC
Sort Method: quicksort Memory: 4193kB
Buffers: shared hit=2452594
CTE customer_records
-> Limit (cost=1000.89..39766.92 rows=1001 width=573) (actual time=56.375..2053.986 rows=693.00 loops=1)
Buffers: shared hit=2414231
-> Gather Merge (cost=1000.89..360622.59 rows=9286 width=573) (actual time=56.374..2053.876 rows=693.00 loops=1)
Workers Planned: 4
Workers Launched: 1
Buffers: shared hit=2414231
-> Nested Loop Semi Join (cost=0.83..358516.48 rows=2322 width=573) (actual time=1.034..1090.229 rows=346.50 loops=2)
Buffers: shared hit=2414220
-> Parallel Index Scan using idx_customers_cursor on customers c (cost=0.56..248348.63 rows=222980 width=573) (actual time=0.046..513.658 rows=414514.50 loops=2)
Index Cond: ((org_id = 'r7pwHiekGsqt32qGqcVku6thWFh5aHh8'::text) AND (env = 'live'::text) AND (ROW(created_at, id) < ROW('1777225699690'::numeric, 'PL6dMZw5mewXCvW74aPOOaZ28TVh5K8T'::text)))
Index Searches: 1
Buffers: shared hit=755243
-> Index Only Scan using idx_customer_products_revenuecat_processor on customer_products cp_processor (cost=0.28..0.49 rows=1 width=32) (actual time=0.001..0.001 rows=0.00 loops=829029)
Index Cond: (internal_customer_id = c.internal_id)
Heap Fetches: 549
Index Searches: 829029
Buffers: shared hit=1658977
CTE customer_products_with_prices
-> Nested Loop Left Join (cost=1016.52..12367.59 rows=2002 width=678) (actual time=0.715..2127.082 rows=717.00 loops=1)
Buffers: shared hit=47511
-> Nested Loop Left Join (cost=1016.22..12301.53 rows=2002 width=864) (actual time=0.703..2123.189 rows=717.00 loops=1)
Buffers: shared hit=47511
-> Nested Loop Left Join (cost=16.29..9252.69 rows=2002 width=832) (actual time=0.404..1952.023 rows=717.00 loops=1)
Buffers: shared hit=24135
-> Nested Loop (cost=9.48..9182.21 rows=2002 width=800) (actual time=0.319..1903.139 rows=717.00 loops=1)
Buffers: shared hit=18469
-> Nested Loop (cost=9.05..9125.77 rows=2002 width=550) (actual time=0.057..1901.977 rows=717.00 loops=1)
Buffers: shared hit=18413
-> CTE Scan on customer_records cr_2 (cost=0.00..20.02 rows=1001 width=32) (actual time=0.000..1882.687 rows=693.00 loops=1)
Storage: Memory Maximum Storage: 192kB
Buffers: shared hit=11255
-> Limit (cost=9.05..9.06 rows=2 width=551) (actual time=0.027..0.027 rows=1.03 loops=693)
Buffers: shared hit=7158
-> Sort (cost=9.05..9.06 rows=2 width=551) (actual time=0.026..0.026 rows=1.03 loops=693)
Sort Key: ((SubPlan 2)), cp.created_at DESC
Sort Method: quicksort Memory: 26kB
Buffers: shared hit=7158
-> Index Scan using customer_products_internal_customer_id_idx on customer_products cp (cost=0.56..9.04 rows=2 width=551) (actual time=0.021..0.023 rows=1.03 loops=693)
Index Cond: (internal_customer_id = cr_2.internal_id)
Filter: (status = ANY ('{active,past_due,scheduled}'::text[]))
Rows Removed by Filter: 1
Index Searches: 693
Buffers: shared hit=7158
SubPlan 2
-> Index Scan using plans_pkey on products p (cost=0.41..2.13 rows=1 width=1) (actual time=0.009..0.009 rows=1.00 loops=717)
Index Cond: (internal_id = cp.internal_product_id)
Index Searches: 717
Buffers: shared hit=2868
-> Memoize (cost=0.42..2.14 rows=1 width=282) (actual time=0.001..0.001 rows=1.00 loops=717)
Cache Key: cp.internal_product_id
Cache Mode: logical
Hits: 703 Misses: 14 Evictions: 0 Overflows: 0 Memory Usage: 6kB
Buffers: shared hit=56
-> Index Scan using plans_pkey on products prod (cost=0.41..2.13 rows=1 width=282) (actual time=0.026..0.026 rows=1.00 loops=14)
Index Cond: (internal_id = cp.internal_product_id)
Index Searches: 14
Buffers: shared hit=56
-> Memoize (cost=6.81..6.82 rows=1 width=32) (actual time=0.067..0.068 rows=1.00 loops=717)
Cache Key: cp.id
Cache Mode: binary
Hits: 0 Misses: 717 Evictions: 0 Overflows: 0 Memory Usage: 620kB
Buffers: shared hit=5666
-> Aggregate (cost=6.80..6.81 rows=1 width=32) (actual time=0.066..0.066 rows=1.00 loops=717)
Buffers: shared hit=5666
-> Nested Loop Left Join (cost=0.84..6.78 rows=2 width=871) (actual time=0.032..0.032 rows=0.98 loops=717)
Buffers: shared hit=5666
-> Index Scan using idx_customer_prices_product_id on customer_prices cpr (cost=0.42..2.51 rows=2 width=269) (actual time=0.027..0.028 rows=0.98 loops=717)
Index Cond: (customer_product_id = cp.id)
Index Searches: 717
Buffers: shared hit=2854
-> Index Scan using prices_pkey on prices p_1 (cost=0.42..2.13 rows=1 width=664) (actual time=0.004..0.004 rows=1.00 loops=703)
Index Cond: (id = cpr.price_id)
Index Searches: 703
Buffers: shared hit=2812
-> Memoize (cost=999.93..999.94 rows=1 width=32) (actual time=0.238..0.238 rows=1.00 loops=717)
Cache Key: cp.id
Cache Mode: binary
Hits: 0 Misses: 717 Evictions: 0 Overflows: 0 Memory Usage: 2664kB
Buffers: shared hit=23376
-> Aggregate (cost=999.92..999.93 rows=1 width=32) (actual time=0.235..0.235 rows=1.00 loops=717)
Buffers: shared hit=23376
-> Index Scan using idx_customer_entitlements_product_id on customer_entitlements ce_2 (cost=0.56..78.98 rows=106 width=537) (actual time=0.024..0.035 rows=2.04 loops=717)
Index Cond: (customer_product_id = cp.id)
Index Searches: 717
Buffers: shared hit=4318
SubPlan 3
-> Nested Loop (cost=0.84..4.29 rows=1 width=32) (actual time=0.022..0.022 rows=1.00 loops=1466)
Buffers: shared hit=11728
-> Index Scan using entitlements_pkey on entitlements e (cost=0.43..2.15 rows=1 width=359) (actual time=0.004..0.004 rows=1.00 loops=1466)
Index Cond: (id = ce_2.entitlement_id)
Index Searches: 1466
Buffers: shared hit=5864
-> Index Scan using features_pkey on features f (cost=0.41..2.13 rows=1 width=349) (actual time=0.006..0.006 rows=1.00 loops=1466)
Index Cond: (internal_id = (e.internal_feature_id)::text)
Index Searches: 1466
Buffers: shared hit=5864
SubPlan 4
-> Aggregate (cost=1.14..1.15 rows=1 width=32) (actual time=0.006..0.006 rows=1.00 loops=1466)
Buffers: shared hit=1466
-> Seq Scan on replaceables r (cost=0.00..1.12 rows=2 width=154) (actual time=0.005..0.005 rows=0.00 loops=1466)
Filter: (cus_ent_id = ce_2.id)
Rows Removed by Filter: 21
Buffers: shared hit=1466
SubPlan 5
-> Aggregate (cost=3.24..3.25 rows=1 width=32) (actual time=0.014..0.014 rows=1.00 loops=1466)
Buffers: shared hit=5864
-> Index Scan using idx_rollovers_cus_ent_id on rollovers ro (cost=0.56..3.17 rows=4 width=125) (actual time=0.011..0.011 rows=0.00 loops=1466)
Index Cond: (cus_ent_id = ce_2.id)
Index Searches: 1466
Buffers: shared hit=5864
-> Memoize (cost=0.30..2.01 rows=1 width=139) (actual time=0.000..0.000 rows=0.00 loops=717)
Cache Key: cp.free_trial_id
Cache Mode: logical
Hits: 716 Misses: 1 Evictions: 0 Overflows: 0 Memory Usage: 1kB
-> Index Scan using free_trials_pkey on free_trials ft (cost=0.29..2.00 rows=1 width=139) (actual time=0.002..0.002 rows=0.00 loops=1)
Index Cond: (id = cp.free_trial_id)
Index Searches: 0
-> Hash Right Join (cost=7588.29..59835.44 rows=1001 width=609) (actual time=2202.997..2204.248 rows=693.00 loops=1)
Hash Cond: (cr_1.internal_id = cr.internal_id)
Buffers: shared hit=49650
-> GroupAggregate (cost=7113.76..59337.13 rows=1001 width=64) (actual time=3.305..4.089 rows=693.00 loops=1)
Group Key: cr_1.internal_id
Buffers: shared hit=2079
-> Sort (cost=7113.76..7128.78 rows=6006 width=569) (actual time=3.295..3.370 rows=693.00 loops=1)
Sort Key: cr_1.internal_id COLLATE "C"
Sort Method: quicksort Memory: 57kB
Buffers: shared hit=2079
-> Nested Loop Left Join (cost=6.58..6736.82 rows=6006 width=569) (actual time=0.021..2.976 rows=693.00 loops=1)
Buffers: shared hit=2079
-> CTE Scan on customer_records cr_1 (cost=0.00..20.02 rows=1001 width=32) (actual time=0.000..0.128 rows=693.00 loops=1)
Storage: Memory Maximum Storage: 192kB
-> Subquery Scan on ce (cost=6.58..6.65 rows=6 width=537) (actual time=0.004..0.004 rows=0.00 loops=693)
Buffers: shared hit=2079
-> Limit (cost=6.58..6.59 rows=6 width=445) (actual time=0.004..0.004 rows=0.00 loops=693)
Buffers: shared hit=2079
-> Sort (cost=6.58..6.59 rows=6 width=445) (actual time=0.003..0.003 rows=0.00 loops=693)
Sort Key: ce_1.id DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=2079
-> Index Scan using idx_customer_entitlements_loose_customer_expires on customer_entitlements ce_1 (cost=0.43..6.50 rows=6 width=445) (actual time=0.003..0.003 rows=0.00 loops=693)
Index Cond: (internal_customer_id = cr_1.internal_id)
Filter: ((expires_at IS NULL) OR (expires_at > (EXTRACT(epoch FROM now()) * '1000'::numeric)))
Index Searches: 693
Buffers: shared hit=2079
SubPlan 7
-> Nested Loop (cost=0.84..4.29 rows=1 width=32) (never executed)
-> Index Scan using entitlements_pkey on entitlements e_1 (cost=0.43..2.15 rows=1 width=359) (never executed)
Index Cond: (id = ce.entitlement_id)
Index Searches: 0
-> Index Scan using features_pkey on features f_1 (cost=0.41..2.13 rows=1 width=349) (never executed)
Index Cond: (internal_id = (e_1.internal_feature_id)::text)
Index Searches: 0
SubPlan 8
-> Aggregate (cost=1.14..1.15 rows=1 width=32) (never executed)
-> Seq Scan on replaceables r_1 (cost=0.00..1.12 rows=2 width=154) (never executed)
Filter: (cus_ent_id = ce.id)
SubPlan 9
-> Aggregate (cost=3.24..3.25 rows=1 width=32) (never executed)
-> Index Scan using idx_rollovers_cus_ent_id on rollovers ro_1 (cost=0.56..3.17 rows=4 width=125) (never executed)
Index Cond: (cus_ent_id = ce.id)
Index Searches: 0
-> Hash (cost=462.02..462.02 rows=1001 width=577) (actual time=2199.659..2199.670 rows=693.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 4181kB
Buffers: shared hit=47571
-> Hash Left Join (cost=436.72..462.02 rows=1001 width=577) (actual time=2198.002..2198.662 rows=693.00 loops=1)
Hash Cond: (cr.internal_id = cs.internal_customer_id)
Buffers: shared hit=47571
-> Hash Left Join (cost=183.56..206.22 rows=1001 width=545) (actual time=2196.834..2197.301 rows=693.00 loops=1)
Hash Cond: (cr.internal_id = cpa.internal_customer_id)
Buffers: shared hit=47543
-> CTE Scan on customer_records cr (cost=0.00..20.02 rows=1001 width=513) (actual time=56.380..56.454 rows=693.00 loops=1)
Storage: Memory Maximum Storage: 192kB
Buffers: shared hit=32
-> Hash (cost=178.66..178.66 rows=392 width=64) (actual time=2140.446..2140.449 rows=668.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 4040kB
Buffers: shared hit=47511
-> Subquery Scan on cpa (cost=149.82..178.66 rows=392 width=64) (actual time=2134.030..2140.018 rows=668.00 loops=1)
Buffers: shared hit=47511
-> GroupAggregate (cost=149.82..174.74 rows=392 width=64) (actual time=2134.028..2139.898 rows=668.00 loops=1)
Group Key: cpwp.internal_customer_id
Buffers: shared hit=47511
-> Sort (cost=149.82..154.83 rows=2002 width=152) (actual time=2133.990..2134.133 rows=717.00 loops=1)
Sort Key: cpwp.internal_customer_id COLLATE "C", cpwp.created_at DESC
Sort Method: quicksort Memory: 3669kB
Buffers: shared hit=47511
-> CTE Scan on customer_products_with_prices cpwp (cost=0.00..40.04 rows=2002 width=152) (actual time=0.729..2132.964 rows=717.00 loops=1)
Storage: Memory Maximum Storage: 3621kB
Buffers: shared hit=47511
-> Hash (cost=250.66..250.66 rows=200 width=64) (actual time=1.163..1.170 rows=7.00 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
Buffers: shared hit=28
-> Subquery Scan on cs (cost=246.16..250.66 rows=200 width=64) (actual time=1.155..1.165 rows=7.00 loops=1)
Buffers: shared hit=28
-> HashAggregate (cost=246.16..248.66 rows=200 width=64) (actual time=1.154..1.162 rows=7.00 loops=1)
Group Key: s.internal_customer_id
Batches: 1 Memory Usage: 40kB
Buffers: shared hit=28
-> Subquery Scan on s (cost=190.28..230.92 rows=2032 width=298) (actual time=1.092..1.120 rows=7.00 loops=1)
Buffers: shared hit=28
-> HashAggregate (cost=190.28..210.60 rows=2032 width=213) (actual time=1.085..1.108 rows=7.00 loops=1)
Group Key: cpwp_1.internal_customer_id, s_1.id, s_1.stripe_id, s_1.stripe_schedule_id, s_1.created_at, s_1.usage_features, s_1.metadata, s_1.org_id, s_1.env, s_1.current_period_start, s_1.current_period_end
Batches: 1 Memory Usage: 105kB
Buffers: shared hit=28
-> Nested Loop (cost=0.43..134.40 rows=2032 width=213) (actual time=0.170..1.066 rows=7.00 loops=1)
Buffers: shared hit=28
-> Nested Loop (cost=0.00..80.08 rows=2002 width=64) (actual time=0.144..0.955 rows=7.00 loops=1)
-> CTE Scan on customer_products_with_prices cpwp_1 (cost=0.00..40.04 rows=2002 width=64) (actual time=0.001..0.404 rows=717.00 loops=1)
Storage: Memory Maximum Storage: 3621kB
-> Function Scan on unnest cpwp_sub (cost=0.00..0.01 rows=1 width=32) (actual time=0.000..0.000 rows=0.01 loops=717)
-> Memoize (cost=0.43..2.15 rows=1 width=181) (actual time=0.014..0.015 rows=1.00 loops=7)
Cache Key: cpwp_sub.stripe_id
Cache Mode: logical
Hits: 0 Misses: 7 Evictions: 0 Overflows: 0 Memory Usage: 2kB
Buffers: shared hit=28
-> Index Scan using idx_subscriptions_stripe_id on subscriptions s_1 (cost=0.42..2.14 rows=1 width=181) (actual time=0.012..0.013 rows=1.00 loops=7)
Index Cond: (stripe_id = cpwp_sub.stripe_id)
Index Searches: 7
Buffers: shared hit=28
Planning:
Buffers: shared hit=60
Planning Time: 2.677 ms
Execution Time: 2321.477 ms
```