| 1 | # Normalization
|
|---|
| 2 |
|
|---|
| 3 | This phase deliberately ignores the design from [ERModel](../P1-ConceptualModel/ERModel.md)
|
|---|
| 4 | (P1) and [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) (P2) as a starting
|
|---|
| 5 | point. Instead it starts over from a single flat relation containing every attribute of the
|
|---|
| 6 | model, derives the functional dependencies that hold on it, and decomposes it formally,
|
|---|
| 7 | step by step, using only Armstrong's axioms and the standard normal-form definitions. The
|
|---|
| 8 | [final section](#final-result-and-discussion) compares what falls out of that process with
|
|---|
| 9 | the P2 design.
|
|---|
| 10 |
|
|---|
| 11 | ## De-normalized database form
|
|---|
| 12 |
|
|---|
| 13 | ### Building one relation out of the whole model
|
|---|
| 14 |
|
|---|
| 15 | The 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
|
|---|
| 18 | relationships `Holds` and `Contains`. Eight more relationships (`QuotedOn`, `PlacedOn`,
|
|---|
| 19 | `Places`, `Records`, `Settles`, `Fills`, `Aggregates`, `Owns`) carry no attributes of their
|
|---|
| 20 | own — in Chen notation they need none, because the diagram expresses the link itself as a
|
|---|
| 21 | relationship, not a column. A single flat relation has no such device: the only way to keep
|
|---|
| 22 | one entity's rows pointed at another's is a plain attribute holding the referenced key,
|
|---|
| 23 | which is exactly what P2's ER-to-relational transformation already introduces for each of
|
|---|
| 24 | those 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
|
|---|
| 27 | below for that reason — not because they were copied from P2's design, but because a "single
|
|---|
| 28 | table with everything in it" cannot represent the model at all without them.
|
|---|
| 29 |
|
|---|
| 30 | Every attribute name is prefixed by a two-or-three-letter code for the entity/relationship it
|
|---|
| 31 | came 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
|
|---|
| 33 | not 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
|
|---|
| 49 | relationships with their own attributes, and giving each its own surrogate key (rather than
|
|---|
| 50 | relying solely on the `{user,crypto}` / `{watchlist,crypto}` pair) is the same design choice
|
|---|
| 51 | already justified in [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#descriptive-representation-of-the-relational-schema).
|
|---|
| 52 |
|
|---|
| 53 | This gives **one relation, `R_EDUBERZA`, of 68 attributes:**
|
|---|
| 54 |
|
|---|
| 55 | ```
|
|---|
| 56 | R_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 |
|
|---|
| 75 | Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount —
|
|---|
| 76 | nothing here is a list or a nested record), so `R_EDUBERZA` satisfies 1NF as soon as it is
|
|---|
| 77 | written down. Whether it satisfies anything beyond that is exactly what the rest of this page
|
|---|
| 78 | checks.
|
|---|
| 79 |
|
|---|
| 80 | ## Functional dependencies
|
|---|
| 81 |
|
|---|
| 82 | ### Canonical cover
|
|---|
| 83 |
|
|---|
| 84 | Read directly off the model: each entity's/relationship's own key determines its own
|
|---|
| 85 | attributes, nothing more. This is already minimal — no functional dependency below has an
|
|---|
| 86 | extraneous attribute on its left side, and no dependent attribute is repeated on the right
|
|---|
| 87 | side 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
|
|---|
| 111 | different quote currencies (that is the entire point of the market entity), so two rows can
|
|---|
| 112 | share `M_CRYPTO_ID` and disagree on `M_ID`. `M_QUOTE_CURRENCY` alone fails the same way in the
|
|---|
| 113 | other direction. Neither attribute is extraneous, so the left side of FD7 cannot shrink. The
|
|---|
| 114 | same 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
|
|---|
| 117 | holds 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
|
|---|
| 120 | other dependency in the set — for instance, nothing outside FD1 mentions `U_AVAILABLE_BALANCE`,
|
|---|
| 121 | so 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 |
|
|---|
| 125 | Six 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
|
|---|
| 128 | domain as some other attribute's key. Because of that, every dependency that holds on the
|
|---|
| 129 | referenced 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 |
|
|---|
| 146 | None of these is added to the canonical cover — each is *derivable* from FD1–FD17 by
|
|---|
| 147 | transitivity plus the foreign-key identity, which is exactly why a canonical cover excludes
|
|---|
| 148 | them. They matter anyway: they are precisely the transitive dependencies the 3NF check below
|
|---|
| 149 | has 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
|
|---|
| 155 | order's id says anything about which market-candle row, or which unrelated transaction, or
|
|---|
| 156 | which watchlist item is in the same tuple of `R_EDUBERZA` — a user can exist with zero of any
|
|---|
| 157 | of them, and having one order says nothing about how many holdings, trades or candles exist
|
|---|
| 158 | alongside it. (The one FK that crosses between two of these — `T_RELATED_ORDER` — is
|
|---|
| 159 | nullable, so it cannot be relied on to always connect a transaction row back to an order.)
|
|---|
| 160 | That 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
|
|---|
| 163 | identifying 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 |
|
|---|
| 187 | The closure now contains all 68 attributes, so the set is a superkey; removing any one of its
|
|---|
| 188 | ten 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
|
|---|
| 190 | key.
|
|---|
| 191 |
|
|---|
| 192 | **It is not the only one.** Any attribute that is itself a determinant of a whole cluster can
|
|---|
| 193 | stand in for that cluster's id — `U_USERNAME` or `U_EMAIL` for `U_ID` (FD2/FD3), `C_SYMBOL`
|
|---|
| 194 | for `C_ID` (FD5), `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` for `M_ID` (FD7), `{H_USER_ID,
|
|---|
| 195 | H_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 ×
|
|---|
| 197 | 1 × 2 × 1 × 2 = 96 candidate keys in total. The all-surrogate-id combination above is chosen
|
|---|
| 198 | as **primary key** for the same reason `id` was chosen over `username`/`email`/`symbol`/etc.
|
|---|
| 199 | per entity in [ERModel](../P1-ConceptualModel/ERModel.md): it is opaque, and none of its parts
|
|---|
| 200 | are things a user would ever legitimately change.
|
|---|
| 201 |
|
|---|
| 202 | **Normal form of `R_EDUBERZA` before decomposition:** 1NF only, and barely that — see 2NF
|
|---|
| 203 | below. It cannot be in 2NF, 3NF or BCNF, since each of those requires 2NF as a precondition.
|
|---|
| 204 |
|
|---|
| 205 | ## 1NF decomposition
|
|---|
| 206 |
|
|---|
| 207 | No decomposition happens at this step. 1NF requires atomic, single-valued attributes and no
|
|---|
| 208 | repeating groups; `R_EDUBERZA` was built that way from the start (every column above is a
|
|---|
| 209 | single 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
|
|---|
| 216 | in 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
|
|---|
| 221 | key. Every single functional dependency in the canonical cover (FD1–FD17) has a left side
|
|---|
| 222 | that is a **proper subset** of the ten-attribute primary key — `U_ID` alone, `C_ID` alone, …,
|
|---|
| 223 | down to the two-attribute `{WI_WATCHLIST_ID, WI_CRYPTO_ID}`. There is no non-prime attribute
|
|---|
| 224 | in `R_EDUBERZA` that depends on the whole ten-attribute key and nothing smaller. In other
|
|---|
| 225 | words, *every* non-prime attribute violates 2NF at once — the violation is not a handful of
|
|---|
| 226 | stray columns to peel off, it is the entire relation, because gluing ten independent record
|
|---|
| 227 | types together under one artificial composite key was never going to satisfy 2NF to begin
|
|---|
| 228 | with.
|
|---|
| 229 |
|
|---|
| 230 | **Decomposition.** This uses 3NF/BCNF **synthesis** (Bernstein's algorithm) rather than the
|
|---|
| 231 | binary decomposition algorithm: since the canonical cover is already in hand (as the phase
|
|---|
| 232 | instructions recommend building first), synthesis creates one relation per left-hand side in
|
|---|
| 233 | the cover directly, instead of hunting for one offending dependency at a time and splitting
|
|---|
| 234 | in 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 |
|
|---|
| 249 | Every 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
|
|---|
| 251 | determining, so 2NF holds trivially in each).
|
|---|
| 252 |
|
|---|
| 253 | **Dependency preservation.** FD1–FD17 is the canonical cover of `R_EDUBERZA`. Each FD's
|
|---|
| 254 | determinant and every one of its dependent attributes land inside exactly one of the ten new
|
|---|
| 255 | relations (see the "Source FDs" column above — no FD is split across two relations). The
|
|---|
| 256 | union of the FDs that hold on `R_USERS, …, R_WATCHLIST_ITEMS` is therefore exactly FD1–FD17
|
|---|
| 257 | again: nothing was lost.
|
|---|
| 258 |
|
|---|
| 259 | **Lossless join.** For every pair (referencing relation, referenced relation) connected by a
|
|---|
| 260 | foreign 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 →
|
|---|
| 264 | R_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 →
|
|---|
| 266 | R_WATCHLISTS.W_ID` / `R_WATCHLIST_ITEMS.WI_CRYPTO_ID → R_CRYPTO.C_ID` — the join attribute on
|
|---|
| 267 | the "one" side is that relation's own primary key (`U_ID`, `C_ID`, `M_ID`, `O_ID`, `W_ID`).
|
|---|
| 268 | A join on a foreign key equated to the primary key it references is the textbook sufficient
|
|---|
| 269 | condition for a lossless decomposition (`Ri ∩ Rj` is a key of `Rj`), so re-joining all ten
|
|---|
| 270 | relations on their foreign-key/primary-key pairs reconstructs `R_EDUBERZA` exactly, with no
|
|---|
| 271 | spurious rows and none missing.
|
|---|
| 272 |
|
|---|
| 273 | ## 3NF decomposition
|
|---|
| 274 |
|
|---|
| 275 | **Relations analyzed:** each of the ten relations produced above, individually.
|
|---|
| 276 |
|
|---|
| 277 | For each relation, 3NF asks whether any non-prime attribute is *transitively* dependent on a
|
|---|
| 278 | key — i.e. determined by another non-prime attribute rather than directly by the key. This is
|
|---|
| 279 | exactly where the foreign-key-carried dependencies from
|
|---|
| 280 | [Dependencies carried by foreign keys](#dependencies-carried-by-foreign-keys) have to be
|
|---|
| 281 | checked, because that table is precisely the list of "dependency that would cause a problem at
|
|---|
| 282 | the 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`
|
|---|
| 288 | that violates 3NF. They are not: the 2NF step above already put them in `R_CRYPTO`, keyed
|
|---|
| 289 | directly by `C_ID` (FD4), because FD4 — not the derived `M_CRYPTO_ID → C_SYMBOL` — is what the
|
|---|
| 290 | canonical cover actually contains. `R_MARKETS` itself has no attribute that determines another
|
|---|
| 291 | non-prime attribute of `R_MARKETS`; the transitive dependency is real, but it points *out* of
|
|---|
| 292 | the relation, not within it.
|
|---|
| 293 |
|
|---|
| 294 | The 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
|
|---|
| 297 | non-key attribute set that depends only on their own relation's key, never on the foreign key
|
|---|
| 298 | itself. 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
|
|---|
| 300 | attribute that another non-prime attribute of the *same* relation determines.
|
|---|
| 301 |
|
|---|
| 302 | **Conclusion:** synthesising directly from the canonical cover in the 2NF step already
|
|---|
| 303 | avoided every transitive dependency — there is nothing left to decompose for 3NF. All ten
|
|---|
| 304 | relations 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
|
|---|
| 309 | determinant of every functional dependency that holds on the relation must be a candidate key
|
|---|
| 310 | of 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 |
|
|---|
| 325 | Every determinant in every relation is one of that relation's own candidate keys. **All ten
|
|---|
| 326 | relations are already in BCNF** — the highest of the four normal forms this phase asks for,
|
|---|
| 327 | reached in the same step that fixed 2NF. This is not a coincidence: it happens because the
|
|---|
| 328 | canonical cover already grouped each relation's own key directly against its own attributes
|
|---|
| 329 | with no attribute appearing on the right side of two different relations' dependencies, which
|
|---|
| 330 | is exactly what synthesis from a canonical cover guarantees when, as here, none of the
|
|---|
| 331 | per-cluster functional dependencies overlap.
|
|---|
| 332 |
|
|---|
| 333 | No further decomposition is possible or necessary; splitting any of the ten relations further
|
|---|
| 334 | would only separate attributes that already depend on the *whole* key of a BCNF relation,
|
|---|
| 335 | which cannot fix anything and only costs a join.
|
|---|
| 336 |
|
|---|
| 337 | ## Final result and discussion
|
|---|
| 338 |
|
|---|
| 339 | ### Normalized relational model
|
|---|
| 340 |
|
|---|
| 341 | ```
|
|---|
| 342 | R_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)
|
|---|
| 344 | R_CRYPTO (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT)
|
|---|
| 345 | R_MARKETS (M_ID, M_CRYPTO_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT)
|
|---|
| 346 | R_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)
|
|---|
| 348 | R_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)
|
|---|
| 350 | R_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)
|
|---|
| 352 | R_MARKET_TRADES (MT_ID, MT_MARKET_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY,
|
|---|
| 353 | MT_SIDE, MT_SOURCE)
|
|---|
| 354 | R_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)
|
|---|
| 356 | R_WATCHLISTS (W_ID, W_USER_ID → R_USERS, W_NAME, W_CREATED_AT)
|
|---|
| 357 | R_WATCHLIST_ITEMS(WI_ID, WI_WATCHLIST_ID → R_WATCHLISTS, WI_CRYPTO_ID → R_CRYPTO, WI_ADDED_AT)
|
|---|
| 358 | ```
|
|---|
| 359 |
|
|---|
| 360 | Ten 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,
|
|---|
| 366 | R_MARKET_CANDLES, R_WATCHLISTS, R_WATCHLIST_ITEMS` are, attribute for attribute and key for
|
|---|
| 367 | key, `users, crypto, markets, holdings, orders, transactions, market_trades, market_candles,
|
|---|
| 368 | watchlists, watchlist_items` from
|
|---|
| 369 | [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md). Every foreign key matches,
|
|---|
| 370 | every candidate key matches (including the less obvious composite ones — `{user_id,
|
|---|
| 371 | crypto_id}` on `holdings`, `{crypto_id, quote_currency}` on `markets`, `{market_id, timeframe,
|
|---|
| 372 | candle_time}` on `market_candles`), and the normal form matches (P2 already claimed 3NF; this
|
|---|
| 373 | phase shows the stronger result that the design is actually in BCNF).
|
|---|
| 374 |
|
|---|
| 375 | That is not a coincidence of two people happening to agree — it is what should happen when a
|
|---|
| 376 | design is derived correctly twice by two different methods from the same underlying model:
|
|---|
| 377 | P2 got here by applying the standard ER-to-relational transformation rules (each entity
|
|---|
| 378 | becomes a table on its own key, each attributed M:N relationship becomes a table on the
|
|---|
| 379 | combined key, each attributeless 1:N relationship becomes a foreign key on the "many" side).
|
|---|
| 380 | This phase got here by ignoring that transformation entirely, writing down only the
|
|---|
| 381 | attributes and the functional dependencies they obey, and mechanically applying 2NF/3NF/BCNF
|
|---|
| 382 | synthesis. Landing on the same ten relations either means the P2 transformation rules are
|
|---|
| 383 | sound for this particular model (which they are, for exactly the reason [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation)
|
|---|
| 384 | already argued: single-column UUID primary keys everywhere rule out partial dependencies by
|
|---|
| 385 | construction, and no non-key attribute references another non-key attribute anywhere in the
|
|---|
| 386 | model, which rules out transitive dependencies too), or it is a coincidence spanning ten
|
|---|
| 387 | independently-checked relations and dozens of functional dependencies — the first explanation
|
|---|
| 388 | is 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
|
|---|
| 392 | in `transactions` — kept as a stored column anyway for read performance
|
|---|
| 393 | ([RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation) calls this out
|
|---|
| 394 | explicitly as an accepted denormalisation). Nothing in this phase's functional-dependency
|
|---|
| 395 | analysis can see that `H_AVG_PRICE` is derivable from `T_*` rows rather than stored
|
|---|
| 396 | independently — FD8 (`H_ID → H_AVG_PRICE`) is a perfectly ordinary functional dependency
|
|---|
| 397 | either way, because *derivability from a different relation's rows* is a property of the data
|
|---|
| 398 | and the application logic that maintains it (see
|
|---|
| 399 | [UseCase0004](../P3-UseCaseModel/UseCase0004.md)'s `ON CONFLICT … DO UPDATE`), not something
|
|---|
| 400 | that shows up as a violation of any single-relation normal form. Formal normalization and "no
|
|---|
| 401 | column is a cached computation of other columns" are related but different concerns; this
|
|---|
| 402 | phase only checked the first one.
|
|---|
| 403 |
|
|---|
| 404 | **Which design is used going forward:** P2's, unchanged. Since the two designs coincide
|
|---|
| 405 | exactly, "restructuring the database objects" means confirming there is nothing to change
|
|---|
| 406 | rather than writing new DDL. [`server/db/schema_creation.sql`](../../server/db/schema_creation.sql)
|
|---|
| 407 | already 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
|
|---|
| 413 | change. [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) has been updated with a
|
|---|
| 414 | short note pointing here as the formal validation of its normal-form claim.
|
|---|