@@ -1081,14 +1081,14 @@ public function setup(): void
10811081 */
10821082 private function createTable (string $ tableName , string $ type , array $ indexes ): void
10831083 {
1084- $ columns = ['id String ' ];
1084+ $ columns = ['id String ' . $ this -> getColumnCodec ( ' id ' ) ];
10851085
10861086 foreach ($ this ->getAttributes ($ type ) as $ attribute ) {
10871087 /** @var string $id */
10881088 $ id = $ attribute ['$id ' ];
10891089
10901090 if ($ id === 'time ' ) {
1091- $ columns [] = 'time DateTime64(3) ' ;
1091+ $ columns [] = 'time DateTime64(3) ' . $ this -> getColumnCodec ( ' time ' ) ;
10921092 } else {
10931093 $ columns [] = $ this ->getColumnDefinition ($ id , $ type );
10941094 }
@@ -1099,17 +1099,20 @@ private function createTable(string $tableName, string $type, array $indexes): v
10991099 $ columns [] = 'tenant Nullable(String) ' ;
11001100 }
11011101
1102- // Build indexes
1102+ // Build indexes. `indexType` selects per-column between bloom_filter
1103+ // (high-cardinality strings) and set(0) (low-cardinality enums that
1104+ // are too dense for blooms to ever skip — see plausible-research.md).
11031105 $ indexDefs = [];
11041106 foreach ($ indexes as $ index ) {
11051107 /** @var string $indexName */
11061108 $ indexName = $ index ['$id ' ];
11071109 /** @var array<string> $attributes */
11081110 $ attributes = $ index ['attributes ' ];
1111+ $ indexType = is_string ($ index ['indexType ' ] ?? null ) ? $ index ['indexType ' ] : 'bloom_filter ' ;
11091112 $ escapedIndexName = $ this ->escapeIdentifier ($ indexName );
11101113 $ escapedAttributes = array_map (fn ($ attr ) => $ this ->escapeIdentifier ($ attr ), $ attributes );
11111114 $ attributeList = implode (', ' , $ escapedAttributes );
1112- $ indexDefs [] = "INDEX {$ escapedIndexName } ( {$ attributeList }) TYPE bloom_filter GRANULARITY 1 " ;
1115+ $ indexDefs [] = "INDEX {$ escapedIndexName } ( {$ attributeList }) TYPE { $ indexType } GRANULARITY 1 " ;
11131116 }
11141117
11151118 $ escapedDatabaseAndTable = $ this ->escapeIdentifier ($ this ->database ) . '. ' . $ this ->escapeIdentifier ($ tableName );
@@ -1153,12 +1156,12 @@ private function createDailyTable(): void
11531156 $ columns = [
11541157 'metric String ' ,
11551158 'value Int64 ' ,
1156- 'time DateTime64(3) ' ,
1159+ 'time DateTime64(3) CODEC(Delta(4), LZ4) ' ,
11571160 'resource LowCardinality(Nullable(String)) ' ,
1158- 'resourceId Nullable(String) ' ,
1159- 'resourceInternalId Nullable(String) ' ,
1160- 'teamId Nullable(String) ' ,
1161- 'teamInternalId Nullable(String) ' ,
1161+ 'resourceId Nullable(String) CODEC(ZSTD(3)) ' ,
1162+ 'resourceInternalId Nullable(String) CODEC(ZSTD(3)) ' ,
1163+ 'teamId Nullable(String) CODEC(ZSTD(3)) ' ,
1164+ 'teamInternalId Nullable(String) CODEC(ZSTD(3)) ' ,
11621165 ];
11631166
11641167 if ($ this ->sharedTables ) {
@@ -1365,8 +1368,11 @@ private function getColumnType(string $id, string $type = 'event'): string
13651368
13661369 $ lowCardinality = [
13671370 'country ' , 'region ' , 'service ' , 'resource ' ,
1368- 'osCode ' , 'osName ' , 'clientType ' , 'clientCode ' , 'clientName ' ,
1369- 'clientEngine ' , 'deviceName ' , 'deviceBrand ' ,
1371+ 'osCode ' , 'osName ' , 'osVersion ' ,
1372+ 'clientType ' , 'clientCode ' , 'clientName ' , 'clientVersion ' ,
1373+ 'clientEngine ' , 'clientEngineVersion ' ,
1374+ 'deviceName ' , 'deviceBrand ' , 'deviceModel ' ,
1375+ 'path ' , 'hostname ' ,
13701376 ];
13711377
13721378 if (in_array ($ id , $ lowCardinality , true )) {
@@ -1389,9 +1395,38 @@ protected function getColumnDefinition(string $id, string $type = 'event'): stri
13891395 {
13901396 $ chType = $ this ->getColumnType ($ id , $ type );
13911397 $ escapedId = $ this ->escapeIdentifier ($ id );
1398+ $ codec = $ this ->getColumnCodec ($ id );
1399+ if ($ codec !== '' ) {
1400+ return "{$ escapedId } {$ chType } {$ codec }" ;
1401+ }
13921402 return "{$ escapedId } {$ chType }" ;
13931403 }
13941404
1405+ /**
1406+ * Return the per-column ClickHouse CODEC clause (Plausible-style
1407+ * compression hygiene) for the events/gauges tables. Empty string when
1408+ * no codec is overridden for this column.
1409+ */
1410+ private function getColumnCodec (string $ id ): string
1411+ {
1412+ if ($ id === 'time ' ) {
1413+ return 'CODEC(Delta(4), LZ4) ' ;
1414+ }
1415+
1416+ $ zstdColumns = [
1417+ 'id ' , 'path ' , 'hostname ' ,
1418+ 'resourceId ' , 'resourceInternalId ' ,
1419+ 'teamId ' , 'teamInternalId ' ,
1420+ 'osVersion ' , 'clientVersion ' , 'clientEngineVersion ' , 'deviceModel ' ,
1421+ ];
1422+
1423+ if (in_array ($ id , $ zstdColumns , true )) {
1424+ return 'CODEC(ZSTD(3)) ' ;
1425+ }
1426+
1427+ return '' ;
1428+ }
1429+
13951430 /**
13961431 * Validate metric data for batch operations.
13971432 *
@@ -2274,20 +2309,28 @@ public function findDaily(array $queries = []): array
22742309 $ parsed = $ this ->parseQueries ($ queries , Usage::TYPE_EVENT );
22752310 $ whereData = $ this ->buildWhereClause ($ parsed ['filters ' ], $ parsed ['params ' ]);
22762311
2277- $ dailyColumns = ['metric ' , ' value ' , 'time ' ];
2312+ $ groupByColumns = ['metric ' , 'time ' ];
22782313 if ($ this ->sharedTables ) {
2279- $ dailyColumns [] = 'tenant ' ;
2314+ $ groupByColumns [] = 'tenant ' ;
2315+ }
2316+
2317+ $ selectExpressions = [];
2318+ foreach ($ groupByColumns as $ column ) {
2319+ $ selectExpressions [] = $ this ->escapeIdentifier ($ column );
22802320 }
2281- $ selectColumns = implode (', ' , array_map (fn ($ c ) => $ this ->escapeIdentifier ($ c ), $ dailyColumns ));
2321+ $ selectExpressions [] = 'sum(`value`) AS `value` ' ;
2322+
2323+ $ selectColumns = implode (', ' , $ selectExpressions );
2324+ $ groupBySql = implode (', ' , array_map (fn ($ c ) => $ this ->escapeIdentifier ($ c ), $ groupByColumns ));
22822325
22832326 $ orderClause = !empty ($ parsed ['orderBy ' ]) ? ' ORDER BY ' . implode (', ' , $ parsed ['orderBy ' ]) : '' ;
22842327 $ limitClause = isset ($ parsed ['limit ' ]) ? ' LIMIT {limit:UInt64} ' : '' ;
22852328 $ offsetClause = isset ($ parsed ['offset ' ]) ? ' OFFSET {offset:UInt64} ' : '' ;
22862329
2287- // The daily table is SummingMergeTree. Reading raw rows returns
2288- // un-merged duplicates until background merges run. FINAL forces
2289- // merge-on-read so callers always see fully-collapsed values .
2290- $ sql = "SELECT {$ selectColumns } FROM {$ fromTable } FINAL {$ whereData ['clause ' ]}{$ orderClause }{$ limitClause }{$ offsetClause } FORMAT JSON " ;
2330+ // SummingMergeTree collapses on background merges; reading raw rows
2331+ // sums them via explicit GROUP BY at query time. Net effect matches
2332+ // FROM … FINAL but keeps the read parallelisable .
2333+ $ sql = "SELECT {$ selectColumns } FROM {$ fromTable }{$ whereData ['clause ' ]} GROUP BY { $ groupBySql }{$ orderClause }{$ limitClause }{$ offsetClause } FORMAT JSON " ;
22912334
22922335 return $ this ->parseResults ($ this ->query ($ sql , $ whereData ['params ' ]), Usage::TYPE_EVENT );
22932336 }
0 commit comments