Skip to main content

mz-demo-data

Trigger: "demo data", "synthetic data", "live operational data", "generate fake data", "create a demo", "load generator alternative", or wants realistic continuously-updating data in Materialize for a demo. Also "I need streaming data to show off X" or "build a demo schema for <domain>". Use this to stand up auctions/bids, ecommerce, banking, IoT, clickstream, or zoo demos — or to design a new domain in the same style.

소스 정보

저장소
MaterializeInc/agent-skills
최근 소스 활동
2026년 10월 1일 13:38
감지된 SKILL.md 언어
영어
스타
3
포크
2

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
12 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
mz-demo-data
description
Trigger: "demo data", "synthetic data", "live operational data", "generate fake data", "create a demo", "load generator alternative", or wants realistic continuously-updating data in Materialize for a demo. Also "I need streaming data to show off X" or "build a demo schema for <domain>". Use this to stand up auctions/bids, ecommerce, banking, IoT, clickstream, or zoo demos — or to design a new domain in the same style.
# Live operational demo data, generated entirely in SQL This skill builds continuously-updating, realistic synthetic data inside a running Materialize instance — no Kafka, no external load generator, no seed scripts. Everything is plain views over `mz_now()`. The technique comes from [this blog post][blog]. Use the catalog at the bottom for an existing domain; use the rubric to design new ones. [blog]: https://github.com/frankmcsherry/blog/blob/master/posts/2024-05-19.md ## Quickstart ```sh psql -p 6875 -h localhost -U materialize -f assets/scaffold.sql psql -p 6875 -h localhost -U materialize -f assets/common/people.sql psql -p 6875 -h localhost -U materialize -f assets/domains/auctions.sql ``` Everything lands in its own `materialize_demo` schema, so nothing in the user's existing schemas is touched. Every object is created `IF NOT EXISTS`, so re-running any file is safe, but it also means re-running an *edited* file keeps the old definitions. To rebuild, run `assets/teardown.sql` (it drops the schema) and load again. Then in a psql session: ```sql SET search_path = materialize_demo; COPY (SUBSCRIBE (SELECT COUNT(*) FROM auctions) WITH (progress = true)) TO STDOUT; ``` You should see a heartbeat tick once per second. The window is backfilled on load, so the count starts at retention/tick (default 86,400) and stays there as both ends of the window advance. To change the window or tick rate, set them before `\i`. They only take effect on a fresh load; if the scaffold is already there, run `assets/teardown.sql` first. ```sql \set retention '6 hours' \set tick '1 second' \i assets/scaffold.sql ``` To tear everything down: `psql -f assets/teardown.sql`. ## First contact: walking a fresh user from zero to demo **Read this section first if you're an agent and a user shows up wanting a demo.** The skill is meant to feel guided, not like a SQL reference. Drive the user through these turns: ### Turn 1 — establish the connection Ask once if it isn't already in context: > What's the connection? A `psql` command, a `MATERIALIZE_URL`, or > host/port/user/db? Accept any of: a connection string, env var, or the four flags. Don't proceed until you can run `psql -c 'SELECT mz_version();'` and get a row back. If they say "localhost defaults", use `psql -p 6875 -h localhost -U materialize -d materialize`. ### Turn 2 — pick a domain If the user named one ("show me a banking demo"), check the catalog at the bottom of this file. If it matches, jump to Turn 5 with that file. If they have a domain in mind that isn't in the catalog ("I work in logistics", "we do telecom billing", "give me healthcare"), continue to Turn 3 — we'll build a new domain together. **Do not** force-fit their domain onto one in the catalog; that's worse than a fresh one. If they don't have a domain in mind, suggest 2–3 from the catalog with a one-line hook each and let them pick. Banking is the strongest correctness story; zoo is there to show that a domain can be anything, not just orders and sensors. ### Turn 3 — propose the model in plain language **Not SQL yet.** Show the user the entities and relationships in English, plus the invariant you'd bake in. Format: > Here's what I'd build: > - **\<TopEntity\>** (one per moment) — fields: a, b, c > - **\<ChildEntity\>** (3–5 per top-level) — fields: x, y > - **\<Lookup\>** (static, N rows) — purpose > > **Invariant baked in:** \<one sentence — what must always hold\> > > **Joins to people?** \<yes/no, and why\> > > Sound right, or different entities? Wait for their reaction. **Don't write SQL until they sign off.** They will usually correct one of: an entity name, a missing field, the invariant, or the cardinality. Cheap to iterate here, expensive later. ### Turn 4 — show the byte budget Once entities are confirmed, show the byte allocation explicitly: > Per top-level row, 16 random bytes: > ``` > [0..2] id 24-bit > [3] person_id mod 256, FK to people > [4] kind mod N > [5] n_children 1..K > [6] time offset for due_at > [7..] free > ``` > Children re-hash `(parent.random || child_index)` for their own bytes. This is the last cheap-to-change step. If the user wants higher cardinality in some field, swap bytes here, not later. ### Turn 5 — write and load Copy `assets/domains/_template.sql` to `assets/domains/<their>.sql` and fill it in following the byte budget. Then: ```sh # Every file is IF NOT EXISTS — safe to re-run. psql ... -f assets/scaffold.sql psql ... -f assets/common/people.sql # only if domain joins people psql ... -f assets/domains/<their>.sql ``` If you are iterating on the domain file, re-running it will not replace views that already exist. Drop the domain's objects first (`DROP VIEW materialize_demo.<name>_core CASCADE` takes its whole chain), or run `assets/teardown.sql` and load everything again. ### Turn 6 — prove it works, hand over the wheel Show the user three queries, after `SET search_path = materialize_demo;`: 1. **Heartbeat** — confirms data is flowing: ```sql COPY (SUBSCRIBE (SELECT COUNT(*) FROM <top_table>) WITH (progress = true)) TO STDOUT; ``` 2. **Invariant** — confirms correctness; should return the expected fixed value (usually 0). 3. **One "cool" query** — the demo payoff. For aggregations, `SELECT ... GROUP BY ...`; for cross-domain, a join through `people`. Then offer: "Want to layer another domain on top? You'll see joins through the shared `people` table stay live too." ### Things to *not* do during first contact - Don't dump the catalog up front. The user came with a domain or wants guidance — give them one path, not a menu. - Don't ship SQL without the plain-language proposal first. - Don't skip the byte budget. It's where misalignments hide (e.g., two fields sharing byte 5 by accident). - Don't claim "it's done" until the heartbeat ticks and the invariant query returns its expected value against THEIR Materialize instance. - Don't add columns the user didn't ask for. The template's free bytes exist; leave them free unless the user names a use. ## The two-layer model Every demo built with this skill has exactly two layers: 1. **Scaffold** (stable). The `moments` view — a sliding window of timestamps — and the `random` view — MD5(moment) producing 16 deterministic bytes per moment. **Do not edit per-domain.** Always loaded first; the same scaffold serves every domain. Lives in `assets/scaffold.sql`. 2. **Domain** (creative). Views that turn `random` bytes into entities, relationships, and aggregates. One file per domain in `assets/domains/`. Domains can compose: load multiple and they cross-join naturally via the shared `people` table. Knowing where the line is matters. If you find yourself wanting to edit the scaffold from a domain file, you almost certainly want to add another view on top instead. ## The four design rules for a domain Every domain in the catalog follows these four rules. Apply them when designing a new one. ### 1. PKs are derived deterministically from `moment` A row's primary key is a function of its moment's random bytes: ```sql get_byte(random, 0) + get_byte(random, 1) * 256 + get_byte(random, 2) * 65536 AS id ``` **Why:** the same moment always yields the same id, so re-derivation is stable even though the underlying view is a sliding window. PKs naturally vanish from the system when their moment falls out of retention. ### 2. FKs come from re-derivation, not declared constraints A child row is generated by re-hashing the parent's random bytes: ```sql WITH expanded AS ( SELECT id AS parent_id, ... digest(random::text || generate_series(1, n_children)::text, 'md5') AS random FROM parent_core ) SELECT ... FROM expanded; ``` **Why:** when the parent's moment falls out of retention, every child derived from it vanishes simultaneously. Referential integrity for free, no `FOREIGN KEY` declarations needed. This is the key insight that makes the whole approach work. ### 3. Distributions are byte-mask choices Cardinality is controlled by which bytes you read: | Pattern | Cardinality | |----------------------------------------|------------------| | `get_byte(random, 0)` | 256 | | `get_byte(random, 0) + get_byte(random, 1) * 256` | 65,536 | | `mod(get_byte(random, 0), 5)` | 5 | | `get_byte(random, 0) < 80` | ~31% boolean | **Why:** explicit and predictable. A 256-account banking demo uses one byte mod 64 for from-account; a 16M-id auction marketplace uses three bytes. Pick the cardinality you need. ### 4. Evolution is `moment + interval` Time-relative fields are derived from the moment plus a random offset: ```sql moment + (get_byte(random, 6)::text || ' minutes')::interval AS end_time ``` **Why:** the field is monotone in the moment, so downstream views can filter on it sensibly. Combined with rule #1, this gives you a temporal lifecycle (start, end, expiry) tied to the entity's identity. ## Invariant-by-construction patterns The strongest demos rely on invariants that **cannot be violated** because of how the data is constructed. Two patterns: **Sum-to-zero (double entry).** Each event emits two child rows with opposite signs (`banking.sql`). `SUM(ledger_entries.amount) = 0` holds at every consistent timestamp, regardless of concurrent transaction volume. This is the headline Materialize correctness demo and it's almost impossible to fake with off-the-shelf data. **Count matches declared fanout.** A parent row declares `n_items`, and uses `generate_series(1, n_items)` to emit children. `COUNT(children) = SUM(parent.n_items)` is invariant (`ecommerce.sql`). State it in aggregate; the per-parent form breaks on id collisions (next section). When designing a new domain, look for an invariant of this form. A demo without one is weaker — it shows speed but not correctness. ## Invariants vs. id collisions The blog's construction derives primary keys from 24 bits of MD5 entropy (`get_byte(random, 0..2)` packed into an int). At default settings — 86,400 moments in retention — the birthday paradox guarantees roughly 230–500 id collisions in steady state. **This is intentional and expected.** The blog accepts it; this skill keeps it for fidelity to the blog and to keep parent ids in a tractable range. Collisions affect which invariant shapes you can honestly claim: **Survives collisions (claim freely):** - Aggregate equalities. `COUNT(children) = SUM(n_children over all parents)`. - Sum-to-zero. `SUM(ledger_entries.amount) = 0`. - Subset / FK by re-derivation. `every child.parent_id has matching parent` — re-derivation guarantees this even when multiple parents share an id (the child joins to *some* row with that id). - Distribution-shape claims. `~10% of page_views are /checkout`. **Does NOT survive collisions (don't claim, or restate as aggregate):** - Per-parent fanout equality. `for every parent, COUNT(children with this id) = parent.n_children`. Two parents sharing an id will sum their children, breaking the equality. - Per-id uniqueness. `every id appears exactly once`. - Per-row temporal monotonicity that depends on id grouping. When the user asks "what's the invariant," default to the aggregate form unless the construction makes per-id claims structurally true (e.g. double-entry, where each transaction emits exactly two entries no matter what id arithmetic does). If a demo *requires* unique ids (e.g. a customer-facing dashboard that treats id as a key), the fix is to either widen the id space (use 4+ bytes), use `EXTRACT(EPOCH FROM moment)::bigint` as the id (collision-free by construction), or accept ~0.5% noise as a feature ("real data is messy"). All three are reasonable; the catalog uses the blog's 24-bit form for fidelity. ## Adapting to a new domain When walking a user through this, follow the **First contact** protocol above — propose-then-confirm, don't write SQL upfront. The mechanical checklist is below. To add `assets/domains/<yours>.sql`, work through these steps in order: 1. **List the entities.** Top-level (one per moment), child rows (per parent via fanout), shared lookups (static). 2. **Pick the byte budget for each top-level entity.** Random gives you 16 bytes per moment. Allocate bytes to fields by cardinality: id (3 bytes), FK to people (1 byte mod 256), category (1 byte mod N), timing offsets (1 byte), free for amount/value (2–3 bytes). 3. **Decide which existing shared tables you reference.** Today: `people`. If you reference people, your domain joins the cross-domain "same person appears in multiple data products" story automatically. 4. **Identify the invariant.** What sum, count, or equality must hold by construction? Bake it in via fanout or two-legged emission, not as a `CHECK` constraint. 5. **Copy the template.** `cp assets/domains/_template.sql assets/domains/<yours>.sql` and fill in the TODOs. The template already encodes the standard `_core` view → public MV → child MV → aggregate VIEW pattern, with every object in `materialize_demo` and created `IF NOT EXISTS`. Keep that structure. 6. **Add two validation queries** at the bottom: a `SUBSCRIBE` heartbeat and an invariant query that should always return 0 (or a fixed value). ## Validation For every domain, two queries: ```sql -- Heartbeat: confirms the domain is live and producing rows. COPY (SUBSCRIBE (SELECT COUNT(*) FROM <table>) WITH (progress = true)) TO STDOUT; -- Invariant: should return a fixed value (usually 0). SELECT ... ; ``` If the heartbeat ticks but the invariant drifts, the byte budget is misaligned — likely two entities sharing the same byte for different purposes. Re-allocate. ## The catalog | File | Domain | Highlights | Joins `people`? | |---|---|---|---| | `domains/auctions.sql` | Auctions & bids | Marketplace; lifecycle (end_time); winning-bid demo | No | | `domains/ecommerce.sql` | Orders, line items, totals | Multi-row child fanout; `order_totals` aggregate-as-view | Yes | | `domains/banking.sql` | Accounts, double-entry txns | **`SUM(balances) = 0`** invariant. Strongest correctness demo. | Yes | | `domains/iot.sql` | Devices, readings, alerts | High cardinality; threshold alerts; per-site rollup | No | | `domains/clickstream.sql`| Sessions, page views, funnel | Funnel analytics; conversion rate by channel | Yes | | `domains/zoo.sql` | Zoo visits, ratings, shipments | Three invariants at once, richest domain. Proof that a domain needn't be boring: front-of-house ratings correlate with back-of-house consultations by construction. | Yes | Loading more than one ecommerce/banking/clickstream domain at once gives you the cross-product demo: "show me Person 042's orders, their transactions, and their browsing session — all live, all consistent." ## File map ``` skills/mz-demo-data/ ├── SKILL.md ← you are here ├── README.md short human intro └── assets/ ├── scaffold.sql moments + random; do not edit per-domain ├── teardown.sql drops the materialize_demo schema ├── common/ │ └── people.sql 256-identity pool, shared across domains └── domains/ ├── _template.sql ← copy this to start a new domain ├── auctions.sql ├── ecommerce.sql ├── banking.sql ├── iot.sql ├── clickstream.sql └── zoo.sql ```
GitHub에서 보기