Ctrl K
ComunidadGuías…

demo/run_demo.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""pg_sage capability demo — setup + load generator.

Creates the `pgsage_demo` database on the health_pg container, seeds the
capability scenarios (seed.sql), and then hammers the read queries so they
accumulate in pg_stat_statements where pg_sage's optimizer/advisor can see
them. Each query is sent as a TOP-LEVEL statement (pg_stat_statements default
track='top' ignores nested EXECUTE), so the load is generated client-side and
piped to psql.

Usage:
    python run_demo.py --setup          # create db + run seed.sql (one time)
    python run_demo.py                  # one round of load
    python run_demo.py --rounds 5       # keep totals climbing for a few min
    python run_demo.py --setup --rounds 3

Everything runs through `docker exec health_pg psql` so no local driver or
psql client is required.
"""
import argparse
import os
import random
import subprocess
import sys
import time

CONTAINER = "health_pg"
DB = "pgsage_demo"
SEED_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "seed.sql")


def psql(dbname, sql, ignore_errors=True):
    """Pipe a SQL buffer to psql inside the container; return (rc, out)."""
    flags = "-q" if ignore_errors else "-q -v ON_ERROR_STOP=1"
    cmd = [
        "docker", "exec", "-i", CONTAINER,
        "psql", "-U", "postgres", "-d", dbname, "-X",
    ] + flags.split()
    proc = subprocess.run(cmd, input=sql, capture_output=True,
                          text=True, encoding="utf-8", errors="replace")
    return proc.returncode, (proc.stdout or "") + (proc.stderr or "")


def setup():
    print(f"[setup] creating database {DB} ...")
    # CREATE DATABASE must not run inside a txn block; psql sends it standalone.
    rc, out = psql("postgres",
                   f"DROP DATABASE IF EXISTS {DB} WITH (FORCE);\n"
                   f"CREATE DATABASE {DB};\n")
    if "ERROR" in out:
        print(out)
    with open(SEED_FILE, "r", encoding="utf-8") as f:
        seed = f.read()
    print(f"[setup] seeding scenarios from {os.path.basename(SEED_FILE)} ...")
    rc, out = psql(DB, seed, ignore_errors=False)
    print(out.strip()[-2000:])
    if rc != 0:
        print("[setup] seed FAILED", file=sys.stderr)
        sys.exit(1)
    print("[setup] done.")


def rand_vec(dim=256):
    return "[" + ",".join(f"{random.random():.4f}" for _ in range(dim)) + "]"


def build_load_sql():
    """One round of mixed top-level queries hitting every scenario."""
    parts = []
    types = ["click", "view", "purchase", "signup", "logout"]
    regions = ["NA", "EU", "APAC", "LATAM"]
    statuses = ["pending", "paid", "shipped", "cancelled"]

    # A — missing index on orders.customer_id (point lookups, seq scan).
    for _ in range(120):
        cid = random.randint(1, 5000)
        parts.append(
            f"SELECT count(*), sum(total_cents) FROM orders "
            f"WHERE customer_id = {cid};")

    # B — unindexed FK join orders <-> order_items.
    for _ in range(60):
        cid = random.randint(1, 5000)
        parts.append(
            f"SELECT count(*) FROM orders o JOIN order_items i "
            f"ON i.order_id = o.id WHERE o.customer_id = {cid};")

    # C — jsonb containment (wants a GIN index).
    for _ in range(80):
        t = random.choice(types)
        parts.append(
            f"SELECT count(*) FROM events "
            f"WHERE payload @> '{{\"type\":\"{t}\"}}'::jsonb;")

    # D — vector KNN (wants an HNSW index).
    for _ in range(40):
        parts.append(
            f"SELECT id FROM documents "
            f"ORDER BY embedding <-> '{rand_vec()}'::vector LIMIT 10;")

    # E — big GROUP BY / sort that spills at work_mem=64kB (wants more work_mem).
    for _ in range(25):
        parts.append(
            "SELECT customer_id, count(*) c, sum(total_cents) s FROM orders "
            "GROUP BY customer_id ORDER BY s DESC LIMIT 50;")

    # F — 3-way join with selective filters (query-rewrite / join-hint candidate).
    for _ in range(40):
        r = random.choice(regions)
        s = random.choice(statuses)
        parts.append(
            "SELECT o.id, c.name, i.sku "
            "FROM orders o JOIN customers c ON c.id = o.customer_id "
            "JOIN order_items i ON i.order_id = o.id "
            f"WHERE c.region = '{r}' AND o.status = '{s}' LIMIT 100;")

    random.shuffle(parts)
    return "\n".join(parts) + "\n"


def load_round(n):
    sql = build_load_sql()
    t0 = time.time()
    rc, out = psql(DB, sql)
    dt = time.time() - t0
    errs = out.count("ERROR")
    print(f"[load] round {n}: {sql.count(';')} queries in {dt:.1f}s"
          + (f"  ({errs} errors)" if errs else ""))
    if errs:
        # surface the first error so problems aren't silent
        for line in out.splitlines():
            if "ERROR" in line:
                print("       " + line)
                break


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--setup", action="store_true",
                    help="(re)create + seed the pgsage_demo database first")
    ap.add_argument("--rounds", type=int, default=1,
                    help="how many load rounds to run (default 1)")
    ap.add_argument("--sleep", type=float, default=20.0,
                    help="seconds between rounds (default 20)")
    args = ap.parse_args()

    if args.setup:
        setup()

    for i in range(1, args.rounds + 1):
        load_round(i)
        if i < args.rounds:
            time.sleep(args.sleep)
    print("[done] pg_stat_statements populated — pg_sage will analyze on its "
          "next cycle (~60s).")


if __name__ == "__main__":
    main()