Temporal Patterns#
Use global_iter() with math functions to make generated data change shape over the life of a workload. This lets you model real-world patterns like shifting popularity, price inflation, seasonal traffic, periodic spikes, and sensor calibration drift; all without external scripts.
Every pattern on this page uses the same building block: an expression that takes global_iter() as input and produces a value whose distribution or magnitude changes as the iteration counter grows.
The full config is at
examples/temporal_patterns/crdb.yaml.
Globals Are User-Defined#
Names in the following globals: block are arbitrary and become variables available in any expression. The patterns below use names like total_iters and initial_skew, but you can name them whatever you like.
Patterns#
Zipf Skew Drift#
Product popularity starts nearly uniform and concentrates over time. The zipf() skew parameter is linearly interpolated from initial_skew to final_skew using global_iter().
Globals
let total_iters = 10000
let products = 100
let initial_skew = 1.2 # nearly uniform
let final_skew = 3.0 # heavily concentrated on the first productglobals:
total_iters: 10000
products: 100
initial_skew: 1.2 # nearly uniform
final_skew: 3.0 # heavily concentrated on the first productExpression
zipf(initial_skew + (final_skew - initial_skew) * global_iter() / total_iters, 1, products - 1)Config
insert_view(type: exec) `INSERT INTO product_views (product_id, skew_at_time, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::INT8)` (
zipf(initial_skew + (final_skew - initial_skew) * global_iter() / total_iters, 1, products - 1),
initial_skew + (final_skew - initial_skew) * global_iter() / total_iters,
global_iter()
)- name: insert_view
type: exec
args:
- zipf(initial_skew + (final_skew - initial_skew) * global_iter() / total_iters, 1, products - 1)
- initial_skew + (final_skew - initial_skew) * global_iter() / total_iters
- global_iter()
query: |-
INSERT INTO product_views (product_id, skew_at_time, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::INT8)Verification query
SELECT
bucket,
count(*) AS total,
count(*) FILTER (WHERE product_id = 0) AS top_product,
round((count(*) FILTER (WHERE product_id = 0))::FLOAT8 / count(*)::FLOAT8 * 100, 1) AS top_pct,
repeat('▒', (round((count(*) FILTER (WHERE product_id = 0))::FLOAT8 / count(*)::FLOAT8 * 40))::INT) AS histogram
FROM (
SELECT *, ntile(10) OVER (ORDER BY iteration) AS bucket
FROM product_views
)
GROUP BY bucket
ORDER BY bucket; bucket | total | top_product | top_pct | histogram
---------+-------+-------------+---------+-------------------------------------------
1 | 2644 | 1225 | 46.3 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | 2643 | 1959 | 74.1 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | 2643 | 2440 | 92.3 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | 2643 | 2593 | 98.1 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | 2643 | 2632 | 99.6 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | 2643 | 2637 | 99.8 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | 2643 | 2641 | 99.9 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | 2643 | 2642 | 100 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | 2643 | 2643 | 100 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | 2643 | 2643 | 100 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Tuning
initial_skew/final_skew- controls how much the distribution shifts. A smaller gap produces a subtler drift; a larger gap makes it dramatic. Values near 1.0 are nearly uniform, values above 3.0 concentrate almost all traffic on the lowest-ranked items.total_iters- the iteration count at whichfinal_skewis reached. After this point the skew parameter stays atfinal_skew. Set this to roughly match your expected total iterations (see Estimating total iterations).
Logarithmic Growth#
Prices rise steeply at first then plateau. Models inflation, adoption curves, or any quantity with diminishing returns.
Globals
let base_price = 50.0
let products = 100globals:
base_price: 50.0
products: 100Expression
floor(base_price * (1.0 + log(1.0 + global_iter() / 1000.0)) * 100.0) / 100.0Config
insert_price(type: exec) `INSERT INTO price_history (product_id, price, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::INT8)` (
uniform(1, products),
floor(base_price * (1.0 + log(1.0 + global_iter() / 1000.0)) * 100.0) / 100.0,
global_iter()
)- name: insert_price
type: exec
args:
- uniform(1, products)
- floor(base_price * (1.0 + log(1.0 + global_iter() / 1000.0)) * 100.0) / 100.0
- global_iter()
query: |-
INSERT INTO price_history (product_id, price, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::INT8)Verification query
SELECT
bucket,
round(avg(price)::NUMERIC, 2) AS avg_price,
round(min(price)::NUMERIC, 2) AS min_price,
round(max(price)::NUMERIC, 2) AS max_price,
repeat('▒', (round(avg(price) / max(avg(price)) OVER () * 40))::INT) AS histogram
FROM (
SELECT *, ntile(10) OVER (ORDER BY iteration) AS bucket
FROM price_history
)
GROUP BY bucket
ORDER BY bucket; bucket | avg_price | min_price | max_price | histogram
---------+-----------+-----------+-----------+-------------------------------------------
1 | 105.75 | 50.09 | 139.66 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | 156.93 | 139.67 | 170.72 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | 185.96 | 170.73 | 200.89 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | 211.99 | 200.90 | 221.88 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | 229.27 | 221.89 | 236.09 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | 242.19 | 236.10 | 247.77 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | 252.52 | 247.78 | 256.94 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | 260.88 | 256.95 | 264.72 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | 268.11 | 264.72 | 271.35 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | 274.40 | 271.35 | 277.37 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Tuning
base_price- starting price before any growth.- The divisor inside
log(1.0 + global_iter() / 1000.0)controls how quickly the curve rises. A smaller divisor (e.g.100.0) makes it steeper; a larger divisor (e.g.10000.0) flattens it. floor(... * 100.0) / 100.0rounds to cents. Remove thefloor/division to keep full precision.
Sine Wave Seasonality#
Request counts oscillate around a slowly growing baseline. Models daily/weekly traffic cycles, seasonal patterns, or any periodic signal riding on a trend.
Globals
let traffic_period = 50000
let traffic_amplitude = 100.0
let base_traffic = 100.0globals:
traffic_period: 50000
traffic_amplitude: 100.0
base_traffic: 100.0Expression
floor(abs(base_traffic + 0.5 * sqrt(global_iter()) + amplitude * sin(2.0 * pi * global_iter() / period)))Config
insert_traffic(type: exec) `INSERT INTO traffic_log (endpoint, request_count, iteration)
VALUES ($1, $2::INT, $3::INT8)` (
set_rand(['GET /products', 'GET /orders', 'POST /checkout', 'GET /search'], []),
floor(abs(base_traffic + 0.5 * sqrt(global_iter()) + traffic_amplitude * sin(2.0 * pi * global_iter() / traffic_period))),
global_iter()
)- name: insert_traffic
type: exec
args:
- set_rand(['GET /products', 'GET /orders', 'POST /checkout', 'GET /search'], [])
- floor(abs(base_traffic + 0.5 * sqrt(global_iter()) + traffic_amplitude * sin(2.0 * pi * global_iter() / traffic_period)))
- global_iter()
query: |-
INSERT INTO traffic_log (endpoint, request_count, iteration)
VALUES ($1, $2::INT, $3::INT8)Verification query
SELECT
bucket,
round(avg(request_count)::NUMERIC, 1) AS avg_requests,
min(request_count) AS min_requests,
max(request_count) AS max_requests,
repeat('▒', (round(avg(request_count) / max(avg(request_count)) OVER () * 40))::INT) AS histogram
FROM (
SELECT *, ntile(20) OVER (ORDER BY iteration) AS bucket
FROM traffic_log
)
GROUP BY bucket
ORDER BY bucket; bucket | avg_requests | min_requests | max_requests | histogram
---------+--------------+--------------+--------------+-------------------------------------------
1 | 149.5 | 101 | 186 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | 212.8 | 186 | 236 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | 248.8 | 236 | 257 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | 253.9 | 247 | 257 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | 232.8 | 215 | 247 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | 192.7 | 170 | 215 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | 147.7 | 127 | 170 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | 111.6 | 100 | 127 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | 96.9 | 96 | 100 | ▒▒▒▒▒▒▒▒▒▒▒▒
10 | 107.2 | 98 | 121 | ▒▒▒▒▒▒▒▒▒▒▒▒▒
11 | 142.9 | 121 | 167 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
12 | 199.3 | 167 | 229 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
13 | 256.6 | 229 | 282 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
14 | 301.4 | 283 | 316 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
15 | 322.7 | 316 | 325 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
16 | 315.4 | 303 | 324 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
17 | 282.0 | 258 | 302 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
18 | 233.1 | 209 | 258 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
19 | 186.0 | 167 | 209 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
20 | 154.7 | 148 | 167 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Tuning
traffic_period- iterations per full sine cycle. This is the most important parameter. If each bucket in your verification query averages over a full cycle or more, the sine wave averages to zero and you only see the baseline trend. See Matching period to your run.traffic_amplitude- peak deviation from the baseline. Must be large enough relative to the baseline to be visible. Ifbase_trafficis 100 and amplitude is 10, the wave is only a 10% wobble.0.5 * sqrt(global_iter())- the growth coefficient on the baseline trend. A larger coefficient makes the upward trend dominate the wave; a smaller one lets the oscillation stand out.abs(...)- prevents negative request counts when the trough of the sine wave dips below zero.
Periodic Spikes#
Severity surges at regular intervals then drops to near zero. Models periodic maintenance windows, batch job impacts, or recurring failure modes.
Expression
pow(cos(pi * mod(global_iter(), interval) / interval), 2.0) * 10.0cos² produces a smooth pulse: maximum (10.0) when mod(iter, interval) = 0, minimum (0.0) at the midpoint of each interval.
Globals
let spike_interval = 25000globals:
spike_interval: 25000Config
insert_error(type: exec) `INSERT INTO error_events (error_code, severity, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::INT8)` (
uniform(500, 599),
pow(cos(pi * mod(global_iter(), spike_interval) / spike_interval), 2.0) * 10.0,
global_iter()
)- name: insert_error
type: exec
args:
- uniform(500, 599)
- pow(cos(pi * mod(global_iter(), spike_interval) / spike_interval), 2.0) * 10.0
- global_iter()
query: |-
INSERT INTO error_events (error_code, severity, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::INT8)Verification query
SELECT
bucket,
round(avg(severity)::NUMERIC, 2) AS avg_severity,
round(max(severity)::NUMERIC, 2) AS max_severity,
repeat('▒', (round(avg(severity) / max(avg(severity)) OVER () * 40))::INT) AS histogram
FROM (
SELECT *, ntile(20) OVER (ORDER BY iteration) AS bucket
FROM error_events
)
GROUP BY bucket
ORDER BY bucket; bucket | avg_severity | max_severity | histogram
---------+--------------+--------------+-------------------------------------------
1 | 8.06 | 10.00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | 1.43 | 4.67 | ▒▒▒▒▒▒
3 | 2.85 | 6.56 | ▒▒▒▒▒▒▒▒▒▒▒▒
4 | 9.05 | 10.00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | 6.34 | 9.58 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | 0.59 | 2.20 | ▒▒▒
7 | 4.52 | 8.16 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | 9.38 | 10.00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | 4.34 | 8.12 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | 0.52 | 1.97 | ▒▒
11 | 5.91 | 9.36 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
12 | 9.21 | 10.00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
13 | 3.37 | 7.09 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
14 | 1.09 | 3.91 | ▒▒▒▒▒
15 | 7.41 | 9.92 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
16 | 8.50 | 10.00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
17 | 2.03 | 5.37 | ▒▒▒▒▒▒▒▒▒
18 | 1.92 | 5.11 | ▒▒▒▒▒▒▒▒
19 | 8.37 | 10.00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
20 | 7.42 | 9.96 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Tuning
spike_interval- iterations between spike peaks. Same rule astraffic_period: if the interval is shorter than the iteration width of a single bucket, each bucket averages over multiple full cycles and flattens to ~5.0. See Matching period to your run.- The
* 10.0multiplier at the end sets the peak severity. Change it to scale the spike magnitude.
Bounded Drift (Arctangent Saturation)#
Sensor readings drift from a true value and asymptotically approach a maximum offset. Models calibration drift, battery degradation, or any quantity that grows quickly then saturates.
Expression (upward drift)
100.0 + (2.0 * atan(sqrt(global_iter()) / 100.0) / pi) * 15.0 + noiseExpression (downward drift)
100.0 - (2.0 * atan(sqrt(global_iter()) / 100.0) / pi) * 15.0 + noise2 * atan(x) / pi maps any positive input to the range (0, 1). Multiplying by 15 bounds the drift to 0-15 units. The sqrt inside atan gives fast initial movement that slows as it approaches the asymptote. Flip + to - to model decay (e.g. battery degradation) instead of growth.
Config
insert_sensor(type: exec) `INSERT INTO sensor_calibration (sensor_id, reading, drift_offset, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::FLOAT8, $4::INT8)` (
uniform(1, 50),
100.0 + (2.0 * atan(sqrt(global_iter()) / 100.0) / pi) * 15.0 + norm_f(0, 0.5, -2, 2, 2),
2.0 * atan(sqrt(global_iter()) / 100.0) / pi * 15.0,
global_iter()
)- name: insert_sensor
type: exec
args:
- uniform(1, 50)
- 100.0 + (2.0 * atan(sqrt(global_iter()) / 100.0) / pi) * 15.0 + norm_f(0, 0.5, -2, 2, 2)
- 2.0 * atan(sqrt(global_iter()) / 100.0) / pi * 15.0
- global_iter()
query: |-
INSERT INTO sensor_calibration (sensor_id, reading, drift_offset, iteration)
VALUES ($1::INT, $2::FLOAT8, $3::FLOAT8, $4::INT8)Verification query
SELECT
bucket,
round(avg(drift_offset)::NUMERIC, 3) AS avg_drift,
round(avg(reading)::NUMERIC, 2) AS avg_reading,
repeat('▒', (round(avg(drift_offset) / max(avg(drift_offset)) OVER () * 40))::INT) AS histogram
FROM (
SELECT *, ntile(10) OVER (ORDER BY iteration) AS bucket
FROM sensor_calibration
)
GROUP BY bucket
ORDER BY bucket;Upward drift - readings climb from 100 toward ~115:
bucket | avg_drift | avg_reading | histogram
---------+-----------+-------------+-------------------------------------------
1 | 6.068 | 106.06 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | 9.114 | 109.12 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | 10.209 | 110.22 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | 10.853 | 110.86 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | 11.292 | 111.30 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | 11.621 | 111.62 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | 11.866 | 111.87 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | 12.068 | 112.05 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | 12.235 | 112.21 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | 12.376 | 112.37 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Downward drift - readings decay from 100 toward ~85:
bucket | avg_drift | avg_reading | histogram
---------+-----------+-------------+-------------------------------------------
1 | -12.376 | 87.63 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | -12.235 | 87.79 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | -12.068 | 87.95 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | -11.866 | 88.13 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | -11.621 | 88.38 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | -11.292 | 88.70 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | -10.853 | 89.14 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | -10.209 | 89.78 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | -9.114 | 90.88 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | -6.068 | 93.94 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Tuning
- The
* 15.0multiplier sets the maximum drift (asymptote). Change it to control how far the reading can wander. - The
/ 100.0insideatan(sqrt(iter) / 100.0)controls how quickly saturation is reached. A smaller divisor reaches the asymptote sooner. norm_f(0, 0.5, -2, 2, 2)adds Gaussian noise to individual readings. Increase the stddev (second arg) for noisier data.
Estimating Total Iterations#
The actual number of global_iter() increments depends on workers, duration, query complexity, and hardware. This matters because periodic parameters (traffic_period, spike_interval) must be tuned relative to the actual iteration count - not the configured total_iters global.
Estimate total iterations from three values:
total_iterations = workers * duration_seconds / avg_latency_seconds| Variable | Meaning |
|---|---|
workers | Number of concurrent workers (-w flag) |
duration_seconds | Run duration in seconds (-d flag) |
avg_latency_seconds | Average time for one iteration (query execution + overhead) |
Example: 10 workers, 60s duration, 5ms average latency:
10 * 60 / 0.005 = 120,000 iterationsThis assumes each worker spends all its time executing queries. In practice, connection overhead and GC pauses reduce throughput slightly, so treat the result as an upper bound.
Matching Period to Your Run#
The verification queries use ntile(N) to divide rows into N equal-sized buckets by iteration order. For a periodic pattern to be visible in the histogram, each bucket must cover less than one full cycle. If a bucket spans one or more complete cycles, the periodic component averages out and disappears.
The rule: each bucket’s iteration width should be less than the period.
bucket_width = total_iterations / number_of_bucketsFor the pattern to be clearly visible:
period > bucket_widthFor 2 visible cycles across all buckets (a good default):
period = total_iterations / 2Example: with ~100,000 actual iterations and 20 buckets:
- Each bucket spans ~5,000 iterations
traffic_period: 50000-> 2 full cycles, 10 buckets per cycle - clear wavetraffic_period: 5000-> 20 full cycles, 1 cycle per bucket - wave averages out, histogram looks monotonictraffic_period: 500-> 200 cycles per bucket - completely flat
The same logic applies to spike_interval. If your histogram looks flat when you expect oscillation, increase the period.
Combining Patterns#
All five patterns run concurrently using run_weights to control the mix:
weights {
insert_view = 25
insert_price = 20
insert_traffic = 25
insert_error = 15
insert_sensor = 15
}run_weights:
insert_view: 25
insert_price: 20
insert_traffic: 25
insert_error: 15
insert_sensor: 15Adjust weights to emphasise specific patterns or to control relative table sizes. The weights don’t affect the drift expressions themselves - global_iter() increments globally regardless of which statement executes.
Correlated Multi-Table Signals PRO#
The patterns above work per-expression. Signals let you declare a shared time-varying pattern once and have multiple tables track it with lag and correlation.
Defining Signals#
Signals pre-compute an array of values at init time. Define them in the signals section using either a duration range or an explicit length.
Duration-based (computes length automatically):
signal traffic(from: '2024-01-01T00:00:00Z', to: '2024-01-08T00:00:00Z', interval: '5m') {
1000 + 400 * sin(2 * pi * i / 288)
}signals:
traffic:
from: '2024-01-01T00:00:00Z'
to: '2024-01-08T00:00:00Z'
interval: 5m
expr: 1000 + 400 * sin(2 * pi * i / 288)Length-based (explicit number of data points):
signal promo_boost(length: 2016) {
floor(50 * pow(cos(pi * mod(i, 500) / 500), 20))
}signals:
promo_boost:
length: 2016
expr: floor(50 * pow(cos(pi * mod(i, 500) / 500), 20))The variable i is the index into the signal buffer (0 to length-1). All built-in math functions and globals are available in signal expressions.
Consuming Signals#
Three functions read signal values:
| Function | Description |
|---|---|
signal('name') | Value at the current iter(), wrapping around |
signal_at('name', index) | Value at a specific index |
signal_correlated('name', lag, correlation) | Lagged value with correlated noise |
Correlated Signals#
signal_correlated is the key function for multi-table patterns. It takes three arguments:
- name - the signal to correlate with
- lag - how far behind to look (integer for iterations, or a duration string like
'2h'if the signal was defined withfrom/to/interval) - correlation - a value from -1 to 1 controlling how closely the output tracks the signal
The correlation math:
result = r × S[iter - lag] + (1 - |r|) × mean(S) + √(1 - r²) × stddev(S) × noise| Correlation | Behaviour |
|---|---|
| 1.0 | Exact copy of the lagged signal |
| 0.7 | Follows the shape with moderate noise |
| 0.3 | Weak tracking, mostly noise around the mean |
| 0.0 | Random noise centred on the signal mean |
| -1.0 | Inverted copy of the lagged signal |
Example: Cascading Ecommerce Effects#
Simulates a week of correlated observability data in a 5-minute run. As traffic ramps up, orders follow with a 15-minute lag, support tickets follow with a 2-hour lag, and server errors spike after promotional events. Each table reads from the signal buffers using signal_at() with global_iter() as the index, and timestamps are computed SQL-side using modular arithmetic to wrap at the week boundary.
signal traffic(from: '2024-01-01T00:00:00Z', to: '2024-01-08T00:00:00Z', interval: '5m') {
1000 + 400 * sin(2 * 3.14159265 * i / 288) + 200 * sin(2 * 3.14159265 * i / 2016)
}
signal promo_boost(length: 2016) {
floor(50 * pow(cos(3.14159265 * mod(i, 500) / 500), 20))
}
run {
insert_page_views(type: exec)
`INSERT INTO page_views (ts, view_count, page)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3
)` (
global_iter(),
floor(signal_at('traffic', int(global_iter())) + signal_at('promo_boost', int(global_iter()))),
set_rand(['home','product','cart','checkout'], [40,30,20,10])
)
insert_orders(type: exec)
`INSERT INTO orders (ts, order_count, amount)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3::DECIMAL
)` (
global_iter(),
floor(abs(signal_at('traffic', int(global_iter()) - 3) * 0.7 + norm(0, 300, -500, 500) * 0.3)),
uniform_f(10, 500, 2)
)
insert_tickets(type: exec)
`INSERT INTO support_tickets (ts, ticket_count, category)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3
)` (
global_iter(),
floor(abs(signal_at('traffic', int(global_iter()) - 24) * 0.3 + norm(0, 300, -500, 500) * 0.7)),
set_rand(['billing','shipping','refund','other'], [30,35,20,15])
)
insert_errors(type: exec)
`INSERT INTO server_errors (ts, error_count, error_type)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3
)` (
global_iter(),
floor(abs(signal_at('promo_boost', int(global_iter()) - 6) * 0.5 + norm(0, 10, -25, 25) * 0.5)),
set_rand(['timeout','500','oom','connection_reset'], [35,30,20,15])
)
}
weights {
insert_page_views = 25
insert_orders = 25
insert_tickets = 25
insert_errors = 25
}signals:
traffic:
from: '2024-01-01T00:00:00Z'
to: '2024-01-08T00:00:00Z'
interval: 5m
expr: 1000 + 400 * sin(2 * 3.14159265 * i / 288) + 200 * sin(2 * 3.14159265 * i / 2016)
promo_boost:
length: 2016
expr: floor(50 * pow(cos(3.14159265 * mod(i, 500) / 500), 20))
run:
- name: insert_page_views
type: exec
args:
- global_iter()
- floor(signal_at('traffic', int(global_iter())) + signal_at('promo_boost', int(global_iter())))
- set_rand(['home','product','cart','checkout'], [40,30,20,10])
query: |-
INSERT INTO page_views (ts, view_count, page)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3
)
- name: insert_orders
type: exec
args:
- global_iter()
- floor(abs(signal_at('traffic', int(global_iter()) - 3) * 0.7 + norm(0, 300, -500, 500) * 0.3))
- uniform_f(10, 500, 2)
query: |-
INSERT INTO orders (ts, order_count, amount)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3::DECIMAL
)
- name: insert_tickets
type: exec
args:
- global_iter()
- floor(abs(signal_at('traffic', int(global_iter()) - 24) * 0.3 + norm(0, 300, -500, 500) * 0.7))
- set_rand(['billing','shipping','refund','other'], [30,35,20,15])
query: |-
INSERT INTO support_tickets (ts, ticket_count, category)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3
)
- name: insert_errors
type: exec
args:
- global_iter()
- floor(abs(signal_at('promo_boost', int(global_iter()) - 6) * 0.5 + norm(0, 10, -25, 25) * 0.5))
- set_rand(['timeout','500','oom','connection_reset'], [35,30,20,15])
query: |-
INSERT INTO server_errors (ts, error_count, error_type)
VALUES (
'2024-01-01T00:00:00Z'::TIMESTAMPTZ + (mod($1, 2016) * INTERVAL '5 minutes'),
$2::INT,
$3
)
run_weights:
insert_page_views: 25
insert_orders: 25
insert_tickets: 25
insert_errors: 25The full config is at
examples/correlated_signals/crdb.yaml. Run with-w 1 -d 5mfor a single-worker 5-minute simulation covering one week of data.
Verification queries
Daily traffic pattern - view counts should show a sine wave across 24-hour buckets:
SELECT
extract(hour FROM ts) AS hour,
round(avg(view_count)::NUMERIC, 0) AS avg_views,
repeat('▒', (round(avg(view_count) / max(avg(view_count)) OVER () * 40))::INT) AS histogram
FROM page_views
GROUP BY hour
ORDER BY hour; hour | avg_views | histogram
-------+-----------+-------------------------------------------
0 | 1053 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
1 | 1159 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | 1250 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | 1321 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | 1374 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | 1408 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | 1407 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | 1381 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | 1325 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | 1255 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | 1166 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
11 | 1064 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
12 | 959 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
13 | 861 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
14 | 768 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
15 | 692 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
16 | 642 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
17 | 612 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
18 | 614 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
19 | 641 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
20 | 691 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
21 | 759 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
22 | 854 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
23 | 951 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒Order-traffic correlation - orders should loosely follow page views with a lag:
SELECT
date_trunc('hour', v.ts) AS hour,
repeat('▒', (round(avg(v.view_count) / max(avg(v.view_count)) OVER () * 20))::INT) AS views,
repeat('▒', (round(avg(o.order_count) / max(avg(o.order_count)) OVER () * 20))::INT) AS orders
FROM page_views v
JOIN orders o ON date_trunc('hour', v.ts) = date_trunc('hour', o.ts)
GROUP BY date_trunc('hour', v.ts)
ORDER BY hour
LIMIT 24; hour | views | orders
-------------------------+--------------------+---------------------
2024-01-01 00:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 01:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 02:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 03:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 04:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 05:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 06:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 07:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 08:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 09:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 10:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 11:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 12:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 13:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 14:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 15:00:00+00 | ▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
2024-01-01 16:00:00+00 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
2024-01-01 17:00:00+00 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒
2024-01-01 18:00:00+00 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒
2024-01-01 19:00:00+00 | ▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
2024-01-01 20:00:00+00 | ▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
2024-01-01 21:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 22:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 23:00:00+00 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒Promo boost spikes - errors should spike shortly after promotional periods:
SELECT
date_trunc('hour', ts) AS hour,
sum(error_count) AS total_errors,
repeat('▒', greatest(1, (round(sum(error_count)::FLOAT8 / max(sum(error_count)::FLOAT8) OVER () * 40))::INT)) AS histogram
FROM server_errors
GROUP BY hour
ORDER BY hour
LIMIT 24; hour | total_errors | histogram
-------------------------+--------------+-----------------------------------------
2024-01-01 00:00:00+00 | 10656 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 01:00:00+00 | 9844 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 02:00:00+00 | 8972 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 03:00:00+00 | 6428 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 04:00:00+00 | 4179 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2024-01-01 05:00:00+00 | 2635 | ▒▒▒▒▒▒▒▒▒▒
2024-01-01 06:00:00+00 | 1961 | ▒▒▒▒▒▒▒
2024-01-01 07:00:00+00 | 1534 | ▒▒▒▒▒▒
2024-01-01 08:00:00+00 | 1540 | ▒▒▒▒▒▒
2024-01-01 09:00:00+00 | 1428 | ▒▒▒▒▒
2024-01-01 10:00:00+00 | 1652 | ▒▒▒▒▒▒
2024-01-01 11:00:00+00 | 1553 | ▒▒▒▒▒▒
2024-01-01 12:00:00+00 | 1706 | ▒▒▒▒▒▒
2024-01-01 13:00:00+00 | 1490 | ▒▒▒▒▒
2024-01-01 14:00:00+00 | 1522 | ▒▒▒▒▒
2024-01-01 15:00:00+00 | 1534 | ▒▒▒▒▒▒
2024-01-01 16:00:00+00 | 1581 | ▒▒▒▒▒▒
2024-01-01 17:00:00+00 | 1500 | ▒▒▒▒▒
2024-01-01 18:00:00+00 | 1628 | ▒▒▒▒▒▒
2024-01-01 19:00:00+00 | 1635 | ▒▒▒▒▒▒
2024-01-01 20:00:00+00 | 1724 | ▒▒▒▒▒▒
2024-01-01 21:00:00+00 | 1770 | ▒▒▒▒▒▒
2024-01-01 22:00:00+00 | 1369 | ▒▒▒▒▒
2024-01-01 23:00:00+00 | 1530 | ▒▒▒▒▒▒Ticket lag - support tickets should trail traffic peaks by ~2 hours:
SELECT
extract(hour FROM v.ts)::INT AS hour,
repeat('▒', (round(avg(v.view_count) / max(avg(v.view_count)) OVER () * 20))::INT) AS traffic,
repeat('▒', greatest(1, (round(avg(t.ticket_count) / max(avg(t.ticket_count)) OVER () * 20))::INT)) AS tickets
FROM page_views v
JOIN support_tickets t ON date_trunc('hour', v.ts) = date_trunc('hour', t.ts)
GROUP BY extract(hour FROM v.ts)
ORDER BY hour
LIMIT 24; hour | traffic | tickets
-------+----------------------+-----------------------
0 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒
1 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
2 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
3 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
4 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
5 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
6 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
7 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
8 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
9 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
10 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
11 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
12 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
13 | ▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
14 | ▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
15 | ▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒▒
16 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒
17 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒
18 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
19 | ▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
20 | ▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
21 | ▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒
22 | ▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒
23 | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒▒▒▒▒▒Tuning
- Signal length should match or exceed the number of iterations in your run. Values wrap around when
global_iter()exceeds the signal length, repeating the pattern. - Lag is in iterations. If your signal has 5-minute intervals: 15 min = 3 iterations, 2 hours = 24 iterations.
- Correlation is controlled by the
* corrand* (1 - corr)weights on the signal and noise terms. Usenorm()for Gaussian noise scaled to the signal’s range. - Wrap correlated outputs in
floor(abs(...))to avoid negative or fractional counts. int(global_iter())is required when passing tosignal_at()becauseglobal_iter()returnsint64andsignal_at()expectsint.