A-speed

每个人都是🏆
  新随笔  :: 管理

测试

Posted on 2026-10-27 20:58  a-speed  阅读(3)  评论(0)    收藏  举报

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Greenplum 多Segment 执行计划动画(含语音讲解)</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "Segoe UI", "Microsoft YaHei", sans-serif; background: #0f172a; color: #e2e8f0; }
#app { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { font-size: 20px; margin-bottom: 4px; color: #7dd3fc; }
.sql { font-family: Consolas, monospace; font-size: 12px; color: #a5b4fc; background: #1e293b; padding: 8px 12px; border-radius: 6px; margin-bottom: 12px; }
#stage { width: 100%; background: #0f172a; border: 1px solid #334155; border-radius: 10px; }
#controls { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
button { background: #2563eb; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
button:hover { background: #1d4ed8; }
button.secondary { background: #475569; }
button.on { background: #16a34a; }
#phaseLabel { font-size: 14px; color: #fbbf24; min-width: 320px; }
#subtitle { min-height: 48px; background: #1e293b; border-radius: 6px; padding: 10px 14px; margin-top: 12px;
font-size: 15px; color: #e2e8f0; line-height: 1.6; border-left: 4px solid #38bdf8; }
#log { font-family: Consolas, monospace; font-size: 12px; color: #94a3b8; background: #1e293b; border-radius: 6px;
padding: 10px; height: 120px; overflow-y: auto; margin-top: 12px; }
#log div { margin-bottom: 2px; }
</style>
</head>
<body>
<div id="app">
<h1>🎬🔊 Greenplum 多 Segment 分布式执行计划动画(中文语音讲解)</h1>
<div class="sql">SELECT c.region, SUM(o.amount) FROM orders o JOIN customers c ON o.cid = c.id WHERE o.amount &gt; 100 GROUP BY c.region;</div>

<svg id="stage" viewBox="0 0 1160 640"></svg>

<div id="controls">
<button id="playBtn">▶ 播放</button>
<button id="pauseBtn" class="secondary">⏸ 暂停</button>
<button id="resetBtn" class="secondary">🔄 重置</button>
<button id="voiceBtn" class="on">🔊 语音: 开</button>
<button id="rateBtn" class="secondary">语速: 正常</button>
<span id="phaseLabel">准备就绪</span>
</div>

<div id="subtitle">点击 ▶ 播放,将同步播放中文语音讲解。字幕会显示当前解说内容。</div>
<div id="log"></div>
</div>

<script>
// ============ 配置 ============
const NS = "http://www.w3.org/2000/svg";
const svg = document.getElementById("stage");
const phaseLabel = document.getElementById("phaseLabel");
const subtitleEl = document.getElementById("subtitle");
const logEl = document.getElementById("log");
const SEG_N = 4;
const SEG_X = [230, 520, 810, 1100];
const MASTER = { x: 665, y: 60 };
const SCAN_Y = 300;
const JOIN_Y = 420;
const AGG_Y = 520;

// ============ 中文语音引擎 ============
const synth = window.speechSynthesis;
let voiceOn = true;
let voiceRate = 1.0;
let zhVoice = null;

function pickVoice() {
const voices = synth.getVoices();
// 优先选择中文语音
zhVoice = voices.find(v => v.lang === "zh-CN") ||
voices.find(v => v.lang.startsWith("zh")) || null;
}
pickVoice();
synth.onvoiceschanged = pickVoice;

function speak(text) {
if (!voiceOn || !("speechSynthesis" in window)) return;
synth.cancel(); // 打断上一句,保证与画面同步
const u = new SpeechSynthesisUtterance(text);
u.lang = "zh-CN";
if (zhVoice) u.voice = zhVoice;
u.rate = voiceRate;
u.pitch = 1.0;
synth.speak(u);
}

function setSubtitle(t) { subtitleEl.textContent = "🎙 " + t; }

// ============ SVG 工具 ============
function el(tag, attrs, text) {
const e = document.createElementNS(NS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
if (text) e.textContent = text;
svg.appendChild(e);
return e;
}

function drawStatic() {
el("rect", { x: MASTER.x-70, y: MASTER.y-22, width: 140, height: 44, rx: 10, fill: "#78350f", stroke: "#fbbf24", "stroke-width": 2 });
el("text", { x: MASTER.x, y: MASTER.y+5, "text-anchor": "middle", fill: "#fde68a", "font-size": 15, "font-weight": "bold" }, "MASTER");
for (let i = 0; i < SEG_N; i++) {
el("line", { x1: MASTER.x, y1: MASTER.y+22, x2: SEG_X[i], y2: SCAN_Y-45, stroke: "#1e293b", "stroke-width": 3 });
}
for (let i = 0; i < SEG_N; i++) {
const x = SEG_X[i];
const colors = ["#0ea5e9", "#22c55e", "#f97316", "#a78bfa"];
el("rect", { x: x-80, y: SCAN_Y-45, width: 160, height: 320, rx: 10, fill: "#1e293b", stroke: colors[i], "stroke-width": 2, opacity: 0.4, id: "segbox"+i });
el("text", { x: x, y: SCAN_Y-25, "text-anchor": "middle", fill: colors[i], "font-size": 13, "font-weight": "bold" }, "Segment "+i);
el("rect", { x: x-60, y: SCAN_Y, width: 120, height: 30, rx: 5, fill: "#334155" });
el("text", { x: x, y: SCAN_Y+19, "text-anchor": "middle", fill: "#cbd5e1", "font-size": 11, id: "scantext"+i }, "Seq Scan (空闲)");
el("rect", { x: x-60, y: JOIN_Y, width: 120, height: 30, rx: 5, fill: "#334155" });
el("text", { x: x, y: JOIN_Y+19, "text-anchor": "middle", fill: "#cbd5e1", "font-size": 11, id: "jointext"+i }, "Hash Join (空闲)");
el("rect", { x: x-60, y: AGG_Y, width: 120, height: 30, rx: 5, fill: "#334155" });
el("text", { x: x, y: AGG_Y+19, "text-anchor": "middle", fill: "#cbd5e1", "font-size": 11, id: "aggtext"+i }, "HashAggregate (空闲)");
el("text", { x: x, y: AGG_Y+50, "text-anchor": "middle", fill: "#64748b", "font-size": 10, id: "stat"+i }, "rows: -");
}
el("text", { x: 20, y: 620, fill: "#64748b", "font-size": 11 }, "蓝色=并行扫描 红色=Redistribute Motion 绿色=并行Join 紫色=Gather Motion");
}

// ============ 动画工具 ============
let paused = false;
let timers = [];

function log(msg, color) {
const d = document.createElement("div");
d.style.color = color || "#94a3b8";
d.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
}

function setPhase(t) { phaseLabel.textContent = "▶ " + t; }

function moveDot(x1, y1, x2, y2, color, dur, onDone) {
const dot = el("circle", { cx: x1, cy: y1, r: 6, fill: color, opacity: 0.9 });
const t0 = performance.now();
function step(t) {
if (paused) { requestAnimationFrame(step); return; }
const p = Math.min((t - t0) / dur, 1);
dot.setAttribute("cx", x1 + (x2-x1)*p);
dot.setAttribute("cy", y1 + (y2-y1)*p);
if (p < 1) requestAnimationFrame(step);
else { dot.remove(); if (onDone) onDone(); }
}
requestAnimationFrame(step);
}

function highlight(id, on) {
const e = document.getElementById(id);
if (e) e.setAttribute("opacity", on ? 1 : 0.4);
}

function setNodeText(id, text, color) {
const e = document.getElementById(id);
if (e) { e.textContent = text; e.setAttribute("fill", color || "#22d3ee"); }
}

function wait(ms, fn) {
const t = setTimeout(() => { if (!paused) fn(); else wait(100, fn); }, ms);
timers.push(t);
}

// ============ 各阶段(含中文解说) ============
function phase1() {
setPhase("阶段1: 计划生成与分发");
const narration = "大家好,欢迎观看 Greenplum 分布式执行计划动画。" +
"现在,客户端把一条统计查询发给了主节点 Master。" +
"Master 负责解析这条 SQL,并生成一棵分布式的并行执行计划。" +
"计划树中包含了关键的数据移动节点,也就是 Motion 节点。" +
"接下来,Master 会把这棵计划分发给下面的四个数据节点,即 Segment," +
"每个 Segment 都会收到一份相同的计划拷贝。请看金色的数据包正在分发。";
speak(narration);
setSubtitle("Master接收SQL后生成分布式并行计划(含Motion节点),并把计划分发给所有Segment。Motion是MPP架构中负责数据移动的关键算子。");

log("Master: 生成并行计划,Slice 1 = 扫描+本地聚合, Slice 0 = Join+全局聚合", "#fbbf24");
for (let i = 0; i < SEG_N; i++) {
wait(i*200, () => {
moveDot(MASTER.x, MASTER.y+22, SEG_X[i], SCAN_Y-45, "#fbbf24", 500, () => {
highlight("segbox"+i, true);
log(`计划分发至 Segment ${i}`);
});
});
}
wait(2200, phase2);
}

function phase2() {
setPhase("阶段2: 并行扫描与本地预聚合");
const narration = "现在进入第二阶段,并行数据扫描。" +
"请注意,每个 Segment 只存储整张表的一部分数据," +
"通常是根据分布键的哈希值切分的,每个节点大约存储四分之一。" +
"因此四个 Segment 可以同时扫描,各自处理自己的本地数据," +
"这就是大规模并行处理带来的线性加速能力。" +
"同时,过滤条件也下推到本地执行,只有金额大于一百的行才会被保留," +
"这样可以在数据源头就大幅减少数据量。";
speak(narration);
setSubtitle("4个Segment同时扫描各自存储的1/4数据,并本地执行过滤(WHERE)和预聚合——数据本地性+并行度是MPP加速的核心。");

log("并行执行: 每个 Segment 只扫描自己存储的 1/4 数据 → 线性加速!", "#38bdf8");
for (let i = 0; i < SEG_N; i++) {
wait(i*150, () => {
setNodeText("scantext"+i, "Seq Scan 扫描中...", "#38bdf8");
setNodeText("jointext"+i, "本地预聚合", "#22c55e");
for (let k = 0; k < 5; k++) {
wait(k*180, () => moveDot(SEG_X[i]-40, SCAN_Y+40, SEG_X[i]-40, JOIN_Y-5, "#38bdf8", 400));
wait(k*180, () => moveDot(SEG_X[i]+40, SCAN_Y+40, SEG_X[i]+40, JOIN_Y-5, "#38bdf8", 400));
}
wait(1100, () => {
const rows = 20000 + Math.floor(Math.random()*5000);
const s = document.getElementById("stat"+i);
s.textContent = "rows: " + rows.toLocaleString();
s.setAttribute("fill", "#22c55e");
log(`Segment ${i}: 本地扫描+过滤+预聚合完成 (${rows.toLocaleString()} 行)`);
});
});
}
wait(3200, phase3);
}

function phase3() {
setPhase("阶段3: Redistribute Motion 数据重分布");
const narration = "接下来是本动画最关键的环节,数据重分布,也就是 Redistribute Motion。" +
"我们的订单表是按客户编号分布的,而客户表却是按地区分布的。" +
"两表要按照客户编号做连接,但相同客户编号的数据,现在散落在不同的节点上。" +
"所以数据库必须按照连接键,也就是客户编号,对数据进行重新哈希分布," +
"保证相同编号的行,一定落在同一个 Segment 上,连接才能在本地完成。" +
"请看红色的数据流,正在节点之间交叉传输。" +
"这里有个重要的优化经验:如果分布键设计得当,让连接键恰好就是分布键," +
"这个昂贵的重分布步骤就可以完全避免。";
speak(narration);
setSubtitle("customers按region分布、JOIN键是cid → 两表数据不对齐,必须按HASH(cid)重分布(Redistribute Motion),这是MPP中成本最高的操作之一!");

log("Motion 节点触发: 按 HASH(cid) 重新分布数据", "#f43f5e");
for (let i = 0; i < SEG_N; i++) {
wait(i*250, () => {
setNodeText("scantext"+i, "→ Redistribute", "#f43f5e");
for (let j = 0; j < SEG_N; j++) {
if (i === j) continue;
wait(j*150, () => {
moveDot(SEG_X[i], SCAN_Y+15, SEG_X[j], SCAN_Y+15, "#f43f5e", 700, () => {
moveDot(SEG_X[j], SCAN_Y+15, SEG_X[j], JOIN_Y-5, "#f43f5e", 300);
});
});
}
});
}
wait(3200, () => {
for (let i = 0; i < SEG_N; i++) setNodeText("scantext"+i, "重分布完成", "#f43f5e");
phase4();
});
}

function phase4() {
setPhase("阶段4: 并行 Hash Join 与局部聚合");
const narration = "数据重分布完成之后,每个 Segment 上都拥有了执行连接所需的全部数据。" +
"于是四个 Segment 再次并行工作,各自在本地完成哈希连接," +
"这个过程不需要任何额外的数据移动,效率非常高。" +
"连接完成后,紧接着在本地做第一阶段的分组聚合," +
"把每个地区在小计先算出来,数据量进一步缩小。" +
"注意,这还不是最终结果,因为同一个地区的数据可能分布在多个节点上。";
speak(narration);
setSubtitle("每个Segment本地完成Hash Join(零数据移动),然后做两阶段聚合的第一阶段(局部GROUP BY),大幅减少传给Master的数据量。");

log("并行本地 Hash Join + 局部聚合", "#22c55e");
for (let i = 0; i < SEG_N; i++) {
wait(i*200, () => {
setNodeText("jointext"+i, "Hash Join ✓", "#22c55e");
setNodeText("aggtext"+i, "局部聚合中...", "#22c55e");
for (let k = 0; k < 4; k++) {
wait(k*200, () => moveDot(SEG_X[i], JOIN_Y+35, SEG_X[i], AGG_Y-5, "#22c55e", 350));
}
wait(1000, () => {
setNodeText("aggtext"+i, "局部聚合 ✓", "#22c55e");
log(`Segment ${i}: Join + 局部聚合完成`);
});
});
}
wait(2600, phase5);
}

function phase5() {
setPhase("阶段5: Gather Motion 结果汇总");
const narration = "最后一个阶段,结果汇总。" +
"所有 Segment 的局部聚合结果,通过 Gather Motion,也就是收集动作," +
"经过高速互联网络传回 Master 节点。" +
"Master 在这里完成第二阶段的全局聚合,把各个节点的部分和合并成最终的总和," +
"然后将结果集返回给客户端。" +
"回顾整个流程,我们看到了大规模并行处理的完整图景:" +
"并行扫描、数据重分布、并行连接、两阶段聚合、结果汇总。" +
"感谢观看,希望这能帮助你理解 Greenplum 的分布式执行原理!";
speak(narration);
setSubtitle("所有Segment的局部结果通过Gather Motion汇聚到Master,Master完成全局SUM合并后返回客户端——查询结束!");

log("Gather Motion: 结果流向 Master", "#c084fc");
for (let i = 0; i < SEG_N; i++) {
wait(i*250, () => {
moveDot(SEG_X[i], AGG_Y+15, MASTER.x, MASTER.y+30, "#c084fc", 800, () => {
log(`Segment ${i} 结果已到达 Master`);
});
});
}
wait(2000, () => {
setPhase("✅ 查询完成!");
log("Master: 全局聚合完成, 结果返回客户端", "#fbbf24");
for (let i = 0; i < SEG_N; i++)
document.getElementById("stat"+i).setAttribute("fill", "#fbbf24");
});
}

// ============ 控制与启动 ============
let started = false;
const rates = [0.8, 1.0, 1.25, 1.5];
let rateIdx = 1;

function reset() {
timers.forEach(clearTimeout); timers = [];
synth.cancel();
svg.innerHTML = ""; paused = false;
logEl.innerHTML = "";
drawStatic();
setPhase("准备就绪");
setSubtitle("点击 ▶ 播放,将同步播放中文语音讲解。字幕会显示当前解说内容。");
started = false;
}
function start() {
reset();
started = true;
phase1();
}

document.getElementById("playBtn").onclick = () => { paused = false; if (!started) start(); };
document.getElementById("pauseBtn").onclick = () => { paused = true; synth.pause(); setPhase("⏸ 已暂停"); };
document.getElementById("resetBtn").onclick = reset;

document.getElementById("voiceBtn").onclick = function() {
voiceOn = !voiceOn;
this.textContent = voiceOn ? "🔊 语音: 开" : "🔇 语音: 关";
this.classList.toggle("on", voiceOn);
if (!voiceOn) synth.cancel();
};

document.getElementById("rateBtn").onclick = function() {
rateIdx = (rateIdx + 1) % rates.length;
voiceRate = rates[rateIdx];
this.textContent = "语速: " + (voiceRate === 1.0 ? "正常" : voiceRate + "x");
};

drawStatic();
log("提示: 建议使用 Chrome/Edge 浏览器以获得最佳中文语音效果。", "#7dd3fc");
log("点击 ▶ 播放 开始动画+语音讲解。", "#7dd3fc");
</script>
</body>
</html>