1616 * Uses a single MergeTree table with a 'type' column ('event' or 'gauge')
1717 * and query-time aggregation (SUM for events, argMax for gauges).
1818 *
19- * A SummingMergeTree materialized view is created for billing totals (events only) .
19+ * A SummingMergeTree materialized view pre-aggregates events by day for fast billing/analytics queries .
2020 *
2121 * Features:
2222 * - Single MergeTree table for all metrics (no period fan-out)
@@ -881,7 +881,7 @@ private function formatParamValue(mixed $value): string
881881 /**
882882 * Setup ClickHouse table structure.
883883 *
884- * Creates a single MergeTree table and a SummingMergeTree materialized view for billing .
884+ * Creates a single MergeTree table and a SummingMergeTree daily materialized view for events .
885885 *
886886 * @throws Exception
887887 */
@@ -949,81 +949,21 @@ public function setup(): void
949949
950950 $ this ->query ($ createTableSql );
951951
952- // Create billing target table (SummingMergeTree)
953- $ billingTableName = $ tableName . '_billing ' ;
954- $ escapedBillingTable = $ this ->escapeIdentifier ($ this ->database ) . '. ' . $ this ->escapeIdentifier ($ billingTableName );
955-
956- $ billingColumns = [
957- 'metric String ' ,
958- 'value Int64 ' ,
959- 'time DateTime64(3) ' ,
960- ];
961-
962- if ($ this ->sharedTables ) {
963- array_splice ($ billingColumns , 1 , 0 , ['tenant Nullable(String) ' ]);
964- }
965-
966- $ billingColumnDefs = implode (", \n " , $ billingColumns );
967- $ billingOrderBy = $ this ->sharedTables ? '(tenant, metric, time) ' : '(metric, time) ' ;
968-
969- $ createBillingTableSql = "
970- CREATE TABLE IF NOT EXISTS {$ escapedBillingTable } (
971- {$ billingColumnDefs }
972- )
973- ENGINE = SummingMergeTree()
974- ORDER BY {$ billingOrderBy }
975- SETTINGS allow_nullable_key = 1
976- " ;
977-
978- $ this ->query ($ createBillingTableSql );
979-
980- // Create materialized view for billing (events only)
981- $ billingMvName = $ tableName . '_billing_mv ' ;
982- $ escapedBillingMv = $ this ->escapeIdentifier ($ this ->database ) . '. ' . $ this ->escapeIdentifier ($ billingMvName );
983-
984- $ tenantSelect = $ this ->sharedTables ? 'tenant ' : "'' as tenant " ;
985- $ groupByClause = $ this ->sharedTables ? 'metric, tenant, time ' : 'metric, time ' ;
986-
987- $ billingSelectColumns = $ this ->sharedTables
988- ? "metric, \n {$ tenantSelect }, \n sum(value) as value, \n toStartOfMonth(time) as time "
989- : "metric, \n sum(value) as value, \n toStartOfMonth(time) as time " ;
990-
991- $ createBillingMvSql = "
992- CREATE MATERIALIZED VIEW IF NOT EXISTS {$ escapedBillingMv }
993- TO {$ escapedBillingTable }
994- AS SELECT
995- {$ billingSelectColumns }
996- FROM {$ escapedDatabaseAndTable }
997- WHERE type = 'event'
998- GROUP BY {$ groupByClause }
999- " ;
1000-
1001- $ this ->query ($ createBillingMvSql );
1002-
1003952 // Create daily aggregation target table (SummingMergeTree) for events
953+ // Same column definitions as source table — just pre-aggregated by day
1004954 $ dailyTableName = $ tableName . '_daily ' ;
1005955 $ escapedDailyTable = $ this ->escapeIdentifier ($ this ->database ) . '. ' . $ this ->escapeIdentifier ($ dailyTableName );
1006956
1007- $ dailyColumns = [
1008- 'metric String ' ,
1009- 'value Int64 ' ,
1010- 'time DateTime64(3) ' ,
1011- ];
1012-
1013- if ($ this ->sharedTables ) {
1014- array_splice ($ dailyColumns , 1 , 0 , ['tenant Nullable(String) ' ]);
1015- }
1016-
1017- $ dailyColumnDefs = implode (", \n " , $ dailyColumns );
1018957 $ dailyOrderBy = $ this ->sharedTables ? '(tenant, metric, time) ' : '(metric, time) ' ;
1019958
1020959 $ createDailyTableSql = "
1021960 CREATE TABLE IF NOT EXISTS {$ escapedDailyTable } (
1022- {$ dailyColumnDefs }
961+ {$ columnDefs }{ $ indexDefs }
1023962 )
1024963 ENGINE = SummingMergeTree()
1025964 ORDER BY {$ dailyOrderBy }
1026- SETTINGS allow_nullable_key = 1
965+ PARTITION BY toYYYYMM(time)
966+ SETTINGS index_granularity = 8192, allow_nullable_key = 1
1027967 " ;
1028968
1029969 $ this ->query ($ createDailyTableSql );
@@ -1032,18 +972,21 @@ public function setup(): void
1032972 $ dailyMvName = $ tableName . '_daily_mv ' ;
1033973 $ escapedDailyMv = $ this ->escapeIdentifier ($ this ->database ) . '. ' . $ this ->escapeIdentifier ($ dailyMvName );
1034974
1035- $ dailySelectColumns = $ this ->sharedTables
1036- ? "metric, \n {$ tenantSelect }, \n sum(value) as value, \n toStartOfDay(time) as time "
1037- : "metric, \n sum(value) as value, \n toStartOfDay(time) as time " ;
975+ $ tenantSelect = $ this ->sharedTables ? 'tenant ' : "'' as tenant " ;
976+ $ groupByClause = $ this ->sharedTables ? 'metric, tenant ' : 'metric ' ;
977+
978+ $ dailySelect = $ this ->sharedTables
979+ ? "generateUUIDv4() as id, metric, {$ tenantSelect }, sum(value) as value, 'event' as type, toStartOfDay(time) as time, '{}' as tags "
980+ : "generateUUIDv4() as id, metric, sum(value) as value, 'event' as type, toStartOfDay(time) as time, '{}' as tags " ;
1038981
1039982 $ createDailyMvSql = "
1040983 CREATE MATERIALIZED VIEW IF NOT EXISTS {$ escapedDailyMv }
1041984 TO {$ escapedDailyTable }
1042985 AS SELECT
1043- {$ dailySelectColumns }
986+ {$ dailySelect }
1044987 FROM {$ escapedDatabaseAndTable }
1045988 WHERE type = 'event'
1046- GROUP BY {$ groupByClause }
989+ GROUP BY {$ groupByClause }, toStartOfDay(time)
1047990 " ;
1048991
1049992 $ this ->query ($ createDailyMvSql );
0 commit comments