- add links to interactive HTML results for storage format evaluation and foreign-key architecture study - update descriptions in README to reflect the new structure of research documents - clarify the contents of the research directory in the README
572 lines
45 KiB
HTML
572 lines
45 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Foreign-Key Architecture Study - Tribology Lab Data Storage</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
*{box-sizing:border-box}
|
|
html,body{margin:0;padding:0}
|
|
body{font-family:"IBM Plex Sans",system-ui,sans-serif;color:#16181d;background:#eef0f2;-webkit-font-smoothing:antialiased}
|
|
::-webkit-scrollbar{width:10px;height:10px}
|
|
::-webkit-scrollbar-thumb{background:#cfd4da;border-radius:6px;border:2px solid #eef0f2}
|
|
::-webkit-scrollbar-track{background:transparent}
|
|
.mono{font-family:"IBM Plex Mono",monospace}
|
|
|
|
.app{display:flex;flex-direction:column;height:100vh;width:100%;overflow:hidden;background:#eef0f2}
|
|
header.top{display:flex;align-items:center;gap:14px;height:56px;flex:none;padding:0 22px;background:#fff;border-bottom:1px solid #e2e5ea}
|
|
.logo{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:7px;background:#16181d;color:#fff;font-family:"IBM Plex Mono",monospace;font-weight:600;font-size:12px}
|
|
.htitle{display:flex;flex-direction:column;line-height:1.25;white-space:nowrap}
|
|
.htitle b{font-size:14.5px;font-weight:600;letter-spacing:-.01em}
|
|
.htitle span{font-size:11.5px;color:#838a95}
|
|
.hright{margin-left:auto;display:flex;align-items:center;gap:8px}
|
|
.chip{font-family:"IBM Plex Mono",monospace;font-size:11px;color:#838a95;background:#f3f5f7;border:1px solid #e6e8ec;padding:4px 10px;border-radius:6px}
|
|
.chip-ok{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#0f7a45;background:#e7f6ee;padding:4px 10px;border-radius:6px;font-weight:600}
|
|
.chip-ok i{width:7px;height:7px;border-radius:7px;background:#18a05a}
|
|
|
|
.frame{display:flex;flex:1;min-height:0}
|
|
nav.side{width:248px;flex:none;background:#fff;border-right:1px solid #e2e5ea;padding:14px 12px;display:flex;flex-direction:column;overflow-y:auto}
|
|
.navcap{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.1em;color:#a4abb4;padding:6px 11px 8px}
|
|
nav.side a{display:flex;align-items:center;gap:11px;width:100%;padding:8px 11px;border-radius:8px;cursor:pointer;text-decoration:none;font-size:13.5px;margin:1px 0;color:#475059;font-weight:500}
|
|
nav.side a .tag{width:24px;height:19px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-family:"IBM Plex Mono",monospace;font-size:9px;font-weight:600;flex:none;background:#eceef1;color:#8a909b}
|
|
nav.side a.active{background:#eaf1fe;color:#16181d;font-weight:600}
|
|
nav.side a.active .tag{background:#1f5fdb;color:#fff}
|
|
.sidefoot{margin-top:auto;padding:14px 11px 6px;border-top:1px solid #eef0f2}
|
|
.sidefoot .cap{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.08em;color:#a4abb4;margin-bottom:8px}
|
|
.sidefoot .txt{font-size:12px;color:#6b727c;line-height:1.6}
|
|
|
|
main{flex:1;min-width:0;overflow-y:auto;padding:26px 30px 60px;scroll-behavior:smooth}
|
|
section{margin-bottom:44px;scroll-margin-top:10px}
|
|
h1{margin:0;font-size:21px;font-weight:600;letter-spacing:-.02em}
|
|
h2{margin:0 0 4px;font-size:18px;font-weight:600;letter-spacing:-.02em}
|
|
.sub{margin:4px 0 18px;font-size:13px;color:#838a95;max-width:860px;line-height:1.55}
|
|
.card{background:#fff;border:1px solid #e6e8ec;border-radius:11px;padding:18px 20px;margin-bottom:14px}
|
|
.card h3{margin:0 0 10px;font-size:14px;font-weight:600}
|
|
.card p{margin:0 0 10px;font-size:13px;color:#3d424b;line-height:1.6;max-width:880px}
|
|
.card p:last-child{margin-bottom:0}
|
|
.card ul{margin:6px 0 4px;padding-left:20px}
|
|
.card li{font-size:13px;color:#3d424b;line-height:1.65;margin-bottom:6px;max-width:840px}
|
|
.muted{color:#838a95}
|
|
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px}
|
|
.kpi{background:#fff;border:1px solid #e6e8ec;border-radius:10px;padding:15px 16px}
|
|
.kpi .l{font-size:11.5px;color:#838a95;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
|
|
.kpi .v{font-family:"IBM Plex Mono",monospace;font-size:26px;font-weight:600;margin-top:7px;line-height:1}
|
|
.kpi .s{font-size:11.5px;color:#a4abb4;margin-top:5px}
|
|
.v-bad{color:#b23b4e}.v-good{color:#0f7a45}.v-neutral{color:#16181d}
|
|
|
|
table.data{width:100%;border-collapse:collapse}
|
|
table.data th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:#838a95;font-weight:600;text-align:left;padding:9px 12px;background:#f7f8fa;border-bottom:1px solid #eef0f2}
|
|
table.data td{font-size:12.5px;color:#3d424b;padding:11px 12px;border-bottom:1px solid #f4f5f7;vertical-align:top}
|
|
table.data td.mono, table.data th.num{font-family:"IBM Plex Mono",monospace}
|
|
table.data th.num, table.data td.num{text-align:right;font-family:"IBM Plex Mono",monospace}
|
|
.delta-bad{color:#b23b4e;font-weight:600}
|
|
.delta-ok{color:#0f7a45;font-weight:600}
|
|
.delta-zero{color:#838a95}
|
|
|
|
pre.code{background:#16181d;color:#dfe3e9;border-radius:9px;padding:14px 16px;font-family:"IBM Plex Mono",monospace;font-size:12px;line-height:1.65;overflow-x:auto;margin:12px 0 0}
|
|
pre.code .c{color:#7e8794}
|
|
pre.code .k{color:#7fb1ff}
|
|
pre.code .hl{color:#ff9db0}
|
|
|
|
.erd-wrap{overflow-x:auto;padding:8px 0}
|
|
.erd{position:relative;margin:0 auto}
|
|
.erd svg.wire{position:absolute;inset:0;pointer-events:none}
|
|
.tbl{position:absolute;background:#fff;border:1px solid #d4dae2;border-radius:9px;box-shadow:0 1px 4px rgba(20,30,50,.05);overflow:hidden}
|
|
.tbl.bulk{border:2px solid #1f5fdb;box-shadow:0 3px 12px rgba(31,95,219,.12)}
|
|
.tbl-h{background:#16181d;color:#fff;padding:9px 13px;font-family:"IBM Plex Mono",monospace;font-size:12px;font-weight:600;display:flex;justify-content:space-between;gap:8px}
|
|
.tbl.bulk .tbl-h{background:#1f5fdb}
|
|
.tbl-h .sub2{color:#7e8794;font-weight:400}
|
|
.tbl.bulk .tbl-h .sub2{color:#bcd2f7}
|
|
.tr{display:flex;justify-content:space-between;gap:8px;padding:6px 13px;font-size:11.5px;color:#3d424b;align-items:center}
|
|
.tr .f{font-family:"IBM Plex Mono",monospace}
|
|
.tr .t{font-family:"IBM Plex Mono",monospace;font-weight:600;font-size:10.5px;white-space:nowrap}
|
|
.tr.pk{background:#eaf1fe}.tr.pk .t{color:#1f5fdb}
|
|
.tr.fk{background:#fbf2e8}.tr.fk .t{color:#c08428}
|
|
.tr.ref .t{color:#9aa1ab}
|
|
.tr.idx{background:#fbe9ec}.tr.idx .t{color:#b23b4e}
|
|
.tr .ty{color:#a4abb4}
|
|
.notecard{position:absolute;background:#f7f8fa;border:1px dashed #c9cfd8;border-radius:9px;padding:12px 14px;font-size:11.5px;color:#5b6472;line-height:1.55}
|
|
.notecard b{font-family:"IBM Plex Mono",monospace;font-size:11px;color:#3d424b}
|
|
|
|
.hbars{display:flex;flex-direction:column;gap:12px;margin-top:6px}
|
|
.hbar{display:flex;align-items:center;gap:14px}
|
|
.hbar .lab{width:250px;flex:none;font-size:12.5px;color:#3d424b}
|
|
.hbar .lab small{display:block;color:#a4abb4;font-size:11px}
|
|
.hbar .trk{flex:1;background:#f1f3f6;border-radius:6px;overflow:hidden}
|
|
.hbar .fill{height:14px;border-radius:6px}
|
|
.hbar .val{font-family:"IBM Plex Mono",monospace;font-size:12.5px;color:#16181d;width:120px;flex:none;text-align:right}
|
|
.col-a{background:#9aa6b6}.col-b{background:#1f5fdb}.col-c{background:#c0476b}
|
|
.legend{display:flex;gap:16px;flex-wrap:wrap;margin:2px 0 10px}
|
|
.legend span{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:#5b6472}
|
|
.legend i{width:10px;height:10px;border-radius:3px;display:inline-block}
|
|
|
|
.callout{border-left:3px solid #1f5fdb;background:#f5f8fe;border-radius:0 9px 9px 0;padding:12px 16px;font-size:13px;color:#2b3038;line-height:1.6;margin:12px 0;max-width:880px}
|
|
.callout.warn{border-left-color:#b23b4e;background:#fdf3f5}
|
|
.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600}
|
|
.pill.rec{color:#0f7a45;background:#e7f6ee}
|
|
.pill.base{color:#5b6472;background:#eef0f2}
|
|
.pill.costly{color:#b23b4e;background:#fbe9ec}
|
|
.foot{font-size:11.5px;color:#a4abb4;border-top:1px solid #e6e8ec;padding-top:14px;margin-top:30px;line-height:1.7}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
|
|
<header class="top">
|
|
<div class="logo">FK</div>
|
|
<div class="htitle">
|
|
<b>Foreign-Key Architecture Study</b>
|
|
<span>Tribology lab data storage · bulk-table key design</span>
|
|
</div>
|
|
<div class="hright">
|
|
<span class="chip">SQLite · 4.32M rows measured</span>
|
|
<span class="chip-ok"><i></i>Measured 2026-07-11</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="frame">
|
|
|
|
<nav class="side" id="sidenav">
|
|
<div class="navcap">NAVIGATE</div>
|
|
<a href="#overview" class="active"><span class="tag">OV</span><span>Overview</span></a>
|
|
<a href="#dataset"><span class="tag">DS</span><span>Sample Dataset</span></a>
|
|
<a href="#variant-a"><span class="tag">A</span><span>Variant A · Composite PK</span></a>
|
|
<a href="#variant-b"><span class="tag">B</span><span>Variant B · Enforced FK</span></a>
|
|
<a href="#variant-c"><span class="tag">C</span><span>Variant C · Surrogate id</span></a>
|
|
<a href="#method"><span class="tag">BM</span><span>Benchmark Method</span></a>
|
|
<a href="#storage"><span class="tag">ST</span><span>Storage & Load</span></a>
|
|
<a href="#reads"><span class="tag">RD</span><span>Read Performance</span></a>
|
|
<a href="#analysis"><span class="tag">AN</span><span>Why It Differs</span></a>
|
|
<a href="#verdict"><span class="tag">RC</span><span>Recommendation</span></a>
|
|
<div class="sidefoot">
|
|
<div class="cap">CONTEXT</div>
|
|
<div class="txt">Key-schema study for the bulk measurement tables of the LabDataStorageEvaluation pipeline (SQLite / PostgreSQL benchmark targets).</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main id="main">
|
|
|
|
<!-- ================= OVERVIEW ================= -->
|
|
<section id="overview">
|
|
<h1>Overview</h1>
|
|
<p class="sub">Three candidate key schemas for the two largest measurement tables (<span class="mono">friction_cycles</span>, 1.44M rows and <span class="mono">friction_loop_points</span>, 2.88M rows) were built from the same simulated corpus and measured for disk footprint, load time and six read patterns. In every variant the <b>relations between tables are single-column integer foreign keys</b> (<span class="mono">track_id</span>, <span class="mono">coupon_id</span>); the variants differ only in how a bulk row is identified inside its own table.</p>
|
|
|
|
<div class="kpis">
|
|
<div class="kpi"><div class="l">Disk, variant C vs A</div><div class="v v-bad">+74%</div><div class="s">190.2 vs 109.1 MiB</div></div>
|
|
<div class="kpi"><div class="l">GROUP BY scan, C vs A</div><div class="v v-bad">+59%</div><div class="s">135.9 vs 85.7 ms (Q5 pattern)</div></div>
|
|
<div class="kpi"><div class="l">Enforced FK cost (B)</div><div class="v v-good">+0.5 s</div><div class="s">load only; disk and reads = A</div></div>
|
|
<div class="kpi"><div class="l">Point lookups</div><div class="v v-neutral">±2%</div><div class="s">identical across A / B / C</div></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>The three variants</h3>
|
|
<ul>
|
|
<li><b>Variant A - composite natural PK.</b> Row identity of a bulk row is its natural composite key <span class="mono">(track_id, cycle)</span>, stored as a clustered <span class="mono">WITHOUT ROWID</span> table. FK columns are plain integers, validated by the loader against the corpus manifest (no engine constraint).</li>
|
|
<li><b>Variant B - composite natural PK + enforced FK.</b> Same physical layout as A, plus <span class="mono">REFERENCES</span> constraints with <span class="mono">PRAGMA foreign_keys=ON</span>, so the engine itself guarantees referential integrity.</li>
|
|
<li><b>Variant C - surrogate key on every table.</b> Classic style: each table gets a single-column auto-increment key named <span class="mono"><singular>_id</span> (<span class="mono">friction_cycle_id</span>, never a bare <span class="mono">id</span>). Correctness then requires an additional <span class="mono">UNIQUE(track_id, cycle)</span> index - a second B-tree.</li>
|
|
</ul>
|
|
<div class="callout">Referential integrity is <b>not</b> what differs between the variants: every relation is a single integer FK in all three. What differs is the number of B-trees the engine must store and traverse per bulk table: one (A, B) or two (C).</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= DATASET ================= -->
|
|
<section id="dataset">
|
|
<h2>Sample Dataset</h2>
|
|
<p class="sub">All measurements ran against the real generated corpus of the evaluation pipeline - deterministic, physically plausible tribology data (seed 20260711). The two bulk tables and their parent dimension were loaded in full.</p>
|
|
<div class="card" style="padding:0;overflow:hidden">
|
|
<table class="data">
|
|
<thead><tr><th>Table</th><th>Grain</th><th class="num">Rows loaded</th><th>Columns (beyond keys)</th><th>Source files</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="mono">tracks</td><td>one friction track (3 per tested coupon)</td><td class="num">1,440</td><td>track_code, coupon_id, run_id, environment, load_mN, ...</td><td class="mono">track_info.csv × 1,440</td></tr>
|
|
<tr><td class="mono">friction_cycles</td><td>one reciprocating cycle of one track</td><td class="num">1,440,000</td><td>cof</td><td class="mono">cof_vs_cycle.csv × 1,440</td></tr>
|
|
<tr><td class="mono">friction_loop_points</td><td>one position-resolved loop point (10 loops × 200 pts per track)</td><td class="num">2,880,000</td><td>position_um, friction_force_mN</td><td class="mono">friction_loops.csv × 1,440</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Corpus context</h3>
|
|
<p>The corpus models a Pt-Au coating study: 4 deposition batches → 12 wafers → 588 coupons (480 friction-tested) → 1,440 tracks → 1.44M cycle records. Generated size: 141.8 MiB of CSV; the production target for extrapolation is 600 GB - 6 TB, which is why per-row bytes on the bulk tables matter.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= VARIANT A ================= -->
|
|
<section id="variant-a">
|
|
<h2>Variant A · Composite Natural Primary Key <span class="pill base" style="vertical-align:2px">baseline</span></h2>
|
|
<p class="sub">The bulk row is identified by what it physically is: cycle <span class="mono">n</span> of track <span class="mono">t</span>. The composite PK doubles as the clustering order, so the table itself is the only B-tree. FK columns are unenforced integers; integrity is guaranteed by row-count and checksum validation against the corpus manifest.</p>
|
|
|
|
<div class="card">
|
|
<div class="erd-wrap"><div class="erd" style="width:1000px;height:440px">
|
|
<svg class="wire" viewBox="0 0 1000 440" width="1000" height="440">
|
|
<path d="M270 200 H360 V77 H450" stroke="#c5cdd8" stroke-width="1.5" fill="none"/>
|
|
<path d="M270 240 H360 V269 H450" stroke="#c5cdd8" stroke-width="1.5" fill="none"/>
|
|
<circle cx="270" cy="200" r="3.5" fill="#1f5fdb"/><circle cx="270" cy="240" r="3.5" fill="#1f5fdb"/>
|
|
<text x="280" y="193" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1f5fdb">1</text>
|
|
<text x="280" y="233" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1f5fdb">1</text>
|
|
<text x="432" y="70" font-family="IBM Plex Mono, monospace" font-size="11" fill="#9aa1ab">∞</text>
|
|
<text x="432" y="262" font-family="IBM Plex Mono, monospace" font-size="11" fill="#9aa1ab">∞</text>
|
|
</svg>
|
|
<div class="tbl" style="left:20px;top:120px;width:250px">
|
|
<div class="tbl-h"><span>tracks</span><span class="sub2">dimension</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">track_code</span><span class="t" style="color:#1f5fdb">UK</span></div>
|
|
<div class="tr fk"><span class="f">coupon_id</span><span class="t">FK</span></div>
|
|
<div class="tr fk"><span class="f">run_id</span><span class="t">FK</span></div>
|
|
<div class="tr"><span class="f">environment</span><span class="ty">text</span></div>
|
|
<div class="tr"><span class="f">load_mn</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="tbl bulk" style="left:450px;top:20px;width:280px">
|
|
<div class="tbl-h"><span>friction_cycles</span><span class="sub2">1.44M rows</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK · ref</span></div>
|
|
<div class="tr pk"><span class="f">cycle</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">cof</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="tbl bulk" style="left:450px;top:185px;width:280px">
|
|
<div class="tbl-h"><span>friction_loop_points</span><span class="sub2">2.88M rows</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK · ref</span></div>
|
|
<div class="tr pk"><span class="f">cycle</span><span class="t">PK</span></div>
|
|
<div class="tr pk"><span class="f">pt</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">position_um</span><span class="ty">real</span></div>
|
|
<div class="tr"><span class="f">friction_force_mn</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="notecard" style="left:770px;top:130px;width:210px">
|
|
<b>wear</b> and <b>track_summary</b> follow the same pattern: single-column PK = FK <b>track_id</b> (1:1 with tracks).<br><br>
|
|
<b>ref</b> = integer reference to tracks(track_id), validated by the loader, no engine constraint.
|
|
</div>
|
|
</div></div>
|
|
<pre class="code">CREATE TABLE friction_cycles (
|
|
track_id INTEGER NOT NULL, <span class="c">-- reference to tracks(track_id), loader-validated</span>
|
|
cycle INTEGER NOT NULL,
|
|
cof REAL NOT NULL,
|
|
<span class="k">PRIMARY KEY (track_id, cycle)</span>
|
|
) <span class="k">WITHOUT ROWID</span>; <span class="c">-- clustered: the PK is the table's only B-tree</span></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= VARIANT B ================= -->
|
|
<section id="variant-b">
|
|
<h2>Variant B · Composite PK + Enforced Foreign Keys <span class="pill rec" style="vertical-align:2px">recommended</span></h2>
|
|
<p class="sub">Physically identical to Variant A - same clustered layout, same single B-tree per bulk table. The only change: FK columns carry real <span class="mono">REFERENCES</span> constraints and every connection runs <span class="mono">PRAGMA foreign_keys=ON</span>, so the DBMS itself rejects an orphan row. Measured cost: +0.5 s of load time on 4.32M rows; zero cost on disk and reads.</p>
|
|
|
|
<div class="card">
|
|
<div class="erd-wrap"><div class="erd" style="width:1000px;height:440px">
|
|
<svg class="wire" viewBox="0 0 1000 440" width="1000" height="440">
|
|
<path d="M270 200 H360 V77 H450" stroke="#c5cdd8" stroke-width="1.5" fill="none"/>
|
|
<path d="M270 240 H360 V269 H450" stroke="#c5cdd8" stroke-width="1.5" fill="none"/>
|
|
<circle cx="270" cy="200" r="3.5" fill="#1f5fdb"/><circle cx="270" cy="240" r="3.5" fill="#1f5fdb"/>
|
|
<text x="280" y="193" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1f5fdb">1</text>
|
|
<text x="280" y="233" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1f5fdb">1</text>
|
|
<text x="432" y="70" font-family="IBM Plex Mono, monospace" font-size="11" fill="#9aa1ab">∞</text>
|
|
<text x="432" y="262" font-family="IBM Plex Mono, monospace" font-size="11" fill="#9aa1ab">∞</text>
|
|
</svg>
|
|
<div class="tbl" style="left:20px;top:120px;width:250px">
|
|
<div class="tbl-h"><span>tracks</span><span class="sub2">dimension</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">track_code</span><span class="t" style="color:#1f5fdb">UK</span></div>
|
|
<div class="tr fk"><span class="f">coupon_id</span><span class="t">FK</span></div>
|
|
<div class="tr fk"><span class="f">run_id</span><span class="t">FK</span></div>
|
|
<div class="tr"><span class="f">environment</span><span class="ty">text</span></div>
|
|
<div class="tr"><span class="f">load_mn</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="tbl bulk" style="left:450px;top:20px;width:280px">
|
|
<div class="tbl-h"><span>friction_cycles</span><span class="sub2">1.44M rows</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK</span></div>
|
|
<div class="tr fk"><span class="f">track_id → tracks</span><span class="t">FK ✓</span></div>
|
|
<div class="tr pk"><span class="f">cycle</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">cof</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="tbl bulk" style="left:450px;top:190px;width:280px">
|
|
<div class="tbl-h"><span>friction_loop_points</span><span class="sub2">2.88M rows</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK</span></div>
|
|
<div class="tr fk"><span class="f">track_id → tracks</span><span class="t">FK ✓</span></div>
|
|
<div class="tr pk"><span class="f">cycle</span><span class="t">PK</span></div>
|
|
<div class="tr pk"><span class="f">pt</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">position_um, friction_force_mn</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="notecard" style="left:770px;top:130px;width:210px">
|
|
<b>PRAGMA foreign_keys = ON</b> on every connection - SQLite silently ignores FK clauses without it.<br><br>
|
|
FK checks are lookups into the already-indexed parent PK, which is why the measured overhead is only ~6% of load time.
|
|
</div>
|
|
</div></div>
|
|
<pre class="code">PRAGMA foreign_keys = ON;
|
|
|
|
CREATE TABLE friction_cycles (
|
|
track_id INTEGER NOT NULL <span class="k">REFERENCES tracks(track_id)</span>,
|
|
cycle INTEGER NOT NULL,
|
|
cof REAL NOT NULL,
|
|
<span class="k">PRIMARY KEY (track_id, cycle)</span>
|
|
) <span class="k">WITHOUT ROWID</span>;</pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= VARIANT C ================= -->
|
|
<section id="variant-c">
|
|
<h2>Variant C · Surrogate <span class="mono" style="font-size:16px"><singular>_id</span> on Every Table <span class="pill costly" style="vertical-align:2px">+74% disk</span></h2>
|
|
<p class="sub">The uniform classic design: every table, including the bulk fact tables, gets a single-column auto-increment key (<span class="mono">friction_cycle_id</span>, <span class="mono">friction_loop_point_id</span> - by convention always <span class="mono"><table name in singular>_id</span>, never a bare <span class="mono">id</span>). In SQLite the id column itself is nearly free (it aliases the rowid), but correctness still demands <span class="mono">UNIQUE(track_id, cycle)</span> - and that unique index is a second B-tree holding a copy of almost every column.</p>
|
|
|
|
<div class="card">
|
|
<div class="erd-wrap"><div class="erd" style="width:1000px;height:520px">
|
|
<svg class="wire" viewBox="0 0 1000 520" width="1000" height="520">
|
|
<path d="M270 240 H360 V100 H450" stroke="#c5cdd8" stroke-width="1.5" fill="none"/>
|
|
<path d="M270 280 H360 V345 H450" stroke="#c5cdd8" stroke-width="1.5" fill="none"/>
|
|
<circle cx="270" cy="240" r="3.5" fill="#1f5fdb"/><circle cx="270" cy="280" r="3.5" fill="#1f5fdb"/>
|
|
<text x="280" y="233" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1f5fdb">1</text>
|
|
<text x="280" y="273" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1f5fdb">1</text>
|
|
<text x="432" y="93" font-family="IBM Plex Mono, monospace" font-size="11" fill="#9aa1ab">∞</text>
|
|
<text x="432" y="338" font-family="IBM Plex Mono, monospace" font-size="11" fill="#9aa1ab">∞</text>
|
|
</svg>
|
|
<div class="tbl" style="left:20px;top:160px;width:250px">
|
|
<div class="tbl-h"><span>tracks</span><span class="sub2">dimension</span></div>
|
|
<div class="tr pk"><span class="f">track_id</span><span class="t">PK</span></div>
|
|
<div class="tr"><span class="f">track_code</span><span class="t" style="color:#1f5fdb">UK</span></div>
|
|
<div class="tr fk"><span class="f">coupon_id</span><span class="t">FK</span></div>
|
|
<div class="tr fk"><span class="f">run_id</span><span class="t">FK</span></div>
|
|
<div class="tr"><span class="f">environment</span><span class="ty">text</span></div>
|
|
<div class="tr"><span class="f">load_mn</span><span class="ty">real</span></div>
|
|
</div>
|
|
<div class="tbl bulk" style="left:450px;top:20px;width:280px">
|
|
<div class="tbl-h"><span>friction_cycles</span><span class="sub2">1.44M rows</span></div>
|
|
<div class="tr pk"><span class="f">friction_cycle_id</span><span class="t">PK</span></div>
|
|
<div class="tr fk"><span class="f">track_id → tracks</span><span class="t">FK ✓</span></div>
|
|
<div class="tr"><span class="f">cycle</span><span class="ty">int</span></div>
|
|
<div class="tr"><span class="f">cof</span><span class="ty">real</span></div>
|
|
<div class="tr idx"><span class="f">UNIQUE (track_id, cycle)</span><span class="t">2nd B-tree</span></div>
|
|
</div>
|
|
<div class="tbl bulk" style="left:450px;top:235px;width:280px">
|
|
<div class="tbl-h"><span>friction_loop_points</span><span class="sub2">2.88M rows</span></div>
|
|
<div class="tr pk"><span class="f">friction_loop_point_id</span><span class="t">PK</span></div>
|
|
<div class="tr fk"><span class="f">track_id → tracks</span><span class="t">FK ✓</span></div>
|
|
<div class="tr"><span class="f">cycle</span><span class="ty">int</span></div>
|
|
<div class="tr"><span class="f">pt</span><span class="ty">int</span></div>
|
|
<div class="tr"><span class="f">position_um</span><span class="ty">real</span></div>
|
|
<div class="tr"><span class="f">friction_force_mn</span><span class="ty">real</span></div>
|
|
<div class="tr idx"><span class="f">UNIQUE (track_id, cycle, pt)</span><span class="t">2nd B-tree</span></div>
|
|
</div>
|
|
<div class="notecard" style="left:770px;top:180px;width:210px">
|
|
The surrogate key is <b>never referenced</b> by any other table - no row in the schema points at an individual cycle.<br><br>
|
|
The UNIQUE index is <b>mandatory for correctness</b> (it prevents duplicate cycles), so its cost cannot be avoided in this variant.
|
|
</div>
|
|
</div></div>
|
|
<pre class="code">CREATE TABLE friction_cycles (
|
|
<span class="hl">friction_cycle_id INTEGER PRIMARY KEY</span>, <span class="c">-- rowid alias; unused by any FK</span>
|
|
track_id INTEGER NOT NULL REFERENCES tracks(track_id),
|
|
cycle INTEGER NOT NULL,
|
|
cof REAL NOT NULL,
|
|
<span class="hl">UNIQUE (track_id, cycle)</span> <span class="c">-- required; duplicates the table</span>
|
|
);</pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= METHOD ================= -->
|
|
<section id="method">
|
|
<h2>Benchmark Method</h2>
|
|
<p class="sub">Identical protocol for all three variants; only the DDL differs.</p>
|
|
<div class="card">
|
|
<ul>
|
|
<li><b>Data:</b> the full generated corpus subset - <span class="mono">tracks</span> (1,440), <span class="mono">friction_cycles</span> (1,440,000), <span class="mono">friction_loop_points</span> (2,880,000) - read from the same CSV files in the same order for every variant.</li>
|
|
<li><b>Load:</b> SQLite, <span class="mono">PRAGMA journal_mode=OFF, synchronous=OFF, cache_size=-262144</span>; inserts via <span class="mono">executemany</span> in 50k-row batches; <span class="mono">VACUUM</span> before measuring the file size.</li>
|
|
<li><b>Reads:</b> warm OS page cache, best of 3 runs per pattern; fixed deterministic probe sets (39 tracks for range reads, 150 <span class="mono">(track_id, cycle)</span> pairs for point lookups); result-set sizes asserted, so every variant answers the identical question.</li>
|
|
<li><b>Caveat:</b> at 150 MB everything fits in RAM, which <i>flatters</i> variant C - at the 600 GB production target the working set exceeds RAM, and twice the pages means twice the cache pressure and disk reads. Warm-cache deltas are therefore a lower bound.</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= STORAGE ================= -->
|
|
<section id="storage">
|
|
<h2>Results · Storage & Load</h2>
|
|
<p class="sub">File size after VACUUM; load time includes CSV parsing (identical work in all variants).</p>
|
|
|
|
<div class="card" style="padding:0;overflow:hidden">
|
|
<table class="data">
|
|
<thead><tr><th>Variant</th><th>Bulk-table keys</th><th class="num">Load time (s)</th><th class="num">DB size (MiB)</th><th class="num">Size vs A</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><b>A</b> · composite PK</td><td>PK (track_id, cycle), unenforced refs</td><td class="num">7.5</td><td class="num">109.1</td><td class="num delta-zero">baseline</td></tr>
|
|
<tr><td><b>B</b> · composite PK + FK</td><td>PK (track_id, cycle), REFERENCES enforced</td><td class="num">8.0</td><td class="num">109.1</td><td class="num delta-ok">±0%</td></tr>
|
|
<tr><td><b>C</b> · surrogate id</td><td>friction_cycle_id PK + UNIQUE(track_id, cycle)</td><td class="num">8.8</td><td class="num">190.2</td><td class="num delta-bad">+74%</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Database size after VACUUM (MiB, scale 0-200)</h3>
|
|
<div class="hbars">
|
|
<div class="hbar"><span class="lab">Variant A<small>composite PK, unenforced refs</small></span><div class="trk"><div class="fill col-a" style="width:54.6%"></div></div><span class="val">109.1 MiB</span></div>
|
|
<div class="hbar"><span class="lab">Variant B<small>composite PK + enforced FK</small></span><div class="trk"><div class="fill col-b" style="width:54.6%"></div></div><span class="val">109.1 MiB</span></div>
|
|
<div class="hbar"><span class="lab">Variant C<small>surrogate id + UNIQUE index</small></span><div class="trk"><div class="fill col-c" style="width:95.1%"></div></div><span class="val">190.2 MiB</span></div>
|
|
</div>
|
|
<h3 style="margin-top:20px">Load time, 4.32M rows (s, scale 0-10)</h3>
|
|
<div class="hbars">
|
|
<div class="hbar"><span class="lab">Variant A</span><div class="trk"><div class="fill col-a" style="width:75%"></div></div><span class="val">7.5 s</span></div>
|
|
<div class="hbar"><span class="lab">Variant B</span><div class="trk"><div class="fill col-b" style="width:80%"></div></div><span class="val">8.0 s</span></div>
|
|
<div class="hbar"><span class="lab">Variant C</span><div class="trk"><div class="fill col-c" style="width:88%"></div></div><span class="val">8.8 s</span></div>
|
|
</div>
|
|
<div class="callout" style="margin-top:16px">Extrapolated to the 600 GB production target, the +74% of variant C on the two hottest tables translates into <b>hundreds of gigabytes of extra disk</b> and a doubled page count competing for the same RAM cache.</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= READS ================= -->
|
|
<section id="reads">
|
|
<h2>Results · Read Performance</h2>
|
|
<p class="sub">Warm OS cache, best of 3 runs. Q1 / Q5 refer to the benchmark scenarios of the evaluation pipeline.</p>
|
|
|
|
<div class="card" style="padding:0;overflow:hidden">
|
|
<table class="data">
|
|
<thead><tr><th>Read pattern</th><th>Unit</th><th class="num">A</th><th class="num">B</th><th class="num">C</th><th class="num">C vs A</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>Point lookup of one row by <span class="mono">(track_id, cycle)</span></td><td class="mono">µs/op</td><td class="num">24.8</td><td class="num">23.6</td><td class="num">24.3</td><td class="num delta-zero">~0%</td></tr>
|
|
<tr><td>Q1: all 1,000 cycles of one track</td><td class="mono">ms/track</td><td class="num">0.473</td><td class="num">0.485</td><td class="num">0.533</td><td class="num delta-bad">+13%</td></tr>
|
|
<tr><td>All 2,000 loop points of one track</td><td class="mono">ms/track</td><td class="num">1.267</td><td class="num">1.299</td><td class="num">1.336</td><td class="num delta-bad">+5%</td></tr>
|
|
<tr><td>Full scan + AVG over 1.44M cycles</td><td class="mono">ms</td><td class="num">64.9</td><td class="num">64.9</td><td class="num">70.7</td><td class="num delta-bad">+9%</td></tr>
|
|
<tr><td>AVG with GROUP BY track_id over 1.44M cycles (Q5 pattern)</td><td class="mono">ms</td><td class="num">85.7</td><td class="num">89.1</td><td class="num">135.9</td><td class="num delta-bad">+59%</td></tr>
|
|
<tr><td>Full scan over 2.88M loop points</td><td class="mono">ms</td><td class="num">152.5</td><td class="num">150.8</td><td class="num">165.4</td><td class="num delta-bad">+8%</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Relative read time, variant A = 100% (lower is better)</h3>
|
|
<div class="legend">
|
|
<span><i class="col-a"></i>A · composite PK</span>
|
|
<span><i class="col-b"></i>B · + enforced FK</span>
|
|
<span><i class="col-c"></i>C · surrogate id + UNIQUE</span>
|
|
</div>
|
|
<svg viewBox="0 0 1080 300" style="width:100%;height:auto;display:block">
|
|
<line x1="60" y1="246" x2="1080" y2="246" stroke="#d8dce1" stroke-width="1"/>
|
|
<line x1="60" y1="183.2" x2="1080" y2="183.2" stroke="#eef0f2" stroke-width="1"/>
|
|
<line x1="60" y1="120.4" x2="1080" y2="120.4" stroke="#c9d4e6" stroke-width="1" stroke-dasharray="4 3"/>
|
|
<line x1="60" y1="57.7" x2="1080" y2="57.7" stroke="#eef0f2" stroke-width="1"/>
|
|
<text x="54" y="246" text-anchor="end" dominant-baseline="middle" font-family="IBM Plex Mono, monospace" font-size="9" fill="#a4abb4">0%</text>
|
|
<text x="54" y="183.2" text-anchor="end" dominant-baseline="middle" font-family="IBM Plex Mono, monospace" font-size="9" fill="#a4abb4">50%</text>
|
|
<text x="54" y="120.4" text-anchor="end" dominant-baseline="middle" font-family="IBM Plex Mono, monospace" font-size="9" fill="#7e8ba1">100%</text>
|
|
<text x="54" y="57.7" text-anchor="end" dominant-baseline="middle" font-family="IBM Plex Mono, monospace" font-size="9" fill="#a4abb4">150%</text>
|
|
|
|
<!-- group 0: point lookup -->
|
|
<rect x="78" y="120.4" width="38" height="125.6" rx="2.5" fill="#9aa6b6"/>
|
|
<rect x="124" y="126.5" width="38" height="119.5" rx="2.5" fill="#1f5fdb"/>
|
|
<rect x="170" y="123.0" width="38" height="123.0" rx="2.5" fill="#c0476b"/>
|
|
<text x="143" y="262" text-anchor="middle" font-size="10" fill="#5b6472">Point lookup</text>
|
|
<!-- group 1: Q1 cycles -->
|
|
<rect x="245" y="120.4" width="38" height="125.6" rx="2.5" fill="#9aa6b6"/>
|
|
<rect x="291" y="117.3" width="38" height="128.7" rx="2.5" fill="#1f5fdb"/>
|
|
<rect x="337" y="104.5" width="38" height="141.5" rx="2.5" fill="#c0476b"/>
|
|
<text x="310" y="262" text-anchor="middle" font-size="10" fill="#5b6472">Q1: track cycles</text>
|
|
<!-- group 2: loop points range -->
|
|
<rect x="411" y="120.4" width="38" height="125.6" rx="2.5" fill="#9aa6b6"/>
|
|
<rect x="457" y="117.3" width="38" height="128.7" rx="2.5" fill="#1f5fdb"/>
|
|
<rect x="503" y="113.7" width="38" height="132.3" rx="2.5" fill="#c0476b"/>
|
|
<text x="476" y="262" text-anchor="middle" font-size="10" fill="#5b6472">Track loop points</text>
|
|
<!-- group 3: full scan avg -->
|
|
<rect x="578" y="120.4" width="38" height="125.6" rx="2.5" fill="#9aa6b6"/>
|
|
<rect x="624" y="120.4" width="38" height="125.6" rx="2.5" fill="#1f5fdb"/>
|
|
<rect x="670" y="109.3" width="38" height="136.7" rx="2.5" fill="#c0476b"/>
|
|
<text x="643" y="262" text-anchor="middle" font-size="10" fill="#5b6472">Full scan + AVG</text>
|
|
<!-- group 4: group by -->
|
|
<rect x="745" y="120.4" width="38" height="125.6" rx="2.5" fill="#9aa6b6"/>
|
|
<rect x="791" y="115.4" width="38" height="130.6" rx="2.5" fill="#1f5fdb"/>
|
|
<rect x="837" y="46.9" width="38" height="199.1" rx="2.5" fill="#c0476b"/>
|
|
<text x="856" y="40" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="10" font-weight="600" fill="#b23b4e">+59%</text>
|
|
<text x="810" y="262" text-anchor="middle" font-size="10" fill="#5b6472">GROUP BY track (Q5)</text>
|
|
<!-- group 5: loops scan -->
|
|
<rect x="911" y="120.4" width="38" height="125.6" rx="2.5" fill="#9aa6b6"/>
|
|
<rect x="957" y="121.8" width="38" height="124.2" rx="2.5" fill="#1f5fdb"/>
|
|
<rect x="1003" y="109.8" width="38" height="136.2" rx="2.5" fill="#c0476b"/>
|
|
<text x="976" y="262" text-anchor="middle" font-size="10" fill="#5b6472">Loop points scan</text>
|
|
|
|
<text x="570" y="290" text-anchor="middle" font-size="10" fill="#838a95">dashed line = variant A baseline (100%)</text>
|
|
</svg>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= ANALYSIS ================= -->
|
|
<section id="analysis">
|
|
<h2>Why It Differs</h2>
|
|
<p class="sub">The whole difference reduces to one thing: how many B-trees per bulk table, and in what order the rows physically live.</p>
|
|
|
|
<div class="card">
|
|
<div class="erd-wrap"><div class="erd" style="width:1000px;height:230px">
|
|
<svg class="wire" viewBox="0 0 1000 230" width="1000" height="230">
|
|
<path d="M700 115 H760" stroke="#b23b4e" stroke-width="1.6" fill="none" stroke-dasharray="5 4"/>
|
|
<text x="730" y="105" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="10" fill="#b23b4e">extra hop</text>
|
|
</svg>
|
|
<div class="tbl" style="left:20px;top:30px;width:380px;border-color:#0f9d8a">
|
|
<div class="tbl-h" style="background:#0f9d8a"><span>Variants A / B · one B-tree</span></div>
|
|
<div class="tr"><span class="f">clustered table keyed by (track_id, cycle)</span></div>
|
|
<div class="tr"><span class="f">descent → leaf → sequential range scan</span></div>
|
|
<div class="tr"><span class="f muted">rows of one track are physically adjacent</span></div>
|
|
</div>
|
|
<div class="tbl" style="left:450px;top:10px;width:250px;border-color:#c0476b">
|
|
<div class="tbl-h" style="background:#c0476b"><span>Variant C · B-tree 1</span></div>
|
|
<div class="tr"><span class="f">UNIQUE index (track_id, cycle)</span></div>
|
|
<div class="tr"><span class="f muted">stores key columns + rowid again</span></div>
|
|
</div>
|
|
<div class="tbl" style="left:760px;top:80px;width:220px;border-color:#c0476b">
|
|
<div class="tbl-h" style="background:#c0476b"><span>Variant C · B-tree 2</span></div>
|
|
<div class="tr"><span class="f">table keyed by friction_cycle_id</span></div>
|
|
<div class="tr"><span class="f muted">per-row lookup to fetch cof</span></div>
|
|
</div>
|
|
</div></div>
|
|
<ul>
|
|
<li><b>+74% disk.</b> In SQLite the surrogate id column is a rowid alias and costs nothing in the table itself - but the mandatory <span class="mono">UNIQUE(track_id, cycle)</span> index is a full second B-tree whose entries contain almost every column of the row. Two trees instead of one on a 3-column table is close to a doubling.</li>
|
|
<li><b>+59% on grouped aggregation (the pipeline's forced-full-scan Q5).</b> In A/B the table is clustered by <span class="mono">track_id</span>, so <span class="mono">GROUP BY track_id</span> streams the tree in group order. In C the engine walks the UNIQUE index for ordering and hops into the table B-tree for every <span class="mono">cof</span> value - the classic double access path.</li>
|
|
<li><b>+5-13% on ranges and plain scans.</b> More pages (190 vs 109 MiB) means more page traversals and cell decoding, even fully cached.</li>
|
|
<li><b>~0% on point lookups.</b> Tree depth is the same either way; a single-row probe does not expose the second tree.</li>
|
|
<li><b>PostgreSQL outlook.</b> PG has no WITHOUT ROWID: the composite-PK table is a heap plus one PK index either way, but variant C still adds the id column to every heap tuple <i>and</i> the second index - expect +30-50% on the bulk tables, plus higher COPY-time FK validation cost.</li>
|
|
</ul>
|
|
<div class="callout warn"><b>Integrity is a constant across all variants.</b> Every relation is a single-column integer FK. Choosing C buys naming uniformity, not more integrity - and the surrogate key it adds is never referenced by anything.</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================= RECOMMENDATION ================= -->
|
|
<section id="verdict">
|
|
<h2>Recommendation</h2>
|
|
<div class="card" style="border-left:4px solid #0f7a45">
|
|
<h3><span class="pill rec">Variant B</span> Composite natural PK on bulk tables + enforced foreign keys</h3>
|
|
<ul>
|
|
<li>Referential integrity is guaranteed by the DBMS itself at a measured cost of <b>+0.5 s of load time</b> and nothing else.</li>
|
|
<li>Disk footprint and read profile are identical to the leanest variant A: <b>109.1 MiB</b>, single clustered B-tree per bulk table.</li>
|
|
<li>Dimension tables (<span class="mono">batches</span>, <span class="mono">wafers</span>, <span class="mono">coupons</span>, <span class="mono">runs</span>, <span class="mono">tracks</span>, <span class="mono">instruments</span>) keep their classic single-column surrogate keys - <span class="mono"><singular>_id</span>, never a bare <span class="mono">id</span> - because other tables reference their rows.</li>
|
|
<li>Variant C remains a valid design where fact rows must be individually addressable from outside (annotations, reprocessing links). No table in this schema references an individual cycle or loop point, so its cost (+74% disk, up to +59% on aggregation, growing at 600 GB scale) buys nothing here.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="foot">
|
|
Measured 2026-07-11 on the deterministic simulated corpus (seed 20260711, 141.8 MiB CSV) · SQLite, warm OS cache, best of 3 runs ·
|
|
scripts: <span class="mono">measure_key_variants.py</span> (build + size + load + Q1) and <span class="mono">read_benchmarks.py</span> (six read patterns) ·
|
|
companion page: <a href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_Storage_Evaluation.html" style="color:#1f5fdb;text-decoration:none">Storage Format Evaluation</a> (full study results).
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
var links = Array.prototype.slice.call(document.querySelectorAll('#sidenav a[href^="#"]'));
|
|
var byId = {};
|
|
links.forEach(function(a){ byId[a.getAttribute('href').slice(1)] = a; });
|
|
var sections = Array.prototype.slice.call(document.querySelectorAll('main section'));
|
|
var main = document.getElementById('main');
|
|
function setActive(id){
|
|
links.forEach(function(a){ a.classList.remove('active'); });
|
|
if(byId[id]) byId[id].classList.add('active');
|
|
}
|
|
links.forEach(function(a){
|
|
a.addEventListener('click', function(){ setActive(a.getAttribute('href').slice(1)); });
|
|
});
|
|
var ticking = false;
|
|
main.addEventListener('scroll', function(){
|
|
if(ticking) return; ticking = true;
|
|
requestAnimationFrame(function(){
|
|
var top = main.scrollTop + 90, current = sections[0].id;
|
|
sections.forEach(function(s){ if(s.offsetTop <= top) current = s.id; });
|
|
setActive(current);
|
|
ticking = false;
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|