source: docs/P5-Normalization/Normalization.md@ a531b45

main
Last change on this file since a531b45 was a531b45, checked in by Stefan <trsunovstefan@…>, 13 days ago

Update .md file

  • Property mode set to 100644
File size: 27.4 KB
Line 
1# Normalization
2
3This phase deliberately ignores the design from [ERModel](../P1-ConceptualModel/ERModel.md)
4(P1) and [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) (P2) as a starting
5point. Instead it starts over from a single flat relation containing every attribute of the
6model, derives the functional dependencies that hold on it, and decomposes it formally,
7step by step. The
8[final section](#final-result-and-discussion) compares what falls out of that process with
9the P2 design.
10
11## De-normalized database form
12
13### Building one relation out of the whole model
14
15The ER model has ten entity/relationship sets carrying attributes (see
16[ERModel](../P1-ConceptualModel/ERModel.md)): `Users`, `Cryptos`, `Markets`, `Orders`,
17`Transactions`, `MarketTrades`, `MarketCandles`, `Watchlists`, and the two attributed
18relationships `Holds` and `Contains`. Eight more relationships (`QuotedOn`, `PlacedOn`,
19`Places`, `Records`, `Settles`, `Fills`, `Aggregates`, `Owns`) carry no attributes of their
20own — in Chen notation they need none, because the diagram expresses the link itself as a
21relationship, not a column. A single flat relation has no such device: the only way to keep
22one entity's rows pointed at another's is a plain attribute holding the referenced key,
23which is exactly what P2's ER-to-relational transformation already introduces for each of
24those eight relationships (`markets.crypto_id`, `orders.market_id`, `orders.user_id`,
25`transactions.user_id`, `transactions.related_order`, `market_trades.market_id`,
26`market_candles.market_id`, `watchlists.user_id`). Those linking attributes are included
27below for that reason — not because they were copied from P2's design, but because a "single
28table with everything in it" cannot represent the model at all without them.
29
30Every attribute name is prefixed by a two-or-three-letter code for the entity/relationship it
31came from, because several names repeat across the model (`id`, `created_at`, `quantity`,
32`type`, `name`, `price`, `side` all appear more than once) and the de-normalized relation may
33not contain duplicate names.
34
35| Prefix | Origin (P1 entity / relationship) | Attributes |
36|---|---|---|
37| `U_` | Users | `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` |
38| `C_` | Cryptos | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` |
39| `M_` | Markets (+ `QuotedOn`) | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` |
40| `H_` | `Holds` (+ surrogate key) | `H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` |
41| `O_` | Orders (+ `PlacedOn`, `Places`) | `O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` |
42| `T_` | Transactions (+ `Records`, `Settles`) | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` |
43| `MT_` | MarketTrades (+ `Fills`) | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` |
44| `MC_` | MarketCandles (+ `Aggregates`) | `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` |
45| `W_` | Watchlists (+ `Owns`) | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` |
46| `WI_` | `Contains` (+ surrogate key) | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` |
47
48`H_ID` and `WI_ID` exist for the same reason they exist in P2: `Holds` and `Contains` are M:N
49relationships with their own attributes, and giving each its own surrogate key (rather than
50relying solely on the `{user,crypto}` / `{watchlist,crypto}` pair) is the same design choice
51already justified in [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#descriptive-representation-of-the-relational-schema).
52
53This gives **one relation, `R_EDUBERZA`, of 68 attributes:**
54
55```
56R_EDUBERZA(
57 U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE,
58 U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT,
59 C_ID, C_SYMBOL, C_NAME, C_CREATED_AT,
60 M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT,
61 H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE,
62 H_CREATED_AT, H_UPDATED_AT,
63 O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE,
64 O_PLACED_AT, O_EXECUTED_AT,
65 T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT,
66 T_DESCRIPTION,
67 MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE,
68 MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME,
69 MC_CANDLE_TIME,
70 W_ID, W_USER_ID, W_NAME, W_CREATED_AT,
71 WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT
72)
73```
74
75Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount —
76nothing here is a list or a nested record), so `R_EDUBERZA` satisfies 1NF as soon as it is
77written down. Whether it satisfies anything beyond that is exactly what the rest of this page
78checks.
79
80## Functional dependencies
81
82### Canonical cover
83
84Read directly off the model: each entity's/relationship's own key determines its own
85attributes, nothing more. This is already minimal — no functional dependency below has an
86extraneous attribute on its left side, and no dependent attribute is repeated on the right
87side of more than one dependency, which is what "canonical cover" requires.
88
89| # | Functional dependency | Source |
90|---|---|---|
91| FD1 | `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | Users |
92| FD2 | `U_USERNAME → U_ID` | Users (`UNIQUE(username)`) |
93| FD3 | `U_EMAIL → U_ID` | Users (`UNIQUE(email)`) |
94| FD4 | `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` | Cryptos |
95| FD5 | `C_SYMBOL → C_ID` | Cryptos (`UNIQUE(symbol)`) |
96| FD6 | `M_ID → M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | Markets |
97| FD7 | `M_CRYPTO_ID, M_QUOTE_CURRENCY → M_ID` | Markets (`UNIQUE(crypto_id, quote_currency)`) |
98| FD8 | `H_ID → H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | Holds |
99| FD9 | `H_USER_ID, H_CRYPTO_ID → H_ID` | Holds (`UNIQUE(user_id, crypto_id)`) |
100| FD10 | `O_ID → O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | Orders |
101| FD11 | `T_ID → T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | Transactions |
102| FD12 | `MT_ID → MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | MarketTrades |
103| FD13 | `MC_ID → MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | MarketCandles |
104| FD14 | `MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` | MarketCandles (`UNIQUE(market_id, timeframe, candle_time)`) |
105| FD15 | `W_ID → W_USER_ID, W_NAME, W_CREATED_AT` | Watchlists |
106| FD16 | `WI_ID → WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | Contains |
107| FD17 | `WI_WATCHLIST_ID, WI_CRYPTO_ID → WI_ID` | Contains (`UNIQUE(watchlist_id, crypto_id)`) |
108
109**Minimality, checked by example (Markets):** could FD7 drop an attribute from its left side?
110`M_CRYPTO_ID` alone does not determine `M_ID` — many markets can reference the same crypto in
111different quote currencies (that is the entire point of the market entity), so two rows can
112share `M_CRYPTO_ID` and disagree on `M_ID`. `M_QUOTE_CURRENCY` alone fails the same way in the
113other direction. Neither attribute is extraneous, so the left side of FD7 cannot shrink. The
114same check applies to FD9, FD14 and FD17, whose composite left sides come directly from the
115`UNIQUE` constraints already justified per-relation in
116[RelationalDesign](../P2-RelationalDesign/RelationalDesign.md); none of those constraints
117holds on a proper subset of its columns either.
118
119**No redundant dependency:** each of FD1–FD17 has a right side that is not implied by any
120other dependency in the set — for instance, nothing outside FD1 mentions `U_AVAILABLE_BALANCE`,
121so FD1 cannot be derived from the rest and cannot be dropped. This set is the canonical cover.
122
123### Dependencies carried by foreign keys
124
125Six attributes above are foreign keys: `M_CRYPTO_ID`, `H_USER_ID`, `H_CRYPTO_ID`,
126`O_USER_ID`, `O_MARKET_ID`, `T_USER_ID`, `T_RELATED_ORDER`, `MT_MARKET_ID`, `MC_MARKET_ID`,
127`W_USER_ID`, `WI_WATCHLIST_ID`, `WI_CRYPTO_ID` — each one draws its values from the same
128domain as some other attribute's key. Because of that, every dependency that holds on the
129referenced key also holds, by substitution, on the referencing attribute:
130
131| Foreign key | References | Therefore also determines |
132|---|---|---|
133| `M_CRYPTO_ID` | `C_ID` | `C_SYMBOL, C_NAME, C_CREATED_AT` |
134| `H_USER_ID` | `U_ID` | all of `U_*` |
135| `H_CRYPTO_ID` | `C_ID` | all of `C_*` |
136| `O_USER_ID` | `U_ID` | all of `U_*` |
137| `O_MARKET_ID` | `M_ID` | all of `M_*`, and transitively all of `C_*` |
138| `T_USER_ID` | `U_ID` | all of `U_*` |
139| `T_RELATED_ORDER` | `O_ID` | all of `O_*`, and transitively `U_*`, `M_*`, `C_*` (when not null) |
140| `MT_MARKET_ID` | `M_ID` | all of `M_*`, transitively `C_*` |
141| `MC_MARKET_ID` | `M_ID` | all of `M_*`, transitively `C_*` |
142| `W_USER_ID` | `U_ID` | all of `U_*` |
143| `WI_WATCHLIST_ID` | `W_ID` | all of `W_*`, transitively `U_*` |
144| `WI_CRYPTO_ID` | `C_ID` | all of `C_*` |
145
146None of these is added to the canonical cover — each is *derivable* from FD1–FD17 by
147transitivity plus the foreign-key identity, which is exactly why a canonical cover excludes
148them. They matter anyway: they are precisely the transitive dependencies the 3NF check below
149has to rule out.
150
151## Candidate keys and primary key
152
153`Orders`, `Transactions`, `MarketTrades`, `MarketCandles`, `Holds`, `Watchlists` and
154`Contains` are, with respect to each other, independent record types: nothing about an
155order's id says anything about which market-candle row, or which unrelated transaction, or
156which watchlist item is in the same tuple of `R_EDUBERZA` — a user can exist with zero of any
157of them, and having one order says nothing about how many holdings, trades or candles exist
158alongside it. (The one FK that crosses between two of these — `T_RELATED_ORDER` — is
159nullable, so it cannot be relied on to always connect a transaction row back to an order.)
160That means no proper subset of attributes can functionally determine all 68 attributes of
161`R_EDUBERZA`: the only way to pin down a `H_*` value, an `O_*` value, a `T_*` value, an
162`MT_*` value, an `MC_*` value, a `W_*` value *and* a `WI_*` value at once is to state one
163identifying attribute from each cluster explicitly.
164
165**Chosen primary key** (closure shown below):
166
167```
168{ U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID }
169```
170
171**Closure check**, applying FD1–FD17 in turn to this set:
172
173| Step | Attributes added | Dependency used |
174|---|---|---|
175| start | `U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID` | — |
176| 1 | `U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | FD1 (`U_ID → …`) |
177| 2 | `C_SYMBOL, C_NAME, C_CREATED_AT` | FD4 |
178| 3 | `M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | FD6 |
179| 4 | `H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | FD8 |
180| 5 | `O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | FD10 |
181| 6 | `T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | FD11 |
182| 7 | `MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | FD12 |
183| 8 | `MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | FD13 |
184| 9 | `W_USER_ID, W_NAME, W_CREATED_AT` | FD15 |
185| 10 | `WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | FD16 |
186
187The closure now contains all 68 attributes, so the set is a superkey; removing any one of its
188ten attributes drops an entire cluster that nothing else in the set can reach (e.g. drop
189`T_ID` and no remaining attribute determines any `T_*` value), so it is minimal — a candidate
190key.
191
192**It is not the only one.** Any attribute that is itself a determinant of a whole cluster can
193stand in for that cluster's id — `U_USERNAME` or `U_EMAIL` for `U_ID` (FD2/FD3), `C_SYMBOL`
194for `C_ID` (FD5), `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` for `M_ID` (FD7), `{H_USER_ID,
195H_CRYPTO_ID}` for `H_ID` (FD9), `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` for `MC_ID`
196(FD14), `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` for `WI_ID` (FD17) — giving 3 × 2 × 2 × 2 × 1 × 1 ×
1971 × 2 × 1 × 2 = 96 candidate keys in total. The all-surrogate-id combination above is chosen
198as **primary key** for the same reason `id` was chosen over `username`/`email`/`symbol`/etc.
199per entity in [ERModel](../P1-ConceptualModel/ERModel.md): it is opaque, and none of its parts
200are things a user would ever legitimately change.
201
202**Normal form of `R_EDUBERZA` before decomposition:** 1NF only, and barely that — see 2NF
203below. It cannot be in 2NF, 3NF or BCNF, since each of those requires 2NF as a precondition.
204
205## 1NF decomposition
206
207No decomposition happens at this step. 1NF requires atomic, single-valued attributes and no
208repeating groups; `R_EDUBERZA` was built that way from the start (every column above is a
209single scalar), so the relation already satisfies 1NF as written in
210[De-normalized database form](#de-normalized-database-form). The real work starts at 2NF.
211
212## 2NF decomposition
213
214**Relation analyzed:** `R_EDUBERZA`, all 68 attributes, primary key
215`{U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID}` (10 attributes), FD1–FD17
216in force.
217
218**Current normal form:** 1NF only (previous section).
219
220**Violations:** 2NF forbids a non-prime attribute from depending on *part* of a candidate
221key. Every single functional dependency in the canonical cover (FD1–FD17) has a left side
222that is a **proper subset** of the ten-attribute primary key — `U_ID` alone, `C_ID` alone, …,
223down to the two-attribute `{WI_WATCHLIST_ID, WI_CRYPTO_ID}`. There is no non-prime attribute
224in `R_EDUBERZA` that depends on the whole ten-attribute key and nothing smaller. In other
225words, *every* non-prime attribute violates 2NF at once — the violation is not a handful of
226stray columns to peel off, it is the entire relation, because gluing ten independent record
227types together under one artificial composite key was never going to satisfy 2NF to begin
228with.
229
230**Decomposition.** This uses 3NF/BCNF **synthesis** (Bernstein's algorithm) rather than the
231binary decomposition algorithm: since the canonical cover is already in hand (as the phase
232instructions recommend building first), synthesis creates one relation per left-hand side in
233the cover directly, instead of hunting for one offending dependency at a time and splitting
234in two repeatedly. Grouping FD1–FD17 by determinant produces ten relations:
235
236| New relation | Attributes | Key(s) | Source FDs |
237|---|---|---|---|
238| `R_USERS` | `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | `U_ID`, `U_USERNAME`, `U_EMAIL` | FD1, FD2, FD3 |
239| `R_CRYPTO` | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | `C_ID`, `C_SYMBOL` | FD4, FD5 |
240| `R_MARKETS` | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` | FD6, FD7 |
241| `R_HOLDINGS` | `H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` | FD8, FD9 |
242| `R_ORDERS` | `O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | `O_ID` | FD10 |
243| `R_TRANSACTIONS` | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | `T_ID` | FD11 |
244| `R_MARKET_TRADES` | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | `MT_ID` | FD12 |
245| `R_MARKET_CANDLES` | `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | FD13, FD14 |
246| `R_WATCHLISTS` | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | `W_ID` | FD15 |
247| `R_WATCHLIST_ITEMS` | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | FD16, FD17 |
248
249Every one of these ten relations now has **all** of its non-prime attributes depending on its
250**whole** key (in every case there is only one non-composite or one designated key doing the
251determining, so 2NF holds trivially in each).
252
253**Dependency preservation.** FD1–FD17 is the canonical cover of `R_EDUBERZA`. Each FD's
254determinant and every one of its dependent attributes land inside exactly one of the ten new
255relations (see the "Source FDs" column above — no FD is split across two relations). The
256union of the FDs that hold on `R_USERS, …, R_WATCHLIST_ITEMS` is therefore exactly FD1–FD17
257again: nothing was lost.
258
259**Lossless join.** For every pair (referencing relation, referenced relation) connected by a
260foreign key — `R_MARKETS.M_CRYPTO_ID → R_CRYPTO.C_ID`, `R_HOLDINGS.H_USER_ID → R_USERS.U_ID` /
261`R_HOLDINGS.H_CRYPTO_ID → R_CRYPTO.C_ID`, `R_ORDERS.O_USER_ID → R_USERS.U_ID` /
262`R_ORDERS.O_MARKET_ID → R_MARKETS.M_ID`, `R_TRANSACTIONS.T_USER_ID → R_USERS.U_ID` /
263`R_TRANSACTIONS.T_RELATED_ORDER → R_ORDERS.O_ID`, `R_MARKET_TRADES.MT_MARKET_ID →
264R_MARKETS.M_ID`, `R_MARKET_CANDLES.MC_MARKET_ID → R_MARKETS.M_ID`,
265`R_WATCHLISTS.W_USER_ID → R_USERS.U_ID`, `R_WATCHLIST_ITEMS.WI_WATCHLIST_ID →
266R_WATCHLISTS.W_ID` / `R_WATCHLIST_ITEMS.WI_CRYPTO_ID → R_CRYPTO.C_ID` — the join attribute on
267the "one" side is that relation's own primary key (`U_ID`, `C_ID`, `M_ID`, `O_ID`, `W_ID`).
268A join on a foreign key equated to the primary key it references is the textbook sufficient
269condition for a lossless decomposition (`Ri ∩ Rj` is a key of `Rj`), so re-joining all ten
270relations on their foreign-key/primary-key pairs reconstructs `R_EDUBERZA` exactly, with no
271spurious rows and none missing.
272
273## 3NF decomposition
274
275**Relations analyzed:** each of the ten relations produced above, individually.
276
277For each relation, 3NF asks whether any non-prime attribute is *transitively* dependent on a
278key — i.e. determined by another non-prime attribute rather than directly by the key. This is
279exactly where the foreign-key-carried dependencies from
280[Dependencies carried by foreign keys](#dependencies-carried-by-foreign-keys) have to be
281checked, because that table is precisely the list of "dependency that would cause a problem at
282the next higher normal form" the phase template asks for.
283
284**Worked example — `R_MARKETS`.** Its key `M_ID` determines `M_CRYPTO_ID`, and
285`M_CRYPTO_ID → C_SYMBOL, C_NAME, C_CREATED_AT` also holds (`M_CRYPTO_ID` draws its values from
286`C_ID`'s domain). If `C_SYMBOL`, `C_NAME` and `C_CREATED_AT` were still columns of
287`R_MARKETS`, this would be exactly the transitive dependency `M_ID → M_CRYPTO_ID → C_SYMBOL`
288that violates 3NF. They are not: the 2NF step above already put them in `R_CRYPTO`, keyed
289directly by `C_ID` (FD4), because FD4 — not the derived `M_CRYPTO_ID → C_SYMBOL` — is what the
290canonical cover actually contains. `R_MARKETS` itself has no attribute that determines another
291non-prime attribute of `R_MARKETS`; the transitive dependency is real, but it points *out* of
292the relation, not within it.
293
294The same reasoning applies to every other foreign key in the list: `H_USER_ID`/`H_CRYPTO_ID`,
295`O_USER_ID`/`O_MARKET_ID`, `T_USER_ID`/`T_RELATED_ORDER`, `MT_MARKET_ID`, `MC_MARKET_ID`,
296`W_USER_ID`, `WI_WATCHLIST_ID`/`WI_CRYPTO_ID` are all foreign keys sitting *alongside* a
297non-key attribute set that depends only on their own relation's key, never on the foreign key
298itself. None of `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_ORDERS`, `R_TRANSACTIONS`,
299`R_MARKET_TRADES`, `R_MARKET_CANDLES`, `R_WATCHLISTS`, `R_WATCHLIST_ITEMS` has a non-prime
300attribute that another non-prime attribute of the *same* relation determines.
301
302**Conclusion:** synthesising directly from the canonical cover in the 2NF step already
303avoided every transitive dependency — there is nothing left to decompose for 3NF. All ten
304relations from the previous section satisfy 3NF unchanged.
305
306## BCNF if possible
307
308**Relations analyzed:** the same ten relations, checked against the stricter BCNF rule: every
309determinant of every functional dependency that holds on the relation must be a candidate key
310of that relation (3NF allows an exception when the dependent side is prime; BCNF does not).
311
312| Relation | Functional dependencies in force | Determinant | Is it a candidate key? |
313|---|---|---|---|
314| `R_USERS` | FD1, FD2, FD3 | `U_ID`, `U_USERNAME`, `U_EMAIL` | Yes — all three are candidate keys |
315| `R_CRYPTO` | FD4, FD5 | `C_ID`, `C_SYMBOL` | Yes — both candidate keys |
316| `R_MARKETS` | FD6, FD7 | `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` | Yes — both candidate keys |
317| `R_HOLDINGS` | FD8, FD9 | `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` | Yes — both candidate keys |
318| `R_ORDERS` | FD10 | `O_ID` | Yes — the only candidate key |
319| `R_TRANSACTIONS` | FD11 | `T_ID` | Yes — the only candidate key |
320| `R_MARKET_TRADES` | FD12 | `MT_ID` | Yes — the only candidate key |
321| `R_MARKET_CANDLES` | FD13, FD14 | `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | Yes — both candidate keys |
322| `R_WATCHLISTS` | FD15 | `W_ID` | Yes — the only candidate key |
323| `R_WATCHLIST_ITEMS` | FD16, FD17 | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | Yes — both candidate keys |
324
325Every determinant in every relation is one of that relation's own candidate keys. **All ten
326relations are already in BCNF** — the highest of the four normal forms this phase asks for,
327reached in the same step that fixed 2NF. This is not a coincidence: it happens because the
328canonical cover already grouped each relation's own key directly against its own attributes
329with no attribute appearing on the right side of two different relations' dependencies, which
330is exactly what synthesis from a canonical cover guarantees when, as here, none of the
331per-cluster functional dependencies overlap.
332
333No further decomposition is possible or necessary; splitting any of the ten relations further
334would only separate attributes that already depend on the *whole* key of a BCNF relation,
335which cannot fix anything and only costs a join.
336
337## Final result and discussion
338
339### Normalized relational model
340
341```
342R_USERS (U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH,
343 U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT)
344R_CRYPTO (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT)
345R_MARKETS (M_ID, M_CRYPTO_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT)
346R_HOLDINGS (H_ID, H_USER_ID → R_USERS, H_CRYPTO_ID → R_CRYPTO, H_QUANTITY,
347 H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT)
348R_ORDERS (O_ID, O_USER_ID → R_USERS, O_MARKET_ID → R_MARKETS, O_SIDE, O_TYPE,
349 O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT)
350R_TRANSACTIONS (T_ID, T_USER_ID → R_USERS, T_TYPE, T_AMOUNT, T_CURRENCY,
351 T_RELATED_ORDER → R_ORDERS, T_CREATED_AT, T_DESCRIPTION)
352R_MARKET_TRADES (MT_ID, MT_MARKET_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY,
353 MT_SIDE, MT_SOURCE)
354R_MARKET_CANDLES (MC_ID, MC_MARKET_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW,
355 MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME)
356R_WATCHLISTS (W_ID, W_USER_ID → R_USERS, W_NAME, W_CREATED_AT)
357R_WATCHLIST_ITEMS(WI_ID, WI_WATCHLIST_ID → R_WATCHLISTS, WI_CRYPTO_ID → R_CRYPTO, WI_ADDED_AT)
358```
359
360Ten relations, every one in BCNF, connected by the eleven foreign keys spelled out above.
361
362### Discussion
363
364**This is the P2 design.** Strip the `U_`/`C_`/`M_`/… prefixes back to plain column names and
365`R_USERS, R_CRYPTO, R_MARKETS, R_HOLDINGS, R_ORDERS, R_TRANSACTIONS, R_MARKET_TRADES,
366R_MARKET_CANDLES, R_WATCHLISTS, R_WATCHLIST_ITEMS` are, attribute for attribute and key for
367key, `users, crypto, markets, holdings, orders, transactions, market_trades, market_candles,
368watchlists, watchlist_items` from
369[RelationalDesign](../P2-RelationalDesign/RelationalDesign.md). Every foreign key matches,
370every candidate key matches (including the less obvious composite ones — `{user_id,
371crypto_id}` on `holdings`, `{crypto_id, quote_currency}` on `markets`, `{market_id, timeframe,
372candle_time}` on `market_candles`), and the normal form matches (P2 already claimed 3NF; this
373phase shows the stronger result that the design is actually in BCNF).
374
375That is not a coincidence of two people happening to agree — it is what should happen when a
376design is derived correctly twice by two different methods from the same underlying model:
377P2 got here by applying the standard ER-to-relational transformation rules (each entity
378becomes a table on its own key, each attributed M:N relationship becomes a table on the
379combined key, each attributeless 1:N relationship becomes a foreign key on the "many" side).
380This phase got here by ignoring that transformation entirely, writing down only the
381attributes and the functional dependencies they obey, and mechanically applying 2NF/3NF/BCNF
382synthesis. Landing on the same ten relations either means the P2 transformation rules are
383sound for this particular model (which they are, for exactly the reason [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation)
384already argued: single-column UUID primary keys everywhere rule out partial dependencies by
385construction, and no non-key attribute references another non-key attribute anywhere in the
386model, which rules out transitive dependencies too), or it is a coincidence spanning ten
387independently-checked relations and dozens of functional dependencies — the first explanation
388is the only credible one.
389
390**The one substantive difference** is `holdings.avg_price`, which P2 documents as a
391*derived* attribute — the running weighted-average buy price, recomputable from the `buy` rows
392in `transactions` — kept as a stored column anyway for read performance
393([RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation) calls this out
394explicitly as an accepted denormalisation). Nothing in this phase's functional-dependency
395analysis can see that `H_AVG_PRICE` is derivable from `T_*` rows rather than stored
396independently — FD8 (`H_ID → H_AVG_PRICE`) is a perfectly ordinary functional dependency
397either way, because *derivability from a different relation's rows* is a property of the data
398and the application logic that maintains it (see
399[UseCase0004](../P3-UseCaseModel/UseCase0004.md)'s `ON CONFLICT … DO UPDATE`), not something
400that shows up as a violation of any single-relation normal form. Formal normalization and "no
401column is a cached computation of other columns" are related but different concerns; this
402phase only checked the first one.
403
404**Which design is used going forward:** P2's, unchanged. Since the two designs coincide
405exactly, "restructuring the database objects" means confirming there is nothing to change
406rather than writing new DDL. [`server/db/schema_creation.sql`](../../server/db/schema_creation.sql)
407already matches `R_USERS`…`R_WATCHLIST_ITEMS` column-for-column (including
408`holdings.reserved_quantity`, added between P2 and this phase — see
409[RelationalDesignAIUsage](../P2-RelationalDesign/RelationalDesignAIUsage.md#session-3--2026-09-16)
410— which is `H_RESERVED_QUANTITY` above, correctly grouped under `R_HOLDINGS`'s key alongside
411`H_QUANTITY` and not treated as needing a relation of its own). P4's prototype
412(`server/trade.go`, `server/portfolio.go`) keeps working against the same schema without
413change. [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) has been updated with a
414short note pointing here as the formal validation of its normal-form claim.
Note: See TracBrowser for help on using the repository browser.