Properties of a Relation
π Relation Names Must Be Unique
Every table in a database needs its own distinct name.
You can't have two separate tables both called
Studentin the same schema.
π Attribute Names Must Be Unique Within a Table
No two columns in the same table can share a name.
A
Studenttable can't have two different columns both labeledName.
π Tuples (Rows) Cannot Repeat
Every row in a table has to be distinct β no exact duplicates allowed.
This is typically enforced through a primary key.
β Example:
βοΈ (101, "Alice", 20)
βοΈ (102, "Bob", 21)
β (101, "Alice", 20) β Exact duplicate of the first row β not allowedπ Row Order Is Irrelevant
A table is a set of rows, not a list β there's no inherent "first" or "last."
Rearranging rows doesn't change what the relation represents.
π Column Order Is Irrelevant
Where a column sits doesn't affect the table's meaning.
(ID, Name, Age)describes the same relation as(Name, Age, ID).
π Every Value Must Be Atomic
Each cell can hold exactly one indivisible value β never a list or set.
This requirement is what defines First Normal Form (1NF).
β Not allowed: Subjects = {"Math", "Science"} β
Correct: Subject = "Math"
π A Column's Values Share One Domain
Every entry in a column must come from the same data type or value set.
An
Agecolumn should only ever hold integers like18or21β never text like"twenty".
π NULLs Are Permitted
A relation can store NULL when a value is unknown or doesn't apply.
That said, leaning on NULLs too heavily undermines data integrity β use them sparingly.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Properties of a Relation β Try It Yourself</title>
<style>
.infographic-relsandbox {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #16a34a;
--good-soft: #dcfce7;
--bad: #dc2626;
--bad-soft: #fee2e2;
--info: #a16207;
--info-soft: #fef3c7;
--display-font: Georgia, 'Iowan Old Style', serif;
--body-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--mono-font: 'SF Mono', Consolas, monospace;
background: var(--paper);
color: var(--ink);
font-family: var(--body-font);
max-width: 900px;
margin: 0 auto;
box-sizing: border-box;
overflow-x: hidden;
}
.infographic-relsandbox[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4ade80;
--good-soft: #123420;
--bad: #f87171;
--bad-soft: #3a1414;
--info: #fbbf24;
--info-soft: #3a2f0e;
}
.infographic-relsandbox * { box-sizing: border-box; }
.rs-head { text-align: center; margin-bottom: 0.25rem; }
.rs-head h2 {
font-family: var(--display-font);
font-size: 1.9rem;
margin: 0 0 0.5rem;
letter-spacing: -0.01em;
}
.rs-head p {
color: var(--ink-soft);
font-size: 1rem;
margin: 0 auto;
max-width: 560px;
line-height: 1.5;
}
.rs-frame {
background: var(--paper-alt);
border: 1px solid var(--line);
border-radius: 10px;
padding: 1.25rem;
margin-top: 1.75rem;
}
.rs-frame-title {
font-family: var(--display-font);
font-size: 1.05rem;
margin: 0 0 0.85rem;
}
.rs-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; min-width: 0; }
table.rs-table {
width: 100%;
min-width: 460px;
border-collapse: collapse;
background: var(--paper);
border-radius: 8px;
font-size: 0.9rem;
}
table.rs-table th, table.rs-table td {
border: 1px solid var(--line);
padding: 0.55rem 0.7rem;
text-align: left;
white-space: nowrap;
}
table.rs-table th { background: var(--accent); color: #fff; font-weight: 700; font-size: 0.82rem; }
table.rs-table td.is-null { color: var(--ink-soft); font-style: italic; }
table.rs-table tr.is-new td { animation: rs-row-in 0.5s ease; }
@keyframes rs-row-in { from { background: var(--accent-soft); } to { background: transparent; } }
.rs-row-controls {
display: flex;
gap: 0.5rem;
margin-top: 0.85rem;
flex-wrap: wrap;
}
.rs-btn {
font: inherit;
font-size: 0.82rem;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 7px;
padding: 0.45rem 0.85rem;
cursor: pointer;
color: var(--ink);
transition: border-color 0.15s;
}
.rs-btn:hover { border-color: var(--accent); }
.rs-form {
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px dashed var(--line);
}
.rs-form-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-soft); margin: 0 0 0.7rem; }
.rs-form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.6rem;
margin-bottom: 0.85rem;
}
.rs-field label { display: block; font-size: 0.75rem; color: var(--ink-soft); margin-bottom: 0.25rem; }
.rs-field input {
width: 100%;
font: inherit;
font-size: 0.88rem;
padding: 0.5rem 0.6rem;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--paper);
color: var(--ink);
}
.rs-field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.rs-add-btn {
font: inherit;
font-size: 0.88rem;
background: var(--accent);
color: #fff;
border: none;
border-radius: 7px;
padding: 0.6rem 1.3rem;
cursor: pointer;
}
.rs-add-btn:hover { opacity: 0.9; }
.rs-msg {
margin-top: 0.9rem;
padding: 0.7rem 0.9rem;
border-radius: 8px;
font-size: 0.85rem;
line-height: 1.5;
display: none;
}
.rs-msg.is-visible { display: block; }
.rs-msg.msg-bad { background: var(--bad-soft); color: var(--bad); }
.rs-msg.msg-good { background: var(--good-soft); color: var(--good); }
.rs-msg.msg-info { background: var(--info-soft); color: var(--info); }
.rs-extra-row {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.rs-rules {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
margin-top: 1.75rem;
}
.rs-rule-chip {
font-size: 0.72rem;
background: var(--paper-alt);
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.35rem 0.75rem;
color: var(--ink-soft);
transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.2s;
}
.rs-rule-chip.is-hit {
background: var(--accent);
color: #fff;
border-color: var(--accent);
transform: scale(1.08);
}
@media (prefers-reduced-motion: reduce) {
.infographic-relsandbox *, .infographic-relsandbox *::before, .infographic-relsandbox *::after {
transition-duration: 0.001ms !important;
animation-duration: 0.001ms !important;
}
}
</style>
</head>
<body>
<div class="infographic-relsandbox" id="relsandbox">
<div class="rs-head">
<h2>Properties of a Relation β Try It Yourself</h2>
<p>Add rows to the Student table below. Break a rule on purpose and see which one stops you.</p>
</div>
<div class="rs-frame">
<p class="rs-frame-title">Student</p>
<div class="rs-table-scroll">
<table class="rs-table" id="rsTable"></table>
</div>
<div class="rs-row-controls">
<button class="rs-btn" id="rsShuffleRows" type="button">π Shuffle rows</button>
<button class="rs-btn" id="rsShuffleCols" type="button">π Shuffle columns</button>
<button class="rs-btn" id="rsDupTable" type="button">Try: add another "Student" table</button>
<button class="rs-btn" id="rsDupCol" type="button">Try: add another "Name" column</button>
</div>
<div class="rs-form">
<p class="rs-form-title">Add a row</p>
<div class="rs-form-grid">
<div class="rs-field"><label for="rsId">ID (PK)</label><input id="rsId" type="text" placeholder="e.g. 103"></div>
<div class="rs-field"><label for="rsName">Name</label><input id="rsName" type="text" placeholder="e.g. Chloe"></div>
<div class="rs-field"><label for="rsAge">Age</label><input id="rsAge" type="text" placeholder="e.g. 19"></div>
<div class="rs-field"><label for="rsSubject">Subject</label><input id="rsSubject" type="text" placeholder="e.g. Physics"></div>
</div>
<button class="rs-add-btn" id="rsAddRow" type="button">Add row</button>
<div class="rs-msg" id="rsMsg"></div>
</div>
</div>
<div class="rs-rules" id="rsRules">
<span class="rs-rule-chip" data-rule="1">01 Unique relation name</span>
<span class="rs-rule-chip" data-rule="2">02 Unique attribute name</span>
<span class="rs-rule-chip" data-rule="3">03 Unique tuples</span>
<span class="rs-rule-chip" data-rule="4">04 Row order doesn't matter</span>
<span class="rs-rule-chip" data-rule="5">05 Column order doesn't matter</span>
<span class="rs-rule-chip" data-rule="6">06 Atomic values</span>
<span class="rs-rule-chip" data-rule="7">07 Same domain per column</span>
<span class="rs-rule-chip" data-rule="8">08 Nulls may exist</span>
<span class="rs-rule-chip" data-rule="9">09 Primary key β NULL</span>
</div>
</div>
<script>
(function () {
const wrapper = document.getElementById('relsandbox');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
let columns = [
{ key: 'id', label: 'ID (PK)' },
{ key: 'name', label: 'Name' },
{ key: 'age', label: 'Age' },
{ key: 'subject', label: 'Subject' }
];
let rows = [
{ id: '101', name: 'Alice', age: '20', subject: 'Math' },
{ id: '102', name: 'Bob', age: '21', subject: 'Science' }
];
let lastAddedIndex = -1;
const table = document.getElementById('rsTable');
const msg = document.getElementById('rsMsg');
const rules = document.querySelectorAll('.rs-rule-chip');
function render() {
const thead = '<thead><tr>' + columns.map(c => '<th>' + c.label + '</th>').join('') + '</tr></thead>';
const tbody = '<tbody>' + rows.map((r, i) => {
const cells = columns.map(c => {
const val = r[c.key];
return val === '' || val === undefined
? '<td class="is-null">NULL</td>'
: '<td>' + val + '</td>';
}).join('');
return '<tr' + (i === lastAddedIndex ? ' class="is-new"' : '') + '>' + cells + '</tr>';
}).join('') + '</tbody>';
table.innerHTML = thead + tbody;
}
function showMsg(text, type) {
msg.textContent = text;
msg.className = 'rs-msg is-visible msg-' + type;
}
function hitRule(n) {
rules.forEach(r => r.classList.remove('is-hit'));
const chip = document.querySelector('.rs-rule-chip[data-rule="' + n + '"]');
if (chip) {
chip.classList.add('is-hit');
chip.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
}
function shuffle(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
document.getElementById('rsShuffleRows').addEventListener('click', () => {
rows = shuffle(rows);
lastAddedIndex = -1;
render();
hitRule(4);
showMsg('Rows reordered β it\u2019s still the exact same relation. Row order carries no meaning.', 'info');
});
document.getElementById('rsShuffleCols').addEventListener('click', () => {
columns = shuffle(columns);
render();
hitRule(5);
showMsg('Columns reordered β still the exact same relation. Column order carries no meaning.', 'info');
});
document.getElementById('rsDupTable').addEventListener('click', () => {
hitRule(1);
showMsg('β Can\u2019t create another table named "Student" β every relation name must be unique in the schema.', 'bad');
});
document.getElementById('rsDupCol').addEventListener('click', () => {
hitRule(2);
showMsg('β Can\u2019t add another column named "Name" β every attribute name must be unique within a table.', 'bad');
});
document.getElementById('rsAddRow').addEventListener('click', () => {
const id = document.getElementById('rsId').value.trim();
const name = document.getElementById('rsName').value.trim();
const age = document.getElementById('rsAge').value.trim();
const subject = document.getElementById('rsSubject').value.trim();
if (!id) {
hitRule(9);
showMsg('β ID can\u2019t be empty β a primary key can never be NULL (this is the Entity Integrity Rule, separate from tuple uniqueness).', 'bad');
return;
}
if (/[,{}]/.test(subject)) {
hitRule(6);
showMsg('β "' + subject + '" isn\u2019t atomic β a cell must hold one indivisible value. Try a single subject like "Math" instead of a list.', 'bad');
return;
}
if (age !== '' && !/^\d+$/.test(age)) {
hitRule(7);
showMsg('β "' + age + '" isn\u2019t a valid Age β every value in a column must come from the same domain (Age = whole numbers only).', 'bad');
return;
}
if (rows.some(r => r.id === id)) {
hitRule(3);
showMsg('β ID "' + id + '" already exists β a primary key value must be unique across all tuples.', 'bad');
return;
}
const dupExact = rows.some(r => r.id === id && r.name === name && r.age === age && r.subject === subject);
if (dupExact) {
hitRule(3);
showMsg('β That row is identical to an existing one β no two tuples in a relation can be exactly the same.', 'bad');
return;
}
rows.push({ id, name, age, subject });
lastAddedIndex = rows.length - 1;
render();
if (!name || !subject) {
hitRule(8);
showMsg('βοΈ Row added, with a NULL where data was left blank β relations may contain NULLs, just don\u2019t overuse them.', 'info');
} else {
hitRule(3);
showMsg('βοΈ Row added β it\u2019s unique, atomic, and within domain. Every rule holds.', 'good');
}
document.getElementById('rsId').value = '';
document.getElementById('rsName').value = '';
document.getElementById('rsAge').value = '';
document.getElementById('rsSubject').value = '';
});
render();
})();
</script>
</body>
</html>.demo {
font-family: system-ui, sans-serif;
padding: 16px;
}
.demo h1 {
color: #2563eb;
margin: 0 0 8px;
}
.demo p {
color: #4b5563;
margin: 0;
}document.querySelector('.demo h1')?.addEventListener('click', () => {
alert('Hello from live preview!');
});