From a84ed917aaa8d921a1e4ea1593b659d05752cf54 Mon Sep 17 00:00:00 2001 From: grootbro Date: Fri, 11 Sep 2026 12:42:07 +0700 Subject: [PATCH 1/2] perf(charts): restrict profile joins to the event window --- packages/db/src/services/chart-sql.test.ts | 38 +++++++++++++++++++++- packages/db/src/services/chart.service.ts | 26 +++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/db/src/services/chart-sql.test.ts b/packages/db/src/services/chart-sql.test.ts index f9c997a7e..062d6f5e8 100644 --- a/packages/db/src/services/chart-sql.test.ts +++ b/packages/db/src/services/chart-sql.test.ts @@ -13,7 +13,7 @@ */ import type { IChartBreakdown, IChartEvent } from '@openpanel/validation'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; -import { ch } from '../clickhouse/client'; +import { ch, formatClickhouseDate } from '../clickhouse/client'; import { getAggregateChartSql as _getAggregateChartSql, getChartSql as _getChartSql, @@ -713,3 +713,39 @@ describe('chart.service / single-pass total_count', () => { await explain(sql); }); }); + +describe('chart.service / profile event window', () => { + for (const [name, build] of [ + ['timeseries', getChartSql], + ['aggregate', getAggregateChartSql], + ] as const) { + it(`${name} restricts profile reads to the same project, event and dates`, async () => { + const sql = await build({ + event: event(), + breakdowns: [breakdown('profile.properties.plan')], + interval: 'day', + startDate: '2026-09-01 00:00:00', + endDate: '2026-09-02 00:00:00', + projectId: PROJECT_ID, + timezone: 'UTC', + }); + expect(sql).toContain(`id IN (SELECT profile_id FROM events WHERE project_id = '${PROJECT_ID}' AND created_at >= toDateTime('${formatClickhouseDate('2026-09-01 00:00:00')}') AND created_at <= toDateTime('${formatClickhouseDate('2026-09-02 00:00:00')}') AND name = 'screen_view')`); + expect(sql).toContain('FROM profiles FINAL'); + if (chReachable) await explain(sql); + }); + + it(`${name} keeps all event names for wildcard series`, async () => { + const sql = await build({ + event: event({ name: '*' }), + breakdowns: [breakdown('profile.properties.plan')], + interval: 'day', + startDate: '', + endDate: '', + projectId: PROJECT_ID, + timezone: 'UTC', + }); + expect(sql).toContain(`id IN (SELECT profile_id FROM events WHERE project_id = '${PROJECT_ID}')`); + expect(sql).not.toContain("name = '*'"); + }); + } +}); diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index 3e121c11e..2427f4aa0 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -462,6 +462,26 @@ export function rewriteProfilePropertyRefs(sql: string, keys: string[]): string return out; } +function profileEventWindow({ + projectId, + startDate, + endDate, + event, +}: Pick) { + const conditions = [`project_id = ${sqlstring.escape(projectId)}`]; + if (startDate) { + conditions.push(`created_at >= toDateTime('${formatClickhouseDate(startDate)}')`); + } + if (endDate) { + conditions.push(`created_at <= toDateTime('${formatClickhouseDate(endDate)}')`); + } + if (event.name !== '*') { + conditions.push(`name = ${sqlstring.escape(event.name)}`); + } + // Filter by the join key before FINAL without excluding profiles updated outside the event window. + return `id IN (SELECT profile_id FROM ${TABLE_NAMES.events} WHERE ${conditions.join(' AND ')})`; +} + export async function getChartSql({ event, breakdowns: initialBreakdowns, @@ -691,7 +711,8 @@ export async function getChartSql({ 'profile', `SELECT ${selectFields.join(', ')} FROM ${TABLE_NAMES.profiles} FINAL - WHERE project_id = ${sqlstring.escape(projectId)}` + WHERE project_id = ${sqlstring.escape(projectId)} + AND ${profileEventWindow({ projectId, startDate, endDate, event })}` ); // Use the CTE reference in the main query @@ -1073,7 +1094,8 @@ export async function getAggregateChartSql({ 'profile', `SELECT ${selectFields.join(', ')} FROM ${TABLE_NAMES.profiles} FINAL - WHERE project_id = ${sqlstring.escape(projectId)}` + WHERE project_id = ${sqlstring.escape(projectId)} + AND ${profileEventWindow({ projectId, startDate, endDate, event })}` ); sb.joins.profiles = profilesJoinRef; From 505f9d6a249c2ead00ae7253d129d4a16475d25e Mon Sep 17 00:00:00 2001 From: grootbro Date: Sat, 12 Sep 2026 15:25:06 +0700 Subject: [PATCH 2/2] refactor(charts): build profile window with clix --- packages/db/src/services/chart-sql.test.ts | 2 +- packages/db/src/services/chart.service.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/db/src/services/chart-sql.test.ts b/packages/db/src/services/chart-sql.test.ts index 062d6f5e8..95542d51e 100644 --- a/packages/db/src/services/chart-sql.test.ts +++ b/packages/db/src/services/chart-sql.test.ts @@ -729,7 +729,7 @@ describe('chart.service / profile event window', () => { projectId: PROJECT_ID, timezone: 'UTC', }); - expect(sql).toContain(`id IN (SELECT profile_id FROM events WHERE project_id = '${PROJECT_ID}' AND created_at >= toDateTime('${formatClickhouseDate('2026-09-01 00:00:00')}') AND created_at <= toDateTime('${formatClickhouseDate('2026-09-02 00:00:00')}') AND name = 'screen_view')`); + expect(sql).toContain(`id IN (SELECT profile_id FROM events WHERE project_id = '${PROJECT_ID}' AND created_at >= toDateTime('${formatClickhouseDate('2026-09-01 00:00:00')}') AND created_at <= toDateTime('${formatClickhouseDate('2026-09-02 00:00:00')}') AND name = ('screen_view'))`); expect(sql).toContain('FROM profiles FINAL'); if (chReachable) await explain(sql); }); diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index 2427f4aa0..b2ccd4020 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -9,7 +9,8 @@ import { type IReportInput, } from '@openpanel/validation'; import sqlstring from 'sqlstring'; -import { formatClickhouseDate, TABLE_NAMES } from '../clickhouse/client'; +import { ch, formatClickhouseDate, TABLE_NAMES } from '../clickhouse/client'; +import { clix } from '../clickhouse/query-builder'; import { db } from '../prisma-client'; import { createSqlBuilder } from '../sql-builder'; import { buildTypedClause, hasTypedCast, isTypedOperator } from './filter-cast'; @@ -468,18 +469,22 @@ function profileEventWindow({ endDate, event, }: Pick) { - const conditions = [`project_id = ${sqlstring.escape(projectId)}`]; + const query = clix(ch) + .select(['profile_id']) + .from(TABLE_NAMES.events) + .where('project_id', '=', projectId); if (startDate) { - conditions.push(`created_at >= toDateTime('${formatClickhouseDate(startDate)}')`); + query.where('created_at', '>=', clix.datetime(startDate, 'toDateTime')); } if (endDate) { - conditions.push(`created_at <= toDateTime('${formatClickhouseDate(endDate)}')`); + query.where('created_at', '<=', clix.datetime(endDate, 'toDateTime')); } if (event.name !== '*') { - conditions.push(`name = ${sqlstring.escape(event.name)}`); + // Event names must remain string literals, even when they look like dates. + query.where('name', '=', clix.exp(sqlstring.escape(event.name))); } // Filter by the join key before FINAL without excluding profiles updated outside the event window. - return `id IN (SELECT profile_id FROM ${TABLE_NAMES.events} WHERE ${conditions.join(' AND ')})`; + return `id IN (${query.toSQL()})`; } export async function getChartSql({