mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
The event database has metric, metric_5m, metric_30m, series, series_6h,
series_1d, series_1w and descriptor. The exponential-histogram, reduced-metric
and metric-metadata read paths name eight more tables that were declared in
tables.go and did not exist, so every one of those queries failed on a missing
table. This creates seven of them.
pkg/telemetrymetrics/schema.sql is the DEFINITION, not a migration — same
posture as the traces and logs schema.sql beside it, and as the analytics DDL
in hanzoai/cloud. CREATE TABLE IF NOT EXISTS is a no-op against a store that
already has the table, so the file is replayable and is the single statement of
what these tables ARE. No version suffix, no ALTER path, no chain.
metric_attribute AggregatingMergeTree — SimpleAggregateFunction(min|max)
makes first/last-reported a merge-time fold, so the table
stays one row per (metric, attribute, value) instead of one
per scrape. Keyed on the metric+attribute identity, not on
temporality as descriptor is, so the tuple IN and GROUP BY
at metadata.go:2583-2588 are a sort-key match and the same
label under two temporalities cannot double-count.
metric_buffer ReplacingMergeTree(inserted_at_unix_milli)
series_buffer ReplacingMergeTree(inserted_at_unix_milli)
— the 24h window. A row's identity IS its sort key, so a
retried write is a duplicate and a duplicate inflates
sum(value) and uniq(fingerprint). is_reduced discriminates
the raw rows from the reduced catalog.
metric_reduced_last ReplacingMergeTree(computed_at)
metric_reduced_sum ReplacingMergeTree(computed_at)
— the reader already dedups with argMax(<v>, computed_at)
GROUP BY (reduced_fingerprint, unix_milli) because buckets
get recomputed. Declaring that identity as the sort key and
computed_at as the version turns it into a merge-time fold.
reduced_sum has no min/max: ReducedValueColumn returns
ok=false there, and columns that exist get answered from.
series_reduced ReplacingMergeTree(inserted_at_unix_milli)
histogram ReplacingMergeTree(inserted_at_unix_milli) — NOT
Aggregating. An AggregateFunction column needs no engine
support: quantilesDDMerge runs at query time. Aggregating
would fold rows sharing a sort key, and here that key is
one data point, so a retry would merge its sketch INTO the
original and double-count the distribution.
org is the first sort-key column everywhere and appears in no PARTITION BY.
PARTITION BY is toDate(...), not toYYYYMM(...): ttl_only_drop_parts drops a whole
part or nothing, so a monthly part would hold the 24h buffers for up to 60 days,
and the reduced tables must age out in lockstep with the metric/series family
they get UNIONed with. ~30 daily partitions per table, one set for all tenants.
event.reduction is deliberately NOT created. Nothing puts it in a FROM, and
label-reduction rules already have one home in the relational store
(metricreductionruletypes: bun table metric_reduction_rule). An absent table
fails loudly; a second home fails silently.
Applied to the live store and verified: all seven report the intended engine,
partition key, sort key, TTL and ttl_only_drop_parts=1; the fifteen real reader
queries execute; the collector's metadataSQLTmpl and expHistSQLTmpl column lists
round-trip; the min/max fold, both Replacing dedups and quantilesDDMerge over a
stored sketch all return the expected values.
Co-authored-by: Hanzo Dev <[email protected]>
367 lines
21 KiB
SQL
367 lines
21 KiB
SQL
-- event — metrics support tables.
|
|
--
|
|
-- THIS IS THE DEFINITION, NOT A MIGRATION. There is no migration chain, no
|
|
-- version suffix and no ALTER path: `CREATE TABLE IF NOT EXISTS` is a no-op
|
|
-- against a store that already has the table, so this file is replayable and
|
|
-- is the single statement of what these tables ARE.
|
|
--
|
|
-- Names are the constants in tables.go — singular, unversioned, and prefixed by
|
|
-- the signal they support, which is what keeps metric_attribute and
|
|
-- span_attribute apart now that both planes share one database. There are no
|
|
-- Distributed wrappers on this deployment, so the local and distributed name
|
|
-- are one string.
|
|
--
|
|
-- The already-deployed half of this signal (metric, metric_5m, metric_30m,
|
|
-- series, series_6h, series_1d, series_1w, descriptor) is NOT restated here.
|
|
-- These eight are the tables the exponential-histogram, reduced-metric and
|
|
-- metric-metadata read paths name and the store does not yet have.
|
|
--
|
|
-- Shape rules honoured by every table below:
|
|
-- * org is the FIRST sort-key column -> rows are tenant-scopable by
|
|
-- construction, contiguous per tenant, one partition set for all tenants.
|
|
-- * the tenant NEVER appears in PARTITION BY -> N tenants x M months part-sets
|
|
-- is the one defect that collapses merges.
|
|
-- * LowCardinality(String) for org/env/temporality/metric_name-like columns.
|
|
-- * DoubleDelta+ZSTD on timestamps, Gorilla+ZSTD on float samples,
|
|
-- Delta(8)+ZSTD on fingerprints, T64+ZSTD on counters, ZSTD(1) on strings.
|
|
-- * bloom_filter skip index only where a read does an equality/IN lookup on a
|
|
-- high-cardinality NON-prefix column. A JOIN key gets none: ClickHouse
|
|
-- probes the build side of a hash join and never consults a skip index, so
|
|
-- an index there is write amplification that is never read.
|
|
-- * TTL ... SETTINGS ttl_only_drop_parts = 1 -> drop whole parts, never rewrite.
|
|
-- * No ReplicatedMergeTree — there is no ZooKeeper on this deployment.
|
|
--
|
|
-- Note on PARTITION BY toDate(...), not toYYYYMM(...). Two reasons, both
|
|
-- functional rather than stylistic:
|
|
-- 1. metric_buffer and series_buffer have a 24h TTL. ttl_only_drop_parts = 1
|
|
-- drops a WHOLE part or nothing, so a monthly part cannot be dropped until
|
|
-- the whole month has expired — a "24h buffer" would hold up to 60 days.
|
|
-- Day-granular parts are what make a sub-day TTL mean anything.
|
|
-- 2. The six sample/series tables must age out in lockstep with the deployed
|
|
-- metric/series family, which is toDate + 30d. metricsexplorer and
|
|
-- inframonitoring UNION a series* read with a series_reduced read; if the
|
|
-- reduced arm lingered a month longer it would list metrics whose raw data
|
|
-- is already gone.
|
|
-- The rule this protects — never put the tenant in PARTITION BY — is honoured:
|
|
-- org appears in no partition expression. Worst-case part-set here is ~30 daily
|
|
-- partitions per table for ONE partition set shared by all tenants, well inside
|
|
-- what merges want.
|
|
--
|
|
-- Note on org: no reader emits `WHERE org = ?` yet, and no writer supplies the
|
|
-- column (the collector INSERTs name their columns explicitly, so org takes its
|
|
-- DEFAULT). That is a code gap, not a schema gap. org sits first precisely so
|
|
-- that adding the predicate turns these reads into prefix seeks without touching
|
|
-- the schema, and so the day a second tenant writes, its rows are already
|
|
-- physically separated.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- metric_attribute — one attribute of one metric; the metric label dimension.
|
|
--
|
|
-- ENGINE AggregatingMergeTree: first/last-reported are min/max folds over an
|
|
-- unbounded restatement stream — every scrape re-asserts "this label existed at
|
|
-- time T". SimpleAggregateFunction(min|max) makes that fold happen at merge
|
|
-- time instead of at query time, which is the only reason the table stays one
|
|
-- row per (metric, attribute, value) instead of one row per scrape. This is
|
|
-- exactly the engine the already-deployed event.descriptor uses, and the
|
|
-- collector's metadata INSERT (o11ydatastoremetrics/exporter.go metadataSQLTmpl)
|
|
-- writes this exact column list.
|
|
--
|
|
-- ORDER BY drops `temporality`, which descriptor leads with. That is the
|
|
-- difference between the two tables and it is deliberate: this one is keyed on
|
|
-- the metric+attribute IDENTITY, so `(metric_name, attr_name, attr_string_value)
|
|
-- IN (...)` (metadata.go:2583) and the GROUP BY beneath it (:2588) are a
|
|
-- sort-key match, and count(*)>0 / groupUniqArray(attr_string_value) cannot
|
|
-- double-count the same label reported under two temporalities. metric_name
|
|
-- leading also serves every LIKE 'system.%' / 'k8s.%' namespace filter.
|
|
--
|
|
-- INDEX on attr_string_value: it is the high-cardinality column (host names, pod
|
|
-- names) and metadata.go:1857 / hosts.go:362 look it up by EQUALITY, but it is
|
|
-- the last sort-key column. bloom_filter is consulted for = and IN. The ILIKE
|
|
-- '%x%' search path is not indexable by any ClickHouse skip index and relies on
|
|
-- the metric_name prefix to bound the scan.
|
|
CREATE TABLE IF NOT EXISTS event.metric_attribute
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`temporality` LowCardinality(String) CODEC(ZSTD(1)),
|
|
`metric_name` LowCardinality(String) CODEC(ZSTD(1)),
|
|
`description` String CODEC(ZSTD(1)),
|
|
`unit` LowCardinality(String) CODEC(ZSTD(1)),
|
|
`type` LowCardinality(String) CODEC(ZSTD(1)),
|
|
`is_monotonic` Bool CODEC(ZSTD(1)),
|
|
`attr_name` LowCardinality(String) CODEC(ZSTD(1)),
|
|
-- 'resource' | 'scope' | 'point' — the literals in the priority CASE at
|
|
-- metadata.go:952-998.
|
|
`attr_type` LowCardinality(String) CODEC(ZSTD(1)),
|
|
`attr_datatype` LowCardinality(String) CODEC(ZSTD(1)),
|
|
`attr_string_value` String CODEC(ZSTD(1)),
|
|
`first_reported_unix_milli` SimpleAggregateFunction(min, UInt64) CODEC(ZSTD(1)),
|
|
`last_reported_unix_milli` SimpleAggregateFunction(max, UInt64) CODEC(ZSTD(1)),
|
|
INDEX by_attr_value attr_string_value TYPE bloom_filter GRANULARITY 4
|
|
)
|
|
ENGINE = AggregatingMergeTree
|
|
PARTITION BY toDate(last_reported_unix_milli / 1000)
|
|
ORDER BY (org, metric_name, attr_name, attr_type, attr_datatype, attr_string_value)
|
|
TTL toDateTime(last_reported_unix_milli / 1000) + INTERVAL 30 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- metric_buffer — raw ~24h sample buffer for reduced metrics.
|
|
--
|
|
-- Read INSTEAD of event.metric when a reduced-metric query lands inside the last
|
|
-- day (statement_builder.go:189-193). Column-for-column event.metric, because
|
|
-- AggregationColumnForSamplesTable puts MetricBufferTableName in the same case
|
|
-- arm as MetricTableName — the value column is `value` and the same
|
|
-- buildTemporalAggregationCTE joins it to the series CTE.
|
|
--
|
|
-- ENGINE ReplacingMergeTree(inserted_at_unix_milli), not MergeTree: the row's
|
|
-- identity IS its sort key — one series, one millisecond, one point — so a
|
|
-- retried write is a duplicate, and a duplicate silently inflates sum(value) and
|
|
-- count(value). Replacing folds it at merge time; the version column makes the
|
|
-- winner deterministic. (The deployed event.metric is plain MergeTree; that is
|
|
-- the older shape, not a target to copy.)
|
|
--
|
|
-- ORDER BY mirrors event.metric after org, because rate()/increase() run a
|
|
-- WINDOW PARTITION BY fingerprint and need physically fingerprint-ordered rows.
|
|
-- No skip index: fingerprint here is a JOIN key, never an IN lookup.
|
|
--
|
|
-- TTL 1 DAY is the whole point of the table — WhichSamplesTableToUse only
|
|
-- reaches for it when start >= now-1d.
|
|
CREATE TABLE IF NOT EXISTS event.metric_buffer
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`env` LowCardinality(String) DEFAULT 'default',
|
|
`temporality` LowCardinality(String) DEFAULT 'Unspecified',
|
|
`metric_name` LowCardinality(String),
|
|
`fingerprint` UInt64 CODEC(Delta(8), ZSTD(1)),
|
|
`unix_milli` Int64 CODEC(DoubleDelta, ZSTD(1)),
|
|
`value` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`flags` UInt32 DEFAULT 0 CODEC(ZSTD(1)),
|
|
`inserted_at_unix_milli` UInt64 DEFAULT 0 CODEC(T64, ZSTD(1))
|
|
)
|
|
ENGINE = ReplacingMergeTree(inserted_at_unix_milli)
|
|
PARTITION BY toDate(unix_milli / 1000)
|
|
ORDER BY (org, env, temporality, metric_name, fingerprint, unix_milli)
|
|
TTL toDateTime(unix_milli / 1000) + INTERVAL 1 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- series_buffer — series dimension for the buffer.
|
|
--
|
|
-- The fingerprint/labels catalog metric_buffer joins through. It carries BOTH
|
|
-- raw and reduced-catalog rows in one table, discriminated by `is_reduced` —
|
|
-- that column is the only thing that makes this table different from
|
|
-- event.series, and every raw read pins it false (statement_builder.go:515,
|
|
-- metadata.go:1935, :2277).
|
|
--
|
|
-- ENGINE ReplacingMergeTree(inserted_at_unix_milli): a series row is re-emitted
|
|
-- every write window with identical content, so re-ingest MUST be idempotent or
|
|
-- uniq(fingerprint) counts drift. Mirrors event.series.
|
|
--
|
|
-- ORDER BY mirrors event.series after org. `is_reduced` is deliberately NOT in
|
|
-- the key: it is a 2-value column that PREWHERE filters for free, and hoisting
|
|
-- it would split the key from event.series' for no seek benefit.
|
|
--
|
|
-- Columns are event.series' exactly (the collector's timeSeriesSQLTmpl list),
|
|
-- so ONE writer shape serves both. attrs/scope_attrs/resource_attrs are
|
|
-- Map(LowCardinality(String), String), never a JSON blob — a blob forces
|
|
-- decompress+parse per row on every query. `labels` is the JSON that
|
|
-- JSONExtractString(labels, '<key>') group-bys read and is the full-text column.
|
|
CREATE TABLE IF NOT EXISTS event.series_buffer
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`env` LowCardinality(String) DEFAULT 'default',
|
|
`temporality` LowCardinality(String) DEFAULT 'Unspecified',
|
|
`metric_name` LowCardinality(String),
|
|
`description` LowCardinality(String) DEFAULT '' CODEC(ZSTD(1)),
|
|
`unit` LowCardinality(String) DEFAULT '' CODEC(ZSTD(1)),
|
|
`type` LowCardinality(String) DEFAULT '' CODEC(ZSTD(1)),
|
|
`is_monotonic` Bool DEFAULT false CODEC(ZSTD(1)),
|
|
`fingerprint` UInt64 CODEC(Delta(8), ZSTD(1)),
|
|
`unix_milli` Int64 CODEC(DoubleDelta, ZSTD(1)),
|
|
`labels` String CODEC(ZSTD(5)),
|
|
`attrs` Map(LowCardinality(String), String) DEFAULT map() CODEC(ZSTD(1)),
|
|
`scope_attrs` Map(LowCardinality(String), String) DEFAULT map() CODEC(ZSTD(1)),
|
|
`resource_attrs` Map(LowCardinality(String), String) DEFAULT map() CODEC(ZSTD(1)),
|
|
`__normalized` Bool DEFAULT true CODEC(ZSTD(1)),
|
|
-- the discriminator: false = the original series, true = the reduced catalog
|
|
`is_reduced` Bool DEFAULT false CODEC(ZSTD(1)),
|
|
`inserted_at_unix_milli` UInt64 DEFAULT 0 CODEC(T64, ZSTD(1))
|
|
)
|
|
ENGINE = ReplacingMergeTree(inserted_at_unix_milli)
|
|
PARTITION BY toDate(unix_milli / 1000)
|
|
ORDER BY (org, env, temporality, metric_name, fingerprint, unix_milli)
|
|
TTL toDateTime(unix_milli / 1000) + INTERVAL 1 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- metric_reduced_last — 60s pre-aggregated samples for gauge-like reduced series.
|
|
--
|
|
-- The last/min/max fold. WhichReducedSamplesTableToUse picks this for everything
|
|
-- that is not a Sum or a Histogram.
|
|
--
|
|
-- ENGINE ReplacingMergeTree(computed_at): the reader ALREADY dedups with
|
|
-- argMax(<value>, computed_at) GROUP BY (reduced_fingerprint, unix_milli)
|
|
-- (statement_builder.go:340-352) because 60s buckets get recomputed. Declaring
|
|
-- that same identity as the sort key and computed_at as the version makes the
|
|
-- dedup a merge-time fold, so the query-time argMax collapses one row instead of
|
|
-- one row per recomputation. Plain MergeTree would keep every recomputation
|
|
-- forever and make the reduced table larger than the raw one it replaces.
|
|
--
|
|
-- The value columns are backticked at every read site (`sum_last`, `count_series`,
|
|
-- `min`, `max`) — `min` and `max` collide with function names and must stay
|
|
-- quoted. count_series is the avg denominator, not a time-additive count.
|
|
--
|
|
-- INDEX on reduced_fingerprint: helpers.go:379-386 and module.go:1124 look it up
|
|
-- with `reduced_fingerprint IN (<subquery>)`, and it is sort-key position 3 —
|
|
-- exactly the equality/IN-on-a-non-prefix-column case bloom_filter serves.
|
|
CREATE TABLE IF NOT EXISTS event.metric_reduced_last
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`metric_name` LowCardinality(String),
|
|
`reduced_fingerprint` UInt64 CODEC(Delta(8), ZSTD(1)),
|
|
`unix_milli` Int64 CODEC(DoubleDelta, ZSTD(1)),
|
|
-- the dedup tiebreak: argMax(<col>, computed_at) at every read site
|
|
`computed_at` DateTime64(3) DEFAULT now64(3) CODEC(DoubleDelta, ZSTD(1)),
|
|
`sum_last` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`count_series` UInt64 CODEC(T64, ZSTD(1)),
|
|
`min` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`max` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
INDEX by_reduced_fingerprint reduced_fingerprint TYPE bloom_filter GRANULARITY 4
|
|
)
|
|
ENGINE = ReplacingMergeTree(computed_at)
|
|
PARTITION BY toDate(unix_milli / 1000)
|
|
ORDER BY (org, metric_name, reduced_fingerprint, unix_milli)
|
|
TTL toDateTime(unix_milli / 1000) + INTERVAL 30 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- metric_reduced_sum — 60s pre-aggregated samples for counter/histogram reduced
|
|
-- series.
|
|
--
|
|
-- Identical role, identical dedup pattern and identical engine reasoning to
|
|
-- metric_reduced_last; WhichReducedSamplesTableToUse (tables.go:398-403) routes
|
|
-- SumType and HistogramType here.
|
|
--
|
|
-- It has NO `min`/`max` on purpose: ReducedValueColumn returns ok=false for
|
|
-- SpaceAggregationMin/Max on the sum table, so the reduced arm of the query is
|
|
-- omitted entirely rather than answered from a column that would be a
|
|
-- cross-series min of pre-summed values — which is not a min of anything real.
|
|
-- Adding those columns speculatively would make a wrong answer available.
|
|
CREATE TABLE IF NOT EXISTS event.metric_reduced_sum
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`metric_name` LowCardinality(String),
|
|
`reduced_fingerprint` UInt64 CODEC(Delta(8), ZSTD(1)),
|
|
`unix_milli` Int64 CODEC(DoubleDelta, ZSTD(1)),
|
|
`computed_at` DateTime64(3) DEFAULT now64(3) CODEC(DoubleDelta, ZSTD(1)),
|
|
`sum` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`count_series` UInt64 CODEC(T64, ZSTD(1)),
|
|
INDEX by_reduced_fingerprint reduced_fingerprint TYPE bloom_filter GRANULARITY 4
|
|
)
|
|
ENGINE = ReplacingMergeTree(computed_at)
|
|
PARTITION BY toDate(unix_milli / 1000)
|
|
ORDER BY (org, metric_name, reduced_fingerprint, unix_milli)
|
|
TTL toDateTime(unix_milli / 1000) + INTERVAL 30 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- series_reduced — series dimension for reduced (label-dropped) metrics.
|
|
--
|
|
-- The fingerprint/labels catalog the metric_reduced_* tables join through, and
|
|
-- the answer to "which reduced metrics exist" (module.go:179).
|
|
--
|
|
-- ENGINE ReplacingMergeTree(inserted_at_unix_milli): a reduced series is
|
|
-- restated each window, and uniq(fingerprint) is read as a time-series COUNT
|
|
-- (module.go:1106, :1249), so duplicates would be visible as inflated
|
|
-- cardinality. Mirrors event.series so the UNION ALL arms in metricsexplorer and
|
|
-- inframonitoring line up.
|
|
--
|
|
-- ORDER BY mirrors event.series after org: every read filters metric_name plus a
|
|
-- unix_milli range and GROUP BYs fingerprint.
|
|
--
|
|
-- INDEX on fingerprint: hosts.go:90 and helpers.go:649 filter
|
|
-- `fingerprint IN (<reduced-fingerprint subquery>)` — an IN lookup on sort-key
|
|
-- position 5, which is what bloom_filter is for. (The buffer/sample tables get
|
|
-- no such index because there fingerprint is only ever a JOIN key.)
|
|
CREATE TABLE IF NOT EXISTS event.series_reduced
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`env` LowCardinality(String) DEFAULT 'default',
|
|
`temporality` LowCardinality(String) DEFAULT 'Unspecified',
|
|
`metric_name` LowCardinality(String),
|
|
`description` LowCardinality(String) DEFAULT '' CODEC(ZSTD(1)),
|
|
`unit` LowCardinality(String) DEFAULT '' CODEC(ZSTD(1)),
|
|
`type` LowCardinality(String) DEFAULT '' CODEC(ZSTD(1)),
|
|
`is_monotonic` Bool DEFAULT false CODEC(ZSTD(1)),
|
|
`fingerprint` UInt64 CODEC(Delta(8), ZSTD(1)),
|
|
`unix_milli` Int64 CODEC(DoubleDelta, ZSTD(1)),
|
|
`labels` String CODEC(ZSTD(5)),
|
|
`attrs` Map(LowCardinality(String), String) DEFAULT map() CODEC(ZSTD(1)),
|
|
`scope_attrs` Map(LowCardinality(String), String) DEFAULT map() CODEC(ZSTD(1)),
|
|
`resource_attrs` Map(LowCardinality(String), String) DEFAULT map() CODEC(ZSTD(1)),
|
|
`__normalized` Bool DEFAULT true CODEC(ZSTD(1)),
|
|
`inserted_at_unix_milli` UInt64 DEFAULT 0 CODEC(T64, ZSTD(1)),
|
|
INDEX by_fingerprint fingerprint TYPE bloom_filter GRANULARITY 4
|
|
)
|
|
ENGINE = ReplacingMergeTree(inserted_at_unix_milli)
|
|
PARTITION BY toDate(unix_milli / 1000)
|
|
ORDER BY (org, env, temporality, metric_name, fingerprint, unix_milli)
|
|
TTL toDateTime(unix_milli / 1000) + INTERVAL 30 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- histogram — exponential-histogram sketch points.
|
|
--
|
|
-- Sketches do not decompose into plain samples, so this IS the samples table for
|
|
-- ExpHistogramType (tables.go:229-232) and there is deliberately no 5m/30m
|
|
-- rollup of it. Column list is the collector's expHistSQLTmpl verbatim.
|
|
--
|
|
-- ENGINE ReplacingMergeTree(inserted_at_unix_milli), NOT AggregatingMergeTree.
|
|
-- An AggregateFunction column needs no engine support to be read: the merge
|
|
-- happens at QUERY time, in `quantilesDDMerge(0.01, <p>)(sketch)[1]`
|
|
-- (statement_builder.go:436-439). What AggregatingMergeTree would add is a
|
|
-- MERGE-time fold of rows sharing a sort key — and here that is actively wrong,
|
|
-- because the sort key is the identity of one data point (one series, one
|
|
-- millisecond). A retried write would have its sketch state merged INTO the
|
|
-- original, double-counting the distribution instead of deduplicating it, and
|
|
-- CountExpressionForSamplesTable's count(*) would still be off. Replacing keeps
|
|
-- exactly one row per point, which is what count(*) means here.
|
|
--
|
|
-- `sketch` is AggregateFunction(quantilesDD(0.01, 0.5, 0.75, 0.9, 0.95, 0.99),
|
|
-- UInt64) — the state the collector serialises. quantilesDDMerge with a
|
|
-- different parameter list merges it correctly (verified on this store).
|
|
CREATE TABLE IF NOT EXISTS event.histogram
|
|
(
|
|
`org` LowCardinality(String) DEFAULT '',
|
|
`env` LowCardinality(String) DEFAULT 'default',
|
|
`temporality` LowCardinality(String) DEFAULT 'Unspecified',
|
|
`metric_name` LowCardinality(String),
|
|
`fingerprint` UInt64 CODEC(Delta(8), ZSTD(1)),
|
|
`unix_milli` Int64 CODEC(DoubleDelta, ZSTD(1)),
|
|
`count` UInt64 CODEC(T64, ZSTD(1)),
|
|
`sum` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`min` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`max` Float64 CODEC(Gorilla, ZSTD(1)),
|
|
`sketch` AggregateFunction(quantilesDD(0.01, 0.5, 0.75, 0.9, 0.95, 0.99), UInt64) CODEC(ZSTD(1)),
|
|
`flags` UInt32 DEFAULT 0 CODEC(ZSTD(1)),
|
|
`inserted_at_unix_milli` UInt64 DEFAULT 0 CODEC(T64, ZSTD(1))
|
|
)
|
|
ENGINE = ReplacingMergeTree(inserted_at_unix_milli)
|
|
PARTITION BY toDate(unix_milli / 1000)
|
|
ORDER BY (org, env, temporality, metric_name, fingerprint, unix_milli)
|
|
TTL toDateTime(unix_milli / 1000) + INTERVAL 30 DAY
|
|
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- NOT CREATED: event.reduction.
|
|
--
|
|
-- telemetrymetrics.ReductionTableName is declared but has no datastore reader —
|
|
-- nothing in the repo ever puts it in a FROM. Label-reduction rules already have
|
|
-- exactly one home, and it is the relational store:
|
|
-- pkg/types/metricreductionruletypes/reduction_rule.go:66 declares
|
|
-- `bun:"table:metric_reduction_rule"`. A rule set is a small mutable
|
|
-- configuration read by primary key, which is what a row store is for and what a
|
|
-- columnar store is worst at. Creating a second home in event would be two
|
|
-- sources of truth for one fact. An absent table fails loudly; a duplicated one
|
|
-- fails silently.
|