From 378542bce06306c6b856ea4f28351b87236b0289 Mon Sep 17 00:00:00 2001 From: Alexander Spicer Date: Wed, 8 Jan 2025 01:28:47 -0800 Subject: [PATCH 1/9] stickiness --- frontend/src/queries/schema.json | 7 +- frontend/src/queries/schema.ts | 5 ++ posthog/hogql/functions/mapping.py | 1 + .../insights/stickiness_query_runner.py | 19 +++- .../test/test_stickiness_query_runner.py | 86 +++++++++++++++++++ posthog/schema.py | 3 + 6 files changed, 116 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 028544987411e..79161991021c2 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -12277,6 +12277,11 @@ "default": "day", "description": "Granularity of the response. Can be one of `hour`, `day`, `week` or `month`" }, + "intervalCount": { + "default": 1, + "description": "How many intervals comprise a period. Only used for cohorts, otherwise default 1.", + "type": "integer" + }, "kind": { "const": "StickinessQuery", "type": "string" @@ -12319,7 +12324,7 @@ "description": "Properties specific to the stickiness insight" } }, - "required": ["kind", "series"], + "required": ["intervalCount", "kind", "series"], "type": "object" }, "StickinessQueryResponse": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 58e779914b070..0fa0ec7e83550 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1561,6 +1561,11 @@ export interface StickinessQuery * @default day */ interval?: IntervalType + /** + * How many intervals comprise a period. Only used for cohorts, otherwise default 1. + * @default 1 + */ + intervalCount: integer /** Events and actions to include */ series: AnyEntityNode[] /** Properties specific to the stickiness insight */ diff --git a/posthog/hogql/functions/mapping.py b/posthog/hogql/functions/mapping.py index 43c9bb4e7c150..ebdddad9b1e2a 100644 --- a/posthog/hogql/functions/mapping.py +++ b/posthog/hogql/functions/mapping.py @@ -453,6 +453,7 @@ def compare_types(arg_types: list[ConstantType], sig_arg_types: tuple[ConstantTy "toSecond": HogQLFunctionMeta("toSecond", 1, 1), "toUnixTimestamp": HogQLFunctionMeta("toUnixTimestamp", 1, 2), "toUnixTimestamp64Milli": HogQLFunctionMeta("toUnixTimestamp64Milli", 1, 1), + "toStartOfInterval": HogQLFunctionMeta("toStartOfInterval", 2, 2), "toStartOfYear": HogQLFunctionMeta("toStartOfYear", 1, 1), "toStartOfISOYear": HogQLFunctionMeta("toStartOfISOYear", 1, 1), "toStartOfQuarter": HogQLFunctionMeta("toStartOfQuarter", 1, 1), diff --git a/posthog/hogql_queries/insights/stickiness_query_runner.py b/posthog/hogql_queries/insights/stickiness_query_runner.py index 1917c32c7cc83..2b6ccff029c27 100644 --- a/posthog/hogql_queries/insights/stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/stickiness_query_runner.py @@ -95,6 +95,18 @@ def _having_clause(self) -> ast.Expr: value = ast.Constant(value=self.query.stickinessFilter.stickinessCriteria.value) return parse_expr(f"""count() {get_count_operator(operator)} {{value}}""", {"value": value}) + def date_to_start_of_interval_hogql(self, date: ast.Expr) -> ast.Call: + return ast.Call( + name="toStartOfInterval", + args=[ + date, + ast.Call( + name=f"toInterval{self.query_date_range.interval_name.capitalize()}", + args=[ast.Constant(value=self.query.intervalCount or 1)], + ), + ], + ) + def _events_query(self, series_with_extra: SeriesWithExtras) -> ast.SelectQuery: inner_query = parse_select( """ @@ -109,9 +121,7 @@ def _events_query(self, series_with_extra: SeriesWithExtras) -> ast.SelectQuery: """, { "aggregation": self._aggregation_expressions(series_with_extra.series), - "start_of_interval": self.query_date_range.date_to_start_of_interval_hogql( - ast.Field(chain=["e", "timestamp"]) - ), + "start_of_interval": self.date_to_start_of_interval_hogql(ast.Field(chain=["e", "timestamp"])), "sample": self._sample_value(), "where_clause": self.where_clause(series_with_extra), "having_clause": self._having_clause(), @@ -169,7 +179,7 @@ def to_queries(self) -> list[ast.SelectQuery]: SELECT sum(num_actors) as num_actors, num_intervals FROM ( SELECT 0 as num_actors, (number + 1) as num_intervals - FROM numbers(dateDiff({interval}, {date_from_start_of_interval}, {date_to_start_of_interval} + {interval_addition})) + FROM numbers(ceil(dateDiff({interval}, {date_from_start_of_interval}, {date_to_start_of_interval} + {interval_addition}) / {intervalCount})) UNION ALL {events_query} ) @@ -181,6 +191,7 @@ def to_queries(self) -> list[ast.SelectQuery]: **date_range.to_placeholders(), "interval_addition": interval_addition, "events_query": self._events_query(series), + "intervalCount": ast.Constant(value=self.query.intervalCount or 1), }, ) diff --git a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py index bc4754d04e9b3..90eb1df4a0147 100644 --- a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py @@ -202,6 +202,7 @@ def _run_query( date_from: Optional[str] = None, date_to: Optional[str] = None, interval: Optional[IntervalType] = None, + intervalCount: Optional[int] = None, properties: Optional[StickinessProperties] = None, filters: Optional[StickinessFilter] = None, filter_test_accounts: Optional[bool] = False, @@ -217,6 +218,7 @@ def _run_query( series=query_series, dateRange=DateRange(date_from=query_date_from, date_to=query_date_to), interval=query_interval, + intervalCount=intervalCount, properties=properties, stickinessFilter=filters, compareFilter=compare_filters, @@ -347,6 +349,90 @@ def test_interval_day(self): 0, ] + def test_interval_2_day(self): + self._create_test_events() + + response = self._run_query(interval=IntervalType.DAY, intervalCount=2) + + result = response.results[0] + + assert result["label"] == "$pageview" + assert result["labels"] == [ + "1 day", + "2 days", + "3 days", + "4 days", + "5 days", + ] + assert result["days"] == [1, 2, 3, 4, 5] + assert result["data"] == [ + 0, + 0, + 0, + 0, + 2, + ] + + def test_interval_2_day_filtering(self): + self._create_events( + [ + SeriesTestData( + distinct_id="p1", + events=[ + Series( + event="$pageview", + timestamps=[ + # Day 1 + "2020-01-11T12:00:00Z", + "2020-01-12T12:00:00Z", + # Day 2 + "2020-01-13T12:00:00Z", + "2020-01-14T12:00:00Z", + # Day 3 + "2020-01-15T12:00:00Z", + "2020-01-16T12:00:00Z", + # Day 4 + "2020-01-17T12:00:00Z", + "2020-01-18T12:00:00Z", + # Day 5 + "2020-01-19T12:00:00Z", + ], + ), + ], + properties={"$browser": "Chrome", "prop": 10, "bool_field": True, "$group_0": "org:1"}, + ), + SeriesTestData( + distinct_id="p2", + events=[ + Series( + event="$pageview", + timestamps=[ + "2020-01-11T12:00:00Z", + "2020-01-13T12:00:00Z", + "2020-01-15T12:00:00Z", + ], + ), + ], + properties={"$browser": "Firefox", "prop": 10, "bool_field": False, "$group_0": "org:1"}, + ), + ] + ) + + response = self._run_query(interval=IntervalType.DAY, intervalCount=2) + + result = response.results[0] + + assert result["label"] == "$pageview" + assert result["labels"] == [ + "1 day", + "2 days", + "3 days", + "4 days", + "5 days", + ] + assert result["days"] == [1, 2, 3, 4, 5] + assert result["data"] == [0, 0, 1, 0, 1] + def test_interval_week(self): self._create_test_events() diff --git a/posthog/schema.py b/posthog/schema.py index dda86c2fe74bf..26142a9a51454 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -5818,6 +5818,9 @@ class StickinessQuery(BaseModel): default=IntervalType.DAY, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`", ) + intervalCount: Optional[int] = Field( + default=1, description="How many intervals comprise a period. Only used for cohorts, otherwise default 1." + ) kind: Literal["StickinessQuery"] = "StickinessQuery" modifiers: Optional[HogQLQueryModifiers] = Field( default=None, description="Modifiers used when performing the query" From 75cd8a00237c5d6cf4fbbec941af692548eb660c Mon Sep 17 00:00:00 2001 From: Alexander Spicer Date: Wed, 8 Jan 2025 02:20:09 -0800 Subject: [PATCH 2/9] stickiness actors --- frontend/src/queries/schema.json | 73 +++++++++++++++++++ frontend/src/queries/schema.ts | 8 +- .../insights/insight_actors_query_runner.py | 8 +- .../test/test_stickiness_query_runner.py | 18 ++++- posthog/hogql_queries/query_runner.py | 2 +- posthog/schema.py | 30 +++++++- posthog/types.py | 5 +- 7 files changed, 134 insertions(+), 10 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 79161991021c2..2457fa7a7bb74 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -211,6 +211,9 @@ { "$ref": "#/definitions/FunnelCorrelationActorsQuery" }, + { + "$ref": "#/definitions/StickinessActorsQuery" + }, { "$ref": "#/definitions/HogQLQuery" } @@ -7702,6 +7705,9 @@ }, { "$ref": "#/definitions/FunnelCorrelationActorsQuery" + }, + { + "$ref": "#/definitions/StickinessActorsQuery" } ] } @@ -12179,6 +12185,73 @@ "enum": ["strict", "unordered", "ordered"], "type": "string" }, + "StickinessActorsQuery": { + "additionalProperties": false, + "properties": { + "breakdown": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/BreakdownValueInt" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "compare": { + "enum": ["current", "previous"], + "type": "string" + }, + "day": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Day" + } + ] + }, + "includeRecordings": { + "type": "boolean" + }, + "interval": { + "description": "An interval selected out of available intervals in source query.", + "type": "integer" + }, + "kind": { + "const": "InsightActorsQuery", + "type": "string" + }, + "modifiers": { + "$ref": "#/definitions/HogQLQueryModifiers", + "description": "Modifiers used when performing the query" + }, + "operator": { + "$ref": "#/definitions/StickinessOperator" + }, + "response": { + "$ref": "#/definitions/ActorsQueryResponse" + }, + "series": { + "type": "integer" + }, + "source": { + "$ref": "#/definitions/InsightQuerySource" + }, + "status": { + "type": "string" + } + }, + "required": ["kind", "source"], + "type": "object" + }, "StickinessFilter": { "additionalProperties": false, "properties": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 0fa0ec7e83550..d19486fecf14a 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1764,7 +1764,7 @@ export type CachedActorsQueryResponse = CachedQueryResponse export interface ActorsQuery extends DataNode { kind: NodeKind.ActorsQuery - source?: InsightActorsQuery | FunnelsActorsQuery | FunnelCorrelationActorsQuery | HogQLQuery + source?: InsightActorsQuery | FunnelsActorsQuery | FunnelCorrelationActorsQuery | StickinessActorsQuery | HogQLQuery select?: HogQLExpression[] search?: string /** Currently only person filters supported. No filters for querying groups. See `filter_conditions()` in actor_strategies.py. */ @@ -2111,6 +2111,10 @@ export interface InsightActorsQuery { kind: NodeKind.InsightActorsQueryOptions - source: InsightActorsQuery | FunnelsActorsQuery | FunnelCorrelationActorsQuery + source: InsightActorsQuery | FunnelsActorsQuery | FunnelCorrelationActorsQuery | StickinessActorsQuery } export interface DatabaseSchemaSchema { diff --git a/posthog/hogql_queries/insights/insight_actors_query_runner.py b/posthog/hogql_queries/insights/insight_actors_query_runner.py index cc6ddca0fd0fd..9ea74439017a2 100644 --- a/posthog/hogql_queries/insights/insight_actors_query_runner.py +++ b/posthog/hogql_queries/insights/insight_actors_query_runner.py @@ -22,6 +22,7 @@ TrendsQuery, FunnelsQuery, LifecycleQuery, + StickinessActorsQuery, ) from posthog.types import InsightActorsQueryNode @@ -62,8 +63,11 @@ def to_query(self) -> ast.SelectQuery | ast.SelectSetQuery: return paths_runner.to_actors_query() elif isinstance(self.source_runner, StickinessQueryRunner): stickiness_runner = cast(StickinessQueryRunner, self.source_runner) - query = cast(InsightActorsQuery, self.query) - return stickiness_runner.to_actors_query(interval_num=int(query.day) if query.day is not None else None) + stickiness_actors_query = cast(StickinessActorsQuery, self.query) + return stickiness_runner.to_actors_query( + interval_num=int(stickiness_actors_query.day) if stickiness_actors_query.day is not None else None, + operator=getattr(stickiness_actors_query, "operator", None), + ) elif isinstance(self.source_runner, LifecycleQueryRunner): lifecycle_runner = cast(LifecycleQueryRunner, self.source_runner) query = cast(InsightActorsQuery, self.query) diff --git a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py index 90eb1df4a0147..946238c74badb 100644 --- a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py @@ -7,6 +7,7 @@ from posthog.clickhouse.client.execute import sync_execute from posthog.hogql.constants import LimitContext from posthog.hogql_queries.insights.stickiness_query_runner import StickinessQueryRunner +from posthog.hogql_queries.query_runner import get_query_runner from posthog.models.action.action import Action from posthog.models.group.util import create_group from posthog.models.group_type_mapping import GroupTypeMapping @@ -34,6 +35,7 @@ StickinessQuery, StickinessQueryResponse, CompareFilter, + StickinessActorsQuery, ) from posthog.settings import HOGQL_INCREASED_MAX_EXECUTION_TIME from posthog.test.base import APIBaseTest, _create_event, _create_person, ClickhouseTestMixin @@ -196,7 +198,7 @@ def _create_test_events(self): ] ) - def _run_query( + def _get_query( self, series: Optional[list[EventsNode | ActionsNode]] = None, date_from: Optional[str] = None, @@ -206,7 +208,6 @@ def _run_query( properties: Optional[StickinessProperties] = None, filters: Optional[StickinessFilter] = None, filter_test_accounts: Optional[bool] = False, - limit_context: Optional[LimitContext] = None, compare_filters: Optional[CompareFilter] = None, ): query_series: list[EventsNode | ActionsNode] = [EventsNode(event="$pageview")] if series is None else series @@ -224,6 +225,10 @@ def _run_query( compareFilter=compare_filters, filterTestAccounts=filter_test_accounts, ) + return query + + def _run_query(self, limit_context: Optional[LimitContext] = None, **kwargs): + query = self._get_query(**kwargs) return StickinessQueryRunner(team=self.team, query=query, limit_context=limit_context).calculate() def test_stickiness_runs(self): @@ -433,6 +438,15 @@ def test_interval_2_day_filtering(self): assert result["days"] == [1, 2, 3, 4, 5] assert result["data"] == [0, 0, 1, 0, 1] + # Test Actors + query = self._get_query(interval=IntervalType.DAY, intervalCount=2) + runner = get_query_runner(query=StickinessActorsQuery(source=query, day=1, operator="exact"), team=self.team) + actors = runner.calculate() + assert 0 == len(actors.results) + runner = get_query_runner(query=StickinessActorsQuery(source=query, day=3, operator="gte"), team=self.team) + actors = runner.calculate() + assert 2 == len(actors.results) + def test_interval_week(self): self._create_test_events() diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index abcaddde2d3cf..1ce37b1d7359b 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -240,7 +240,7 @@ def get_query_runner( limit_context=limit_context, modifiers=modifiers, ) - if kind == "InsightActorsQuery" or kind == "FunnelsActorsQuery" or kind == "FunnelCorrelationActorsQuery": + if kind in ("InsightActorsQuery", "FunnelsActorsQuery", "FunnelCorrelationActorsQuery", "StickinessActorsQuery"): from .insights.insight_actors_query_runner import InsightActorsQueryRunner return InsightActorsQueryRunner( diff --git a/posthog/schema.py b/posthog/schema.py index 26142a9a51454..ae1f25dd8bc67 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -6771,6 +6771,30 @@ class InsightVizNode(BaseModel): vizSpecificOptions: Optional[VizSpecificOptions] = None +class StickinessActorsQuery(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + breakdown: Optional[Union[str, list[str], int]] = None + compare: Optional[Compare] = None + day: Optional[Union[str, int]] = None + includeRecordings: Optional[bool] = None + interval: Optional[int] = Field( + default=None, description="An interval selected out of available intervals in source query." + ) + kind: Literal["InsightActorsQuery"] = "InsightActorsQuery" + modifiers: Optional[HogQLQueryModifiers] = Field( + default=None, description="Modifiers used when performing the query" + ) + operator: Optional[StickinessOperator] = None + response: Optional[ActorsQueryResponse] = None + series: Optional[int] = None + source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] = Field( + ..., discriminator="kind" + ) + status: Optional[str] = None + + class FunnelCorrelationActorsQuery(BaseModel): model_config = ConfigDict( extra="forbid", @@ -6834,7 +6858,7 @@ class InsightActorsQueryOptions(BaseModel): ) kind: Literal["InsightActorsQueryOptions"] = "InsightActorsQueryOptions" response: Optional[InsightActorsQueryOptionsResponse] = None - source: Union[InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery] + source: Union[InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery, StickinessActorsQuery] class ActorsQuery(BaseModel): @@ -6869,7 +6893,9 @@ class ActorsQuery(BaseModel): response: Optional[ActorsQueryResponse] = None search: Optional[str] = None select: Optional[list[str]] = None - source: Optional[Union[InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery, HogQLQuery]] = None + source: Optional[ + Union[InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery, StickinessActorsQuery, HogQLQuery] + ] = None class DataTableNode(BaseModel): diff --git a/posthog/types.py b/posthog/types.py index ea5e3080eae4f..0ee3e5f72155b 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -32,6 +32,7 @@ PathsQuery, StickinessQuery, LifecycleQuery, + StickinessActorsQuery, ) FilterType: TypeAlias = Union[Filter, PathFilter, RetentionFilter, StickinessFilter] @@ -46,7 +47,9 @@ LifecycleQuery, ] -InsightActorsQueryNode: TypeAlias = Union[InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery] +InsightActorsQueryNode: TypeAlias = Union[ + InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery, StickinessActorsQuery +] AnyPropertyFilter: TypeAlias = Union[ EventPropertyFilter, From 8776f51404664bb39db001cbc6558816e8079f56 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:34:31 +0000 Subject: [PATCH 3/9] Update query snapshots --- posthog/api/test/__snapshots__/test_api_docs.ambr | 1 + 1 file changed, 1 insertion(+) diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 862d8507dbb75..e44305ec16a01 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -90,6 +90,7 @@ 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "kind". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "Kind069Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "kind". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "Kind0ddEnum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "kind". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "Kind496Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', + 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "kind". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "Kind642Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "kind". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "KindCfaEnum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "type". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "TypeF73Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', 'Warning: operationId "Funnels" has collisions [(\'/api/environments/{project_id}/insights/funnel/\', \'post\'), (\'/api/projects/{project_id}/insights/funnel/\', \'post\')]. resolving with numeral suffixes.', From 2c63907dfc3c5644e46d24897a668fc5ed606481 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:59:50 +0000 Subject: [PATCH 4/9] Update UI snapshots for `chromium` (2) --- ...-funnel-top-to-bottom-breakdown--light.png | Bin 96532 -> 94841 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png index 969049953221d13be68da648c7c02169548922ff..65392b2c88ce12e1b730e56dee0770548be56e5c 100644 GIT binary patch delta 73792 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 delta 75522 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 erAZ?FA)T+*6FP153O^)~ Date: Wed, 8 Jan 2025 11:20:06 +0000 Subject: [PATCH 5/9] Update UI snapshots for `chromium` (2) --- ...-funnel-top-to-bottom-breakdown--light.png | Bin 94841 -> 96532 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png index 65392b2c88ce12e1b730e56dee0770548be56e5c..969049953221d13be68da648c7c02169548922ff 100644 GIT binary patch delta 75522 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 From 1d4ef2b3a06987be478cba9de1638363f271caca Mon Sep 17 00:00:00 2001 From: Alexander Spicer Date: Wed, 8 Jan 2025 03:39:25 -0800 Subject: [PATCH 6/9] hm --- frontend/src/queries/schema.json | 2 +- frontend/src/queries/schema.ts | 2 +- .../test/__snapshots__/test_schema.ambr | 4075 ----------------- 3 files changed, 2 insertions(+), 4077 deletions(-) delete mode 100644 posthog/clickhouse/test/__snapshots__/test_schema.ambr diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 2457fa7a7bb74..31cb6c18ff164 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -12397,7 +12397,7 @@ "description": "Properties specific to the stickiness insight" } }, - "required": ["intervalCount", "kind", "series"], + "required": ["kind", "series"], "type": "object" }, "StickinessQueryResponse": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index d19486fecf14a..c5d85f4206014 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1565,7 +1565,7 @@ export interface StickinessQuery * How many intervals comprise a period. Only used for cohorts, otherwise default 1. * @default 1 */ - intervalCount: integer + intervalCount?: integer /** Events and actions to include */ series: AnyEntityNode[] /** Properties specific to the stickiness insight */ diff --git a/posthog/clickhouse/test/__snapshots__/test_schema.ambr b/posthog/clickhouse/test/__snapshots__/test_schema.ambr deleted file mode 100644 index 406f73008fd28..0000000000000 --- a/posthog/clickhouse/test/__snapshots__/test_schema.ambr +++ /dev/null @@ -1,4075 +0,0 @@ -# serializer version: 1 -# name: test_create_kafka_events_with_disabled_protobuf - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_json ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_events_json_test', 'group1', 'JSONEachRow') - - SETTINGS kafka_skip_broken_messages = 100 - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_app_metrics2] - ''' - - CREATE TABLE IF NOT EXISTS kafka_app_metrics2 ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - app_source LowCardinality(String), - app_source_id String, - instance_id String, - metric_kind String, - metric_name String, - count Int64 - ) - ENGINE=Kafka('test.kafka.broker:9092', 'clickhouse_app_metrics2_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_app_metrics] - ''' - - CREATE TABLE IF NOT EXISTS kafka_app_metrics ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - plugin_config_id Int64, - category LowCardinality(String), - job_id String, - successes Int64, - successes_on_retry Int64, - failures Int64, - error_uuid UUID, - error_type String, - error_details String CODEC(ZSTD(3)) - ) - ENGINE=Kafka('test.kafka.broker:9092', 'clickhouse_app_metrics_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_error_tracking_issue_fingerprint_overrides] - ''' - - CREATE TABLE IF NOT EXISTS kafka_error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - fingerprint VARCHAR, - issue_id UUID, - is_deleted Int8, - version Int64 - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_error_tracking_issue_fingerprint_test', 'clickhouse-error-tracking-issue-fingerprint-overrides', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_events_dead_letter_queue] - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_dead_letter_queue ON CLUSTER 'posthog' - ( - id UUID, - event_uuid UUID, - event VARCHAR, - properties VARCHAR, - distinct_id VARCHAR, - team_id Int64, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - ip VARCHAR, - site_url VARCHAR, - now DateTime64(6, 'UTC'), - raw_payload VARCHAR, - error_timestamp DateTime64(6, 'UTC'), - error_location VARCHAR, - error VARCHAR, - tags Array(VARCHAR) - - ) ENGINE = Kafka('test.kafka.broker:9092', 'events_dead_letter_queue_test', 'group1', 'JSONEachRow') - SETTINGS kafka_skip_broken_messages=1000 - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_events_json] - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_json ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_events_json_test', 'group1', 'JSONEachRow') - - SETTINGS kafka_skip_broken_messages = 100 - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_events_recent_json] - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_recent_json ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_events_json_test', 'group1_recent', 'JSONEachRow') - - SETTINGS kafka_skip_broken_messages = 100 - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_groups] - ''' - - CREATE TABLE IF NOT EXISTS kafka_groups ON CLUSTER 'posthog' - ( - group_type_index UInt8, - group_key VARCHAR, - created_at DateTime64, - team_id Int64, - group_properties VARCHAR - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_groups_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_heatmaps] - ''' - - CREATE TABLE IF NOT EXISTS kafka_heatmaps ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - timestamp DateTime64(6, 'UTC'), - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x Int16, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y Int16, - -- stored so that in future we can support other resolutions - scale_factor Int16, - viewport_width Int16, - viewport_height Int16, - -- some elements move when the page scrolls, others do not - pointer_target_fixed Bool, - current_url VARCHAR, - type LowCardinality(String) - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_heatmap_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_ingestion_warnings] - ''' - - CREATE TABLE IF NOT EXISTS kafka_ingestion_warnings ON CLUSTER 'posthog' - ( - team_id Int64, - source LowCardinality(VARCHAR), - type VARCHAR, - details VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC') - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_ingestion_warnings_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_log_entries] - ''' - - CREATE TABLE IF NOT EXISTS kafka_log_entries ON CLUSTER 'posthog' - ( - team_id UInt64, - -- The name of the service or product that generated the logs. - -- Examples: batch_exports - log_source LowCardinality(String), - -- An id for the log source. - -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. - -- Examples: A batch export id, a cronjob id, a plugin id. - log_source_id String, - -- A secondary id e.g. for the instance of log_source that generated this log. - -- This may be ommitted if log_source is a singleton. - -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. - instance_id String, - -- Timestamp indicating when the log was generated. - timestamp DateTime64(6, 'UTC'), - -- The log level. - -- Examples: INFO, WARNING, DEBUG, ERROR. - level LowCardinality(String), - -- The actual log message. - message String - - ) ENGINE = Kafka('test.kafka.broker:9092', 'log_entries_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_performance_events] - ''' - - CREATE TABLE IF NOT EXISTS kafka_performance_events ON CLUSTER 'posthog' - ( - uuid UUID, - session_id String, - window_id String, - pageview_id String, - distinct_id String, - timestamp DateTime64, - time_origin DateTime64(3, 'UTC'), - entry_type LowCardinality(String), - name String, - team_id Int64, - current_url String, - start_time Float64, - duration Float64, - redirect_start Float64, - redirect_end Float64, - worker_start Float64, - fetch_start Float64, - domain_lookup_start Float64, - domain_lookup_end Float64, - connect_start Float64, - secure_connection_start Float64, - connect_end Float64, - request_start Float64, - response_start Float64, - response_end Float64, - decoded_body_size Int64, - encoded_body_size Int64, - initiator_type LowCardinality(String), - next_hop_protocol LowCardinality(String), - render_blocking_status LowCardinality(String), - response_status Int64, - transfer_size Int64, - largest_contentful_paint_element String, - largest_contentful_paint_render_time Float64, - largest_contentful_paint_load_time Float64, - largest_contentful_paint_size Float64, - largest_contentful_paint_id String, - largest_contentful_paint_url String, - dom_complete Float64, - dom_content_loaded_event Float64, - dom_interactive Float64, - load_event_end Float64, - load_event_start Float64, - redirect_count Int64, - navigation_type LowCardinality(String), - unload_event_end Float64, - unload_event_start Float64 - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_performance_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_person] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person ON CLUSTER 'posthog' - ( - id UUID, - created_at DateTime64, - team_id Int64, - properties VARCHAR, - is_identified Int8, - is_deleted Int8, - version UInt64 - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_person_distinct_id2] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person_distinct_id2 ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_distinct_id_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_person_distinct_id] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person_distinct_id ON CLUSTER 'posthog' - ( - distinct_id VARCHAR, - person_id UUID, - team_id Int64, - _sign Nullable(Int8), - is_deleted Nullable(Int8) - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_unique_id_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_person_distinct_id_overrides] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person_distinct_id_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_distinct_id_test', 'clickhouse-person-distinct-id-overrides', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_person_overrides] - ''' - - CREATE TABLE IF NOT EXISTS `posthog_test`.`kafka_person_overrides` - ON CLUSTER 'posthog' - - ENGINE = Kafka( - 'kafka:9092', -- Kafka hosts - 'clickhouse_person_override_test', -- Kafka topic - 'clickhouse-person-overrides', -- Kafka consumer group id - 'JSONEachRow' -- Specify that we should pass Kafka messages as JSON - ) - - -- Take the types from the `person_overrides` table, except for the - -- `created_at`, which we want to use the DEFAULT now() from the - -- `person_overrides` definition. See - -- https://github.com/ClickHouse/ClickHouse/pull/38272 for details of `EMPTY - -- AS SELECT` - EMPTY AS SELECT - team_id, - old_person_id, - override_person_id, - merged_at, - oldest_event, - -- We don't want to insert this column via Kafka, as it's - -- set as a default value in the `person_overrides` table. - -- created_at, - version - FROM `posthog_test`.`person_overrides` - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_plugin_log_entries] - ''' - - CREATE TABLE IF NOT EXISTS kafka_plugin_log_entries ON CLUSTER 'posthog' - ( - id UUID, - team_id Int64, - plugin_id Int64, - plugin_config_id Int64, - timestamp DateTime64(6, 'UTC'), - source VARCHAR, - type VARCHAR, - message VARCHAR, - instance_id UUID - - ) ENGINE = Kafka('test.kafka.broker:9092', 'plugin_log_entries_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_session_recording_events] - ''' - - CREATE TABLE IF NOT EXISTS kafka_session_recording_events ON CLUSTER 'posthog' - ( - uuid UUID, - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - session_id VARCHAR, - window_id VARCHAR, - snapshot_data VARCHAR, - created_at DateTime64(6, 'UTC') - - - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_session_recording_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_kafka_table_with_different_kafka_host[kafka_session_replay_events] - ''' - - CREATE TABLE IF NOT EXISTS kafka_session_replay_events ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - first_timestamp DateTime64(6, 'UTC'), - last_timestamp DateTime64(6, 'UTC'), - first_url Nullable(VARCHAR), - urls Array(String), - click_count Int64, - keypress_count Int64, - mouse_activity_count Int64, - active_milliseconds Int64, - console_log_count Int64, - console_warn_count Int64, - console_error_count Int64, - size Int64, - event_count Int64, - message_count Int64, - snapshot_source LowCardinality(Nullable(String)), - snapshot_library Nullable(String) - ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_session_replay_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[app_metrics2] - ''' - - CREATE TABLE IF NOT EXISTS app_metrics2 ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - -- The name of the service or product that generated the metrics. - -- Examples: plugins, hog - app_source LowCardinality(String), - -- An id for the app source. - -- Set app_source to avoid collision with ids from other app sources if the id generation is not safe. - -- Examples: A plugin id, a hog application id - app_source_id String, - -- A secondary id e.g. for the instance of app_source that generated this metric. - -- This may be ommitted if app_source is a singleton. - -- Examples: A plugin config id, a hog application config id - instance_id String, - metric_kind LowCardinality(String), - metric_name LowCardinality(String), - count SimpleAggregateFunction(sum, Int64) - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) - ENGINE=Distributed('posthog', 'posthog_test', 'sharded_app_metrics2', rand()) - - ''' -# --- -# name: test_create_table_query[app_metrics2_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS app_metrics2_mv ON CLUSTER 'posthog' - TO posthog_test.sharded_app_metrics2 - AS SELECT - team_id, - timestamp, - app_source, - app_source_id, - instance_id, - metric_kind, - metric_name, - count - FROM posthog_test.kafka_app_metrics2 - - ''' -# --- -# name: test_create_table_query[app_metrics] - ''' - - CREATE TABLE IF NOT EXISTS app_metrics ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - plugin_config_id Int64, - category LowCardinality(String), - job_id String, - successes SimpleAggregateFunction(sum, Int64), - successes_on_retry SimpleAggregateFunction(sum, Int64), - failures SimpleAggregateFunction(sum, Int64), - error_uuid UUID, - error_type String, - error_details String CODEC(ZSTD(3)) - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) - ENGINE=Distributed('posthog', 'posthog_test', 'sharded_app_metrics', rand()) - - ''' -# --- -# name: test_create_table_query[app_metrics_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS app_metrics_mv ON CLUSTER 'posthog' - TO posthog_test.sharded_app_metrics - AS SELECT - team_id, - timestamp, - plugin_config_id, - category, - job_id, - successes, - successes_on_retry, - failures, - error_uuid, - error_type, - error_details - FROM posthog_test.kafka_app_metrics - - ''' -# --- -# name: test_create_table_query[channel_definition] - ''' - - CREATE TABLE IF NOT EXISTS channel_definition ON CLUSTER 'posthog' ( - domain String NOT NULL, - kind String NOT NULL, - domain_type String NULL, - type_if_paid String NULL, - type_if_organic String NULL - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.channel_definition', '{replica}-{shard}') - ORDER BY (domain, kind); - - ''' -# --- -# name: test_create_table_query[cohortpeople] - ''' - - CREATE TABLE IF NOT EXISTS cohortpeople ON CLUSTER 'posthog' - ( - person_id UUID, - cohort_id Int64, - team_id Int64, - sign Int8, - version UInt64 - ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.cohortpeople', '{replica}-{shard}', sign) - Order By (team_id, cohort_id, person_id, version) - - - ''' -# --- -# name: test_create_table_query[distributed_events_recent] - ''' - - CREATE TABLE IF NOT EXISTS distributed_events_recent ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64(), _timestamp_ms DateTime64 - - ) ENGINE = Distributed('posthog_single_shard', 'posthog_test', 'events_recent', sipHash64(distinct_id)) - - ''' -# --- -# name: test_create_table_query[error_tracking_issue_fingerprint_overrides] - ''' - - CREATE TABLE IF NOT EXISTS error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - fingerprint VARCHAR, - issue_id UUID, - is_deleted Int8, - version Int64 - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - , INDEX kafka_timestamp_minmax_error_tracking_issue_fingerprint_overrides _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.error_tracking_issue_fingerprint_overrides', '{replica}-{shard}', version) - - ORDER BY (team_id, fingerprint) - SETTINGS index_granularity = 512 - - ''' -# --- -# name: test_create_table_query[error_tracking_issue_fingerprint_overrides_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS error_tracking_issue_fingerprint_overrides_mv ON CLUSTER 'posthog' - TO posthog_test.error_tracking_issue_fingerprint_overrides - AS SELECT - team_id, - fingerprint, - issue_id, - is_deleted, - version, - _timestamp, - _offset, - _partition - FROM posthog_test.kafka_error_tracking_issue_fingerprint_overrides - WHERE version > 0 -- only store updated rows, not newly inserted ones - - ''' -# --- -# name: test_create_table_query[events] - ''' - - CREATE TABLE IF NOT EXISTS events ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - , $group_0 VARCHAR COMMENT 'column_materializer::$group_0' - , $group_1 VARCHAR COMMENT 'column_materializer::$group_1' - , $group_2 VARCHAR COMMENT 'column_materializer::$group_2' - , $group_3 VARCHAR COMMENT 'column_materializer::$group_3' - , $group_4 VARCHAR COMMENT 'column_materializer::$group_4' - , $window_id VARCHAR COMMENT 'column_materializer::$window_id' - , $session_id VARCHAR COMMENT 'column_materializer::$session_id' - , elements_chain_href String COMMENT 'column_materializer::elements_chain::href' - , elements_chain_texts Array(String) COMMENT 'column_materializer::elements_chain::texts' - , elements_chain_ids Array(String) COMMENT 'column_materializer::elements_chain::ids' - , elements_chain_elements Array(Enum('a', 'button', 'form', 'input', 'select', 'textarea', 'label')) COMMENT 'column_materializer::elements_chain::elements' - , properties_group_custom Map(String, String), properties_group_feature_flags Map(String, String) - - - , _timestamp DateTime - , _offset UInt64 - , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64() - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_events', sipHash64(distinct_id)) - - ''' -# --- -# name: test_create_table_query[events_dead_letter_queue] - ''' - - CREATE TABLE IF NOT EXISTS events_dead_letter_queue ON CLUSTER 'posthog' - ( - id UUID, - event_uuid UUID, - event VARCHAR, - properties VARCHAR, - distinct_id VARCHAR, - team_id Int64, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - ip VARCHAR, - site_url VARCHAR, - now DateTime64(6, 'UTC'), - raw_payload VARCHAR, - error_timestamp DateTime64(6, 'UTC'), - error_location VARCHAR, - error VARCHAR, - tags Array(VARCHAR) - - - , _timestamp DateTime - , _offset UInt64 - - , INDEX kafka_timestamp_minmax_events_dead_letter_queue _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_dead_letter_queue', '{replica}-{shard}', _timestamp) - ORDER BY (id, event_uuid, distinct_id, team_id) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query[events_dead_letter_queue_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS events_dead_letter_queue_mv ON CLUSTER 'posthog' - TO posthog_test.events_dead_letter_queue - AS SELECT - id, - event_uuid, - event, - properties, - distinct_id, - team_id, - elements_chain, - created_at, - ip, - site_url, - now, - raw_payload, - error_timestamp, - error_location, - error, - tags, - _timestamp, - _offset - FROM posthog_test.kafka_events_dead_letter_queue - - ''' -# --- -# name: test_create_table_query[events_json_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS events_json_mv ON CLUSTER 'posthog' - TO posthog_test.writable_events - AS SELECT - uuid, - event, - properties, - timestamp, - team_id, - distinct_id, - elements_chain, - created_at, - person_id, - person_created_at, - person_properties, - group0_properties, - group1_properties, - group2_properties, - group3_properties, - group4_properties, - group0_created_at, - group1_created_at, - group2_created_at, - group3_created_at, - group4_created_at, - person_mode, - _timestamp, - _offset - FROM posthog_test.kafka_events_json - - ''' -# --- -# name: test_create_table_query[events_recent] - ''' - - CREATE TABLE IF NOT EXISTS events_recent ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - , inserted_at DateTime64(6, 'UTC') DEFAULT NOW64(), _timestamp_ms DateTime64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_recent', '{replica}-{shard}', _timestamp) - PARTITION BY toStartOfHour(inserted_at) - ORDER BY (team_id, toStartOfHour(inserted_at), event, cityHash64(distinct_id), cityHash64(uuid)) - TTL toDateTime(inserted_at) + INTERVAL 7 DAY - - - ''' -# --- -# name: test_create_table_query[events_recent_json_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS events_recent_json_mv ON CLUSTER 'posthog' - TO posthog_test.events_recent - AS SELECT - uuid, - event, - properties, - timestamp, - team_id, - distinct_id, - elements_chain, - created_at, - person_id, - person_created_at, - person_properties, - group0_properties, - group1_properties, - group2_properties, - group3_properties, - group4_properties, - group0_created_at, - group1_created_at, - group2_created_at, - group3_created_at, - group4_created_at, - person_mode, - _timestamp, - _timestamp_ms, - _offset, - _partition - FROM posthog_test.kafka_events_recent_json - - ''' -# --- -# name: test_create_table_query[groups] - ''' - - CREATE TABLE IF NOT EXISTS groups ON CLUSTER 'posthog' - ( - group_type_index UInt8, - group_key VARCHAR, - created_at DateTime64, - team_id Int64, - group_properties VARCHAR - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.groups', '{replica}-{shard}', _timestamp) - Order By (team_id, group_type_index, group_key) - - - ''' -# --- -# name: test_create_table_query[groups_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS groups_mv ON CLUSTER 'posthog' - TO posthog_test.groups - AS SELECT - group_type_index, - group_key, - created_at, - team_id, - group_properties, - _timestamp, - _offset - FROM posthog_test.kafka_groups - - ''' -# --- -# name: test_create_table_query[heatmaps] - ''' - - CREATE TABLE IF NOT EXISTS heatmaps ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - timestamp DateTime64(6, 'UTC'), - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x Int16, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y Int16, - -- stored so that in future we can support other resolutions - scale_factor Int16, - viewport_width Int16, - viewport_height Int16, - -- some elements move when the page scrolls, others do not - pointer_target_fixed Bool, - current_url VARCHAR, - type LowCardinality(String), - _timestamp DateTime, - _offset UInt64, - _partition UInt64 - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_heatmaps', cityHash64(concat(toString(team_id), '-', session_id, '-', toString(toDate(timestamp))))) - - ''' -# --- -# name: test_create_table_query[heatmaps_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS heatmaps_mv ON CLUSTER 'posthog' - TO posthog_test.writable_heatmaps - AS SELECT - session_id, - team_id, - distinct_id, - timestamp, - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y, - -- stored so that in future we can support other resolutions - scale_factor, - viewport_width, - viewport_height, - -- some elements move when the page scrolls, others do not - pointer_target_fixed, - current_url, - type, - _timestamp, - _offset, - _partition - FROM posthog_test.kafka_heatmaps - - ''' -# --- -# name: test_create_table_query[ingestion_warnings] - ''' - - CREATE TABLE IF NOT EXISTS ingestion_warnings ON CLUSTER 'posthog' - ( - team_id Int64, - source LowCardinality(VARCHAR), - type VARCHAR, - details VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC') - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_ingestion_warnings', rand()) - - ''' -# --- -# name: test_create_table_query[ingestion_warnings_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS ingestion_warnings_mv ON CLUSTER 'posthog' - TO posthog_test.ingestion_warnings - AS SELECT - team_id, - source, - type, - details, - timestamp, - _timestamp, - _offset, - _partition - FROM posthog_test.kafka_ingestion_warnings - - ''' -# --- -# name: test_create_table_query[kafka_app_metrics2] - ''' - - CREATE TABLE IF NOT EXISTS kafka_app_metrics2 ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - app_source LowCardinality(String), - app_source_id String, - instance_id String, - metric_kind String, - metric_name String, - count Int64 - ) - ENGINE=Kafka('kafka:9092', 'clickhouse_app_metrics2_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_app_metrics] - ''' - - CREATE TABLE IF NOT EXISTS kafka_app_metrics ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - plugin_config_id Int64, - category LowCardinality(String), - job_id String, - successes Int64, - successes_on_retry Int64, - failures Int64, - error_uuid UUID, - error_type String, - error_details String CODEC(ZSTD(3)) - ) - ENGINE=Kafka('kafka:9092', 'clickhouse_app_metrics_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_error_tracking_issue_fingerprint_overrides] - ''' - - CREATE TABLE IF NOT EXISTS kafka_error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - fingerprint VARCHAR, - issue_id UUID, - is_deleted Int8, - version Int64 - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_error_tracking_issue_fingerprint_test', 'clickhouse-error-tracking-issue-fingerprint-overrides', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_events_dead_letter_queue] - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_dead_letter_queue ON CLUSTER 'posthog' - ( - id UUID, - event_uuid UUID, - event VARCHAR, - properties VARCHAR, - distinct_id VARCHAR, - team_id Int64, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - ip VARCHAR, - site_url VARCHAR, - now DateTime64(6, 'UTC'), - raw_payload VARCHAR, - error_timestamp DateTime64(6, 'UTC'), - error_location VARCHAR, - error VARCHAR, - tags Array(VARCHAR) - - ) ENGINE = Kafka('kafka:9092', 'events_dead_letter_queue_test', 'group1', 'JSONEachRow') - SETTINGS kafka_skip_broken_messages=1000 - ''' -# --- -# name: test_create_table_query[kafka_events_json] - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_json ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_events_json_test', 'group1', 'JSONEachRow') - - SETTINGS kafka_skip_broken_messages = 100 - - ''' -# --- -# name: test_create_table_query[kafka_events_recent_json] - ''' - - CREATE TABLE IF NOT EXISTS kafka_events_recent_json ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_events_json_test', 'group1_recent', 'JSONEachRow') - - SETTINGS kafka_skip_broken_messages = 100 - - ''' -# --- -# name: test_create_table_query[kafka_groups] - ''' - - CREATE TABLE IF NOT EXISTS kafka_groups ON CLUSTER 'posthog' - ( - group_type_index UInt8, - group_key VARCHAR, - created_at DateTime64, - team_id Int64, - group_properties VARCHAR - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_groups_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_heatmaps] - ''' - - CREATE TABLE IF NOT EXISTS kafka_heatmaps ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - timestamp DateTime64(6, 'UTC'), - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x Int16, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y Int16, - -- stored so that in future we can support other resolutions - scale_factor Int16, - viewport_width Int16, - viewport_height Int16, - -- some elements move when the page scrolls, others do not - pointer_target_fixed Bool, - current_url VARCHAR, - type LowCardinality(String) - ) ENGINE = Kafka('kafka:9092', 'clickhouse_heatmap_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_ingestion_warnings] - ''' - - CREATE TABLE IF NOT EXISTS kafka_ingestion_warnings ON CLUSTER 'posthog' - ( - team_id Int64, - source LowCardinality(VARCHAR), - type VARCHAR, - details VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC') - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_ingestion_warnings_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_log_entries] - ''' - - CREATE TABLE IF NOT EXISTS kafka_log_entries ON CLUSTER 'posthog' - ( - team_id UInt64, - -- The name of the service or product that generated the logs. - -- Examples: batch_exports - log_source LowCardinality(String), - -- An id for the log source. - -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. - -- Examples: A batch export id, a cronjob id, a plugin id. - log_source_id String, - -- A secondary id e.g. for the instance of log_source that generated this log. - -- This may be ommitted if log_source is a singleton. - -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. - instance_id String, - -- Timestamp indicating when the log was generated. - timestamp DateTime64(6, 'UTC'), - -- The log level. - -- Examples: INFO, WARNING, DEBUG, ERROR. - level LowCardinality(String), - -- The actual log message. - message String - - ) ENGINE = Kafka('kafka:9092', 'log_entries_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_performance_events] - ''' - - CREATE TABLE IF NOT EXISTS kafka_performance_events ON CLUSTER 'posthog' - ( - uuid UUID, - session_id String, - window_id String, - pageview_id String, - distinct_id String, - timestamp DateTime64, - time_origin DateTime64(3, 'UTC'), - entry_type LowCardinality(String), - name String, - team_id Int64, - current_url String, - start_time Float64, - duration Float64, - redirect_start Float64, - redirect_end Float64, - worker_start Float64, - fetch_start Float64, - domain_lookup_start Float64, - domain_lookup_end Float64, - connect_start Float64, - secure_connection_start Float64, - connect_end Float64, - request_start Float64, - response_start Float64, - response_end Float64, - decoded_body_size Int64, - encoded_body_size Int64, - initiator_type LowCardinality(String), - next_hop_protocol LowCardinality(String), - render_blocking_status LowCardinality(String), - response_status Int64, - transfer_size Int64, - largest_contentful_paint_element String, - largest_contentful_paint_render_time Float64, - largest_contentful_paint_load_time Float64, - largest_contentful_paint_size Float64, - largest_contentful_paint_id String, - largest_contentful_paint_url String, - dom_complete Float64, - dom_content_loaded_event Float64, - dom_interactive Float64, - load_event_end Float64, - load_event_start Float64, - redirect_count Int64, - navigation_type LowCardinality(String), - unload_event_end Float64, - unload_event_start Float64 - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_performance_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_person] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person ON CLUSTER 'posthog' - ( - id UUID, - created_at DateTime64, - team_id Int64, - properties VARCHAR, - is_identified Int8, - is_deleted Int8, - version UInt64 - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_person_distinct_id2] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person_distinct_id2 ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_distinct_id_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_person_distinct_id] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person_distinct_id ON CLUSTER 'posthog' - ( - distinct_id VARCHAR, - person_id UUID, - team_id Int64, - _sign Nullable(Int8), - is_deleted Nullable(Int8) - ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_unique_id_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_person_distinct_id_overrides] - ''' - - CREATE TABLE IF NOT EXISTS kafka_person_distinct_id_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_distinct_id_test', 'clickhouse-person-distinct-id-overrides', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_person_overrides] - ''' - - CREATE TABLE IF NOT EXISTS `posthog_test`.`kafka_person_overrides` - ON CLUSTER 'posthog' - - ENGINE = Kafka( - 'kafka:9092', -- Kafka hosts - 'clickhouse_person_override_test', -- Kafka topic - 'clickhouse-person-overrides', -- Kafka consumer group id - 'JSONEachRow' -- Specify that we should pass Kafka messages as JSON - ) - - -- Take the types from the `person_overrides` table, except for the - -- `created_at`, which we want to use the DEFAULT now() from the - -- `person_overrides` definition. See - -- https://github.com/ClickHouse/ClickHouse/pull/38272 for details of `EMPTY - -- AS SELECT` - EMPTY AS SELECT - team_id, - old_person_id, - override_person_id, - merged_at, - oldest_event, - -- We don't want to insert this column via Kafka, as it's - -- set as a default value in the `person_overrides` table. - -- created_at, - version - FROM `posthog_test`.`person_overrides` - - ''' -# --- -# name: test_create_table_query[kafka_plugin_log_entries] - ''' - - CREATE TABLE IF NOT EXISTS kafka_plugin_log_entries ON CLUSTER 'posthog' - ( - id UUID, - team_id Int64, - plugin_id Int64, - plugin_config_id Int64, - timestamp DateTime64(6, 'UTC'), - source VARCHAR, - type VARCHAR, - message VARCHAR, - instance_id UUID - - ) ENGINE = Kafka('kafka:9092', 'plugin_log_entries_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_session_recording_events] - ''' - - CREATE TABLE IF NOT EXISTS kafka_session_recording_events ON CLUSTER 'posthog' - ( - uuid UUID, - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - session_id VARCHAR, - window_id VARCHAR, - snapshot_data VARCHAR, - created_at DateTime64(6, 'UTC') - - - ) ENGINE = Kafka('kafka:9092', 'clickhouse_session_recording_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[kafka_session_replay_events] - ''' - - CREATE TABLE IF NOT EXISTS kafka_session_replay_events ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - first_timestamp DateTime64(6, 'UTC'), - last_timestamp DateTime64(6, 'UTC'), - first_url Nullable(VARCHAR), - urls Array(String), - click_count Int64, - keypress_count Int64, - mouse_activity_count Int64, - active_milliseconds Int64, - console_log_count Int64, - console_warn_count Int64, - console_error_count Int64, - size Int64, - event_count Int64, - message_count Int64, - snapshot_source LowCardinality(Nullable(String)), - snapshot_library Nullable(String) - ) ENGINE = Kafka('kafka:9092', 'clickhouse_session_replay_events_test', 'group1', 'JSONEachRow') - - ''' -# --- -# name: test_create_table_query[log_entries] - ''' - - CREATE TABLE IF NOT EXISTS log_entries ON CLUSTER 'posthog' - ( - team_id UInt64, - -- The name of the service or product that generated the logs. - -- Examples: batch_exports - log_source LowCardinality(String), - -- An id for the log source. - -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. - -- Examples: A batch export id, a cronjob id, a plugin id. - log_source_id String, - -- A secondary id e.g. for the instance of log_source that generated this log. - -- This may be ommitted if log_source is a singleton. - -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. - instance_id String, - -- Timestamp indicating when the log was generated. - timestamp DateTime64(6, 'UTC'), - -- The log level. - -- Examples: INFO, WARNING, DEBUG, ERROR. - level LowCardinality(String), - -- The actual log message. - message String - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.log_entries', '{replica}-{shard}', _timestamp) - PARTITION BY toStartOfHour(timestamp) ORDER BY (team_id, log_source, log_source_id, instance_id, timestamp) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query[log_entries_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS log_entries_mv ON CLUSTER 'posthog' - TO posthog_test.log_entries - AS SELECT - team_id, - log_source, - log_source_id, - instance_id, - timestamp, - level, - message, - _timestamp, - _offset - FROM posthog_test.kafka_log_entries - - ''' -# --- -# name: test_create_table_query[performance_events] - ''' - - CREATE TABLE IF NOT EXISTS performance_events ON CLUSTER 'posthog' - ( - uuid UUID, - session_id String, - window_id String, - pageview_id String, - distinct_id String, - timestamp DateTime64, - time_origin DateTime64(3, 'UTC'), - entry_type LowCardinality(String), - name String, - team_id Int64, - current_url String, - start_time Float64, - duration Float64, - redirect_start Float64, - redirect_end Float64, - worker_start Float64, - fetch_start Float64, - domain_lookup_start Float64, - domain_lookup_end Float64, - connect_start Float64, - secure_connection_start Float64, - connect_end Float64, - request_start Float64, - response_start Float64, - response_end Float64, - decoded_body_size Int64, - encoded_body_size Int64, - initiator_type LowCardinality(String), - next_hop_protocol LowCardinality(String), - render_blocking_status LowCardinality(String), - response_status Int64, - transfer_size Int64, - largest_contentful_paint_element String, - largest_contentful_paint_render_time Float64, - largest_contentful_paint_load_time Float64, - largest_contentful_paint_size Float64, - largest_contentful_paint_id String, - largest_contentful_paint_url String, - dom_complete Float64, - dom_content_loaded_event Float64, - dom_interactive Float64, - load_event_end Float64, - load_event_start Float64, - redirect_count Int64, - navigation_type LowCardinality(String), - unload_event_end Float64, - unload_event_start Float64 - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_performance_events', sipHash64(session_id)) - - ''' -# --- -# name: test_create_table_query[performance_events_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS performance_events_mv ON CLUSTER 'posthog' - TO posthog_test.writeable_performance_events - AS SELECT - uuid, session_id, window_id, pageview_id, distinct_id, timestamp, time_origin, entry_type, name, team_id, current_url, start_time, duration, redirect_start, redirect_end, worker_start, fetch_start, domain_lookup_start, domain_lookup_end, connect_start, secure_connection_start, connect_end, request_start, response_start, response_end, decoded_body_size, encoded_body_size, initiator_type, next_hop_protocol, render_blocking_status, response_status, transfer_size, largest_contentful_paint_element, largest_contentful_paint_render_time, largest_contentful_paint_load_time, largest_contentful_paint_size, largest_contentful_paint_id, largest_contentful_paint_url, dom_complete, dom_content_loaded_event, dom_interactive, load_event_end, load_event_start, redirect_count, navigation_type, unload_event_end, unload_event_start - ,_timestamp, _offset, _partition - FROM posthog_test.kafka_performance_events - - ''' -# --- -# name: test_create_table_query[person] - ''' - - CREATE TABLE IF NOT EXISTS person ON CLUSTER 'posthog' - ( - id UUID, - created_at DateTime64, - team_id Int64, - properties VARCHAR, - is_identified Int8, - is_deleted Int8, - version UInt64 - - - , _timestamp DateTime - , _offset UInt64 - - , INDEX kafka_timestamp_minmax_person _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person', '{replica}-{shard}', version) - Order By (team_id, id) - - - ''' -# --- -# name: test_create_table_query[person_distinct_id2] - ''' - - CREATE TABLE IF NOT EXISTS person_distinct_id2 ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - - , _timestamp DateTime - , _offset UInt64 - - , _partition UInt64 - , INDEX kafka_timestamp_minmax_person_distinct_id2 _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id2', '{replica}-{shard}', version) - - ORDER BY (team_id, distinct_id) - SETTINGS index_granularity = 512 - - ''' -# --- -# name: test_create_table_query[person_distinct_id2_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS person_distinct_id2_mv ON CLUSTER 'posthog' - TO posthog_test.person_distinct_id2 - AS SELECT - team_id, - distinct_id, - person_id, - is_deleted, - version, - _timestamp, - _offset, - _partition - FROM posthog_test.kafka_person_distinct_id2 - - ''' -# --- -# name: test_create_table_query[person_distinct_id] - ''' - - CREATE TABLE IF NOT EXISTS person_distinct_id ON CLUSTER 'posthog' - ( - distinct_id VARCHAR, - person_id UUID, - team_id Int64, - _sign Int8 DEFAULT 1, - is_deleted Int8 ALIAS if(_sign==-1, 1, 0) - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id', '{replica}-{shard}', _sign) - Order By (team_id, distinct_id, person_id) - - - ''' -# --- -# name: test_create_table_query[person_distinct_id_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS person_distinct_id_mv ON CLUSTER 'posthog' - TO posthog_test.person_distinct_id - AS SELECT - distinct_id, - person_id, - team_id, - coalesce(_sign, if(is_deleted==0, 1, -1)) AS _sign, - _timestamp, - _offset - FROM posthog_test.kafka_person_distinct_id - - ''' -# --- -# name: test_create_table_query[person_distinct_id_overrides] - ''' - - CREATE TABLE IF NOT EXISTS person_distinct_id_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - , INDEX kafka_timestamp_minmax_person_distinct_id_overrides _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id_overrides', '{replica}-{shard}', version) - - ORDER BY (team_id, distinct_id) - SETTINGS index_granularity = 512 - - ''' -# --- -# name: test_create_table_query[person_distinct_id_overrides_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS person_distinct_id_overrides_mv ON CLUSTER 'posthog' - TO posthog_test.person_distinct_id_overrides - AS SELECT - team_id, - distinct_id, - person_id, - is_deleted, - version, - _timestamp, - _offset, - _partition - FROM posthog_test.kafka_person_distinct_id_overrides - WHERE version > 0 -- only store updated rows, not newly inserted ones - - ''' -# --- -# name: test_create_table_query[person_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS person_mv ON CLUSTER 'posthog' - TO posthog_test.person - AS SELECT - id, - created_at, - team_id, - properties, - is_identified, - is_deleted, - version, - _timestamp, - _offset - FROM posthog_test.kafka_person - - ''' -# --- -# name: test_create_table_query[person_overrides] - ''' - - CREATE TABLE IF NOT EXISTS `posthog_test`.`person_overrides` - ON CLUSTER 'posthog' ( - team_id INT NOT NULL, - - -- When we merge two people `old_person_id` and `override_person_id`, we - -- want to keep track of a mapping from the `old_person_id` to the - -- `override_person_id`. This allows us to join with the - -- `sharded_events` table to find all events that were associated with - -- the `old_person_id` and update them to be associated with the - -- `override_person_id`. - old_person_id UUID NOT NULL, - override_person_id UUID NOT NULL, - - -- The timestamp the merge of the two people was completed. - merged_at DateTime64(6, 'UTC') NOT NULL, - -- The timestamp of the oldest event associated with the - -- `old_person_id`. - oldest_event DateTime64(6, 'UTC') NOT NULL, - -- The timestamp rows are created. This isn't part of the JOIN process - -- with the events table but rather a housekeeping column to allow us to - -- see when the row was created. This shouldn't have any impact of the - -- JOIN as it will be stored separately with the Wide ClickHouse table - -- storage. - created_at DateTime64(6, 'UTC') DEFAULT now(), - - -- the specific version of the `old_person_id` mapping. This is used to - -- allow us to discard old mappings as new ones are added. This version - -- will be provided by the corresponding PostgreSQL - --`posthog_personoverrides` table - version INT NOT NULL - ) - - -- By specifying Replacing merge tree on version, we allow ClickHouse to - -- discard old versions of a `old_person_id` mapping. This should help keep - -- performance in check as new versions are added. Note that given we can - -- have partitioning by `oldest_event` which will change as we update - -- `person_id` on old partitions. - -- - -- We also need to ensure that the data is replicated to all replicas in the - -- cluster, as we do not have any constraints on person_id and which shard - -- associated events are on. To do this we use the ReplicatedReplacingMergeTree - -- engine specifying a static `zk_path`. This will cause the Engine to - -- consider all replicas as the same. See - -- https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication - -- for details. - ENGINE = ReplicatedReplacingMergeTree( - -- NOTE: for testing we use a uuid to ensure that we don't get conflicts - -- when the tests tear down and recreate the table. - '/clickhouse/tables/{uuid}noshard/posthog_test.person_overrides', - '{replica}-{shard}', - version - ) - - -- We partition the table by the `oldest_event` column. This allows us to - -- handle updating the events table partition by partition, progressing each - -- override partition by partition in lockstep with the events table. Note - -- that this means it is possible that we have a mapping from - -- `old_person_id` in multiple partitions during the merge process. - PARTITION BY toYYYYMM(oldest_event) - - -- We want to collapse down on the `old_person_id` such that we end up with - -- the newest known mapping for it in the table. Query side we will need to - -- ensure that we are always querying the latest version of the mapping. - ORDER BY (team_id, old_person_id) - - ''' -# --- -# name: test_create_table_query[person_overrides_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS `posthog_test`.`person_overrides_mv` - ON CLUSTER 'posthog' - TO `posthog_test`.`person_overrides` - AS SELECT - team_id, - old_person_id, - override_person_id, - merged_at, - oldest_event, - -- We don't want to insert this column via Kafka, as it's - -- set as a default value in the `person_overrides` table. - -- created_at, - version - FROM `posthog_test`.`kafka_person_overrides` - - ''' -# --- -# name: test_create_table_query[person_static_cohort] - ''' - - CREATE TABLE IF NOT EXISTS person_static_cohort ON CLUSTER 'posthog' - ( - id UUID, - person_id UUID, - cohort_id Int64, - team_id Int64 - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_static_cohort', '{replica}-{shard}', _timestamp) - Order By (team_id, cohort_id, person_id, id) - - - ''' -# --- -# name: test_create_table_query[plugin_log_entries] - ''' - - CREATE TABLE IF NOT EXISTS plugin_log_entries ON CLUSTER 'posthog' - ( - id UUID, - team_id Int64, - plugin_id Int64, - plugin_config_id Int64, - timestamp DateTime64(6, 'UTC'), - source VARCHAR, - type VARCHAR, - message VARCHAR, - instance_id UUID - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.plugin_log_entries', '{replica}-{shard}', _timestamp) - PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query[plugin_log_entries_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS plugin_log_entries_mv ON CLUSTER 'posthog' - TO posthog_test.plugin_log_entries - AS SELECT - id, - team_id, - plugin_id, - plugin_config_id, - timestamp, - source, - type, - message, - instance_id, - _timestamp, - _offset - FROM posthog_test.kafka_plugin_log_entries - - ''' -# --- -# name: test_create_table_query[raw_sessions] - ''' - - CREATE TABLE IF NOT EXISTS raw_sessions ON CLUSTER 'posthog' - ( - team_id Int64, - session_id_v7 UInt128, -- integer representation of a uuidv7 - - -- ClickHouse will pick the latest value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - -- urls - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - -- device - initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - - -- geoip - -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment - initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- attribution - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- Count pageview, autocapture, and screen events for providing totals. - -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were - -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be - -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to - -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're - -- satisfied that counts are accurate. - pageview_count SimpleAggregateFunction(sum, Int64), - pageview_uniq AggregateFunction(uniq, Nullable(UUID)), - autocapture_count SimpleAggregateFunction(sum, Int64), - autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), - screen_count SimpleAggregateFunction(sum, Int64), - screen_uniq AggregateFunction(uniq, Nullable(UUID)), - - -- replay - maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv - - -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these - page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), - - -- web vitals - vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_raw_sessions', cityHash64(session_id_v7)) - - ''' -# --- -# name: test_create_table_query[raw_sessions_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS raw_sessions_mv ON CLUSTER 'posthog' - TO posthog_test.writable_raw_sessions - AS - - SELECT - team_id, - toUInt128(toUUID(`$session_id`)) as session_id_v7, - - argMaxState(distinct_id, timestamp) as distinct_id, - - min(timestamp) AS min_timestamp, - max(timestamp) AS max_timestamp, - - -- urls - groupUniqArray(nullIf(JSONExtractString(properties, '$current_url'), '')) AS urls, - argMinState(JSONExtractString(properties, '$current_url'), timestamp) as entry_url, - argMaxState(JSONExtractString(properties, '$current_url'), timestamp) as end_url, - argMaxState(JSONExtractString(properties, '$external_click_url'), timestamp) as last_external_click_url, - - -- device - argMinState(JSONExtractString(properties, '$browser'), timestamp) as initial_browser, - argMinState(JSONExtractString(properties, '$browser_version'), timestamp) as initial_browser_version, - argMinState(JSONExtractString(properties, '$os'), timestamp) as initial_os, - argMinState(JSONExtractString(properties, '$os_version'), timestamp) as initial_os_version, - argMinState(JSONExtractString(properties, '$device_type'), timestamp) as initial_device_type, - argMinState(JSONExtractInt(properties, '$viewport_width'), timestamp) as initial_viewport_width, - argMinState(JSONExtractInt(properties, '$viewport_height'), timestamp) as initial_viewport_height, - - -- geoip - argMinState(JSONExtractString(properties, '$geoip_country_code'), timestamp) as initial_geoip_country_code, - argMinState(JSONExtractString(properties, '$geoip_subdivision_1_code'), timestamp) as initial_geoip_subdivision_1_code, - argMinState(JSONExtractString(properties, '$geoip_subdivision_1_name'), timestamp) as initial_geoip_subdivision_1_name, - argMinState(JSONExtractString(properties, '$geoip_subdivision_city_name'), timestamp) as initial_geoip_subdivision_city_name, - argMinState(JSONExtractString(properties, '$geoip_time_zone'), timestamp) as initial_geoip_time_zone, - - -- attribution - argMinState(JSONExtractString(properties, '$referring_domain'), timestamp) as initial_referring_domain, - argMinState(JSONExtractString(properties, 'utm_source'), timestamp) as initial_utm_source, - argMinState(JSONExtractString(properties, 'utm_campaign'), timestamp) as initial_utm_campaign, - argMinState(JSONExtractString(properties, 'utm_medium'), timestamp) as initial_utm_medium, - argMinState(JSONExtractString(properties, 'utm_term'), timestamp) as initial_utm_term, - argMinState(JSONExtractString(properties, 'utm_content'), timestamp) as initial_utm_content, - argMinState(JSONExtractString(properties, 'gclid'), timestamp) as initial_gclid, - argMinState(JSONExtractString(properties, 'gad_source'), timestamp) as initial_gad_source, - argMinState(JSONExtractString(properties, 'gclsrc'), timestamp) as initial_gclsrc, - argMinState(JSONExtractString(properties, 'dclid'), timestamp) as initial_dclid, - argMinState(JSONExtractString(properties, 'gbraid'), timestamp) as initial_gbraid, - argMinState(JSONExtractString(properties, 'wbraid'), timestamp) as initial_wbraid, - argMinState(JSONExtractString(properties, 'fbclid'), timestamp) as initial_fbclid, - argMinState(JSONExtractString(properties, 'msclkid'), timestamp) as initial_msclkid, - argMinState(JSONExtractString(properties, 'twclid'), timestamp) as initial_twclid, - argMinState(JSONExtractString(properties, 'li_fat_id'), timestamp) as initial_li_fat_id, - argMinState(JSONExtractString(properties, 'mc_cid'), timestamp) as initial_mc_cid, - argMinState(JSONExtractString(properties, 'igshid'), timestamp) as initial_igshid, - argMinState(JSONExtractString(properties, 'ttclid'), timestamp) as initial_ttclid, - - -- count - sumIf(1, event='$pageview') as pageview_count, - uniqState(CAST(if(event='$pageview', uuid, NULL) AS Nullable(UUID))) as pageview_uniq, - sumIf(1, event='$autocapture') as autocapture_count, - uniqState(CAST(if(event='$autocapture', uuid, NULL) AS Nullable(UUID))) as autocapture_uniq, - sumIf(1, event='$screen') as screen_count, - uniqState(CAST(if(event='$screen', uuid, NULL) AS Nullable(UUID))) as screen_uniq, - - -- replay - false as maybe_has_session_replay, - - -- perf - uniqUpToState(1)(CAST(if(event='$pageview' OR event='$screen' OR event='$autocapture', uuid, NULL) AS Nullable(UUID))) as page_screen_autocapture_uniq_up_to, - - -- web vitals - argMinState(accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$web_vitals_LCP_value'), ''), 'null'), '^"|"$', ''), 'Float64'), timestamp) as vitals_lcp - FROM posthog_test.sharded_events - WHERE bitAnd(bitShiftRight(toUInt128(accurateCastOrNull(`$session_id`, 'UUID')), 76), 0xF) == 7 -- has a session id and is valid uuidv7) - GROUP BY - team_id, - toStartOfHour(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))), - cityHash64(session_id_v7), - session_id_v7 - - - ''' -# --- -# name: test_create_table_query[session_recording_events] - ''' - - CREATE TABLE IF NOT EXISTS session_recording_events ON CLUSTER 'posthog' - ( - uuid UUID, - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - session_id VARCHAR, - window_id VARCHAR, - snapshot_data VARCHAR, - created_at DateTime64(6, 'UTC') - , has_full_snapshot Int8 COMMENT 'column_materializer::has_full_snapshot', events_summary Array(String) COMMENT 'column_materializer::events_summary', click_count Int8 COMMENT 'column_materializer::click_count', keypress_count Int8 COMMENT 'column_materializer::keypress_count', timestamps_summary Array(DateTime64(6, 'UTC')) COMMENT 'column_materializer::timestamps_summary', first_event_timestamp Nullable(DateTime64(6, 'UTC')) COMMENT 'column_materializer::first_event_timestamp', last_event_timestamp Nullable(DateTime64(6, 'UTC')) COMMENT 'column_materializer::last_event_timestamp', urls Array(String) COMMENT 'column_materializer::urls' - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_session_recording_events', sipHash64(distinct_id)) - - ''' -# --- -# name: test_create_table_query[session_recording_events_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS session_recording_events_mv ON CLUSTER 'posthog' - TO posthog_test.writable_session_recording_events - AS SELECT - uuid, - timestamp, - team_id, - distinct_id, - session_id, - window_id, - snapshot_data, - created_at, - _timestamp, - _offset - FROM posthog_test.kafka_session_recording_events - - ''' -# --- -# name: test_create_table_query[session_replay_events] - ''' - - CREATE TABLE IF NOT EXISTS session_replay_events ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id VARCHAR, - min_first_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_last_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - -- store the first url of the session so we can quickly show that in playlists - first_url AggregateFunction(argMin, Nullable(VARCHAR), DateTime64(6, 'UTC')), - -- but also store each url so we can query by visited page without having to scan all events - -- despite the name we can put mobile screens in here as well to give same functionality across platforms - all_urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - click_count SimpleAggregateFunction(sum, Int64), - keypress_count SimpleAggregateFunction(sum, Int64), - mouse_activity_count SimpleAggregateFunction(sum, Int64), - active_milliseconds SimpleAggregateFunction(sum, Int64), - console_log_count SimpleAggregateFunction(sum, Int64), - console_warn_count SimpleAggregateFunction(sum, Int64), - console_error_count SimpleAggregateFunction(sum, Int64), - -- this column allows us to estimate the amount of data that is being ingested - size SimpleAggregateFunction(sum, Int64), - -- this allows us to count the number of messages received in a session - -- often very useful in incidents or debugging - message_count SimpleAggregateFunction(sum, Int64), - -- this allows us to count the number of snapshot events received in a session - -- often very useful in incidents or debugging - -- because we batch events we expect message_count to be lower than event_count - event_count SimpleAggregateFunction(sum, Int64), - -- which source the snapshots came from Mobile or Web. Web if absent - snapshot_source AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), - -- knowing something is mobile isn't enough, we need to know if e.g. RN or flutter - snapshot_library AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), - _timestamp SimpleAggregateFunction(max, DateTime) - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_session_replay_events', sipHash64(distinct_id)) - - ''' -# --- -# name: test_create_table_query[session_replay_events_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS session_replay_events_mv ON CLUSTER 'posthog' - TO posthog_test.writable_session_replay_events ( - `session_id` String, `team_id` Int64, `distinct_id` String, - `min_first_timestamp` DateTime64(6, 'UTC'), - `max_last_timestamp` DateTime64(6, 'UTC'), - `first_url` AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), - `all_urls` SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - `click_count` Int64, `keypress_count` Int64, - `mouse_activity_count` Int64, `active_milliseconds` Int64, - `console_log_count` Int64, `console_warn_count` Int64, - `console_error_count` Int64, `size` Int64, `message_count` Int64, - `event_count` Int64, - `snapshot_source` AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), - `snapshot_library` AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), - `_timestamp` Nullable(DateTime) - ) - AS SELECT - session_id, - team_id, - any(distinct_id) as distinct_id, - min(first_timestamp) AS min_first_timestamp, - max(last_timestamp) AS max_last_timestamp, - -- TRICKY: ClickHouse will pick a relatively random first_url - -- when it collapses the aggregating merge tree - -- unless we teach it what we want... - -- argMin ignores null values - -- so this will get the first non-null value of first_url - -- for each group of session_id and team_id - -- by min of first_timestamp in the batch - -- this is an aggregate function, not a simple aggregate function - -- so we have to write to argMinState, and query with argMinMerge - argMinState(first_url, first_timestamp) as first_url, - groupUniqArrayArray(urls) as all_urls, - sum(click_count) as click_count, - sum(keypress_count) as keypress_count, - sum(mouse_activity_count) as mouse_activity_count, - sum(active_milliseconds) as active_milliseconds, - sum(console_log_count) as console_log_count, - sum(console_warn_count) as console_warn_count, - sum(console_error_count) as console_error_count, - sum(size) as size, - -- we can count the number of kafka messages instead of sending it explicitly - sum(message_count) as message_count, - sum(event_count) as event_count, - argMinState(snapshot_source, first_timestamp) as snapshot_source, - argMinState(snapshot_library, first_timestamp) as snapshot_library, - max(_timestamp) as _timestamp - FROM posthog_test.kafka_session_replay_events - group by session_id, team_id - - ''' -# --- -# name: test_create_table_query[sessions] - ''' - - CREATE TABLE IF NOT EXISTS sessions ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id SimpleAggregateFunction(any, String), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- create a map of how many times we saw each event - event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), - -- duplicate the event count as a specific column for pageviews and autocaptures, - -- as these are used in some key queries and need to be fast - pageview_count SimpleAggregateFunction(sum, Int64), - autocapture_count SimpleAggregateFunction(sum, Int64), - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_sessions', sipHash64(session_id)) - - ''' -# --- -# name: test_create_table_query[sessions_mv] - ''' - - CREATE MATERIALIZED VIEW IF NOT EXISTS sessions_mv ON CLUSTER 'posthog' - TO posthog_test.writable_sessions - AS - - SELECT - - `$session_id` as session_id, - team_id, - - -- it doesn't matter which distinct_id gets picked (it'll be somewhat random) as they can all join to the right person - any(distinct_id) as distinct_id, - - min(timestamp) AS min_timestamp, - max(timestamp) AS max_timestamp, - - groupUniqArray(replaceRegexpAll(JSONExtractRaw(properties, '$current_url'), '^"|"$', '')) AS urls, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, '$current_url'), '^"|"$', ''), timestamp) as entry_url, - argMaxState(replaceRegexpAll(JSONExtractRaw(properties, '$current_url'), '^"|"$', ''), timestamp) as exit_url, - - argMinState(replaceRegexpAll(JSONExtractRaw(properties, '$referring_domain'), '^"|"$', ''), timestamp) as initial_referring_domain, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_source'), '^"|"$', ''), timestamp) as initial_utm_source, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_campaign'), '^"|"$', ''), timestamp) as initial_utm_campaign, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_medium'), '^"|"$', ''), timestamp) as initial_utm_medium, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_term'), '^"|"$', ''), timestamp) as initial_utm_term, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_content'), '^"|"$', ''), timestamp) as initial_utm_content, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gclid'), '^"|"$', ''), timestamp) as initial_gclid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gad_source'), '^"|"$', ''), timestamp) as initial_gad_source, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gclsrc'), '^"|"$', ''), timestamp) as initial_gclsrc, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'dclid'), '^"|"$', ''), timestamp) as initial_dclid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gbraid'), '^"|"$', ''), timestamp) as initial_gbraid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'wbraid'), '^"|"$', ''), timestamp) as initial_wbraid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'fbclid'), '^"|"$', ''), timestamp) as initial_fbclid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'msclkid'), '^"|"$', ''), timestamp) as initial_msclkid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'twclid'), '^"|"$', ''), timestamp) as initial_twclid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'li_fat_id'), '^"|"$', ''), timestamp) as initial_li_fat_id, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'mc_cid'), '^"|"$', ''), timestamp) as initial_mc_cid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'igshid'), '^"|"$', ''), timestamp) as initial_igshid, - argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'ttclid'), '^"|"$', ''), timestamp) as initial_ttclid, - - sumMap(CAST(([event], [1]), 'Map(String, UInt64)')) as event_count_map, - sumIf(1, event='$pageview') as pageview_count, - sumIf(1, event='$autocapture') as autocapture_count - - FROM posthog_test.sharded_events - WHERE `$session_id` IS NOT NULL AND `$session_id` != '' AND team_id IN (1, 2, 13610, 19279, 21173, 29929, 32050, 9910, 11775, 21129, 31490) - GROUP BY `$session_id`, team_id - - - ''' -# --- -# name: test_create_table_query[sharded_app_metrics2] - ''' - - CREATE TABLE IF NOT EXISTS sharded_app_metrics2 ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - -- The name of the service or product that generated the metrics. - -- Examples: plugins, hog - app_source LowCardinality(String), - -- An id for the app source. - -- Set app_source to avoid collision with ids from other app sources if the id generation is not safe. - -- Examples: A plugin id, a hog application id - app_source_id String, - -- A secondary id e.g. for the instance of app_source that generated this metric. - -- This may be ommitted if app_source is a singleton. - -- Examples: A plugin config id, a hog application config id - instance_id String, - metric_kind LowCardinality(String), - metric_name LowCardinality(String), - count SimpleAggregateFunction(sum, Int64) - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) - ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics2', '{replica}') - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, app_source, app_source_id, instance_id, toStartOfHour(timestamp), metric_kind, metric_name) - - - ''' -# --- -# name: test_create_table_query[sharded_app_metrics] - ''' - - CREATE TABLE IF NOT EXISTS sharded_app_metrics ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - plugin_config_id Int64, - category LowCardinality(String), - job_id String, - successes SimpleAggregateFunction(sum, Int64), - successes_on_retry SimpleAggregateFunction(sum, Int64), - failures SimpleAggregateFunction(sum, Int64), - error_uuid UUID, - error_type String, - error_details String CODEC(ZSTD(3)) - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) - ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics', '{replica}') - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, plugin_config_id, job_id, category, toStartOfHour(timestamp), error_type, error_uuid) - - ''' -# --- -# name: test_create_table_query[sharded_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_events ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - , $group_0 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_0'), '^"|"$', '') COMMENT 'column_materializer::$group_0' - , $group_1 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_1'), '^"|"$', '') COMMENT 'column_materializer::$group_1' - , $group_2 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_2'), '^"|"$', '') COMMENT 'column_materializer::$group_2' - , $group_3 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_3'), '^"|"$', '') COMMENT 'column_materializer::$group_3' - , $group_4 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_4'), '^"|"$', '') COMMENT 'column_materializer::$group_4' - , $window_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$window_id'), '^"|"$', '') COMMENT 'column_materializer::$window_id' - , $session_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$session_id'), '^"|"$', '') COMMENT 'column_materializer::$session_id' - , elements_chain_href String MATERIALIZED extract(elements_chain, '(?::|")href="(.*?)"') - , elements_chain_texts Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")text="(.*?)"')) - , elements_chain_ids Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")attr_id="(.*?)"')) - , elements_chain_elements Array(Enum('a', 'button', 'form', 'input', 'select', 'textarea', 'label')) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?:^|;)(a|button|form|input|select|textarea|label)(?:\.|$|:)')) - , INDEX `minmax_$group_0` `$group_0` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_1` `$group_1` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_2` `$group_2` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_3` `$group_3` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_4` `$group_4` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$window_id` `$window_id` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$session_id` `$session_id` TYPE minmax GRANULARITY 1 - , properties_group_custom Map(String, String) - MATERIALIZED mapSort( - mapFilter((key, _) -> key NOT LIKE '$%' AND key NOT IN ('token', 'distinct_id', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gclid', 'gad_source', 'gclsrc', 'dclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'twclid', 'li_fat_id', 'mc_cid', 'igshid', 'ttclid', 'rdt_cid'), - CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) - ) - CODEC(ZSTD(1)) - , INDEX properties_group_custom_keys_bf mapKeys(properties_group_custom) TYPE bloom_filter, INDEX properties_group_custom_values_bf mapValues(properties_group_custom) TYPE bloom_filter, properties_group_feature_flags Map(String, String) - MATERIALIZED mapSort( - mapFilter((key, _) -> key like '$feature/%', - CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) - ) - CODEC(ZSTD(1)) - , INDEX properties_group_feature_flags_keys_bf mapKeys(properties_group_feature_flags) TYPE bloom_filter, INDEX properties_group_feature_flags_values_bf mapValues(properties_group_feature_flags) TYPE bloom_filter - - - , _timestamp DateTime - , _offset UInt64 - , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64() - - , INDEX kafka_timestamp_minmax_sharded_events _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.events', '{replica}', _timestamp) - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid)) - SAMPLE BY cityHash64(distinct_id) - - - ''' -# --- -# name: test_create_table_query[sharded_heatmaps] - ''' - - CREATE TABLE IF NOT EXISTS sharded_heatmaps ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - timestamp DateTime64(6, 'UTC'), - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x Int16, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y Int16, - -- stored so that in future we can support other resolutions - scale_factor Int16, - viewport_width Int16, - viewport_height Int16, - -- some elements move when the page scrolls, others do not - pointer_target_fixed Bool, - current_url VARCHAR, - type LowCardinality(String), - _timestamp DateTime, - _offset UInt64, - _partition UInt64 - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.heatmaps', '{replica}') - - PARTITION BY toYYYYMM(timestamp) - -- almost always this is being queried by - -- * type, - -- * team_id, - -- * date range, - -- * URL (maybe matching wild cards), - -- * width - -- we'll almost never query this by session id - -- so from least to most cardinality that's - ORDER BY (type, team_id, toDate(timestamp), current_url, viewport_width) - - -- I am purposefully not setting index granularity - -- the default is 8192, and we will be loading a lot of data - -- per query, we tend to copy this 512 around the place but - -- i don't think it applies here - - ''' -# --- -# name: test_create_table_query[sharded_ingestion_warnings] - ''' - - CREATE TABLE IF NOT EXISTS sharded_ingestion_warnings ON CLUSTER 'posthog' - ( - team_id Int64, - source LowCardinality(VARCHAR), - type VARCHAR, - details VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC') - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_ingestion_warnings', '{replica}') - PARTITION BY toYYYYMMDD(timestamp) - ORDER BY (team_id, toHour(timestamp), type, source, timestamp) - - ''' -# --- -# name: test_create_table_query[sharded_performance_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_performance_events ON CLUSTER 'posthog' - ( - uuid UUID, - session_id String, - window_id String, - pageview_id String, - distinct_id String, - timestamp DateTime64, - time_origin DateTime64(3, 'UTC'), - entry_type LowCardinality(String), - name String, - team_id Int64, - current_url String, - start_time Float64, - duration Float64, - redirect_start Float64, - redirect_end Float64, - worker_start Float64, - fetch_start Float64, - domain_lookup_start Float64, - domain_lookup_end Float64, - connect_start Float64, - secure_connection_start Float64, - connect_end Float64, - request_start Float64, - response_start Float64, - response_end Float64, - decoded_body_size Int64, - encoded_body_size Int64, - initiator_type LowCardinality(String), - next_hop_protocol LowCardinality(String), - render_blocking_status LowCardinality(String), - response_status Int64, - transfer_size Int64, - largest_contentful_paint_element String, - largest_contentful_paint_render_time Float64, - largest_contentful_paint_load_time Float64, - largest_contentful_paint_size Float64, - largest_contentful_paint_id String, - largest_contentful_paint_url String, - dom_complete Float64, - dom_content_loaded_event Float64, - dom_interactive Float64, - load_event_end Float64, - load_event_start Float64, - redirect_count Int64, - navigation_type LowCardinality(String), - unload_event_end Float64, - unload_event_start Float64 - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.performance_events', '{replica}') - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, toDate(timestamp), session_id, pageview_id, timestamp) - - - - ''' -# --- -# name: test_create_table_query[sharded_raw_sessions] - ''' - - CREATE TABLE IF NOT EXISTS sharded_raw_sessions ON CLUSTER 'posthog' - ( - team_id Int64, - session_id_v7 UInt128, -- integer representation of a uuidv7 - - -- ClickHouse will pick the latest value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - -- urls - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - -- device - initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - - -- geoip - -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment - initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- attribution - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- Count pageview, autocapture, and screen events for providing totals. - -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were - -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be - -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to - -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're - -- satisfied that counts are accurate. - pageview_count SimpleAggregateFunction(sum, Int64), - pageview_uniq AggregateFunction(uniq, Nullable(UUID)), - autocapture_count SimpleAggregateFunction(sum, Int64), - autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), - screen_count SimpleAggregateFunction(sum, Int64), - screen_uniq AggregateFunction(uniq, Nullable(UUID)), - - -- replay - maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv - - -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these - page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), - - -- web vitals - vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) - ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.raw_sessions', '{replica}') - - PARTITION BY toYYYYMM(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))) - ORDER BY ( - team_id, - toStartOfHour(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))), - cityHash64(session_id_v7), - session_id_v7 - ) - SAMPLE BY cityHash64(session_id_v7) - - ''' -# --- -# name: test_create_table_query[sharded_session_recording_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_session_recording_events ON CLUSTER 'posthog' - ( - uuid UUID, - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - session_id VARCHAR, - window_id VARCHAR, - snapshot_data VARCHAR, - created_at DateTime64(6, 'UTC') - , has_full_snapshot Int8 MATERIALIZED JSONExtractBool(snapshot_data, 'has_full_snapshot'), events_summary Array(String) MATERIALIZED JSONExtract(JSON_QUERY(snapshot_data, '$.events_summary[*]'), 'Array(String)'), click_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 2, events_summary)), keypress_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 5, events_summary)), timestamps_summary Array(DateTime64(6, 'UTC')) MATERIALIZED arraySort(arrayMap((x) -> toDateTime(JSONExtractInt(x, 'timestamp') / 1000), events_summary)), first_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('min', timestamps_summary)), last_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('max', timestamps_summary)), urls Array(String) MATERIALIZED arrayFilter(x -> x != '', arrayMap((x) -> JSONExtractString(x, 'data', 'href'), events_summary)) - - - , _timestamp DateTime - , _offset UInt64 - - , INDEX kafka_timestamp_minmax_sharded_session_recording_events _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_recording_events', '{replica}', _timestamp) - PARTITION BY toYYYYMMDD(timestamp) - ORDER BY (team_id, toHour(timestamp), session_id, timestamp, uuid) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query[sharded_session_replay_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_session_replay_events ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id VARCHAR, - min_first_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_last_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - -- store the first url of the session so we can quickly show that in playlists - first_url AggregateFunction(argMin, Nullable(VARCHAR), DateTime64(6, 'UTC')), - -- but also store each url so we can query by visited page without having to scan all events - -- despite the name we can put mobile screens in here as well to give same functionality across platforms - all_urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - click_count SimpleAggregateFunction(sum, Int64), - keypress_count SimpleAggregateFunction(sum, Int64), - mouse_activity_count SimpleAggregateFunction(sum, Int64), - active_milliseconds SimpleAggregateFunction(sum, Int64), - console_log_count SimpleAggregateFunction(sum, Int64), - console_warn_count SimpleAggregateFunction(sum, Int64), - console_error_count SimpleAggregateFunction(sum, Int64), - -- this column allows us to estimate the amount of data that is being ingested - size SimpleAggregateFunction(sum, Int64), - -- this allows us to count the number of messages received in a session - -- often very useful in incidents or debugging - message_count SimpleAggregateFunction(sum, Int64), - -- this allows us to count the number of snapshot events received in a session - -- often very useful in incidents or debugging - -- because we batch events we expect message_count to be lower than event_count - event_count SimpleAggregateFunction(sum, Int64), - -- which source the snapshots came from Mobile or Web. Web if absent - snapshot_source AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), - -- knowing something is mobile isn't enough, we need to know if e.g. RN or flutter - snapshot_library AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), - _timestamp SimpleAggregateFunction(max, DateTime) - ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_replay_events', '{replica}') - - PARTITION BY toYYYYMM(min_first_timestamp) - -- order by is used by the aggregating merge tree engine to - -- identify candidates to merge, e.g. toDate(min_first_timestamp) - -- would mean we would have one row per day per session_id - -- if CH could completely merge to match the order by - -- it is also used to organise data to make queries faster - -- we want the fewest rows possible but also the fastest queries - -- since we query by date and not by time - -- and order by must be in order of increasing cardinality - -- so we order by date first, then team_id, then session_id - -- hopefully, this is a good balance between the two - ORDER BY (toDate(min_first_timestamp), team_id, session_id) - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query[sharded_sessions] - ''' - - CREATE TABLE IF NOT EXISTS sharded_sessions ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id SimpleAggregateFunction(any, String), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- create a map of how many times we saw each event - event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), - -- duplicate the event count as a specific column for pageviews and autocaptures, - -- as these are used in some key queries and need to be fast - pageview_count SimpleAggregateFunction(sum, Int64), - autocapture_count SimpleAggregateFunction(sum, Int64), - ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sessions', '{replica}') - - PARTITION BY toYYYYMM(min_timestamp) - -- order by is used by the aggregating merge tree engine to - -- identify candidates to merge, e.g. toDate(min_timestamp) - -- would mean we would have one row per day per session_id - -- if CH could completely merge to match the order by - -- it is also used to organise data to make queries faster - -- we want the fewest rows possible but also the fastest queries - -- since we query by date and not by time - -- and order by must be in order of increasing cardinality - -- so we order by date first, then team_id, then session_id - -- hopefully, this is a good balance between the two - ORDER BY (toStartOfDay(min_timestamp), team_id, session_id) - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query[writable_events] - ''' - - CREATE TABLE IF NOT EXISTS writable_events ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - , _timestamp DateTime - , _offset UInt64 - - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_events', sipHash64(distinct_id)) - - ''' -# --- -# name: test_create_table_query[writable_heatmaps] - ''' - - CREATE TABLE IF NOT EXISTS writable_heatmaps ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - timestamp DateTime64(6, 'UTC'), - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x Int16, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y Int16, - -- stored so that in future we can support other resolutions - scale_factor Int16, - viewport_width Int16, - viewport_height Int16, - -- some elements move when the page scrolls, others do not - pointer_target_fixed Bool, - current_url VARCHAR, - type LowCardinality(String), - _timestamp DateTime, - _offset UInt64, - _partition UInt64 - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_heatmaps', cityHash64(concat(toString(team_id), '-', session_id, '-', toString(toDate(timestamp))))) - - ''' -# --- -# name: test_create_table_query[writable_raw_sessions] - ''' - - CREATE TABLE IF NOT EXISTS writable_raw_sessions ON CLUSTER 'posthog' - ( - team_id Int64, - session_id_v7 UInt128, -- integer representation of a uuidv7 - - -- ClickHouse will pick the latest value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - -- urls - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - -- device - initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - - -- geoip - -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment - initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- attribution - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- Count pageview, autocapture, and screen events for providing totals. - -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were - -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be - -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to - -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're - -- satisfied that counts are accurate. - pageview_count SimpleAggregateFunction(sum, Int64), - pageview_uniq AggregateFunction(uniq, Nullable(UUID)), - autocapture_count SimpleAggregateFunction(sum, Int64), - autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), - screen_count SimpleAggregateFunction(sum, Int64), - screen_uniq AggregateFunction(uniq, Nullable(UUID)), - - -- replay - maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv - - -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these - page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), - - -- web vitals - vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_raw_sessions', cityHash64(session_id_v7)) - - ''' -# --- -# name: test_create_table_query[writable_session_recording_events] - ''' - - CREATE TABLE IF NOT EXISTS writable_session_recording_events ON CLUSTER 'posthog' - ( - uuid UUID, - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - session_id VARCHAR, - window_id VARCHAR, - snapshot_data VARCHAR, - created_at DateTime64(6, 'UTC') - - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_session_recording_events', sipHash64(distinct_id)) - - ''' -# --- -# name: test_create_table_query[writable_sessions] - ''' - - CREATE TABLE IF NOT EXISTS writable_sessions ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id SimpleAggregateFunction(any, String), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- create a map of how many times we saw each event - event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), - -- duplicate the event count as a specific column for pageviews and autocaptures, - -- as these are used in some key queries and need to be fast - pageview_count SimpleAggregateFunction(sum, Int64), - autocapture_count SimpleAggregateFunction(sum, Int64), - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_sessions', sipHash64(session_id)) - - ''' -# --- -# name: test_create_table_query[writeable_performance_events] - ''' - - CREATE TABLE IF NOT EXISTS writeable_performance_events ON CLUSTER 'posthog' - ( - uuid UUID, - session_id String, - window_id String, - pageview_id String, - distinct_id String, - timestamp DateTime64, - time_origin DateTime64(3, 'UTC'), - entry_type LowCardinality(String), - name String, - team_id Int64, - current_url String, - start_time Float64, - duration Float64, - redirect_start Float64, - redirect_end Float64, - worker_start Float64, - fetch_start Float64, - domain_lookup_start Float64, - domain_lookup_end Float64, - connect_start Float64, - secure_connection_start Float64, - connect_end Float64, - request_start Float64, - response_start Float64, - response_end Float64, - decoded_body_size Int64, - encoded_body_size Int64, - initiator_type LowCardinality(String), - next_hop_protocol LowCardinality(String), - render_blocking_status LowCardinality(String), - response_status Int64, - transfer_size Int64, - largest_contentful_paint_element String, - largest_contentful_paint_render_time Float64, - largest_contentful_paint_load_time Float64, - largest_contentful_paint_size Float64, - largest_contentful_paint_id String, - largest_contentful_paint_url String, - dom_complete Float64, - dom_content_loaded_event Float64, - dom_interactive Float64, - load_event_end Float64, - load_event_start Float64, - redirect_count Int64, - navigation_type LowCardinality(String), - unload_event_end Float64, - unload_event_start Float64 - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_performance_events', sipHash64(session_id)) - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[channel_definition] - ''' - - CREATE TABLE IF NOT EXISTS channel_definition ON CLUSTER 'posthog' ( - domain String NOT NULL, - kind String NOT NULL, - domain_type String NULL, - type_if_paid String NULL, - type_if_organic String NULL - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.channel_definition', '{replica}-{shard}') - ORDER BY (domain, kind); - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[cohortpeople] - ''' - - CREATE TABLE IF NOT EXISTS cohortpeople ON CLUSTER 'posthog' - ( - person_id UUID, - cohort_id Int64, - team_id Int64, - sign Int8, - version UInt64 - ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.cohortpeople', '{replica}-{shard}', sign) - Order By (team_id, cohort_id, person_id, version) - - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[error_tracking_issue_fingerprint_overrides] - ''' - - CREATE TABLE IF NOT EXISTS error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - fingerprint VARCHAR, - issue_id UUID, - is_deleted Int8, - version Int64 - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - , INDEX kafka_timestamp_minmax_error_tracking_issue_fingerprint_overrides _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.error_tracking_issue_fingerprint_overrides', '{replica}-{shard}', version) - - ORDER BY (team_id, fingerprint) - SETTINGS index_granularity = 512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[events_dead_letter_queue] - ''' - - CREATE TABLE IF NOT EXISTS events_dead_letter_queue ON CLUSTER 'posthog' - ( - id UUID, - event_uuid UUID, - event VARCHAR, - properties VARCHAR, - distinct_id VARCHAR, - team_id Int64, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - ip VARCHAR, - site_url VARCHAR, - now DateTime64(6, 'UTC'), - raw_payload VARCHAR, - error_timestamp DateTime64(6, 'UTC'), - error_location VARCHAR, - error VARCHAR, - tags Array(VARCHAR) - - - , _timestamp DateTime - , _offset UInt64 - - , INDEX kafka_timestamp_minmax_events_dead_letter_queue _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_dead_letter_queue', '{replica}-{shard}', _timestamp) - ORDER BY (id, event_uuid, distinct_id, team_id) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[events_recent] - ''' - - CREATE TABLE IF NOT EXISTS events_recent ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - , inserted_at DateTime64(6, 'UTC') DEFAULT NOW64(), _timestamp_ms DateTime64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_recent', '{replica}-{shard}', _timestamp) - PARTITION BY toStartOfHour(inserted_at) - ORDER BY (team_id, toStartOfHour(inserted_at), event, cityHash64(distinct_id), cityHash64(uuid)) - TTL toDateTime(inserted_at) + INTERVAL 7 DAY - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[groups] - ''' - - CREATE TABLE IF NOT EXISTS groups ON CLUSTER 'posthog' - ( - group_type_index UInt8, - group_key VARCHAR, - created_at DateTime64, - team_id Int64, - group_properties VARCHAR - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.groups', '{replica}-{shard}', _timestamp) - Order By (team_id, group_type_index, group_key) - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[log_entries] - ''' - - CREATE TABLE IF NOT EXISTS log_entries ON CLUSTER 'posthog' - ( - team_id UInt64, - -- The name of the service or product that generated the logs. - -- Examples: batch_exports - log_source LowCardinality(String), - -- An id for the log source. - -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. - -- Examples: A batch export id, a cronjob id, a plugin id. - log_source_id String, - -- A secondary id e.g. for the instance of log_source that generated this log. - -- This may be ommitted if log_source is a singleton. - -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. - instance_id String, - -- Timestamp indicating when the log was generated. - timestamp DateTime64(6, 'UTC'), - -- The log level. - -- Examples: INFO, WARNING, DEBUG, ERROR. - level LowCardinality(String), - -- The actual log message. - message String - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.log_entries', '{replica}-{shard}', _timestamp) - PARTITION BY toStartOfHour(timestamp) ORDER BY (team_id, log_source, log_source_id, instance_id, timestamp) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[person] - ''' - - CREATE TABLE IF NOT EXISTS person ON CLUSTER 'posthog' - ( - id UUID, - created_at DateTime64, - team_id Int64, - properties VARCHAR, - is_identified Int8, - is_deleted Int8, - version UInt64 - - - , _timestamp DateTime - , _offset UInt64 - - , INDEX kafka_timestamp_minmax_person _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person', '{replica}-{shard}', version) - Order By (team_id, id) - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[person_distinct_id2] - ''' - - CREATE TABLE IF NOT EXISTS person_distinct_id2 ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - - , _timestamp DateTime - , _offset UInt64 - - , _partition UInt64 - , INDEX kafka_timestamp_minmax_person_distinct_id2 _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id2', '{replica}-{shard}', version) - - ORDER BY (team_id, distinct_id) - SETTINGS index_granularity = 512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[person_distinct_id] - ''' - - CREATE TABLE IF NOT EXISTS person_distinct_id ON CLUSTER 'posthog' - ( - distinct_id VARCHAR, - person_id UUID, - team_id Int64, - _sign Int8 DEFAULT 1, - is_deleted Int8 ALIAS if(_sign==-1, 1, 0) - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id', '{replica}-{shard}', _sign) - Order By (team_id, distinct_id, person_id) - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[person_distinct_id_overrides] - ''' - - CREATE TABLE IF NOT EXISTS person_distinct_id_overrides ON CLUSTER 'posthog' - ( - team_id Int64, - distinct_id VARCHAR, - person_id UUID, - is_deleted Int8, - version Int64 - - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - , INDEX kafka_timestamp_minmax_person_distinct_id_overrides _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id_overrides', '{replica}-{shard}', version) - - ORDER BY (team_id, distinct_id) - SETTINGS index_granularity = 512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[person_overrides] - ''' - - CREATE TABLE IF NOT EXISTS `posthog_test`.`person_overrides` - ON CLUSTER 'posthog' ( - team_id INT NOT NULL, - - -- When we merge two people `old_person_id` and `override_person_id`, we - -- want to keep track of a mapping from the `old_person_id` to the - -- `override_person_id`. This allows us to join with the - -- `sharded_events` table to find all events that were associated with - -- the `old_person_id` and update them to be associated with the - -- `override_person_id`. - old_person_id UUID NOT NULL, - override_person_id UUID NOT NULL, - - -- The timestamp the merge of the two people was completed. - merged_at DateTime64(6, 'UTC') NOT NULL, - -- The timestamp of the oldest event associated with the - -- `old_person_id`. - oldest_event DateTime64(6, 'UTC') NOT NULL, - -- The timestamp rows are created. This isn't part of the JOIN process - -- with the events table but rather a housekeeping column to allow us to - -- see when the row was created. This shouldn't have any impact of the - -- JOIN as it will be stored separately with the Wide ClickHouse table - -- storage. - created_at DateTime64(6, 'UTC') DEFAULT now(), - - -- the specific version of the `old_person_id` mapping. This is used to - -- allow us to discard old mappings as new ones are added. This version - -- will be provided by the corresponding PostgreSQL - --`posthog_personoverrides` table - version INT NOT NULL - ) - - -- By specifying Replacing merge tree on version, we allow ClickHouse to - -- discard old versions of a `old_person_id` mapping. This should help keep - -- performance in check as new versions are added. Note that given we can - -- have partitioning by `oldest_event` which will change as we update - -- `person_id` on old partitions. - -- - -- We also need to ensure that the data is replicated to all replicas in the - -- cluster, as we do not have any constraints on person_id and which shard - -- associated events are on. To do this we use the ReplicatedReplacingMergeTree - -- engine specifying a static `zk_path`. This will cause the Engine to - -- consider all replicas as the same. See - -- https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication - -- for details. - ENGINE = ReplicatedReplacingMergeTree( - -- NOTE: for testing we use a uuid to ensure that we don't get conflicts - -- when the tests tear down and recreate the table. - '/clickhouse/tables/{uuid}noshard/posthog_test.person_overrides', - '{replica}-{shard}', - version - ) - - -- We partition the table by the `oldest_event` column. This allows us to - -- handle updating the events table partition by partition, progressing each - -- override partition by partition in lockstep with the events table. Note - -- that this means it is possible that we have a mapping from - -- `old_person_id` in multiple partitions during the merge process. - PARTITION BY toYYYYMM(oldest_event) - - -- We want to collapse down on the `old_person_id` such that we end up with - -- the newest known mapping for it in the table. Query side we will need to - -- ensure that we are always querying the latest version of the mapping. - ORDER BY (team_id, old_person_id) - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[person_static_cohort] - ''' - - CREATE TABLE IF NOT EXISTS person_static_cohort ON CLUSTER 'posthog' - ( - id UUID, - person_id UUID, - cohort_id Int64, - team_id Int64 - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_static_cohort', '{replica}-{shard}', _timestamp) - Order By (team_id, cohort_id, person_id, id) - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[plugin_log_entries] - ''' - - CREATE TABLE IF NOT EXISTS plugin_log_entries ON CLUSTER 'posthog' - ( - id UUID, - team_id Int64, - plugin_id Int64, - plugin_config_id Int64, - timestamp DateTime64(6, 'UTC'), - source VARCHAR, - type VARCHAR, - message VARCHAR, - instance_id UUID - - , _timestamp DateTime - , _offset UInt64 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.plugin_log_entries', '{replica}-{shard}', _timestamp) - PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_app_metrics2] - ''' - - CREATE TABLE IF NOT EXISTS sharded_app_metrics2 ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - -- The name of the service or product that generated the metrics. - -- Examples: plugins, hog - app_source LowCardinality(String), - -- An id for the app source. - -- Set app_source to avoid collision with ids from other app sources if the id generation is not safe. - -- Examples: A plugin id, a hog application id - app_source_id String, - -- A secondary id e.g. for the instance of app_source that generated this metric. - -- This may be ommitted if app_source is a singleton. - -- Examples: A plugin config id, a hog application config id - instance_id String, - metric_kind LowCardinality(String), - metric_name LowCardinality(String), - count SimpleAggregateFunction(sum, Int64) - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) - ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics2', '{replica}') - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, app_source, app_source_id, instance_id, toStartOfHour(timestamp), metric_kind, metric_name) - - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_app_metrics] - ''' - - CREATE TABLE IF NOT EXISTS sharded_app_metrics ON CLUSTER 'posthog' - ( - team_id Int64, - timestamp DateTime64(6, 'UTC'), - plugin_config_id Int64, - category LowCardinality(String), - job_id String, - successes SimpleAggregateFunction(sum, Int64), - successes_on_retry SimpleAggregateFunction(sum, Int64), - failures SimpleAggregateFunction(sum, Int64), - error_uuid UUID, - error_type String, - error_details String CODEC(ZSTD(3)) - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) - ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics', '{replica}') - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, plugin_config_id, job_id, category, toStartOfHour(timestamp), error_type, error_uuid) - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_events ON CLUSTER 'posthog' - ( - uuid UUID, - event VARCHAR, - properties VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - elements_chain VARCHAR, - created_at DateTime64(6, 'UTC'), - person_id UUID, - person_created_at DateTime64, - person_properties VARCHAR Codec(ZSTD(3)), - group0_properties VARCHAR Codec(ZSTD(3)), - group1_properties VARCHAR Codec(ZSTD(3)), - group2_properties VARCHAR Codec(ZSTD(3)), - group3_properties VARCHAR Codec(ZSTD(3)), - group4_properties VARCHAR Codec(ZSTD(3)), - group0_created_at DateTime64, - group1_created_at DateTime64, - group2_created_at DateTime64, - group3_created_at DateTime64, - group4_created_at DateTime64, - person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) - - , $group_0 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_0'), '^"|"$', '') COMMENT 'column_materializer::$group_0' - , $group_1 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_1'), '^"|"$', '') COMMENT 'column_materializer::$group_1' - , $group_2 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_2'), '^"|"$', '') COMMENT 'column_materializer::$group_2' - , $group_3 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_3'), '^"|"$', '') COMMENT 'column_materializer::$group_3' - , $group_4 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_4'), '^"|"$', '') COMMENT 'column_materializer::$group_4' - , $window_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$window_id'), '^"|"$', '') COMMENT 'column_materializer::$window_id' - , $session_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$session_id'), '^"|"$', '') COMMENT 'column_materializer::$session_id' - , elements_chain_href String MATERIALIZED extract(elements_chain, '(?::|")href="(.*?)"') - , elements_chain_texts Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")text="(.*?)"')) - , elements_chain_ids Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")attr_id="(.*?)"')) - , elements_chain_elements Array(Enum('a', 'button', 'form', 'input', 'select', 'textarea', 'label')) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?:^|;)(a|button|form|input|select|textarea|label)(?:\.|$|:)')) - , INDEX `minmax_$group_0` `$group_0` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_1` `$group_1` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_2` `$group_2` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_3` `$group_3` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$group_4` `$group_4` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$window_id` `$window_id` TYPE minmax GRANULARITY 1 - , INDEX `minmax_$session_id` `$session_id` TYPE minmax GRANULARITY 1 - , properties_group_custom Map(String, String) - MATERIALIZED mapSort( - mapFilter((key, _) -> key NOT LIKE '$%' AND key NOT IN ('token', 'distinct_id', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gclid', 'gad_source', 'gclsrc', 'dclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'twclid', 'li_fat_id', 'mc_cid', 'igshid', 'ttclid', 'rdt_cid'), - CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) - ) - CODEC(ZSTD(1)) - , INDEX properties_group_custom_keys_bf mapKeys(properties_group_custom) TYPE bloom_filter, INDEX properties_group_custom_values_bf mapValues(properties_group_custom) TYPE bloom_filter, properties_group_feature_flags Map(String, String) - MATERIALIZED mapSort( - mapFilter((key, _) -> key like '$feature/%', - CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) - ) - CODEC(ZSTD(1)) - , INDEX properties_group_feature_flags_keys_bf mapKeys(properties_group_feature_flags) TYPE bloom_filter, INDEX properties_group_feature_flags_values_bf mapValues(properties_group_feature_flags) TYPE bloom_filter - - - , _timestamp DateTime - , _offset UInt64 - , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64() - - , INDEX kafka_timestamp_minmax_sharded_events _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.events', '{replica}', _timestamp) - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid)) - SAMPLE BY cityHash64(distinct_id) - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_heatmaps] - ''' - - CREATE TABLE IF NOT EXISTS sharded_heatmaps ON CLUSTER 'posthog' - ( - session_id VARCHAR, - team_id Int64, - distinct_id VARCHAR, - timestamp DateTime64(6, 'UTC'), - -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - x Int16, - -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid - y Int16, - -- stored so that in future we can support other resolutions - scale_factor Int16, - viewport_width Int16, - viewport_height Int16, - -- some elements move when the page scrolls, others do not - pointer_target_fixed Bool, - current_url VARCHAR, - type LowCardinality(String), - _timestamp DateTime, - _offset UInt64, - _partition UInt64 - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.heatmaps', '{replica}') - - PARTITION BY toYYYYMM(timestamp) - -- almost always this is being queried by - -- * type, - -- * team_id, - -- * date range, - -- * URL (maybe matching wild cards), - -- * width - -- we'll almost never query this by session id - -- so from least to most cardinality that's - ORDER BY (type, team_id, toDate(timestamp), current_url, viewport_width) - - -- I am purposefully not setting index granularity - -- the default is 8192, and we will be loading a lot of data - -- per query, we tend to copy this 512 around the place but - -- i don't think it applies here - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_ingestion_warnings] - ''' - - CREATE TABLE IF NOT EXISTS sharded_ingestion_warnings ON CLUSTER 'posthog' - ( - team_id Int64, - source LowCardinality(VARCHAR), - type VARCHAR, - details VARCHAR CODEC(ZSTD(3)), - timestamp DateTime64(6, 'UTC') - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_ingestion_warnings', '{replica}') - PARTITION BY toYYYYMMDD(timestamp) - ORDER BY (team_id, toHour(timestamp), type, source, timestamp) - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_performance_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_performance_events ON CLUSTER 'posthog' - ( - uuid UUID, - session_id String, - window_id String, - pageview_id String, - distinct_id String, - timestamp DateTime64, - time_origin DateTime64(3, 'UTC'), - entry_type LowCardinality(String), - name String, - team_id Int64, - current_url String, - start_time Float64, - duration Float64, - redirect_start Float64, - redirect_end Float64, - worker_start Float64, - fetch_start Float64, - domain_lookup_start Float64, - domain_lookup_end Float64, - connect_start Float64, - secure_connection_start Float64, - connect_end Float64, - request_start Float64, - response_start Float64, - response_end Float64, - decoded_body_size Int64, - encoded_body_size Int64, - initiator_type LowCardinality(String), - next_hop_protocol LowCardinality(String), - render_blocking_status LowCardinality(String), - response_status Int64, - transfer_size Int64, - largest_contentful_paint_element String, - largest_contentful_paint_render_time Float64, - largest_contentful_paint_load_time Float64, - largest_contentful_paint_size Float64, - largest_contentful_paint_id String, - largest_contentful_paint_url String, - dom_complete Float64, - dom_content_loaded_event Float64, - dom_interactive Float64, - load_event_end Float64, - load_event_start Float64, - redirect_count Int64, - navigation_type LowCardinality(String), - unload_event_end Float64, - unload_event_start Float64 - - , _timestamp DateTime - , _offset UInt64 - , _partition UInt64 - - ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.performance_events', '{replica}') - PARTITION BY toYYYYMM(timestamp) - ORDER BY (team_id, toDate(timestamp), session_id, pageview_id, timestamp) - - SETTINGS storage_policy = 'hot_to_cold' - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_raw_sessions] - ''' - - CREATE TABLE IF NOT EXISTS sharded_raw_sessions ON CLUSTER 'posthog' - ( - team_id Int64, - session_id_v7 UInt128, -- integer representation of a uuidv7 - - -- ClickHouse will pick the latest value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - -- urls - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - -- device - initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), - - -- geoip - -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment - initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- attribution - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- Count pageview, autocapture, and screen events for providing totals. - -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were - -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be - -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to - -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're - -- satisfied that counts are accurate. - pageview_count SimpleAggregateFunction(sum, Int64), - pageview_uniq AggregateFunction(uniq, Nullable(UUID)), - autocapture_count SimpleAggregateFunction(sum, Int64), - autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), - screen_count SimpleAggregateFunction(sum, Int64), - screen_uniq AggregateFunction(uniq, Nullable(UUID)), - - -- replay - maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv - - -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these - page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), - - -- web vitals - vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) - ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.raw_sessions', '{replica}') - - PARTITION BY toYYYYMM(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))) - ORDER BY ( - team_id, - toStartOfHour(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))), - cityHash64(session_id_v7), - session_id_v7 - ) - SAMPLE BY cityHash64(session_id_v7) - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_session_recording_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_session_recording_events ON CLUSTER 'posthog' - ( - uuid UUID, - timestamp DateTime64(6, 'UTC'), - team_id Int64, - distinct_id VARCHAR, - session_id VARCHAR, - window_id VARCHAR, - snapshot_data VARCHAR, - created_at DateTime64(6, 'UTC') - , has_full_snapshot Int8 MATERIALIZED JSONExtractBool(snapshot_data, 'has_full_snapshot'), events_summary Array(String) MATERIALIZED JSONExtract(JSON_QUERY(snapshot_data, '$.events_summary[*]'), 'Array(String)'), click_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 2, events_summary)), keypress_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 5, events_summary)), timestamps_summary Array(DateTime64(6, 'UTC')) MATERIALIZED arraySort(arrayMap((x) -> toDateTime(JSONExtractInt(x, 'timestamp') / 1000), events_summary)), first_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('min', timestamps_summary)), last_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('max', timestamps_summary)), urls Array(String) MATERIALIZED arrayFilter(x -> x != '', arrayMap((x) -> JSONExtractString(x, 'data', 'href'), events_summary)) - - - , _timestamp DateTime - , _offset UInt64 - - , INDEX kafka_timestamp_minmax_sharded_session_recording_events _timestamp TYPE minmax GRANULARITY 3 - - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_recording_events', '{replica}', _timestamp) - PARTITION BY toYYYYMMDD(timestamp) - ORDER BY (team_id, toHour(timestamp), session_id, timestamp, uuid) - - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_session_replay_events] - ''' - - CREATE TABLE IF NOT EXISTS sharded_session_replay_events ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id VARCHAR, - min_first_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_last_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - -- store the first url of the session so we can quickly show that in playlists - first_url AggregateFunction(argMin, Nullable(VARCHAR), DateTime64(6, 'UTC')), - -- but also store each url so we can query by visited page without having to scan all events - -- despite the name we can put mobile screens in here as well to give same functionality across platforms - all_urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - click_count SimpleAggregateFunction(sum, Int64), - keypress_count SimpleAggregateFunction(sum, Int64), - mouse_activity_count SimpleAggregateFunction(sum, Int64), - active_milliseconds SimpleAggregateFunction(sum, Int64), - console_log_count SimpleAggregateFunction(sum, Int64), - console_warn_count SimpleAggregateFunction(sum, Int64), - console_error_count SimpleAggregateFunction(sum, Int64), - -- this column allows us to estimate the amount of data that is being ingested - size SimpleAggregateFunction(sum, Int64), - -- this allows us to count the number of messages received in a session - -- often very useful in incidents or debugging - message_count SimpleAggregateFunction(sum, Int64), - -- this allows us to count the number of snapshot events received in a session - -- often very useful in incidents or debugging - -- because we batch events we expect message_count to be lower than event_count - event_count SimpleAggregateFunction(sum, Int64), - -- which source the snapshots came from Mobile or Web. Web if absent - snapshot_source AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), - -- knowing something is mobile isn't enough, we need to know if e.g. RN or flutter - snapshot_library AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), - _timestamp SimpleAggregateFunction(max, DateTime) - ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_replay_events', '{replica}') - - PARTITION BY toYYYYMM(min_first_timestamp) - -- order by is used by the aggregating merge tree engine to - -- identify candidates to merge, e.g. toDate(min_first_timestamp) - -- would mean we would have one row per day per session_id - -- if CH could completely merge to match the order by - -- it is also used to organise data to make queries faster - -- we want the fewest rows possible but also the fastest queries - -- since we query by date and not by time - -- and order by must be in order of increasing cardinality - -- so we order by date first, then team_id, then session_id - -- hopefully, this is a good balance between the two - ORDER BY (toDate(min_first_timestamp), team_id, session_id) - SETTINGS index_granularity=512 - - ''' -# --- -# name: test_create_table_query_replicated_and_storage[sharded_sessions] - ''' - - CREATE TABLE IF NOT EXISTS sharded_sessions ON CLUSTER 'posthog' - ( - -- part of order by so will aggregate correctly - session_id VARCHAR, - -- part of order by so will aggregate correctly - team_id Int64, - -- ClickHouse will pick any value of distinct_id for the session - -- this is fine since even if the distinct_id changes during a session - -- it will still (or should still) map to the same person - distinct_id SimpleAggregateFunction(any, String), - - min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), - max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), - - urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), - entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), - - initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), - - -- create a map of how many times we saw each event - event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), - -- duplicate the event count as a specific column for pageviews and autocaptures, - -- as these are used in some key queries and need to be fast - pageview_count SimpleAggregateFunction(sum, Int64), - autocapture_count SimpleAggregateFunction(sum, Int64), - ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sessions', '{replica}') - - PARTITION BY toYYYYMM(min_timestamp) - -- order by is used by the aggregating merge tree engine to - -- identify candidates to merge, e.g. toDate(min_timestamp) - -- would mean we would have one row per day per session_id - -- if CH could completely merge to match the order by - -- it is also used to organise data to make queries faster - -- we want the fewest rows possible but also the fastest queries - -- since we query by date and not by time - -- and order by must be in order of increasing cardinality - -- so we order by date first, then team_id, then session_id - -- hopefully, this is a good balance between the two - ORDER BY (toStartOfDay(min_timestamp), team_id, session_id) - SETTINGS index_granularity=512 - - ''' -# --- From 4ffeb515cd3df94d13f22545c75d997077b13dd9 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:27:23 +0000 Subject: [PATCH 7/9] Update query snapshots --- .../test/__snapshots__/test_schema.ambr | 4075 +++++++++++++++++ 1 file changed, 4075 insertions(+) create mode 100644 posthog/clickhouse/test/__snapshots__/test_schema.ambr diff --git a/posthog/clickhouse/test/__snapshots__/test_schema.ambr b/posthog/clickhouse/test/__snapshots__/test_schema.ambr new file mode 100644 index 0000000000000..406f73008fd28 --- /dev/null +++ b/posthog/clickhouse/test/__snapshots__/test_schema.ambr @@ -0,0 +1,4075 @@ +# serializer version: 1 +# name: test_create_kafka_events_with_disabled_protobuf + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_json ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_events_json_test', 'group1', 'JSONEachRow') + + SETTINGS kafka_skip_broken_messages = 100 + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_app_metrics2] + ''' + + CREATE TABLE IF NOT EXISTS kafka_app_metrics2 ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + app_source LowCardinality(String), + app_source_id String, + instance_id String, + metric_kind String, + metric_name String, + count Int64 + ) + ENGINE=Kafka('test.kafka.broker:9092', 'clickhouse_app_metrics2_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_app_metrics] + ''' + + CREATE TABLE IF NOT EXISTS kafka_app_metrics ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + plugin_config_id Int64, + category LowCardinality(String), + job_id String, + successes Int64, + successes_on_retry Int64, + failures Int64, + error_uuid UUID, + error_type String, + error_details String CODEC(ZSTD(3)) + ) + ENGINE=Kafka('test.kafka.broker:9092', 'clickhouse_app_metrics_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_error_tracking_issue_fingerprint_overrides] + ''' + + CREATE TABLE IF NOT EXISTS kafka_error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + fingerprint VARCHAR, + issue_id UUID, + is_deleted Int8, + version Int64 + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_error_tracking_issue_fingerprint_test', 'clickhouse-error-tracking-issue-fingerprint-overrides', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_events_dead_letter_queue] + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_dead_letter_queue ON CLUSTER 'posthog' + ( + id UUID, + event_uuid UUID, + event VARCHAR, + properties VARCHAR, + distinct_id VARCHAR, + team_id Int64, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + ip VARCHAR, + site_url VARCHAR, + now DateTime64(6, 'UTC'), + raw_payload VARCHAR, + error_timestamp DateTime64(6, 'UTC'), + error_location VARCHAR, + error VARCHAR, + tags Array(VARCHAR) + + ) ENGINE = Kafka('test.kafka.broker:9092', 'events_dead_letter_queue_test', 'group1', 'JSONEachRow') + SETTINGS kafka_skip_broken_messages=1000 + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_events_json] + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_json ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_events_json_test', 'group1', 'JSONEachRow') + + SETTINGS kafka_skip_broken_messages = 100 + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_events_recent_json] + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_recent_json ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_events_json_test', 'group1_recent', 'JSONEachRow') + + SETTINGS kafka_skip_broken_messages = 100 + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_groups] + ''' + + CREATE TABLE IF NOT EXISTS kafka_groups ON CLUSTER 'posthog' + ( + group_type_index UInt8, + group_key VARCHAR, + created_at DateTime64, + team_id Int64, + group_properties VARCHAR + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_groups_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_heatmaps] + ''' + + CREATE TABLE IF NOT EXISTS kafka_heatmaps ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + timestamp DateTime64(6, 'UTC'), + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x Int16, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y Int16, + -- stored so that in future we can support other resolutions + scale_factor Int16, + viewport_width Int16, + viewport_height Int16, + -- some elements move when the page scrolls, others do not + pointer_target_fixed Bool, + current_url VARCHAR, + type LowCardinality(String) + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_heatmap_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_ingestion_warnings] + ''' + + CREATE TABLE IF NOT EXISTS kafka_ingestion_warnings ON CLUSTER 'posthog' + ( + team_id Int64, + source LowCardinality(VARCHAR), + type VARCHAR, + details VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC') + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_ingestion_warnings_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_log_entries] + ''' + + CREATE TABLE IF NOT EXISTS kafka_log_entries ON CLUSTER 'posthog' + ( + team_id UInt64, + -- The name of the service or product that generated the logs. + -- Examples: batch_exports + log_source LowCardinality(String), + -- An id for the log source. + -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. + -- Examples: A batch export id, a cronjob id, a plugin id. + log_source_id String, + -- A secondary id e.g. for the instance of log_source that generated this log. + -- This may be ommitted if log_source is a singleton. + -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. + instance_id String, + -- Timestamp indicating when the log was generated. + timestamp DateTime64(6, 'UTC'), + -- The log level. + -- Examples: INFO, WARNING, DEBUG, ERROR. + level LowCardinality(String), + -- The actual log message. + message String + + ) ENGINE = Kafka('test.kafka.broker:9092', 'log_entries_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_performance_events] + ''' + + CREATE TABLE IF NOT EXISTS kafka_performance_events ON CLUSTER 'posthog' + ( + uuid UUID, + session_id String, + window_id String, + pageview_id String, + distinct_id String, + timestamp DateTime64, + time_origin DateTime64(3, 'UTC'), + entry_type LowCardinality(String), + name String, + team_id Int64, + current_url String, + start_time Float64, + duration Float64, + redirect_start Float64, + redirect_end Float64, + worker_start Float64, + fetch_start Float64, + domain_lookup_start Float64, + domain_lookup_end Float64, + connect_start Float64, + secure_connection_start Float64, + connect_end Float64, + request_start Float64, + response_start Float64, + response_end Float64, + decoded_body_size Int64, + encoded_body_size Int64, + initiator_type LowCardinality(String), + next_hop_protocol LowCardinality(String), + render_blocking_status LowCardinality(String), + response_status Int64, + transfer_size Int64, + largest_contentful_paint_element String, + largest_contentful_paint_render_time Float64, + largest_contentful_paint_load_time Float64, + largest_contentful_paint_size Float64, + largest_contentful_paint_id String, + largest_contentful_paint_url String, + dom_complete Float64, + dom_content_loaded_event Float64, + dom_interactive Float64, + load_event_end Float64, + load_event_start Float64, + redirect_count Int64, + navigation_type LowCardinality(String), + unload_event_end Float64, + unload_event_start Float64 + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_performance_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_person] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person ON CLUSTER 'posthog' + ( + id UUID, + created_at DateTime64, + team_id Int64, + properties VARCHAR, + is_identified Int8, + is_deleted Int8, + version UInt64 + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_person_distinct_id2] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person_distinct_id2 ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_distinct_id_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_person_distinct_id] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person_distinct_id ON CLUSTER 'posthog' + ( + distinct_id VARCHAR, + person_id UUID, + team_id Int64, + _sign Nullable(Int8), + is_deleted Nullable(Int8) + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_unique_id_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_person_distinct_id_overrides] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person_distinct_id_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_person_distinct_id_test', 'clickhouse-person-distinct-id-overrides', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_person_overrides] + ''' + + CREATE TABLE IF NOT EXISTS `posthog_test`.`kafka_person_overrides` + ON CLUSTER 'posthog' + + ENGINE = Kafka( + 'kafka:9092', -- Kafka hosts + 'clickhouse_person_override_test', -- Kafka topic + 'clickhouse-person-overrides', -- Kafka consumer group id + 'JSONEachRow' -- Specify that we should pass Kafka messages as JSON + ) + + -- Take the types from the `person_overrides` table, except for the + -- `created_at`, which we want to use the DEFAULT now() from the + -- `person_overrides` definition. See + -- https://github.com/ClickHouse/ClickHouse/pull/38272 for details of `EMPTY + -- AS SELECT` + EMPTY AS SELECT + team_id, + old_person_id, + override_person_id, + merged_at, + oldest_event, + -- We don't want to insert this column via Kafka, as it's + -- set as a default value in the `person_overrides` table. + -- created_at, + version + FROM `posthog_test`.`person_overrides` + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_plugin_log_entries] + ''' + + CREATE TABLE IF NOT EXISTS kafka_plugin_log_entries ON CLUSTER 'posthog' + ( + id UUID, + team_id Int64, + plugin_id Int64, + plugin_config_id Int64, + timestamp DateTime64(6, 'UTC'), + source VARCHAR, + type VARCHAR, + message VARCHAR, + instance_id UUID + + ) ENGINE = Kafka('test.kafka.broker:9092', 'plugin_log_entries_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_session_recording_events] + ''' + + CREATE TABLE IF NOT EXISTS kafka_session_recording_events ON CLUSTER 'posthog' + ( + uuid UUID, + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + session_id VARCHAR, + window_id VARCHAR, + snapshot_data VARCHAR, + created_at DateTime64(6, 'UTC') + + + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_session_recording_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_kafka_table_with_different_kafka_host[kafka_session_replay_events] + ''' + + CREATE TABLE IF NOT EXISTS kafka_session_replay_events ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + first_timestamp DateTime64(6, 'UTC'), + last_timestamp DateTime64(6, 'UTC'), + first_url Nullable(VARCHAR), + urls Array(String), + click_count Int64, + keypress_count Int64, + mouse_activity_count Int64, + active_milliseconds Int64, + console_log_count Int64, + console_warn_count Int64, + console_error_count Int64, + size Int64, + event_count Int64, + message_count Int64, + snapshot_source LowCardinality(Nullable(String)), + snapshot_library Nullable(String) + ) ENGINE = Kafka('test.kafka.broker:9092', 'clickhouse_session_replay_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[app_metrics2] + ''' + + CREATE TABLE IF NOT EXISTS app_metrics2 ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + -- The name of the service or product that generated the metrics. + -- Examples: plugins, hog + app_source LowCardinality(String), + -- An id for the app source. + -- Set app_source to avoid collision with ids from other app sources if the id generation is not safe. + -- Examples: A plugin id, a hog application id + app_source_id String, + -- A secondary id e.g. for the instance of app_source that generated this metric. + -- This may be ommitted if app_source is a singleton. + -- Examples: A plugin config id, a hog application config id + instance_id String, + metric_kind LowCardinality(String), + metric_name LowCardinality(String), + count SimpleAggregateFunction(sum, Int64) + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) + ENGINE=Distributed('posthog', 'posthog_test', 'sharded_app_metrics2', rand()) + + ''' +# --- +# name: test_create_table_query[app_metrics2_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS app_metrics2_mv ON CLUSTER 'posthog' + TO posthog_test.sharded_app_metrics2 + AS SELECT + team_id, + timestamp, + app_source, + app_source_id, + instance_id, + metric_kind, + metric_name, + count + FROM posthog_test.kafka_app_metrics2 + + ''' +# --- +# name: test_create_table_query[app_metrics] + ''' + + CREATE TABLE IF NOT EXISTS app_metrics ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + plugin_config_id Int64, + category LowCardinality(String), + job_id String, + successes SimpleAggregateFunction(sum, Int64), + successes_on_retry SimpleAggregateFunction(sum, Int64), + failures SimpleAggregateFunction(sum, Int64), + error_uuid UUID, + error_type String, + error_details String CODEC(ZSTD(3)) + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) + ENGINE=Distributed('posthog', 'posthog_test', 'sharded_app_metrics', rand()) + + ''' +# --- +# name: test_create_table_query[app_metrics_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS app_metrics_mv ON CLUSTER 'posthog' + TO posthog_test.sharded_app_metrics + AS SELECT + team_id, + timestamp, + plugin_config_id, + category, + job_id, + successes, + successes_on_retry, + failures, + error_uuid, + error_type, + error_details + FROM posthog_test.kafka_app_metrics + + ''' +# --- +# name: test_create_table_query[channel_definition] + ''' + + CREATE TABLE IF NOT EXISTS channel_definition ON CLUSTER 'posthog' ( + domain String NOT NULL, + kind String NOT NULL, + domain_type String NULL, + type_if_paid String NULL, + type_if_organic String NULL + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.channel_definition', '{replica}-{shard}') + ORDER BY (domain, kind); + + ''' +# --- +# name: test_create_table_query[cohortpeople] + ''' + + CREATE TABLE IF NOT EXISTS cohortpeople ON CLUSTER 'posthog' + ( + person_id UUID, + cohort_id Int64, + team_id Int64, + sign Int8, + version UInt64 + ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.cohortpeople', '{replica}-{shard}', sign) + Order By (team_id, cohort_id, person_id, version) + + + ''' +# --- +# name: test_create_table_query[distributed_events_recent] + ''' + + CREATE TABLE IF NOT EXISTS distributed_events_recent ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64(), _timestamp_ms DateTime64 + + ) ENGINE = Distributed('posthog_single_shard', 'posthog_test', 'events_recent', sipHash64(distinct_id)) + + ''' +# --- +# name: test_create_table_query[error_tracking_issue_fingerprint_overrides] + ''' + + CREATE TABLE IF NOT EXISTS error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + fingerprint VARCHAR, + issue_id UUID, + is_deleted Int8, + version Int64 + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + , INDEX kafka_timestamp_minmax_error_tracking_issue_fingerprint_overrides _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.error_tracking_issue_fingerprint_overrides', '{replica}-{shard}', version) + + ORDER BY (team_id, fingerprint) + SETTINGS index_granularity = 512 + + ''' +# --- +# name: test_create_table_query[error_tracking_issue_fingerprint_overrides_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS error_tracking_issue_fingerprint_overrides_mv ON CLUSTER 'posthog' + TO posthog_test.error_tracking_issue_fingerprint_overrides + AS SELECT + team_id, + fingerprint, + issue_id, + is_deleted, + version, + _timestamp, + _offset, + _partition + FROM posthog_test.kafka_error_tracking_issue_fingerprint_overrides + WHERE version > 0 -- only store updated rows, not newly inserted ones + + ''' +# --- +# name: test_create_table_query[events] + ''' + + CREATE TABLE IF NOT EXISTS events ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + , $group_0 VARCHAR COMMENT 'column_materializer::$group_0' + , $group_1 VARCHAR COMMENT 'column_materializer::$group_1' + , $group_2 VARCHAR COMMENT 'column_materializer::$group_2' + , $group_3 VARCHAR COMMENT 'column_materializer::$group_3' + , $group_4 VARCHAR COMMENT 'column_materializer::$group_4' + , $window_id VARCHAR COMMENT 'column_materializer::$window_id' + , $session_id VARCHAR COMMENT 'column_materializer::$session_id' + , elements_chain_href String COMMENT 'column_materializer::elements_chain::href' + , elements_chain_texts Array(String) COMMENT 'column_materializer::elements_chain::texts' + , elements_chain_ids Array(String) COMMENT 'column_materializer::elements_chain::ids' + , elements_chain_elements Array(Enum('a', 'button', 'form', 'input', 'select', 'textarea', 'label')) COMMENT 'column_materializer::elements_chain::elements' + , properties_group_custom Map(String, String), properties_group_feature_flags Map(String, String) + + + , _timestamp DateTime + , _offset UInt64 + , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64() + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_events', sipHash64(distinct_id)) + + ''' +# --- +# name: test_create_table_query[events_dead_letter_queue] + ''' + + CREATE TABLE IF NOT EXISTS events_dead_letter_queue ON CLUSTER 'posthog' + ( + id UUID, + event_uuid UUID, + event VARCHAR, + properties VARCHAR, + distinct_id VARCHAR, + team_id Int64, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + ip VARCHAR, + site_url VARCHAR, + now DateTime64(6, 'UTC'), + raw_payload VARCHAR, + error_timestamp DateTime64(6, 'UTC'), + error_location VARCHAR, + error VARCHAR, + tags Array(VARCHAR) + + + , _timestamp DateTime + , _offset UInt64 + + , INDEX kafka_timestamp_minmax_events_dead_letter_queue _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_dead_letter_queue', '{replica}-{shard}', _timestamp) + ORDER BY (id, event_uuid, distinct_id, team_id) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query[events_dead_letter_queue_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS events_dead_letter_queue_mv ON CLUSTER 'posthog' + TO posthog_test.events_dead_letter_queue + AS SELECT + id, + event_uuid, + event, + properties, + distinct_id, + team_id, + elements_chain, + created_at, + ip, + site_url, + now, + raw_payload, + error_timestamp, + error_location, + error, + tags, + _timestamp, + _offset + FROM posthog_test.kafka_events_dead_letter_queue + + ''' +# --- +# name: test_create_table_query[events_json_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS events_json_mv ON CLUSTER 'posthog' + TO posthog_test.writable_events + AS SELECT + uuid, + event, + properties, + timestamp, + team_id, + distinct_id, + elements_chain, + created_at, + person_id, + person_created_at, + person_properties, + group0_properties, + group1_properties, + group2_properties, + group3_properties, + group4_properties, + group0_created_at, + group1_created_at, + group2_created_at, + group3_created_at, + group4_created_at, + person_mode, + _timestamp, + _offset + FROM posthog_test.kafka_events_json + + ''' +# --- +# name: test_create_table_query[events_recent] + ''' + + CREATE TABLE IF NOT EXISTS events_recent ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + , inserted_at DateTime64(6, 'UTC') DEFAULT NOW64(), _timestamp_ms DateTime64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_recent', '{replica}-{shard}', _timestamp) + PARTITION BY toStartOfHour(inserted_at) + ORDER BY (team_id, toStartOfHour(inserted_at), event, cityHash64(distinct_id), cityHash64(uuid)) + TTL toDateTime(inserted_at) + INTERVAL 7 DAY + + + ''' +# --- +# name: test_create_table_query[events_recent_json_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS events_recent_json_mv ON CLUSTER 'posthog' + TO posthog_test.events_recent + AS SELECT + uuid, + event, + properties, + timestamp, + team_id, + distinct_id, + elements_chain, + created_at, + person_id, + person_created_at, + person_properties, + group0_properties, + group1_properties, + group2_properties, + group3_properties, + group4_properties, + group0_created_at, + group1_created_at, + group2_created_at, + group3_created_at, + group4_created_at, + person_mode, + _timestamp, + _timestamp_ms, + _offset, + _partition + FROM posthog_test.kafka_events_recent_json + + ''' +# --- +# name: test_create_table_query[groups] + ''' + + CREATE TABLE IF NOT EXISTS groups ON CLUSTER 'posthog' + ( + group_type_index UInt8, + group_key VARCHAR, + created_at DateTime64, + team_id Int64, + group_properties VARCHAR + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.groups', '{replica}-{shard}', _timestamp) + Order By (team_id, group_type_index, group_key) + + + ''' +# --- +# name: test_create_table_query[groups_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS groups_mv ON CLUSTER 'posthog' + TO posthog_test.groups + AS SELECT + group_type_index, + group_key, + created_at, + team_id, + group_properties, + _timestamp, + _offset + FROM posthog_test.kafka_groups + + ''' +# --- +# name: test_create_table_query[heatmaps] + ''' + + CREATE TABLE IF NOT EXISTS heatmaps ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + timestamp DateTime64(6, 'UTC'), + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x Int16, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y Int16, + -- stored so that in future we can support other resolutions + scale_factor Int16, + viewport_width Int16, + viewport_height Int16, + -- some elements move when the page scrolls, others do not + pointer_target_fixed Bool, + current_url VARCHAR, + type LowCardinality(String), + _timestamp DateTime, + _offset UInt64, + _partition UInt64 + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_heatmaps', cityHash64(concat(toString(team_id), '-', session_id, '-', toString(toDate(timestamp))))) + + ''' +# --- +# name: test_create_table_query[heatmaps_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS heatmaps_mv ON CLUSTER 'posthog' + TO posthog_test.writable_heatmaps + AS SELECT + session_id, + team_id, + distinct_id, + timestamp, + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y, + -- stored so that in future we can support other resolutions + scale_factor, + viewport_width, + viewport_height, + -- some elements move when the page scrolls, others do not + pointer_target_fixed, + current_url, + type, + _timestamp, + _offset, + _partition + FROM posthog_test.kafka_heatmaps + + ''' +# --- +# name: test_create_table_query[ingestion_warnings] + ''' + + CREATE TABLE IF NOT EXISTS ingestion_warnings ON CLUSTER 'posthog' + ( + team_id Int64, + source LowCardinality(VARCHAR), + type VARCHAR, + details VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC') + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_ingestion_warnings', rand()) + + ''' +# --- +# name: test_create_table_query[ingestion_warnings_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS ingestion_warnings_mv ON CLUSTER 'posthog' + TO posthog_test.ingestion_warnings + AS SELECT + team_id, + source, + type, + details, + timestamp, + _timestamp, + _offset, + _partition + FROM posthog_test.kafka_ingestion_warnings + + ''' +# --- +# name: test_create_table_query[kafka_app_metrics2] + ''' + + CREATE TABLE IF NOT EXISTS kafka_app_metrics2 ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + app_source LowCardinality(String), + app_source_id String, + instance_id String, + metric_kind String, + metric_name String, + count Int64 + ) + ENGINE=Kafka('kafka:9092', 'clickhouse_app_metrics2_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_app_metrics] + ''' + + CREATE TABLE IF NOT EXISTS kafka_app_metrics ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + plugin_config_id Int64, + category LowCardinality(String), + job_id String, + successes Int64, + successes_on_retry Int64, + failures Int64, + error_uuid UUID, + error_type String, + error_details String CODEC(ZSTD(3)) + ) + ENGINE=Kafka('kafka:9092', 'clickhouse_app_metrics_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_error_tracking_issue_fingerprint_overrides] + ''' + + CREATE TABLE IF NOT EXISTS kafka_error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + fingerprint VARCHAR, + issue_id UUID, + is_deleted Int8, + version Int64 + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_error_tracking_issue_fingerprint_test', 'clickhouse-error-tracking-issue-fingerprint-overrides', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_events_dead_letter_queue] + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_dead_letter_queue ON CLUSTER 'posthog' + ( + id UUID, + event_uuid UUID, + event VARCHAR, + properties VARCHAR, + distinct_id VARCHAR, + team_id Int64, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + ip VARCHAR, + site_url VARCHAR, + now DateTime64(6, 'UTC'), + raw_payload VARCHAR, + error_timestamp DateTime64(6, 'UTC'), + error_location VARCHAR, + error VARCHAR, + tags Array(VARCHAR) + + ) ENGINE = Kafka('kafka:9092', 'events_dead_letter_queue_test', 'group1', 'JSONEachRow') + SETTINGS kafka_skip_broken_messages=1000 + ''' +# --- +# name: test_create_table_query[kafka_events_json] + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_json ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_events_json_test', 'group1', 'JSONEachRow') + + SETTINGS kafka_skip_broken_messages = 100 + + ''' +# --- +# name: test_create_table_query[kafka_events_recent_json] + ''' + + CREATE TABLE IF NOT EXISTS kafka_events_recent_json ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_events_json_test', 'group1_recent', 'JSONEachRow') + + SETTINGS kafka_skip_broken_messages = 100 + + ''' +# --- +# name: test_create_table_query[kafka_groups] + ''' + + CREATE TABLE IF NOT EXISTS kafka_groups ON CLUSTER 'posthog' + ( + group_type_index UInt8, + group_key VARCHAR, + created_at DateTime64, + team_id Int64, + group_properties VARCHAR + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_groups_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_heatmaps] + ''' + + CREATE TABLE IF NOT EXISTS kafka_heatmaps ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + timestamp DateTime64(6, 'UTC'), + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x Int16, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y Int16, + -- stored so that in future we can support other resolutions + scale_factor Int16, + viewport_width Int16, + viewport_height Int16, + -- some elements move when the page scrolls, others do not + pointer_target_fixed Bool, + current_url VARCHAR, + type LowCardinality(String) + ) ENGINE = Kafka('kafka:9092', 'clickhouse_heatmap_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_ingestion_warnings] + ''' + + CREATE TABLE IF NOT EXISTS kafka_ingestion_warnings ON CLUSTER 'posthog' + ( + team_id Int64, + source LowCardinality(VARCHAR), + type VARCHAR, + details VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC') + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_ingestion_warnings_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_log_entries] + ''' + + CREATE TABLE IF NOT EXISTS kafka_log_entries ON CLUSTER 'posthog' + ( + team_id UInt64, + -- The name of the service or product that generated the logs. + -- Examples: batch_exports + log_source LowCardinality(String), + -- An id for the log source. + -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. + -- Examples: A batch export id, a cronjob id, a plugin id. + log_source_id String, + -- A secondary id e.g. for the instance of log_source that generated this log. + -- This may be ommitted if log_source is a singleton. + -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. + instance_id String, + -- Timestamp indicating when the log was generated. + timestamp DateTime64(6, 'UTC'), + -- The log level. + -- Examples: INFO, WARNING, DEBUG, ERROR. + level LowCardinality(String), + -- The actual log message. + message String + + ) ENGINE = Kafka('kafka:9092', 'log_entries_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_performance_events] + ''' + + CREATE TABLE IF NOT EXISTS kafka_performance_events ON CLUSTER 'posthog' + ( + uuid UUID, + session_id String, + window_id String, + pageview_id String, + distinct_id String, + timestamp DateTime64, + time_origin DateTime64(3, 'UTC'), + entry_type LowCardinality(String), + name String, + team_id Int64, + current_url String, + start_time Float64, + duration Float64, + redirect_start Float64, + redirect_end Float64, + worker_start Float64, + fetch_start Float64, + domain_lookup_start Float64, + domain_lookup_end Float64, + connect_start Float64, + secure_connection_start Float64, + connect_end Float64, + request_start Float64, + response_start Float64, + response_end Float64, + decoded_body_size Int64, + encoded_body_size Int64, + initiator_type LowCardinality(String), + next_hop_protocol LowCardinality(String), + render_blocking_status LowCardinality(String), + response_status Int64, + transfer_size Int64, + largest_contentful_paint_element String, + largest_contentful_paint_render_time Float64, + largest_contentful_paint_load_time Float64, + largest_contentful_paint_size Float64, + largest_contentful_paint_id String, + largest_contentful_paint_url String, + dom_complete Float64, + dom_content_loaded_event Float64, + dom_interactive Float64, + load_event_end Float64, + load_event_start Float64, + redirect_count Int64, + navigation_type LowCardinality(String), + unload_event_end Float64, + unload_event_start Float64 + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_performance_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_person] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person ON CLUSTER 'posthog' + ( + id UUID, + created_at DateTime64, + team_id Int64, + properties VARCHAR, + is_identified Int8, + is_deleted Int8, + version UInt64 + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_person_distinct_id2] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person_distinct_id2 ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_distinct_id_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_person_distinct_id] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person_distinct_id ON CLUSTER 'posthog' + ( + distinct_id VARCHAR, + person_id UUID, + team_id Int64, + _sign Nullable(Int8), + is_deleted Nullable(Int8) + ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_unique_id_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_person_distinct_id_overrides] + ''' + + CREATE TABLE IF NOT EXISTS kafka_person_distinct_id_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_person_distinct_id_test', 'clickhouse-person-distinct-id-overrides', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_person_overrides] + ''' + + CREATE TABLE IF NOT EXISTS `posthog_test`.`kafka_person_overrides` + ON CLUSTER 'posthog' + + ENGINE = Kafka( + 'kafka:9092', -- Kafka hosts + 'clickhouse_person_override_test', -- Kafka topic + 'clickhouse-person-overrides', -- Kafka consumer group id + 'JSONEachRow' -- Specify that we should pass Kafka messages as JSON + ) + + -- Take the types from the `person_overrides` table, except for the + -- `created_at`, which we want to use the DEFAULT now() from the + -- `person_overrides` definition. See + -- https://github.com/ClickHouse/ClickHouse/pull/38272 for details of `EMPTY + -- AS SELECT` + EMPTY AS SELECT + team_id, + old_person_id, + override_person_id, + merged_at, + oldest_event, + -- We don't want to insert this column via Kafka, as it's + -- set as a default value in the `person_overrides` table. + -- created_at, + version + FROM `posthog_test`.`person_overrides` + + ''' +# --- +# name: test_create_table_query[kafka_plugin_log_entries] + ''' + + CREATE TABLE IF NOT EXISTS kafka_plugin_log_entries ON CLUSTER 'posthog' + ( + id UUID, + team_id Int64, + plugin_id Int64, + plugin_config_id Int64, + timestamp DateTime64(6, 'UTC'), + source VARCHAR, + type VARCHAR, + message VARCHAR, + instance_id UUID + + ) ENGINE = Kafka('kafka:9092', 'plugin_log_entries_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_session_recording_events] + ''' + + CREATE TABLE IF NOT EXISTS kafka_session_recording_events ON CLUSTER 'posthog' + ( + uuid UUID, + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + session_id VARCHAR, + window_id VARCHAR, + snapshot_data VARCHAR, + created_at DateTime64(6, 'UTC') + + + ) ENGINE = Kafka('kafka:9092', 'clickhouse_session_recording_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[kafka_session_replay_events] + ''' + + CREATE TABLE IF NOT EXISTS kafka_session_replay_events ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + first_timestamp DateTime64(6, 'UTC'), + last_timestamp DateTime64(6, 'UTC'), + first_url Nullable(VARCHAR), + urls Array(String), + click_count Int64, + keypress_count Int64, + mouse_activity_count Int64, + active_milliseconds Int64, + console_log_count Int64, + console_warn_count Int64, + console_error_count Int64, + size Int64, + event_count Int64, + message_count Int64, + snapshot_source LowCardinality(Nullable(String)), + snapshot_library Nullable(String) + ) ENGINE = Kafka('kafka:9092', 'clickhouse_session_replay_events_test', 'group1', 'JSONEachRow') + + ''' +# --- +# name: test_create_table_query[log_entries] + ''' + + CREATE TABLE IF NOT EXISTS log_entries ON CLUSTER 'posthog' + ( + team_id UInt64, + -- The name of the service or product that generated the logs. + -- Examples: batch_exports + log_source LowCardinality(String), + -- An id for the log source. + -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. + -- Examples: A batch export id, a cronjob id, a plugin id. + log_source_id String, + -- A secondary id e.g. for the instance of log_source that generated this log. + -- This may be ommitted if log_source is a singleton. + -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. + instance_id String, + -- Timestamp indicating when the log was generated. + timestamp DateTime64(6, 'UTC'), + -- The log level. + -- Examples: INFO, WARNING, DEBUG, ERROR. + level LowCardinality(String), + -- The actual log message. + message String + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.log_entries', '{replica}-{shard}', _timestamp) + PARTITION BY toStartOfHour(timestamp) ORDER BY (team_id, log_source, log_source_id, instance_id, timestamp) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query[log_entries_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS log_entries_mv ON CLUSTER 'posthog' + TO posthog_test.log_entries + AS SELECT + team_id, + log_source, + log_source_id, + instance_id, + timestamp, + level, + message, + _timestamp, + _offset + FROM posthog_test.kafka_log_entries + + ''' +# --- +# name: test_create_table_query[performance_events] + ''' + + CREATE TABLE IF NOT EXISTS performance_events ON CLUSTER 'posthog' + ( + uuid UUID, + session_id String, + window_id String, + pageview_id String, + distinct_id String, + timestamp DateTime64, + time_origin DateTime64(3, 'UTC'), + entry_type LowCardinality(String), + name String, + team_id Int64, + current_url String, + start_time Float64, + duration Float64, + redirect_start Float64, + redirect_end Float64, + worker_start Float64, + fetch_start Float64, + domain_lookup_start Float64, + domain_lookup_end Float64, + connect_start Float64, + secure_connection_start Float64, + connect_end Float64, + request_start Float64, + response_start Float64, + response_end Float64, + decoded_body_size Int64, + encoded_body_size Int64, + initiator_type LowCardinality(String), + next_hop_protocol LowCardinality(String), + render_blocking_status LowCardinality(String), + response_status Int64, + transfer_size Int64, + largest_contentful_paint_element String, + largest_contentful_paint_render_time Float64, + largest_contentful_paint_load_time Float64, + largest_contentful_paint_size Float64, + largest_contentful_paint_id String, + largest_contentful_paint_url String, + dom_complete Float64, + dom_content_loaded_event Float64, + dom_interactive Float64, + load_event_end Float64, + load_event_start Float64, + redirect_count Int64, + navigation_type LowCardinality(String), + unload_event_end Float64, + unload_event_start Float64 + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_performance_events', sipHash64(session_id)) + + ''' +# --- +# name: test_create_table_query[performance_events_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS performance_events_mv ON CLUSTER 'posthog' + TO posthog_test.writeable_performance_events + AS SELECT + uuid, session_id, window_id, pageview_id, distinct_id, timestamp, time_origin, entry_type, name, team_id, current_url, start_time, duration, redirect_start, redirect_end, worker_start, fetch_start, domain_lookup_start, domain_lookup_end, connect_start, secure_connection_start, connect_end, request_start, response_start, response_end, decoded_body_size, encoded_body_size, initiator_type, next_hop_protocol, render_blocking_status, response_status, transfer_size, largest_contentful_paint_element, largest_contentful_paint_render_time, largest_contentful_paint_load_time, largest_contentful_paint_size, largest_contentful_paint_id, largest_contentful_paint_url, dom_complete, dom_content_loaded_event, dom_interactive, load_event_end, load_event_start, redirect_count, navigation_type, unload_event_end, unload_event_start + ,_timestamp, _offset, _partition + FROM posthog_test.kafka_performance_events + + ''' +# --- +# name: test_create_table_query[person] + ''' + + CREATE TABLE IF NOT EXISTS person ON CLUSTER 'posthog' + ( + id UUID, + created_at DateTime64, + team_id Int64, + properties VARCHAR, + is_identified Int8, + is_deleted Int8, + version UInt64 + + + , _timestamp DateTime + , _offset UInt64 + + , INDEX kafka_timestamp_minmax_person _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person', '{replica}-{shard}', version) + Order By (team_id, id) + + + ''' +# --- +# name: test_create_table_query[person_distinct_id2] + ''' + + CREATE TABLE IF NOT EXISTS person_distinct_id2 ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + + , _timestamp DateTime + , _offset UInt64 + + , _partition UInt64 + , INDEX kafka_timestamp_minmax_person_distinct_id2 _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id2', '{replica}-{shard}', version) + + ORDER BY (team_id, distinct_id) + SETTINGS index_granularity = 512 + + ''' +# --- +# name: test_create_table_query[person_distinct_id2_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS person_distinct_id2_mv ON CLUSTER 'posthog' + TO posthog_test.person_distinct_id2 + AS SELECT + team_id, + distinct_id, + person_id, + is_deleted, + version, + _timestamp, + _offset, + _partition + FROM posthog_test.kafka_person_distinct_id2 + + ''' +# --- +# name: test_create_table_query[person_distinct_id] + ''' + + CREATE TABLE IF NOT EXISTS person_distinct_id ON CLUSTER 'posthog' + ( + distinct_id VARCHAR, + person_id UUID, + team_id Int64, + _sign Int8 DEFAULT 1, + is_deleted Int8 ALIAS if(_sign==-1, 1, 0) + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id', '{replica}-{shard}', _sign) + Order By (team_id, distinct_id, person_id) + + + ''' +# --- +# name: test_create_table_query[person_distinct_id_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS person_distinct_id_mv ON CLUSTER 'posthog' + TO posthog_test.person_distinct_id + AS SELECT + distinct_id, + person_id, + team_id, + coalesce(_sign, if(is_deleted==0, 1, -1)) AS _sign, + _timestamp, + _offset + FROM posthog_test.kafka_person_distinct_id + + ''' +# --- +# name: test_create_table_query[person_distinct_id_overrides] + ''' + + CREATE TABLE IF NOT EXISTS person_distinct_id_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + , INDEX kafka_timestamp_minmax_person_distinct_id_overrides _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id_overrides', '{replica}-{shard}', version) + + ORDER BY (team_id, distinct_id) + SETTINGS index_granularity = 512 + + ''' +# --- +# name: test_create_table_query[person_distinct_id_overrides_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS person_distinct_id_overrides_mv ON CLUSTER 'posthog' + TO posthog_test.person_distinct_id_overrides + AS SELECT + team_id, + distinct_id, + person_id, + is_deleted, + version, + _timestamp, + _offset, + _partition + FROM posthog_test.kafka_person_distinct_id_overrides + WHERE version > 0 -- only store updated rows, not newly inserted ones + + ''' +# --- +# name: test_create_table_query[person_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS person_mv ON CLUSTER 'posthog' + TO posthog_test.person + AS SELECT + id, + created_at, + team_id, + properties, + is_identified, + is_deleted, + version, + _timestamp, + _offset + FROM posthog_test.kafka_person + + ''' +# --- +# name: test_create_table_query[person_overrides] + ''' + + CREATE TABLE IF NOT EXISTS `posthog_test`.`person_overrides` + ON CLUSTER 'posthog' ( + team_id INT NOT NULL, + + -- When we merge two people `old_person_id` and `override_person_id`, we + -- want to keep track of a mapping from the `old_person_id` to the + -- `override_person_id`. This allows us to join with the + -- `sharded_events` table to find all events that were associated with + -- the `old_person_id` and update them to be associated with the + -- `override_person_id`. + old_person_id UUID NOT NULL, + override_person_id UUID NOT NULL, + + -- The timestamp the merge of the two people was completed. + merged_at DateTime64(6, 'UTC') NOT NULL, + -- The timestamp of the oldest event associated with the + -- `old_person_id`. + oldest_event DateTime64(6, 'UTC') NOT NULL, + -- The timestamp rows are created. This isn't part of the JOIN process + -- with the events table but rather a housekeeping column to allow us to + -- see when the row was created. This shouldn't have any impact of the + -- JOIN as it will be stored separately with the Wide ClickHouse table + -- storage. + created_at DateTime64(6, 'UTC') DEFAULT now(), + + -- the specific version of the `old_person_id` mapping. This is used to + -- allow us to discard old mappings as new ones are added. This version + -- will be provided by the corresponding PostgreSQL + --`posthog_personoverrides` table + version INT NOT NULL + ) + + -- By specifying Replacing merge tree on version, we allow ClickHouse to + -- discard old versions of a `old_person_id` mapping. This should help keep + -- performance in check as new versions are added. Note that given we can + -- have partitioning by `oldest_event` which will change as we update + -- `person_id` on old partitions. + -- + -- We also need to ensure that the data is replicated to all replicas in the + -- cluster, as we do not have any constraints on person_id and which shard + -- associated events are on. To do this we use the ReplicatedReplacingMergeTree + -- engine specifying a static `zk_path`. This will cause the Engine to + -- consider all replicas as the same. See + -- https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication + -- for details. + ENGINE = ReplicatedReplacingMergeTree( + -- NOTE: for testing we use a uuid to ensure that we don't get conflicts + -- when the tests tear down and recreate the table. + '/clickhouse/tables/{uuid}noshard/posthog_test.person_overrides', + '{replica}-{shard}', + version + ) + + -- We partition the table by the `oldest_event` column. This allows us to + -- handle updating the events table partition by partition, progressing each + -- override partition by partition in lockstep with the events table. Note + -- that this means it is possible that we have a mapping from + -- `old_person_id` in multiple partitions during the merge process. + PARTITION BY toYYYYMM(oldest_event) + + -- We want to collapse down on the `old_person_id` such that we end up with + -- the newest known mapping for it in the table. Query side we will need to + -- ensure that we are always querying the latest version of the mapping. + ORDER BY (team_id, old_person_id) + + ''' +# --- +# name: test_create_table_query[person_overrides_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS `posthog_test`.`person_overrides_mv` + ON CLUSTER 'posthog' + TO `posthog_test`.`person_overrides` + AS SELECT + team_id, + old_person_id, + override_person_id, + merged_at, + oldest_event, + -- We don't want to insert this column via Kafka, as it's + -- set as a default value in the `person_overrides` table. + -- created_at, + version + FROM `posthog_test`.`kafka_person_overrides` + + ''' +# --- +# name: test_create_table_query[person_static_cohort] + ''' + + CREATE TABLE IF NOT EXISTS person_static_cohort ON CLUSTER 'posthog' + ( + id UUID, + person_id UUID, + cohort_id Int64, + team_id Int64 + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_static_cohort', '{replica}-{shard}', _timestamp) + Order By (team_id, cohort_id, person_id, id) + + + ''' +# --- +# name: test_create_table_query[plugin_log_entries] + ''' + + CREATE TABLE IF NOT EXISTS plugin_log_entries ON CLUSTER 'posthog' + ( + id UUID, + team_id Int64, + plugin_id Int64, + plugin_config_id Int64, + timestamp DateTime64(6, 'UTC'), + source VARCHAR, + type VARCHAR, + message VARCHAR, + instance_id UUID + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.plugin_log_entries', '{replica}-{shard}', _timestamp) + PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query[plugin_log_entries_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS plugin_log_entries_mv ON CLUSTER 'posthog' + TO posthog_test.plugin_log_entries + AS SELECT + id, + team_id, + plugin_id, + plugin_config_id, + timestamp, + source, + type, + message, + instance_id, + _timestamp, + _offset + FROM posthog_test.kafka_plugin_log_entries + + ''' +# --- +# name: test_create_table_query[raw_sessions] + ''' + + CREATE TABLE IF NOT EXISTS raw_sessions ON CLUSTER 'posthog' + ( + team_id Int64, + session_id_v7 UInt128, -- integer representation of a uuidv7 + + -- ClickHouse will pick the latest value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + -- urls + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + -- device + initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + + -- geoip + -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment + initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- attribution + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- Count pageview, autocapture, and screen events for providing totals. + -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were + -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be + -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to + -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're + -- satisfied that counts are accurate. + pageview_count SimpleAggregateFunction(sum, Int64), + pageview_uniq AggregateFunction(uniq, Nullable(UUID)), + autocapture_count SimpleAggregateFunction(sum, Int64), + autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), + screen_count SimpleAggregateFunction(sum, Int64), + screen_uniq AggregateFunction(uniq, Nullable(UUID)), + + -- replay + maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv + + -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these + page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), + + -- web vitals + vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_raw_sessions', cityHash64(session_id_v7)) + + ''' +# --- +# name: test_create_table_query[raw_sessions_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS raw_sessions_mv ON CLUSTER 'posthog' + TO posthog_test.writable_raw_sessions + AS + + SELECT + team_id, + toUInt128(toUUID(`$session_id`)) as session_id_v7, + + argMaxState(distinct_id, timestamp) as distinct_id, + + min(timestamp) AS min_timestamp, + max(timestamp) AS max_timestamp, + + -- urls + groupUniqArray(nullIf(JSONExtractString(properties, '$current_url'), '')) AS urls, + argMinState(JSONExtractString(properties, '$current_url'), timestamp) as entry_url, + argMaxState(JSONExtractString(properties, '$current_url'), timestamp) as end_url, + argMaxState(JSONExtractString(properties, '$external_click_url'), timestamp) as last_external_click_url, + + -- device + argMinState(JSONExtractString(properties, '$browser'), timestamp) as initial_browser, + argMinState(JSONExtractString(properties, '$browser_version'), timestamp) as initial_browser_version, + argMinState(JSONExtractString(properties, '$os'), timestamp) as initial_os, + argMinState(JSONExtractString(properties, '$os_version'), timestamp) as initial_os_version, + argMinState(JSONExtractString(properties, '$device_type'), timestamp) as initial_device_type, + argMinState(JSONExtractInt(properties, '$viewport_width'), timestamp) as initial_viewport_width, + argMinState(JSONExtractInt(properties, '$viewport_height'), timestamp) as initial_viewport_height, + + -- geoip + argMinState(JSONExtractString(properties, '$geoip_country_code'), timestamp) as initial_geoip_country_code, + argMinState(JSONExtractString(properties, '$geoip_subdivision_1_code'), timestamp) as initial_geoip_subdivision_1_code, + argMinState(JSONExtractString(properties, '$geoip_subdivision_1_name'), timestamp) as initial_geoip_subdivision_1_name, + argMinState(JSONExtractString(properties, '$geoip_subdivision_city_name'), timestamp) as initial_geoip_subdivision_city_name, + argMinState(JSONExtractString(properties, '$geoip_time_zone'), timestamp) as initial_geoip_time_zone, + + -- attribution + argMinState(JSONExtractString(properties, '$referring_domain'), timestamp) as initial_referring_domain, + argMinState(JSONExtractString(properties, 'utm_source'), timestamp) as initial_utm_source, + argMinState(JSONExtractString(properties, 'utm_campaign'), timestamp) as initial_utm_campaign, + argMinState(JSONExtractString(properties, 'utm_medium'), timestamp) as initial_utm_medium, + argMinState(JSONExtractString(properties, 'utm_term'), timestamp) as initial_utm_term, + argMinState(JSONExtractString(properties, 'utm_content'), timestamp) as initial_utm_content, + argMinState(JSONExtractString(properties, 'gclid'), timestamp) as initial_gclid, + argMinState(JSONExtractString(properties, 'gad_source'), timestamp) as initial_gad_source, + argMinState(JSONExtractString(properties, 'gclsrc'), timestamp) as initial_gclsrc, + argMinState(JSONExtractString(properties, 'dclid'), timestamp) as initial_dclid, + argMinState(JSONExtractString(properties, 'gbraid'), timestamp) as initial_gbraid, + argMinState(JSONExtractString(properties, 'wbraid'), timestamp) as initial_wbraid, + argMinState(JSONExtractString(properties, 'fbclid'), timestamp) as initial_fbclid, + argMinState(JSONExtractString(properties, 'msclkid'), timestamp) as initial_msclkid, + argMinState(JSONExtractString(properties, 'twclid'), timestamp) as initial_twclid, + argMinState(JSONExtractString(properties, 'li_fat_id'), timestamp) as initial_li_fat_id, + argMinState(JSONExtractString(properties, 'mc_cid'), timestamp) as initial_mc_cid, + argMinState(JSONExtractString(properties, 'igshid'), timestamp) as initial_igshid, + argMinState(JSONExtractString(properties, 'ttclid'), timestamp) as initial_ttclid, + + -- count + sumIf(1, event='$pageview') as pageview_count, + uniqState(CAST(if(event='$pageview', uuid, NULL) AS Nullable(UUID))) as pageview_uniq, + sumIf(1, event='$autocapture') as autocapture_count, + uniqState(CAST(if(event='$autocapture', uuid, NULL) AS Nullable(UUID))) as autocapture_uniq, + sumIf(1, event='$screen') as screen_count, + uniqState(CAST(if(event='$screen', uuid, NULL) AS Nullable(UUID))) as screen_uniq, + + -- replay + false as maybe_has_session_replay, + + -- perf + uniqUpToState(1)(CAST(if(event='$pageview' OR event='$screen' OR event='$autocapture', uuid, NULL) AS Nullable(UUID))) as page_screen_autocapture_uniq_up_to, + + -- web vitals + argMinState(accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$web_vitals_LCP_value'), ''), 'null'), '^"|"$', ''), 'Float64'), timestamp) as vitals_lcp + FROM posthog_test.sharded_events + WHERE bitAnd(bitShiftRight(toUInt128(accurateCastOrNull(`$session_id`, 'UUID')), 76), 0xF) == 7 -- has a session id and is valid uuidv7) + GROUP BY + team_id, + toStartOfHour(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))), + cityHash64(session_id_v7), + session_id_v7 + + + ''' +# --- +# name: test_create_table_query[session_recording_events] + ''' + + CREATE TABLE IF NOT EXISTS session_recording_events ON CLUSTER 'posthog' + ( + uuid UUID, + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + session_id VARCHAR, + window_id VARCHAR, + snapshot_data VARCHAR, + created_at DateTime64(6, 'UTC') + , has_full_snapshot Int8 COMMENT 'column_materializer::has_full_snapshot', events_summary Array(String) COMMENT 'column_materializer::events_summary', click_count Int8 COMMENT 'column_materializer::click_count', keypress_count Int8 COMMENT 'column_materializer::keypress_count', timestamps_summary Array(DateTime64(6, 'UTC')) COMMENT 'column_materializer::timestamps_summary', first_event_timestamp Nullable(DateTime64(6, 'UTC')) COMMENT 'column_materializer::first_event_timestamp', last_event_timestamp Nullable(DateTime64(6, 'UTC')) COMMENT 'column_materializer::last_event_timestamp', urls Array(String) COMMENT 'column_materializer::urls' + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_session_recording_events', sipHash64(distinct_id)) + + ''' +# --- +# name: test_create_table_query[session_recording_events_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS session_recording_events_mv ON CLUSTER 'posthog' + TO posthog_test.writable_session_recording_events + AS SELECT + uuid, + timestamp, + team_id, + distinct_id, + session_id, + window_id, + snapshot_data, + created_at, + _timestamp, + _offset + FROM posthog_test.kafka_session_recording_events + + ''' +# --- +# name: test_create_table_query[session_replay_events] + ''' + + CREATE TABLE IF NOT EXISTS session_replay_events ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id VARCHAR, + min_first_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_last_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + -- store the first url of the session so we can quickly show that in playlists + first_url AggregateFunction(argMin, Nullable(VARCHAR), DateTime64(6, 'UTC')), + -- but also store each url so we can query by visited page without having to scan all events + -- despite the name we can put mobile screens in here as well to give same functionality across platforms + all_urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + click_count SimpleAggregateFunction(sum, Int64), + keypress_count SimpleAggregateFunction(sum, Int64), + mouse_activity_count SimpleAggregateFunction(sum, Int64), + active_milliseconds SimpleAggregateFunction(sum, Int64), + console_log_count SimpleAggregateFunction(sum, Int64), + console_warn_count SimpleAggregateFunction(sum, Int64), + console_error_count SimpleAggregateFunction(sum, Int64), + -- this column allows us to estimate the amount of data that is being ingested + size SimpleAggregateFunction(sum, Int64), + -- this allows us to count the number of messages received in a session + -- often very useful in incidents or debugging + message_count SimpleAggregateFunction(sum, Int64), + -- this allows us to count the number of snapshot events received in a session + -- often very useful in incidents or debugging + -- because we batch events we expect message_count to be lower than event_count + event_count SimpleAggregateFunction(sum, Int64), + -- which source the snapshots came from Mobile or Web. Web if absent + snapshot_source AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), + -- knowing something is mobile isn't enough, we need to know if e.g. RN or flutter + snapshot_library AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), + _timestamp SimpleAggregateFunction(max, DateTime) + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_session_replay_events', sipHash64(distinct_id)) + + ''' +# --- +# name: test_create_table_query[session_replay_events_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS session_replay_events_mv ON CLUSTER 'posthog' + TO posthog_test.writable_session_replay_events ( + `session_id` String, `team_id` Int64, `distinct_id` String, + `min_first_timestamp` DateTime64(6, 'UTC'), + `max_last_timestamp` DateTime64(6, 'UTC'), + `first_url` AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), + `all_urls` SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + `click_count` Int64, `keypress_count` Int64, + `mouse_activity_count` Int64, `active_milliseconds` Int64, + `console_log_count` Int64, `console_warn_count` Int64, + `console_error_count` Int64, `size` Int64, `message_count` Int64, + `event_count` Int64, + `snapshot_source` AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), + `snapshot_library` AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), + `_timestamp` Nullable(DateTime) + ) + AS SELECT + session_id, + team_id, + any(distinct_id) as distinct_id, + min(first_timestamp) AS min_first_timestamp, + max(last_timestamp) AS max_last_timestamp, + -- TRICKY: ClickHouse will pick a relatively random first_url + -- when it collapses the aggregating merge tree + -- unless we teach it what we want... + -- argMin ignores null values + -- so this will get the first non-null value of first_url + -- for each group of session_id and team_id + -- by min of first_timestamp in the batch + -- this is an aggregate function, not a simple aggregate function + -- so we have to write to argMinState, and query with argMinMerge + argMinState(first_url, first_timestamp) as first_url, + groupUniqArrayArray(urls) as all_urls, + sum(click_count) as click_count, + sum(keypress_count) as keypress_count, + sum(mouse_activity_count) as mouse_activity_count, + sum(active_milliseconds) as active_milliseconds, + sum(console_log_count) as console_log_count, + sum(console_warn_count) as console_warn_count, + sum(console_error_count) as console_error_count, + sum(size) as size, + -- we can count the number of kafka messages instead of sending it explicitly + sum(message_count) as message_count, + sum(event_count) as event_count, + argMinState(snapshot_source, first_timestamp) as snapshot_source, + argMinState(snapshot_library, first_timestamp) as snapshot_library, + max(_timestamp) as _timestamp + FROM posthog_test.kafka_session_replay_events + group by session_id, team_id + + ''' +# --- +# name: test_create_table_query[sessions] + ''' + + CREATE TABLE IF NOT EXISTS sessions ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id SimpleAggregateFunction(any, String), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- create a map of how many times we saw each event + event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), + -- duplicate the event count as a specific column for pageviews and autocaptures, + -- as these are used in some key queries and need to be fast + pageview_count SimpleAggregateFunction(sum, Int64), + autocapture_count SimpleAggregateFunction(sum, Int64), + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_sessions', sipHash64(session_id)) + + ''' +# --- +# name: test_create_table_query[sessions_mv] + ''' + + CREATE MATERIALIZED VIEW IF NOT EXISTS sessions_mv ON CLUSTER 'posthog' + TO posthog_test.writable_sessions + AS + + SELECT + + `$session_id` as session_id, + team_id, + + -- it doesn't matter which distinct_id gets picked (it'll be somewhat random) as they can all join to the right person + any(distinct_id) as distinct_id, + + min(timestamp) AS min_timestamp, + max(timestamp) AS max_timestamp, + + groupUniqArray(replaceRegexpAll(JSONExtractRaw(properties, '$current_url'), '^"|"$', '')) AS urls, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, '$current_url'), '^"|"$', ''), timestamp) as entry_url, + argMaxState(replaceRegexpAll(JSONExtractRaw(properties, '$current_url'), '^"|"$', ''), timestamp) as exit_url, + + argMinState(replaceRegexpAll(JSONExtractRaw(properties, '$referring_domain'), '^"|"$', ''), timestamp) as initial_referring_domain, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_source'), '^"|"$', ''), timestamp) as initial_utm_source, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_campaign'), '^"|"$', ''), timestamp) as initial_utm_campaign, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_medium'), '^"|"$', ''), timestamp) as initial_utm_medium, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_term'), '^"|"$', ''), timestamp) as initial_utm_term, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'utm_content'), '^"|"$', ''), timestamp) as initial_utm_content, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gclid'), '^"|"$', ''), timestamp) as initial_gclid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gad_source'), '^"|"$', ''), timestamp) as initial_gad_source, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gclsrc'), '^"|"$', ''), timestamp) as initial_gclsrc, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'dclid'), '^"|"$', ''), timestamp) as initial_dclid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'gbraid'), '^"|"$', ''), timestamp) as initial_gbraid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'wbraid'), '^"|"$', ''), timestamp) as initial_wbraid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'fbclid'), '^"|"$', ''), timestamp) as initial_fbclid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'msclkid'), '^"|"$', ''), timestamp) as initial_msclkid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'twclid'), '^"|"$', ''), timestamp) as initial_twclid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'li_fat_id'), '^"|"$', ''), timestamp) as initial_li_fat_id, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'mc_cid'), '^"|"$', ''), timestamp) as initial_mc_cid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'igshid'), '^"|"$', ''), timestamp) as initial_igshid, + argMinState(replaceRegexpAll(JSONExtractRaw(properties, 'ttclid'), '^"|"$', ''), timestamp) as initial_ttclid, + + sumMap(CAST(([event], [1]), 'Map(String, UInt64)')) as event_count_map, + sumIf(1, event='$pageview') as pageview_count, + sumIf(1, event='$autocapture') as autocapture_count + + FROM posthog_test.sharded_events + WHERE `$session_id` IS NOT NULL AND `$session_id` != '' AND team_id IN (1, 2, 13610, 19279, 21173, 29929, 32050, 9910, 11775, 21129, 31490) + GROUP BY `$session_id`, team_id + + + ''' +# --- +# name: test_create_table_query[sharded_app_metrics2] + ''' + + CREATE TABLE IF NOT EXISTS sharded_app_metrics2 ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + -- The name of the service or product that generated the metrics. + -- Examples: plugins, hog + app_source LowCardinality(String), + -- An id for the app source. + -- Set app_source to avoid collision with ids from other app sources if the id generation is not safe. + -- Examples: A plugin id, a hog application id + app_source_id String, + -- A secondary id e.g. for the instance of app_source that generated this metric. + -- This may be ommitted if app_source is a singleton. + -- Examples: A plugin config id, a hog application config id + instance_id String, + metric_kind LowCardinality(String), + metric_name LowCardinality(String), + count SimpleAggregateFunction(sum, Int64) + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) + ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics2', '{replica}') + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, app_source, app_source_id, instance_id, toStartOfHour(timestamp), metric_kind, metric_name) + + + ''' +# --- +# name: test_create_table_query[sharded_app_metrics] + ''' + + CREATE TABLE IF NOT EXISTS sharded_app_metrics ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + plugin_config_id Int64, + category LowCardinality(String), + job_id String, + successes SimpleAggregateFunction(sum, Int64), + successes_on_retry SimpleAggregateFunction(sum, Int64), + failures SimpleAggregateFunction(sum, Int64), + error_uuid UUID, + error_type String, + error_details String CODEC(ZSTD(3)) + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) + ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics', '{replica}') + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, plugin_config_id, job_id, category, toStartOfHour(timestamp), error_type, error_uuid) + + ''' +# --- +# name: test_create_table_query[sharded_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_events ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + , $group_0 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_0'), '^"|"$', '') COMMENT 'column_materializer::$group_0' + , $group_1 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_1'), '^"|"$', '') COMMENT 'column_materializer::$group_1' + , $group_2 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_2'), '^"|"$', '') COMMENT 'column_materializer::$group_2' + , $group_3 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_3'), '^"|"$', '') COMMENT 'column_materializer::$group_3' + , $group_4 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_4'), '^"|"$', '') COMMENT 'column_materializer::$group_4' + , $window_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$window_id'), '^"|"$', '') COMMENT 'column_materializer::$window_id' + , $session_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$session_id'), '^"|"$', '') COMMENT 'column_materializer::$session_id' + , elements_chain_href String MATERIALIZED extract(elements_chain, '(?::|")href="(.*?)"') + , elements_chain_texts Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")text="(.*?)"')) + , elements_chain_ids Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")attr_id="(.*?)"')) + , elements_chain_elements Array(Enum('a', 'button', 'form', 'input', 'select', 'textarea', 'label')) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?:^|;)(a|button|form|input|select|textarea|label)(?:\.|$|:)')) + , INDEX `minmax_$group_0` `$group_0` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_1` `$group_1` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_2` `$group_2` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_3` `$group_3` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_4` `$group_4` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$window_id` `$window_id` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$session_id` `$session_id` TYPE minmax GRANULARITY 1 + , properties_group_custom Map(String, String) + MATERIALIZED mapSort( + mapFilter((key, _) -> key NOT LIKE '$%' AND key NOT IN ('token', 'distinct_id', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gclid', 'gad_source', 'gclsrc', 'dclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'twclid', 'li_fat_id', 'mc_cid', 'igshid', 'ttclid', 'rdt_cid'), + CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) + ) + CODEC(ZSTD(1)) + , INDEX properties_group_custom_keys_bf mapKeys(properties_group_custom) TYPE bloom_filter, INDEX properties_group_custom_values_bf mapValues(properties_group_custom) TYPE bloom_filter, properties_group_feature_flags Map(String, String) + MATERIALIZED mapSort( + mapFilter((key, _) -> key like '$feature/%', + CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) + ) + CODEC(ZSTD(1)) + , INDEX properties_group_feature_flags_keys_bf mapKeys(properties_group_feature_flags) TYPE bloom_filter, INDEX properties_group_feature_flags_values_bf mapValues(properties_group_feature_flags) TYPE bloom_filter + + + , _timestamp DateTime + , _offset UInt64 + , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64() + + , INDEX kafka_timestamp_minmax_sharded_events _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.events', '{replica}', _timestamp) + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid)) + SAMPLE BY cityHash64(distinct_id) + + + ''' +# --- +# name: test_create_table_query[sharded_heatmaps] + ''' + + CREATE TABLE IF NOT EXISTS sharded_heatmaps ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + timestamp DateTime64(6, 'UTC'), + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x Int16, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y Int16, + -- stored so that in future we can support other resolutions + scale_factor Int16, + viewport_width Int16, + viewport_height Int16, + -- some elements move when the page scrolls, others do not + pointer_target_fixed Bool, + current_url VARCHAR, + type LowCardinality(String), + _timestamp DateTime, + _offset UInt64, + _partition UInt64 + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.heatmaps', '{replica}') + + PARTITION BY toYYYYMM(timestamp) + -- almost always this is being queried by + -- * type, + -- * team_id, + -- * date range, + -- * URL (maybe matching wild cards), + -- * width + -- we'll almost never query this by session id + -- so from least to most cardinality that's + ORDER BY (type, team_id, toDate(timestamp), current_url, viewport_width) + + -- I am purposefully not setting index granularity + -- the default is 8192, and we will be loading a lot of data + -- per query, we tend to copy this 512 around the place but + -- i don't think it applies here + + ''' +# --- +# name: test_create_table_query[sharded_ingestion_warnings] + ''' + + CREATE TABLE IF NOT EXISTS sharded_ingestion_warnings ON CLUSTER 'posthog' + ( + team_id Int64, + source LowCardinality(VARCHAR), + type VARCHAR, + details VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC') + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_ingestion_warnings', '{replica}') + PARTITION BY toYYYYMMDD(timestamp) + ORDER BY (team_id, toHour(timestamp), type, source, timestamp) + + ''' +# --- +# name: test_create_table_query[sharded_performance_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_performance_events ON CLUSTER 'posthog' + ( + uuid UUID, + session_id String, + window_id String, + pageview_id String, + distinct_id String, + timestamp DateTime64, + time_origin DateTime64(3, 'UTC'), + entry_type LowCardinality(String), + name String, + team_id Int64, + current_url String, + start_time Float64, + duration Float64, + redirect_start Float64, + redirect_end Float64, + worker_start Float64, + fetch_start Float64, + domain_lookup_start Float64, + domain_lookup_end Float64, + connect_start Float64, + secure_connection_start Float64, + connect_end Float64, + request_start Float64, + response_start Float64, + response_end Float64, + decoded_body_size Int64, + encoded_body_size Int64, + initiator_type LowCardinality(String), + next_hop_protocol LowCardinality(String), + render_blocking_status LowCardinality(String), + response_status Int64, + transfer_size Int64, + largest_contentful_paint_element String, + largest_contentful_paint_render_time Float64, + largest_contentful_paint_load_time Float64, + largest_contentful_paint_size Float64, + largest_contentful_paint_id String, + largest_contentful_paint_url String, + dom_complete Float64, + dom_content_loaded_event Float64, + dom_interactive Float64, + load_event_end Float64, + load_event_start Float64, + redirect_count Int64, + navigation_type LowCardinality(String), + unload_event_end Float64, + unload_event_start Float64 + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.performance_events', '{replica}') + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, toDate(timestamp), session_id, pageview_id, timestamp) + + + + ''' +# --- +# name: test_create_table_query[sharded_raw_sessions] + ''' + + CREATE TABLE IF NOT EXISTS sharded_raw_sessions ON CLUSTER 'posthog' + ( + team_id Int64, + session_id_v7 UInt128, -- integer representation of a uuidv7 + + -- ClickHouse will pick the latest value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + -- urls + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + -- device + initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + + -- geoip + -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment + initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- attribution + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- Count pageview, autocapture, and screen events for providing totals. + -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were + -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be + -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to + -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're + -- satisfied that counts are accurate. + pageview_count SimpleAggregateFunction(sum, Int64), + pageview_uniq AggregateFunction(uniq, Nullable(UUID)), + autocapture_count SimpleAggregateFunction(sum, Int64), + autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), + screen_count SimpleAggregateFunction(sum, Int64), + screen_uniq AggregateFunction(uniq, Nullable(UUID)), + + -- replay + maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv + + -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these + page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), + + -- web vitals + vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) + ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.raw_sessions', '{replica}') + + PARTITION BY toYYYYMM(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))) + ORDER BY ( + team_id, + toStartOfHour(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))), + cityHash64(session_id_v7), + session_id_v7 + ) + SAMPLE BY cityHash64(session_id_v7) + + ''' +# --- +# name: test_create_table_query[sharded_session_recording_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_session_recording_events ON CLUSTER 'posthog' + ( + uuid UUID, + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + session_id VARCHAR, + window_id VARCHAR, + snapshot_data VARCHAR, + created_at DateTime64(6, 'UTC') + , has_full_snapshot Int8 MATERIALIZED JSONExtractBool(snapshot_data, 'has_full_snapshot'), events_summary Array(String) MATERIALIZED JSONExtract(JSON_QUERY(snapshot_data, '$.events_summary[*]'), 'Array(String)'), click_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 2, events_summary)), keypress_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 5, events_summary)), timestamps_summary Array(DateTime64(6, 'UTC')) MATERIALIZED arraySort(arrayMap((x) -> toDateTime(JSONExtractInt(x, 'timestamp') / 1000), events_summary)), first_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('min', timestamps_summary)), last_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('max', timestamps_summary)), urls Array(String) MATERIALIZED arrayFilter(x -> x != '', arrayMap((x) -> JSONExtractString(x, 'data', 'href'), events_summary)) + + + , _timestamp DateTime + , _offset UInt64 + + , INDEX kafka_timestamp_minmax_sharded_session_recording_events _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_recording_events', '{replica}', _timestamp) + PARTITION BY toYYYYMMDD(timestamp) + ORDER BY (team_id, toHour(timestamp), session_id, timestamp, uuid) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query[sharded_session_replay_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_session_replay_events ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id VARCHAR, + min_first_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_last_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + -- store the first url of the session so we can quickly show that in playlists + first_url AggregateFunction(argMin, Nullable(VARCHAR), DateTime64(6, 'UTC')), + -- but also store each url so we can query by visited page without having to scan all events + -- despite the name we can put mobile screens in here as well to give same functionality across platforms + all_urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + click_count SimpleAggregateFunction(sum, Int64), + keypress_count SimpleAggregateFunction(sum, Int64), + mouse_activity_count SimpleAggregateFunction(sum, Int64), + active_milliseconds SimpleAggregateFunction(sum, Int64), + console_log_count SimpleAggregateFunction(sum, Int64), + console_warn_count SimpleAggregateFunction(sum, Int64), + console_error_count SimpleAggregateFunction(sum, Int64), + -- this column allows us to estimate the amount of data that is being ingested + size SimpleAggregateFunction(sum, Int64), + -- this allows us to count the number of messages received in a session + -- often very useful in incidents or debugging + message_count SimpleAggregateFunction(sum, Int64), + -- this allows us to count the number of snapshot events received in a session + -- often very useful in incidents or debugging + -- because we batch events we expect message_count to be lower than event_count + event_count SimpleAggregateFunction(sum, Int64), + -- which source the snapshots came from Mobile or Web. Web if absent + snapshot_source AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), + -- knowing something is mobile isn't enough, we need to know if e.g. RN or flutter + snapshot_library AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), + _timestamp SimpleAggregateFunction(max, DateTime) + ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_replay_events', '{replica}') + + PARTITION BY toYYYYMM(min_first_timestamp) + -- order by is used by the aggregating merge tree engine to + -- identify candidates to merge, e.g. toDate(min_first_timestamp) + -- would mean we would have one row per day per session_id + -- if CH could completely merge to match the order by + -- it is also used to organise data to make queries faster + -- we want the fewest rows possible but also the fastest queries + -- since we query by date and not by time + -- and order by must be in order of increasing cardinality + -- so we order by date first, then team_id, then session_id + -- hopefully, this is a good balance between the two + ORDER BY (toDate(min_first_timestamp), team_id, session_id) + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query[sharded_sessions] + ''' + + CREATE TABLE IF NOT EXISTS sharded_sessions ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id SimpleAggregateFunction(any, String), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- create a map of how many times we saw each event + event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), + -- duplicate the event count as a specific column for pageviews and autocaptures, + -- as these are used in some key queries and need to be fast + pageview_count SimpleAggregateFunction(sum, Int64), + autocapture_count SimpleAggregateFunction(sum, Int64), + ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sessions', '{replica}') + + PARTITION BY toYYYYMM(min_timestamp) + -- order by is used by the aggregating merge tree engine to + -- identify candidates to merge, e.g. toDate(min_timestamp) + -- would mean we would have one row per day per session_id + -- if CH could completely merge to match the order by + -- it is also used to organise data to make queries faster + -- we want the fewest rows possible but also the fastest queries + -- since we query by date and not by time + -- and order by must be in order of increasing cardinality + -- so we order by date first, then team_id, then session_id + -- hopefully, this is a good balance between the two + ORDER BY (toStartOfDay(min_timestamp), team_id, session_id) + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query[writable_events] + ''' + + CREATE TABLE IF NOT EXISTS writable_events ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + , _timestamp DateTime + , _offset UInt64 + + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_events', sipHash64(distinct_id)) + + ''' +# --- +# name: test_create_table_query[writable_heatmaps] + ''' + + CREATE TABLE IF NOT EXISTS writable_heatmaps ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + timestamp DateTime64(6, 'UTC'), + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x Int16, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y Int16, + -- stored so that in future we can support other resolutions + scale_factor Int16, + viewport_width Int16, + viewport_height Int16, + -- some elements move when the page scrolls, others do not + pointer_target_fixed Bool, + current_url VARCHAR, + type LowCardinality(String), + _timestamp DateTime, + _offset UInt64, + _partition UInt64 + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_heatmaps', cityHash64(concat(toString(team_id), '-', session_id, '-', toString(toDate(timestamp))))) + + ''' +# --- +# name: test_create_table_query[writable_raw_sessions] + ''' + + CREATE TABLE IF NOT EXISTS writable_raw_sessions ON CLUSTER 'posthog' + ( + team_id Int64, + session_id_v7 UInt128, -- integer representation of a uuidv7 + + -- ClickHouse will pick the latest value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + -- urls + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + -- device + initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + + -- geoip + -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment + initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- attribution + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- Count pageview, autocapture, and screen events for providing totals. + -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were + -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be + -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to + -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're + -- satisfied that counts are accurate. + pageview_count SimpleAggregateFunction(sum, Int64), + pageview_uniq AggregateFunction(uniq, Nullable(UUID)), + autocapture_count SimpleAggregateFunction(sum, Int64), + autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), + screen_count SimpleAggregateFunction(sum, Int64), + screen_uniq AggregateFunction(uniq, Nullable(UUID)), + + -- replay + maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv + + -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these + page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), + + -- web vitals + vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_raw_sessions', cityHash64(session_id_v7)) + + ''' +# --- +# name: test_create_table_query[writable_session_recording_events] + ''' + + CREATE TABLE IF NOT EXISTS writable_session_recording_events ON CLUSTER 'posthog' + ( + uuid UUID, + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + session_id VARCHAR, + window_id VARCHAR, + snapshot_data VARCHAR, + created_at DateTime64(6, 'UTC') + + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_session_recording_events', sipHash64(distinct_id)) + + ''' +# --- +# name: test_create_table_query[writable_sessions] + ''' + + CREATE TABLE IF NOT EXISTS writable_sessions ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id SimpleAggregateFunction(any, String), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- create a map of how many times we saw each event + event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), + -- duplicate the event count as a specific column for pageviews and autocaptures, + -- as these are used in some key queries and need to be fast + pageview_count SimpleAggregateFunction(sum, Int64), + autocapture_count SimpleAggregateFunction(sum, Int64), + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_sessions', sipHash64(session_id)) + + ''' +# --- +# name: test_create_table_query[writeable_performance_events] + ''' + + CREATE TABLE IF NOT EXISTS writeable_performance_events ON CLUSTER 'posthog' + ( + uuid UUID, + session_id String, + window_id String, + pageview_id String, + distinct_id String, + timestamp DateTime64, + time_origin DateTime64(3, 'UTC'), + entry_type LowCardinality(String), + name String, + team_id Int64, + current_url String, + start_time Float64, + duration Float64, + redirect_start Float64, + redirect_end Float64, + worker_start Float64, + fetch_start Float64, + domain_lookup_start Float64, + domain_lookup_end Float64, + connect_start Float64, + secure_connection_start Float64, + connect_end Float64, + request_start Float64, + response_start Float64, + response_end Float64, + decoded_body_size Int64, + encoded_body_size Int64, + initiator_type LowCardinality(String), + next_hop_protocol LowCardinality(String), + render_blocking_status LowCardinality(String), + response_status Int64, + transfer_size Int64, + largest_contentful_paint_element String, + largest_contentful_paint_render_time Float64, + largest_contentful_paint_load_time Float64, + largest_contentful_paint_size Float64, + largest_contentful_paint_id String, + largest_contentful_paint_url String, + dom_complete Float64, + dom_content_loaded_event Float64, + dom_interactive Float64, + load_event_end Float64, + load_event_start Float64, + redirect_count Int64, + navigation_type LowCardinality(String), + unload_event_end Float64, + unload_event_start Float64 + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_performance_events', sipHash64(session_id)) + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[channel_definition] + ''' + + CREATE TABLE IF NOT EXISTS channel_definition ON CLUSTER 'posthog' ( + domain String NOT NULL, + kind String NOT NULL, + domain_type String NULL, + type_if_paid String NULL, + type_if_organic String NULL + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.channel_definition', '{replica}-{shard}') + ORDER BY (domain, kind); + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[cohortpeople] + ''' + + CREATE TABLE IF NOT EXISTS cohortpeople ON CLUSTER 'posthog' + ( + person_id UUID, + cohort_id Int64, + team_id Int64, + sign Int8, + version UInt64 + ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.cohortpeople', '{replica}-{shard}', sign) + Order By (team_id, cohort_id, person_id, version) + + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[error_tracking_issue_fingerprint_overrides] + ''' + + CREATE TABLE IF NOT EXISTS error_tracking_issue_fingerprint_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + fingerprint VARCHAR, + issue_id UUID, + is_deleted Int8, + version Int64 + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + , INDEX kafka_timestamp_minmax_error_tracking_issue_fingerprint_overrides _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.error_tracking_issue_fingerprint_overrides', '{replica}-{shard}', version) + + ORDER BY (team_id, fingerprint) + SETTINGS index_granularity = 512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[events_dead_letter_queue] + ''' + + CREATE TABLE IF NOT EXISTS events_dead_letter_queue ON CLUSTER 'posthog' + ( + id UUID, + event_uuid UUID, + event VARCHAR, + properties VARCHAR, + distinct_id VARCHAR, + team_id Int64, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + ip VARCHAR, + site_url VARCHAR, + now DateTime64(6, 'UTC'), + raw_payload VARCHAR, + error_timestamp DateTime64(6, 'UTC'), + error_location VARCHAR, + error VARCHAR, + tags Array(VARCHAR) + + + , _timestamp DateTime + , _offset UInt64 + + , INDEX kafka_timestamp_minmax_events_dead_letter_queue _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_dead_letter_queue', '{replica}-{shard}', _timestamp) + ORDER BY (id, event_uuid, distinct_id, team_id) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[events_recent] + ''' + + CREATE TABLE IF NOT EXISTS events_recent ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + , inserted_at DateTime64(6, 'UTC') DEFAULT NOW64(), _timestamp_ms DateTime64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.events_recent', '{replica}-{shard}', _timestamp) + PARTITION BY toStartOfHour(inserted_at) + ORDER BY (team_id, toStartOfHour(inserted_at), event, cityHash64(distinct_id), cityHash64(uuid)) + TTL toDateTime(inserted_at) + INTERVAL 7 DAY + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[groups] + ''' + + CREATE TABLE IF NOT EXISTS groups ON CLUSTER 'posthog' + ( + group_type_index UInt8, + group_key VARCHAR, + created_at DateTime64, + team_id Int64, + group_properties VARCHAR + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.groups', '{replica}-{shard}', _timestamp) + Order By (team_id, group_type_index, group_key) + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[log_entries] + ''' + + CREATE TABLE IF NOT EXISTS log_entries ON CLUSTER 'posthog' + ( + team_id UInt64, + -- The name of the service or product that generated the logs. + -- Examples: batch_exports + log_source LowCardinality(String), + -- An id for the log source. + -- Set log_source to avoid collision with ids from other log sources if the id generation is not safe. + -- Examples: A batch export id, a cronjob id, a plugin id. + log_source_id String, + -- A secondary id e.g. for the instance of log_source that generated this log. + -- This may be ommitted if log_source is a singleton. + -- Examples: A batch export run id, a plugin_config id, a thread id, a process id, a machine id. + instance_id String, + -- Timestamp indicating when the log was generated. + timestamp DateTime64(6, 'UTC'), + -- The log level. + -- Examples: INFO, WARNING, DEBUG, ERROR. + level LowCardinality(String), + -- The actual log message. + message String + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.log_entries', '{replica}-{shard}', _timestamp) + PARTITION BY toStartOfHour(timestamp) ORDER BY (team_id, log_source, log_source_id, instance_id, timestamp) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[person] + ''' + + CREATE TABLE IF NOT EXISTS person ON CLUSTER 'posthog' + ( + id UUID, + created_at DateTime64, + team_id Int64, + properties VARCHAR, + is_identified Int8, + is_deleted Int8, + version UInt64 + + + , _timestamp DateTime + , _offset UInt64 + + , INDEX kafka_timestamp_minmax_person _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person', '{replica}-{shard}', version) + Order By (team_id, id) + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[person_distinct_id2] + ''' + + CREATE TABLE IF NOT EXISTS person_distinct_id2 ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + + , _timestamp DateTime + , _offset UInt64 + + , _partition UInt64 + , INDEX kafka_timestamp_minmax_person_distinct_id2 _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id2', '{replica}-{shard}', version) + + ORDER BY (team_id, distinct_id) + SETTINGS index_granularity = 512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[person_distinct_id] + ''' + + CREATE TABLE IF NOT EXISTS person_distinct_id ON CLUSTER 'posthog' + ( + distinct_id VARCHAR, + person_id UUID, + team_id Int64, + _sign Int8 DEFAULT 1, + is_deleted Int8 ALIAS if(_sign==-1, 1, 0) + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id', '{replica}-{shard}', _sign) + Order By (team_id, distinct_id, person_id) + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[person_distinct_id_overrides] + ''' + + CREATE TABLE IF NOT EXISTS person_distinct_id_overrides ON CLUSTER 'posthog' + ( + team_id Int64, + distinct_id VARCHAR, + person_id UUID, + is_deleted Int8, + version Int64 + + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + , INDEX kafka_timestamp_minmax_person_distinct_id_overrides _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_distinct_id_overrides', '{replica}-{shard}', version) + + ORDER BY (team_id, distinct_id) + SETTINGS index_granularity = 512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[person_overrides] + ''' + + CREATE TABLE IF NOT EXISTS `posthog_test`.`person_overrides` + ON CLUSTER 'posthog' ( + team_id INT NOT NULL, + + -- When we merge two people `old_person_id` and `override_person_id`, we + -- want to keep track of a mapping from the `old_person_id` to the + -- `override_person_id`. This allows us to join with the + -- `sharded_events` table to find all events that were associated with + -- the `old_person_id` and update them to be associated with the + -- `override_person_id`. + old_person_id UUID NOT NULL, + override_person_id UUID NOT NULL, + + -- The timestamp the merge of the two people was completed. + merged_at DateTime64(6, 'UTC') NOT NULL, + -- The timestamp of the oldest event associated with the + -- `old_person_id`. + oldest_event DateTime64(6, 'UTC') NOT NULL, + -- The timestamp rows are created. This isn't part of the JOIN process + -- with the events table but rather a housekeeping column to allow us to + -- see when the row was created. This shouldn't have any impact of the + -- JOIN as it will be stored separately with the Wide ClickHouse table + -- storage. + created_at DateTime64(6, 'UTC') DEFAULT now(), + + -- the specific version of the `old_person_id` mapping. This is used to + -- allow us to discard old mappings as new ones are added. This version + -- will be provided by the corresponding PostgreSQL + --`posthog_personoverrides` table + version INT NOT NULL + ) + + -- By specifying Replacing merge tree on version, we allow ClickHouse to + -- discard old versions of a `old_person_id` mapping. This should help keep + -- performance in check as new versions are added. Note that given we can + -- have partitioning by `oldest_event` which will change as we update + -- `person_id` on old partitions. + -- + -- We also need to ensure that the data is replicated to all replicas in the + -- cluster, as we do not have any constraints on person_id and which shard + -- associated events are on. To do this we use the ReplicatedReplacingMergeTree + -- engine specifying a static `zk_path`. This will cause the Engine to + -- consider all replicas as the same. See + -- https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication + -- for details. + ENGINE = ReplicatedReplacingMergeTree( + -- NOTE: for testing we use a uuid to ensure that we don't get conflicts + -- when the tests tear down and recreate the table. + '/clickhouse/tables/{uuid}noshard/posthog_test.person_overrides', + '{replica}-{shard}', + version + ) + + -- We partition the table by the `oldest_event` column. This allows us to + -- handle updating the events table partition by partition, progressing each + -- override partition by partition in lockstep with the events table. Note + -- that this means it is possible that we have a mapping from + -- `old_person_id` in multiple partitions during the merge process. + PARTITION BY toYYYYMM(oldest_event) + + -- We want to collapse down on the `old_person_id` such that we end up with + -- the newest known mapping for it in the table. Query side we will need to + -- ensure that we are always querying the latest version of the mapping. + ORDER BY (team_id, old_person_id) + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[person_static_cohort] + ''' + + CREATE TABLE IF NOT EXISTS person_static_cohort ON CLUSTER 'posthog' + ( + id UUID, + person_id UUID, + cohort_id Int64, + team_id Int64 + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.person_static_cohort', '{replica}-{shard}', _timestamp) + Order By (team_id, cohort_id, person_id, id) + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[plugin_log_entries] + ''' + + CREATE TABLE IF NOT EXISTS plugin_log_entries ON CLUSTER 'posthog' + ( + id UUID, + team_id Int64, + plugin_id Int64, + plugin_config_id Int64, + timestamp DateTime64(6, 'UTC'), + source VARCHAR, + type VARCHAR, + message VARCHAR, + instance_id UUID + + , _timestamp DateTime + , _offset UInt64 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_noshard/posthog.plugin_log_entries', '{replica}-{shard}', _timestamp) + PARTITION BY toYYYYMMDD(timestamp) ORDER BY (team_id, plugin_id, plugin_config_id, timestamp) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_app_metrics2] + ''' + + CREATE TABLE IF NOT EXISTS sharded_app_metrics2 ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + -- The name of the service or product that generated the metrics. + -- Examples: plugins, hog + app_source LowCardinality(String), + -- An id for the app source. + -- Set app_source to avoid collision with ids from other app sources if the id generation is not safe. + -- Examples: A plugin id, a hog application id + app_source_id String, + -- A secondary id e.g. for the instance of app_source that generated this metric. + -- This may be ommitted if app_source is a singleton. + -- Examples: A plugin config id, a hog application config id + instance_id String, + metric_kind LowCardinality(String), + metric_name LowCardinality(String), + count SimpleAggregateFunction(sum, Int64) + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) + ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics2', '{replica}') + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, app_source, app_source_id, instance_id, toStartOfHour(timestamp), metric_kind, metric_name) + + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_app_metrics] + ''' + + CREATE TABLE IF NOT EXISTS sharded_app_metrics ON CLUSTER 'posthog' + ( + team_id Int64, + timestamp DateTime64(6, 'UTC'), + plugin_config_id Int64, + category LowCardinality(String), + job_id String, + successes SimpleAggregateFunction(sum, Int64), + successes_on_retry SimpleAggregateFunction(sum, Int64), + failures SimpleAggregateFunction(sum, Int64), + error_uuid UUID, + error_type String, + error_details String CODEC(ZSTD(3)) + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) + ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_app_metrics', '{replica}') + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, plugin_config_id, job_id, category, toStartOfHour(timestamp), error_type, error_uuid) + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_events ON CLUSTER 'posthog' + ( + uuid UUID, + event VARCHAR, + properties VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + elements_chain VARCHAR, + created_at DateTime64(6, 'UTC'), + person_id UUID, + person_created_at DateTime64, + person_properties VARCHAR Codec(ZSTD(3)), + group0_properties VARCHAR Codec(ZSTD(3)), + group1_properties VARCHAR Codec(ZSTD(3)), + group2_properties VARCHAR Codec(ZSTD(3)), + group3_properties VARCHAR Codec(ZSTD(3)), + group4_properties VARCHAR Codec(ZSTD(3)), + group0_created_at DateTime64, + group1_created_at DateTime64, + group2_created_at DateTime64, + group3_created_at DateTime64, + group4_created_at DateTime64, + person_mode Enum8('full' = 0, 'propertyless' = 1, 'force_upgrade' = 2) + + , $group_0 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_0'), '^"|"$', '') COMMENT 'column_materializer::$group_0' + , $group_1 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_1'), '^"|"$', '') COMMENT 'column_materializer::$group_1' + , $group_2 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_2'), '^"|"$', '') COMMENT 'column_materializer::$group_2' + , $group_3 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_3'), '^"|"$', '') COMMENT 'column_materializer::$group_3' + , $group_4 VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$group_4'), '^"|"$', '') COMMENT 'column_materializer::$group_4' + , $window_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$window_id'), '^"|"$', '') COMMENT 'column_materializer::$window_id' + , $session_id VARCHAR MATERIALIZED replaceRegexpAll(JSONExtractRaw(properties, '$session_id'), '^"|"$', '') COMMENT 'column_materializer::$session_id' + , elements_chain_href String MATERIALIZED extract(elements_chain, '(?::|")href="(.*?)"') + , elements_chain_texts Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")text="(.*?)"')) + , elements_chain_ids Array(String) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?::|")attr_id="(.*?)"')) + , elements_chain_elements Array(Enum('a', 'button', 'form', 'input', 'select', 'textarea', 'label')) MATERIALIZED arrayDistinct(extractAll(elements_chain, '(?:^|;)(a|button|form|input|select|textarea|label)(?:\.|$|:)')) + , INDEX `minmax_$group_0` `$group_0` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_1` `$group_1` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_2` `$group_2` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_3` `$group_3` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$group_4` `$group_4` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$window_id` `$window_id` TYPE minmax GRANULARITY 1 + , INDEX `minmax_$session_id` `$session_id` TYPE minmax GRANULARITY 1 + , properties_group_custom Map(String, String) + MATERIALIZED mapSort( + mapFilter((key, _) -> key NOT LIKE '$%' AND key NOT IN ('token', 'distinct_id', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gclid', 'gad_source', 'gclsrc', 'dclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'twclid', 'li_fat_id', 'mc_cid', 'igshid', 'ttclid', 'rdt_cid'), + CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) + ) + CODEC(ZSTD(1)) + , INDEX properties_group_custom_keys_bf mapKeys(properties_group_custom) TYPE bloom_filter, INDEX properties_group_custom_values_bf mapValues(properties_group_custom) TYPE bloom_filter, properties_group_feature_flags Map(String, String) + MATERIALIZED mapSort( + mapFilter((key, _) -> key like '$feature/%', + CAST(JSONExtractKeysAndValues(properties, 'String'), 'Map(String, String)')) + ) + CODEC(ZSTD(1)) + , INDEX properties_group_feature_flags_keys_bf mapKeys(properties_group_feature_flags) TYPE bloom_filter, INDEX properties_group_feature_flags_values_bf mapValues(properties_group_feature_flags) TYPE bloom_filter + + + , _timestamp DateTime + , _offset UInt64 + , inserted_at Nullable(DateTime64(6, 'UTC')) DEFAULT NOW64() + + , INDEX kafka_timestamp_minmax_sharded_events _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.events', '{replica}', _timestamp) + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid)) + SAMPLE BY cityHash64(distinct_id) + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_heatmaps] + ''' + + CREATE TABLE IF NOT EXISTS sharded_heatmaps ON CLUSTER 'posthog' + ( + session_id VARCHAR, + team_id Int64, + distinct_id VARCHAR, + timestamp DateTime64(6, 'UTC'), + -- x is the x with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + x Int16, + -- y is the y with resolution applied, the resolution converts high fidelity mouse positions into an NxN grid + y Int16, + -- stored so that in future we can support other resolutions + scale_factor Int16, + viewport_width Int16, + viewport_height Int16, + -- some elements move when the page scrolls, others do not + pointer_target_fixed Bool, + current_url VARCHAR, + type LowCardinality(String), + _timestamp DateTime, + _offset UInt64, + _partition UInt64 + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.heatmaps', '{replica}') + + PARTITION BY toYYYYMM(timestamp) + -- almost always this is being queried by + -- * type, + -- * team_id, + -- * date range, + -- * URL (maybe matching wild cards), + -- * width + -- we'll almost never query this by session id + -- so from least to most cardinality that's + ORDER BY (type, team_id, toDate(timestamp), current_url, viewport_width) + + -- I am purposefully not setting index granularity + -- the default is 8192, and we will be loading a lot of data + -- per query, we tend to copy this 512 around the place but + -- i don't think it applies here + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_ingestion_warnings] + ''' + + CREATE TABLE IF NOT EXISTS sharded_ingestion_warnings ON CLUSTER 'posthog' + ( + team_id Int64, + source LowCardinality(VARCHAR), + type VARCHAR, + details VARCHAR CODEC(ZSTD(3)), + timestamp DateTime64(6, 'UTC') + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sharded_ingestion_warnings', '{replica}') + PARTITION BY toYYYYMMDD(timestamp) + ORDER BY (team_id, toHour(timestamp), type, source, timestamp) + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_performance_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_performance_events ON CLUSTER 'posthog' + ( + uuid UUID, + session_id String, + window_id String, + pageview_id String, + distinct_id String, + timestamp DateTime64, + time_origin DateTime64(3, 'UTC'), + entry_type LowCardinality(String), + name String, + team_id Int64, + current_url String, + start_time Float64, + duration Float64, + redirect_start Float64, + redirect_end Float64, + worker_start Float64, + fetch_start Float64, + domain_lookup_start Float64, + domain_lookup_end Float64, + connect_start Float64, + secure_connection_start Float64, + connect_end Float64, + request_start Float64, + response_start Float64, + response_end Float64, + decoded_body_size Int64, + encoded_body_size Int64, + initiator_type LowCardinality(String), + next_hop_protocol LowCardinality(String), + render_blocking_status LowCardinality(String), + response_status Int64, + transfer_size Int64, + largest_contentful_paint_element String, + largest_contentful_paint_render_time Float64, + largest_contentful_paint_load_time Float64, + largest_contentful_paint_size Float64, + largest_contentful_paint_id String, + largest_contentful_paint_url String, + dom_complete Float64, + dom_content_loaded_event Float64, + dom_interactive Float64, + load_event_end Float64, + load_event_start Float64, + redirect_count Int64, + navigation_type LowCardinality(String), + unload_event_end Float64, + unload_event_start Float64 + + , _timestamp DateTime + , _offset UInt64 + , _partition UInt64 + + ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.performance_events', '{replica}') + PARTITION BY toYYYYMM(timestamp) + ORDER BY (team_id, toDate(timestamp), session_id, pageview_id, timestamp) + + SETTINGS storage_policy = 'hot_to_cold' + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_raw_sessions] + ''' + + CREATE TABLE IF NOT EXISTS sharded_raw_sessions ON CLUSTER 'posthog' + ( + team_id Int64, + session_id_v7 UInt128, -- integer representation of a uuidv7 + + -- ClickHouse will pick the latest value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + distinct_id AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + -- urls + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + end_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + last_external_click_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + -- device + initial_browser AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_browser_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_os_version AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_device_type AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_viewport_width AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + initial_viewport_height AggregateFunction(argMin, Int64, DateTime64(6, 'UTC')), + + -- geoip + -- only store the properties we actually use, as there's tons, see https://posthog.com/docs/cdp/geoip-enrichment + initial_geoip_country_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_code AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_1_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_subdivision_city_name AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_geoip_time_zone AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- attribution + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- Count pageview, autocapture, and screen events for providing totals. + -- It's unclear if we can use the counts as they are not idempotent, and we had a bug on EU where events were + -- double-counted, so the counts were wrong. To get around this, also keep track of the unique uuids. This will be + -- slower and more expensive to store, but will be correct even if events are double-counted, so can be used to + -- verify correctness and as a backup. Ideally we will be able to delete the uniq columns in the future when we're + -- satisfied that counts are accurate. + pageview_count SimpleAggregateFunction(sum, Int64), + pageview_uniq AggregateFunction(uniq, Nullable(UUID)), + autocapture_count SimpleAggregateFunction(sum, Int64), + autocapture_uniq AggregateFunction(uniq, Nullable(UUID)), + screen_count SimpleAggregateFunction(sum, Int64), + screen_uniq AggregateFunction(uniq, Nullable(UUID)), + + -- replay + maybe_has_session_replay SimpleAggregateFunction(max, Bool), -- will be written False to by the events table mv and True to by the replay table mv + + -- as a performance optimisation, also keep track of the uniq events for all of these combined, a bounce is a session with <2 of these + page_screen_autocapture_uniq_up_to AggregateFunction(uniqUpTo(1), Nullable(UUID)), + + -- web vitals + vitals_lcp AggregateFunction(argMin, Nullable(Float64), DateTime64(6, 'UTC')) + ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.raw_sessions', '{replica}') + + PARTITION BY toYYYYMM(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))) + ORDER BY ( + team_id, + toStartOfHour(fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)), 1000))), + cityHash64(session_id_v7), + session_id_v7 + ) + SAMPLE BY cityHash64(session_id_v7) + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_session_recording_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_session_recording_events ON CLUSTER 'posthog' + ( + uuid UUID, + timestamp DateTime64(6, 'UTC'), + team_id Int64, + distinct_id VARCHAR, + session_id VARCHAR, + window_id VARCHAR, + snapshot_data VARCHAR, + created_at DateTime64(6, 'UTC') + , has_full_snapshot Int8 MATERIALIZED JSONExtractBool(snapshot_data, 'has_full_snapshot'), events_summary Array(String) MATERIALIZED JSONExtract(JSON_QUERY(snapshot_data, '$.events_summary[*]'), 'Array(String)'), click_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 2, events_summary)), keypress_count Int8 MATERIALIZED length(arrayFilter((x) -> JSONExtractInt(x, 'type') = 3 AND JSONExtractInt(x, 'data', 'source') = 5, events_summary)), timestamps_summary Array(DateTime64(6, 'UTC')) MATERIALIZED arraySort(arrayMap((x) -> toDateTime(JSONExtractInt(x, 'timestamp') / 1000), events_summary)), first_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('min', timestamps_summary)), last_event_timestamp Nullable(DateTime64(6, 'UTC')) MATERIALIZED if(empty(timestamps_summary), NULL, arrayReduce('max', timestamps_summary)), urls Array(String) MATERIALIZED arrayFilter(x -> x != '', arrayMap((x) -> JSONExtractString(x, 'data', 'href'), events_summary)) + + + , _timestamp DateTime + , _offset UInt64 + + , INDEX kafka_timestamp_minmax_sharded_session_recording_events _timestamp TYPE minmax GRANULARITY 3 + + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_recording_events', '{replica}', _timestamp) + PARTITION BY toYYYYMMDD(timestamp) + ORDER BY (team_id, toHour(timestamp), session_id, timestamp, uuid) + + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_session_replay_events] + ''' + + CREATE TABLE IF NOT EXISTS sharded_session_replay_events ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id VARCHAR, + min_first_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_last_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + -- store the first url of the session so we can quickly show that in playlists + first_url AggregateFunction(argMin, Nullable(VARCHAR), DateTime64(6, 'UTC')), + -- but also store each url so we can query by visited page without having to scan all events + -- despite the name we can put mobile screens in here as well to give same functionality across platforms + all_urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + click_count SimpleAggregateFunction(sum, Int64), + keypress_count SimpleAggregateFunction(sum, Int64), + mouse_activity_count SimpleAggregateFunction(sum, Int64), + active_milliseconds SimpleAggregateFunction(sum, Int64), + console_log_count SimpleAggregateFunction(sum, Int64), + console_warn_count SimpleAggregateFunction(sum, Int64), + console_error_count SimpleAggregateFunction(sum, Int64), + -- this column allows us to estimate the amount of data that is being ingested + size SimpleAggregateFunction(sum, Int64), + -- this allows us to count the number of messages received in a session + -- often very useful in incidents or debugging + message_count SimpleAggregateFunction(sum, Int64), + -- this allows us to count the number of snapshot events received in a session + -- often very useful in incidents or debugging + -- because we batch events we expect message_count to be lower than event_count + event_count SimpleAggregateFunction(sum, Int64), + -- which source the snapshots came from Mobile or Web. Web if absent + snapshot_source AggregateFunction(argMin, LowCardinality(Nullable(String)), DateTime64(6, 'UTC')), + -- knowing something is mobile isn't enough, we need to know if e.g. RN or flutter + snapshot_library AggregateFunction(argMin, Nullable(String), DateTime64(6, 'UTC')), + _timestamp SimpleAggregateFunction(max, DateTime) + ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.session_replay_events', '{replica}') + + PARTITION BY toYYYYMM(min_first_timestamp) + -- order by is used by the aggregating merge tree engine to + -- identify candidates to merge, e.g. toDate(min_first_timestamp) + -- would mean we would have one row per day per session_id + -- if CH could completely merge to match the order by + -- it is also used to organise data to make queries faster + -- we want the fewest rows possible but also the fastest queries + -- since we query by date and not by time + -- and order by must be in order of increasing cardinality + -- so we order by date first, then team_id, then session_id + -- hopefully, this is a good balance between the two + ORDER BY (toDate(min_first_timestamp), team_id, session_id) + SETTINGS index_granularity=512 + + ''' +# --- +# name: test_create_table_query_replicated_and_storage[sharded_sessions] + ''' + + CREATE TABLE IF NOT EXISTS sharded_sessions ON CLUSTER 'posthog' + ( + -- part of order by so will aggregate correctly + session_id VARCHAR, + -- part of order by so will aggregate correctly + team_id Int64, + -- ClickHouse will pick any value of distinct_id for the session + -- this is fine since even if the distinct_id changes during a session + -- it will still (or should still) map to the same person + distinct_id SimpleAggregateFunction(any, String), + + min_timestamp SimpleAggregateFunction(min, DateTime64(6, 'UTC')), + max_timestamp SimpleAggregateFunction(max, DateTime64(6, 'UTC')), + + urls SimpleAggregateFunction(groupUniqArrayArray, Array(String)), + entry_url AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + exit_url AggregateFunction(argMax, String, DateTime64(6, 'UTC')), + + initial_referring_domain AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_campaign AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_medium AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_term AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_utm_content AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gad_source AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gclsrc AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_dclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_gbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_wbraid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_fbclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_msclkid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_twclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_li_fat_id AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_mc_cid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_igshid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + initial_ttclid AggregateFunction(argMin, String, DateTime64(6, 'UTC')), + + -- create a map of how many times we saw each event + event_count_map SimpleAggregateFunction(sumMap, Map(String, Int64)), + -- duplicate the event count as a specific column for pageviews and autocaptures, + -- as these are used in some key queries and need to be fast + pageview_count SimpleAggregateFunction(sum, Int64), + autocapture_count SimpleAggregateFunction(sum, Int64), + ) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/77f1df52-4b43-11e9-910f-b8ca3a9b9f3e_{shard}/posthog.sessions', '{replica}') + + PARTITION BY toYYYYMM(min_timestamp) + -- order by is used by the aggregating merge tree engine to + -- identify candidates to merge, e.g. toDate(min_timestamp) + -- would mean we would have one row per day per session_id + -- if CH could completely merge to match the order by + -- it is also used to organise data to make queries faster + -- we want the fewest rows possible but also the fastest queries + -- since we query by date and not by time + -- and order by must be in order of increasing cardinality + -- so we order by date first, then team_id, then session_id + -- hopefully, this is a good balance between the two + ORDER BY (toStartOfDay(min_timestamp), team_id, session_id) + SETTINGS index_granularity=512 + + ''' +# --- From 0d55dca77ade374b30150d762d119cb767789dc5 Mon Sep 17 00:00:00 2001 From: Alexander Spicer Date: Wed, 8 Jan 2025 13:46:34 -0800 Subject: [PATCH 8/9] hm --- .../insights/stickiness_query_runner.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/posthog/hogql_queries/insights/stickiness_query_runner.py b/posthog/hogql_queries/insights/stickiness_query_runner.py index 2b6ccff029c27..507705c551bba 100644 --- a/posthog/hogql_queries/insights/stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/stickiness_query_runner.py @@ -95,16 +95,25 @@ def _having_clause(self) -> ast.Expr: value = ast.Constant(value=self.query.stickinessFilter.stickinessCriteria.value) return parse_expr(f"""count() {get_count_operator(operator)} {{value}}""", {"value": value}) - def date_to_start_of_interval_hogql(self, date: ast.Expr) -> ast.Call: - return ast.Call( - name="toStartOfInterval", - args=[ - date, - ast.Call( - name=f"toInterval{self.query_date_range.interval_name.capitalize()}", - args=[ast.Constant(value=self.query.intervalCount or 1)], - ), - ], + def date_to_start_of_interval_hogql(self, date: ast.Expr) -> ast.Expr: + if self.query.intervalCount is None or self.query.intervalCount == 1: + return self.query_date_range.date_to_start_of_interval_hogql(ast.Field(chain=["e", "timestamp"])) + + # find the number of intervals back from the end date + age = parse_expr( + """age({interval_name}, {from_date}, {to_date})""", + placeholders={ + "interval_name": ast.Constant(value=self.query_date_range.interval_name), + "from_date": date, + "to_date": self.query_date_range.date_to_as_hogql(), + }, + ) + if not self.query.intervalCount or self.query.intervalCount == 1: + return age + + return parse_expr( + "floor({age} / {interval_count})", + placeholders={"age": age, "interval_count": ast.Constant(value=self.query.intervalCount or 1)}, ) def _events_query(self, series_with_extra: SeriesWithExtras) -> ast.SelectQuery: From 8723b12fa33fd212fe49d837e2b6c4edf9337aa9 Mon Sep 17 00:00:00 2001 From: Alexander Spicer Date: Wed, 8 Jan 2025 14:07:08 -0800 Subject: [PATCH 9/9] qqq --- frontend/src/queries/schema.json | 1 - frontend/src/queries/schema.ts | 1 - posthog/hogql_queries/insights/stickiness_query_runner.py | 6 +++--- posthog/schema.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 31cb6c18ff164..3c049b228ac97 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -12351,7 +12351,6 @@ "description": "Granularity of the response. Can be one of `hour`, `day`, `week` or `month`" }, "intervalCount": { - "default": 1, "description": "How many intervals comprise a period. Only used for cohorts, otherwise default 1.", "type": "integer" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index c5d85f4206014..88dfb2cb6b81c 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1563,7 +1563,6 @@ export interface StickinessQuery interval?: IntervalType /** * How many intervals comprise a period. Only used for cohorts, otherwise default 1. - * @default 1 */ intervalCount?: integer /** Events and actions to include */ diff --git a/posthog/hogql_queries/insights/stickiness_query_runner.py b/posthog/hogql_queries/insights/stickiness_query_runner.py index 507705c551bba..fc69d91b55c99 100644 --- a/posthog/hogql_queries/insights/stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/stickiness_query_runner.py @@ -96,7 +96,7 @@ def _having_clause(self) -> ast.Expr: return parse_expr(f"""count() {get_count_operator(operator)} {{value}}""", {"value": value}) def date_to_start_of_interval_hogql(self, date: ast.Expr) -> ast.Expr: - if self.query.intervalCount is None or self.query.intervalCount == 1: + if self.query.intervalCount is None: return self.query_date_range.date_to_start_of_interval_hogql(ast.Field(chain=["e", "timestamp"])) # find the number of intervals back from the end date @@ -108,12 +108,12 @@ def date_to_start_of_interval_hogql(self, date: ast.Expr) -> ast.Expr: "to_date": self.query_date_range.date_to_as_hogql(), }, ) - if not self.query.intervalCount or self.query.intervalCount == 1: + if self.query.intervalCount == 1: return age return parse_expr( "floor({age} / {interval_count})", - placeholders={"age": age, "interval_count": ast.Constant(value=self.query.intervalCount or 1)}, + placeholders={"age": age, "interval_count": ast.Constant(value=self.query.intervalCount)}, ) def _events_query(self, series_with_extra: SeriesWithExtras) -> ast.SelectQuery: diff --git a/posthog/schema.py b/posthog/schema.py index ae1f25dd8bc67..edbd4877a79e2 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -5819,7 +5819,7 @@ class StickinessQuery(BaseModel): description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`", ) intervalCount: Optional[int] = Field( - default=1, description="How many intervals comprise a period. Only used for cohorts, otherwise default 1." + default=None, description="How many intervals comprise a period. Only used for cohorts, otherwise default 1." ) kind: Literal["StickinessQuery"] = "StickinessQuery" modifiers: Optional[HogQLQueryModifiers] = Field(