| 1 | | == Diagram |
| 2 | | |
| 3 | | [[Image(https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModel_v01.png)]] |
| 4 | | |
| 5 | | == Data requirements |
| 6 | | |
| 7 | | === Entity sets |
| 8 | | |
| 9 | | ==== Users |
| 10 | | Registered participants of the platform. Every action in the simulation is |
| 11 | | attributed to a user, and the two balance attributes are what makes the |
| 12 | | simulation work: cash that is free to trade is tracked separately from cash that |
| 13 | | is currently committed to open positions, so the platform can refuse a purchase |
| 14 | | without having to recompute the whole portfolio first. |
| 15 | | |
| 16 | | - **Candidate keys:** `{id}`, `{username}`, `{email}`. Primary key: **`id`**. |
| 17 | | A surrogate UUID was chosen because it is opaque and stable — `username` and |
| 18 | | `email` are both things a user may legitimately want to change later, and |
| 19 | | every relationship in the diagram points at `Users`, so a mutable key would |
| 20 | | propagate changes across the whole database. |
| 21 | | - **Attributes:** |
| 22 | | - `id` — UUID, required, primary key. |
| 23 | | - `username` — text, max 50, required, unique. |
| 24 | | - `email` — text, max 255, required, unique, must contain `@`. |
| 25 | | - `full_name` — text, max 200, optional. |
| 26 | | - `password_hash` — text, max 255, required. Never the password itself; the |
| 27 | | prototype stores a SHA-256 hex digest. |
| 28 | | - `available_balance` — numeric(18,4), required, default 0, must be ≥ 0. |
| 29 | | - `invested_balance` — numeric(18,4), required, default 0, must be ≥ 0. |
| 30 | | - `created_at` — timestamp with time zone, required, defaults to now. |
| 31 | | - `updated_at` — timestamp with time zone, optional (null until first change). |
| 32 | | |
| 33 | | ==== Cryptos |
| 34 | | The catalog of crypto assets the platform knows about. Kept separate from |
| 35 | | `Markets` because an asset exists independently of the pairs it is traded in — |
| 36 | | the same asset can be quoted against several currencies, and a user's holding is |
| 37 | | in the *asset*, not in a particular pair. |
| 38 | | |
| 39 | | - **Candidate keys:** `{id}`, `{symbol}`. Primary key: **`id`**, for the same |
| 40 | | reason as in `Users`; `symbol` is kept as a unique natural key because that is |
| 41 | | what users type and see. |
| 42 | | - **Attributes:** |
| 43 | | - `id` — UUID, required, primary key. |
| 44 | | - `symbol` — text, max 20, required, unique (e.g. `BTC`). |
| 45 | | - `name` — text, max 255, required (e.g. `Bitcoin`). |
| 46 | | - `created_at` — timestamptz, required, defaults to now. |
| 47 | | |
| 48 | | ==== Markets |
| 49 | | A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is |
| 50 | | where prices live, and it is the thing an order is placed *on*. Modeled as its |
| 51 | | own entity set rather than an attribute of `Cryptos` because a market has its own |
| 52 | | lifecycle — it can be deactivated without deleting the asset — and because |
| 53 | | trades, candles and orders all reference the pair, not the asset. |
| 54 | | |
| 55 | | - **Candidate keys:** `{id}`, `{crypto_id, quote_currency}` — that pair is |
| 56 | | unique by definition, since a given asset can only be quoted once per |
| 57 | | currency. Primary key: **`id`**, so that the many entity sets referencing a |
| 58 | | market carry one narrow column instead of a composite key. |
| 59 | | - **Attributes:** |
| 60 | | - `id` — UUID, required, primary key. |
| 61 | | - `quote_currency` — text, exactly 3 characters, required, default `USD`. |
| 62 | | - `is_active` — boolean, required, default true. Inactive markets are hidden |
| 63 | | from the trading menus but keep their history. |
| 64 | | - `created_at` — timestamptz, required, defaults to now. |
| 65 | | |
| 66 | | ==== Orders |
| 67 | | A user's instruction to buy or sell on a market. Needed as a separate entity set |
| 68 | | because an order is a record of *intent* that outlives its execution: it keeps |
| 69 | | the requested quantity and price even after it has been filled, which is what |
| 70 | | makes the ledger auditable. |
| 71 | | |
| 72 | | - **Candidate keys:** `{id}` only. There is no natural key — the same user can |
| 73 | | place two identical orders on the same market in the same second, and both are |
| 74 | | legitimately distinct. Primary key: **`id`**. |
| 75 | | - **Attributes:** |
| 76 | | - `id` — UUID, required, primary key. |
| 77 | | - `side` — text, required, restricted to `buy` or `sell`. |
| 78 | | - `type` — text, required, restricted to `market` or `limit`. The prototype |
| 79 | | executes only `market` orders; `limit` exists so the model does not have to |
| 80 | | change when limit orders are implemented. |
| 81 | | - `status` — text, required, restricted to `open`, `executed`, `cancelled`. |
| 82 | | - `quantity` — numeric(20,4), required, must be > 0. |
| 83 | | - `price` — numeric(18,6), optional — null for a market order until it fills, |
| 84 | | then the fill price. |
| 85 | | - `placed_at` — timestamptz, required, defaults to now. |
| 86 | | - `executed_at` — timestamptz, optional, set when the order fills. |
| 87 | | |
| 88 | | ==== Transactions |
| 89 | | The financial ledger: every movement of virtual cash, in one place. This exists |
| 90 | | so that a balance is never just a number someone edited — it is the sum of an |
| 91 | | auditable list of entries, which is also what the "explain every step" goal of |
| 92 | | the project needs. |
| 93 | | |
| 94 | | - **Candidate keys:** `{id}` only. Primary key: **`id`**. |
| 95 | | - **Attributes:** |
| 96 | | - `id` — UUID, required, primary key. |
| 97 | | - `type` — text, required, restricted to `deposit`, `buy`, `sell`, `fee`. |
| 98 | | - `amount` — numeric(18,4), required. Signed: negative for money leaving the |
| 99 | | cash balance, positive for money arriving. |
| 100 | | - `currency` — text, exactly 3 characters, required, default `USD`. |
| 101 | | - `created_at` — timestamptz, required, defaults to now. |
| 102 | | - `description` — text, optional, free-form human-readable explanation. |
| 103 | | |
| 104 | | ==== MarketTrades |
| 105 | | Individual executed trades on a market, from the user's own fills and from the |
| 106 | | market simulator. This is the single source of truth for the current price: the |
| 107 | | price of a market is the price of its most recent trade, never a column someone |
| 108 | | writes directly. |
| 109 | | |
| 110 | | - **Candidate keys:** `{id}`. In principle `{market_id, executed_at}` looks |
| 111 | | unique, but two trades can share a timestamp, so it is not a safe key. |
| 112 | | Primary key: **`id`** (a plain auto-incrementing integer here rather than a |
| 113 | | UUID, because this is the highest-volume entity set and it is only ever read |
| 114 | | in timestamp order, never referenced by anything else). |
| 115 | | - **Attributes:** |
| 116 | | - `id` — integer, required, primary key, auto-generated. |
| 117 | | - `executed_at` — timestamptz, required. |
| 118 | | - `price` — numeric(18,6), required, must be > 0. |
| 119 | | - `quantity` — numeric(20,6), required, must be > 0. |
| 120 | | - `side` — text, optional, `buy` or `sell`. |
| 121 | | - `source` — text, max 50, required, default `simulation`. Distinguishes a |
| 122 | | simulated trade from a user's own fill (`user`). |
| 123 | | |
| 124 | | ===== MarketCandles |
| 125 | | OHLCV aggregates per market and timeframe — the data a price chart is drawn |
| 126 | | from. Stored rather than computed on the fly because the point of the project is |
| 127 | | a chart-driven interface, and re-aggregating the whole trade history for every |
| 128 | | screen refresh does not scale. |
| 129 | | |
| 130 | | - **Candidate keys:** `{id}`, and `{market_id, timeframe, candle_time}` — a |
| 131 | | market has exactly one candle per timeframe per time bucket. Primary key: |
| 132 | | **`id`**; the composite is enforced as a uniqueness rule because it is the |
| 133 | | real-world constraint and it is what prevents duplicate candles. |
| 134 | | - **Attributes:** |
| 135 | | - `id` — integer, required, primary key, auto-generated. |
| 136 | | - `timeframe` — text, required, restricted to `1m`, `5m`, `1h`, `1d`. |
| 137 | | - `open`, `high`, `low`, `close` — numeric(18,6), all required. |
| 138 | | - `volume` — numeric(20,6), required. |
| 139 | | - `candle_time` — timestamptz, required — the start of the bucket. |
| 140 | | |
| 141 | | ===== Watchlists |
| 142 | | A named list of assets a user wants to monitor. A separate entity set rather than |
| 143 | | a flag on the relationship between users and assets, because a user may want |
| 144 | | several lists ("long term", "watching today") and each needs its own name. |
| 145 | | |
| 146 | | - **Candidate keys:** `{id}`. `{user_id, name}` would also work if list names |
| 147 | | are required to be unique per user; the model does not impose that, so it is |
| 148 | | not listed as a candidate key. Primary key: **`id`**. |
| 149 | | - **Attributes:** |
| 150 | | - `id` — UUID, required, primary key. |
| 151 | | - `name` — text, max 100, required. |
| 152 | | - `created_at` — timestamptz, required, defaults to now. |
| 153 | | |
| 154 | | ==== Relationships |
| 155 | | |
| 156 | | ===== QuotedOn — Cryptos (1) : Markets (N), total on Markets |
| 157 | | Ties a market to the asset it trades. One asset can be quoted in many markets; |
| 158 | | every market must have exactly one asset, hence total participation on the |
| 159 | | `Markets` side. No attributes of its own. |
| 160 | | |
| 161 | | ===== PlacedOn — Markets (1) : Orders (N), total on Orders |
| 162 | | Records which market an order was placed on. Every order must name a market; |
| 163 | | a market may have no orders yet. No attributes. |
| 164 | | |
| 165 | | ===== Places — Users (1) : Orders (N), total on Orders |
| 166 | | Records who placed an order. Every order belongs to exactly one user; a new user |
| 167 | | has no orders. No attributes. |
| 168 | | |
| 169 | | ===== Records — Users (1) : Transactions (N), total on Transactions |
| 170 | | Attributes each ledger entry to a user. Every entry belongs to exactly one user. |
| 171 | | No attributes. |
| 172 | | |
| 173 | | ===== Settles — Orders (1) : Transactions (N), partial on both sides |
| 174 | | Links a ledger entry to the order that caused it. Partial on the `Transactions` |
| 175 | | side because deposits have no originating order, and partial on the `Orders` side |
| 176 | | because an order that never executes never produces a ledger entry. This is why |
| 177 | | the corresponding column is nullable in P2. No attributes. |
| 178 | | |
| 179 | | ===== Fills — Markets (1) : MarketTrades (N), total on MarketTrades |
| 180 | | Every executed trade happened on exactly one market. No attributes. |
| 181 | | |
| 182 | | ===== Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles |
| 183 | | Every candle summarises trades of exactly one market. No attributes. |
| 184 | | |
| 185 | | ===== Owns — Users (1) : Watchlists (N), total on Watchlists |
| 186 | | Every watchlist belongs to exactly one user. No attributes. |
| 187 | | |
| 188 | | ===== Holds — Users (M) : Cryptos (N), partial on both sides, **with attributes** |
| 189 | | A user's position in an asset. M:N because one user holds many assets and one |
| 190 | | asset is held by many users, and partial on both sides because a user may hold |
| 191 | | nothing and an asset may be held by nobody. Modeled as a relationship rather |
| 192 | | than an entity set because a position has no identity of its own — it is |
| 193 | | entirely described by *which user*, *which asset*, and how much. |
| 194 | | |
| 195 | | - **Attributes:** |
| 196 | | - `quantity` — numeric(20,4), required, must be ≥ 0. |
| 197 | | - `avg_price` — numeric(18,6), required, ≥ 0, **derived** (dashed ellipse): |
| 198 | | the weighted average of the prices at which the position was accumulated. |
| 199 | | It is derivable from the buy history, and is stored anyway so that |
| 200 | | unrealised P/L can be shown without replaying the whole ledger. |
| 201 | | - `created_at` — timestamptz, required, defaults to now. |
| 202 | | - `updated_at` — timestamptz, optional. |
| 203 | | |
| 204 | | ===== Contains — Watchlists (M) : Cryptos (N), partial on both sides, **with attribute** |
| 205 | | Which assets are on which watchlist. M:N: a list holds many assets, an asset |
| 206 | | appears on many lists. Partial on both sides — an empty list is valid and an |
| 207 | | asset need not be on any list. |
| 208 | | |
| 209 | | - **Attributes:** |
| 210 | | - `added_at` — timestamptz, required, defaults to now. Recorded so a list can |
| 211 | | be shown in the order the user built it. |
| 212 | | |
| 213 | | === Entity-Relationship Model History |
| 214 | | |
| 215 | | - **v01** — First complete version. Built from the entity notes in |
| 216 | | [`ep-diagram.md`](ep-diagram.md) (the initial hand-written model), with three |
| 217 | | changes made to that initial model while drawing it: |
| 218 | | 1. `Markets` was promoted from an implied attribute of the asset to its own |
| 219 | | entity set, so that prices, orders, trades and candles can all reference a |
| 220 | | pair rather than an asset. |
| 221 | | 2. `holdings` and `watchlist_items` were re-expressed as the M:N relationships |
| 222 | | `Holds` and `Contains` with their own attributes, instead of entity sets |
| 223 | | with foreign keys — the initial notes listed them as tables, which is a |
| 224 | | relational concept that does not belong in a Chen ERD. |
| 225 | | 3. `avg_price` was marked as a derived attribute rather than a plain one, to |
| 226 | | make the denormalisation explicit rather than hidden. |
| 227 | | |
| 228 | | Reasoning for the AI-assisted part of this phase, and the full interaction log, |
| 229 | | are on https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md. |
| | 1 | == Normalization |
| | 2 | |
| | 3 | This phase deliberately ignores the design from |
| | 4 | [ERModel](../P1-ConceptualModel/ERModel.md) (P1) and |
| | 5 | [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) (P2) as a |
| | 6 | starting point. |
| | 7 | |
| | 8 | Instead, it starts from a single flat relation containing every attribute of the |
| | 9 | model, derives the functional dependencies that hold on that relation, and |
| | 10 | decomposes it formally using Armstrong's axioms and the standard definitions of |
| | 11 | 1NF, 2NF, 3NF and BCNF. |
| | 12 | |
| | 13 | The [Final result and discussion](#final-result-and-discussion) section then |
| | 14 | compares the result of this independent normalization process with the P2 |
| | 15 | relational design. |
| | 16 | |
| | 17 | == De-normalized database form |
| | 18 | |
| | 19 | === Building one relation from the whole model |
| | 20 | |
| | 21 | The ER model contains ten entity/relationship sets carrying attributes: |
| | 22 | |
| | 23 | * `Users` |
| | 24 | * `Cryptos` |
| | 25 | * `Markets` |
| | 26 | * `Holds` |
| | 27 | * `Orders` |
| | 28 | * `Transactions` |
| | 29 | * `MarketTrades` |
| | 30 | * `MarketCandles` |
| | 31 | * `Watchlists` |
| | 32 | * `Contains` |
| | 33 | |
| | 34 | Eight additional relationships (`QuotedOn`, `PlacedOn`, `Places`, `Records`, |
| | 35 | `Settles`, `Fills`, `Aggregates`, `Owns`) carry no attributes of their own. |
| | 36 | |
| | 37 | In Chen notation these relationships require no attributes because the |
| | 38 | relationship itself represents the connection between entity sets. A single flat |
| | 39 | relation has no such mechanism, so the referenced key must be represented by an |
| | 40 | ordinary attribute. The following foreign-key-style attributes are therefore |
| | 41 | included in the flat relation: |
| | 42 | |
| | 43 | * `M_CRYPTO_ID` |
| | 44 | * `O_USER_ID` |
| | 45 | * `O_MARKET_ID` |
| | 46 | * `T_USER_ID` |
| | 47 | * `T_RELATED_ORDER` |
| | 48 | * `MT_MARKET_ID` |
| | 49 | * `MC_MARKET_ID` |
| | 50 | * `W_USER_ID` |
| | 51 | * `H_USER_ID` |
| | 52 | * `H_CRYPTO_ID` |
| | 53 | * `WI_WATCHLIST_ID` |
| | 54 | * `WI_CRYPTO_ID` |
| | 55 | |
| | 56 | These attributes are not copied from the P2 schema as a design decision. They |
| | 57 | are required because a flat relation cannot otherwise preserve the links |
| | 58 | expressed by the original ER relationships. |
| | 59 | |
| | 60 | Because names such as `id`, `created_at`, `quantity`, `type`, `name`, `price` |
| | 61 | and `side` occur in more than one entity or relationship set, every attribute |
| | 62 | is prefixed with a short code identifying its origin. |
| | 63 | |
| | 64 | | Prefix | Origin | Attributes | |
| | 65 | | ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | |
| | 66 | | `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` | |
| | 67 | | `C_` | Cryptos | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | |
| | 68 | | `M_` | Markets + `QuotedOn` | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | |
| | 69 | | `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` | |
| | 70 | | `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` | |
| | 71 | | `T_` | Transactions + `Records`, `Settles` | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | |
| | 72 | | `MT_` | MarketTrades + `Fills` | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | |
| | 73 | | `MC_` | MarketCandles + `Aggregates` | `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | |
| | 74 | | `W_` | Watchlists + `Owns` | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | |
| | 75 | | `WI_` | Contains + surrogate key | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | |
| | 76 | |
| | 77 | `H_ID` and `WI_ID` are included because `Holds` and `Contains` are M:N |
| | 78 | relationships with attributes and therefore require their own identifying |
| | 79 | attribute in the relational representation. |
| | 80 | |
| | 81 | The resulting flat relation is: |
| | 82 | |
| | 83 | ## [source,sql] |
| | 84 | |
| | 85 | R_EDUBERZA( |
| | 86 | U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, |
| | 87 | U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT, |
| | 88 | |
| | 89 | C_ID, C_SYMBOL, C_NAME, C_CREATED_AT, |
| | 90 | |
| | 91 | M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, |
| | 92 | |
| | 93 | H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, |
| | 94 | H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, |
| | 95 | |
| | 96 | O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, |
| | 97 | O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, |
| | 98 | |
| | 99 | T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, |
| | 100 | T_CREATED_AT, T_DESCRIPTION, |
| | 101 | |
| | 102 | MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, |
| | 103 | MT_SIDE, MT_SOURCE, |
| | 104 | |
| | 105 | MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, |
| | 106 | MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, |
| | 107 | |
| | 108 | W_ID, W_USER_ID, W_NAME, W_CREATED_AT, |
| | 109 | |
| | 110 | WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT |
| | 111 | ) |
| | 112 | - |
| | 113 | |
| | 114 | This relation contains 68 attributes. |
| | 115 | |
| | 116 | Every attribute is single-valued and atomic: balances, timestamps, identifiers, |
| | 117 | symbols, amounts and similar values are all scalar values rather than lists or |
| | 118 | nested records. |
| | 119 | |
| | 120 | Therefore `R_EDUBERZA` satisfies 1NF as written. The remaining sections |
| | 121 | determine whether it satisfies 2NF, 3NF or BCNF. |
| | 122 | |
| | 123 | == Functional dependencies |
| | 124 | |
| | 125 | === Canonical cover |
| | 126 | |
| | 127 | The initial functional dependencies are read directly from the identifying |
| | 128 | constraints of the entity and relationship sets. |
| | 129 | |
| | 130 | Each entity's or relationship's key determines the attributes belonging to that |
| | 131 | entity or relationship, while unique candidate keys determine the corresponding |
| | 132 | primary key. |
| | 133 | |
| | 134 | | # | Functional dependency | Source | |
| | 135 | | ---- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | |
| | 136 | | 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 | |
| | 137 | | FD2 | `U_USERNAME -> U_ID` | Users (`UNIQUE(username)`) | |
| | 138 | | FD3 | `U_EMAIL -> U_ID` | Users (`UNIQUE(email)`) | |
| | 139 | | FD4 | `C_ID -> C_SYMBOL, C_NAME, C_CREATED_AT` | Cryptos | |
| | 140 | | FD5 | `C_SYMBOL -> C_ID` | Cryptos (`UNIQUE(symbol)`) | |
| | 141 | | FD6 | `M_ID -> M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | Markets | |
| | 142 | | FD7 | `{M_CRYPTO_ID, M_QUOTE_CURRENCY} -> M_ID` | Markets (`UNIQUE(crypto_id, quote_currency)`) | |
| | 143 | | FD8 | `H_ID -> H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | Holds | |
| | 144 | | FD9 | `{H_USER_ID, H_CRYPTO_ID} -> H_ID` | Holds (`UNIQUE(user_id, crypto_id)`) | |
| | 145 | | 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 | |
| | 146 | | FD11 | `T_ID -> T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | Transactions | |
| | 147 | | FD12 | `MT_ID -> MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | MarketTrades | |
| | 148 | | FD13 | `MC_ID -> MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | MarketCandles | |
| | 149 | | FD14 | `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME} -> MC_ID` | MarketCandles (`UNIQUE(market_id, timeframe, candle_time)`) | |
| | 150 | | FD15 | `W_ID -> W_USER_ID, W_NAME, W_CREATED_AT` | Watchlists | |
| | 151 | | FD16 | `WI_ID -> WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | Contains | |
| | 152 | | FD17 | `{WI_WATCHLIST_ID, WI_CRYPTO_ID} -> WI_ID` | Contains (`UNIQUE(watchlist_id, crypto_id)`) | |
| | 153 | |
| | 154 | No dependency above contains an extraneous attribute on its determinant, and the |
| | 155 | dependent attributes are assigned to the entity or relationship that actually |
| | 156 | owns them. |
| | 157 | |
| | 158 | === Minimality of the composite dependencies |
| | 159 | |
| | 160 | The composite determinants in FD7, FD9, FD14 and FD17 are minimal. |
| | 161 | |
| | 162 | For example, in FD7: |
| | 163 | |
| | 164 | `{M_CRYPTO_ID, M_QUOTE_CURRENCY} -> M_ID` |
| | 165 | |
| | 166 | `M_CRYPTO_ID` alone does not determine `M_ID`, because the same crypto may be |
| | 167 | traded in several quote currencies. `M_QUOTE_CURRENCY` alone also does not |
| | 168 | determine `M_ID`, because many different crypto assets may use the same quote |
| | 169 | currency. |
| | 170 | |
| | 171 | Therefore neither attribute can be removed from the determinant. |
| | 172 | |
| | 173 | The same reasoning applies to: |
| | 174 | |
| | 175 | * FD9: `{H_USER_ID, H_CRYPTO_ID} -> H_ID` |
| | 176 | * FD14: `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME} -> MC_ID` |
| | 177 | * FD17: `{WI_WATCHLIST_ID, WI_CRYPTO_ID} -> WI_ID` |
| | 178 | |
| | 179 | Their left-hand sides are exactly the uniqueness constraints of the corresponding |
| | 180 | relations. |
| | 181 | |
| | 182 | === Dependencies carried by foreign keys |
| | 183 | |
| | 184 | The flat relation also carries dependencies induced by foreign keys. |
| | 185 | |
| | 186 | Because a referencing attribute takes its values from the candidate-key domain of |
| | 187 | another entity, the attributes determined by that referenced key are also fixed |
| | 188 | for the corresponding foreign-key value. |
| | 189 | |
| | 190 | | Foreign key | References | Therefore determines | |
| | 191 | | ----------------- | ---------- | ------------------------------------------------------------------------ | |
| | 192 | | `M_CRYPTO_ID` | `C_ID` | `C_SYMBOL, C_NAME, C_CREATED_AT` | |
| | 193 | | `H_USER_ID` | `U_ID` | all `U_*` attributes | |
| | 194 | | `H_CRYPTO_ID` | `C_ID` | all `C_*` attributes | |
| | 195 | | `O_USER_ID` | `U_ID` | all `U_*` attributes | |
| | 196 | | `O_MARKET_ID` | `M_ID` | all `M_*` attributes, and transitively `C_*` | |
| | 197 | | `T_USER_ID` | `U_ID` | all `U_*` attributes | |
| | 198 | | `T_RELATED_ORDER` | `O_ID` | all `O_*` attributes, and transitively `U_*`, `M_*`, `C_*` when not null | |
| | 199 | | `MT_MARKET_ID` | `M_ID` | all `M_*` attributes, and transitively `C_*` | |
| | 200 | | `MC_MARKET_ID` | `M_ID` | all `M_*` attributes, and transitively `C_*` | |
| | 201 | | `W_USER_ID` | `U_ID` | all `U_*` attributes | |
| | 202 | | `WI_WATCHLIST_ID` | `W_ID` | all `W_*` attributes, and transitively `U_*` | |
| | 203 | | `WI_CRYPTO_ID` | `C_ID` | all `C_*` attributes | |
| | 204 | |
| | 205 | These dependencies are not added as separate entries to the canonical cover. |
| | 206 | They are derivable from FD1-FD17 together with the foreign-key constraints and |
| | 207 | transitivity. |
| | 208 | |
| | 209 | They are nevertheless important during the 3NF analysis because they are the |
| | 210 | source of the transitive dependencies that would exist if the referenced |
| | 211 | attributes were copied into the referencing relation. |
| | 212 | |
| | 213 | == Candidate keys and primary key |
| | 214 | |
| | 215 | === Candidate-key structure of `R_EDUBERZA` |
| | 216 | |
| | 217 | The entity and relationship clusters in `R_EDUBERZA` are structurally |
| | 218 | independent. |
| | 219 | |
| | 220 | For example, an order identifier tells us nothing about which unrelated |
| | 221 | transaction, watchlist item, holding or market-candle tuple happens to coexist |
| | 222 | in the same flat row. A user may have zero orders, zero holdings, zero |
| | 223 | transactions, and zero watchlists, so none of those identifiers can be inferred |
| | 224 | from another cluster. |
| | 225 | |
| | 226 | The nullable `T_RELATED_ORDER` attribute also cannot serve as a universal bridge |
| | 227 | between transactions and orders. |
| | 228 | |
| | 229 | Therefore a key for `R_EDUBERZA` must contain one determinant from each |
| | 230 | independent cluster. |
| | 231 | |
| | 232 | === Chosen primary key |
| | 233 | |
| | 234 | The chosen key is: |
| | 235 | |
| | 236 | ## [source,text] |
| | 237 | |
| | 238 | ## {U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID} |
| | 239 | |
| | 240 | === Closure check |
| | 241 | |
| | 242 | Applying FD1-FD17 to this attribute set gives the following closure: |
| | 243 | |
| | 244 | | Step | Attributes added | Dependency | |
| | 245 | | ----- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | |
| | 246 | | Start | `U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID` | — | |
| | 247 | | 1 | `U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | FD1 | |
| | 248 | | 2 | `C_SYMBOL, C_NAME, C_CREATED_AT` | FD4 | |
| | 249 | | 3 | `M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | FD6 | |
| | 250 | | 4 | `H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | FD8 | |
| | 251 | | 5 | `O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | FD10 | |
| | 252 | | 6 | `T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | FD11 | |
| | 253 | | 7 | `MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | FD12 | |
| | 254 | | 8 | `MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | FD13 | |
| | 255 | | 9 | `W_USER_ID, W_NAME, W_CREATED_AT` | FD15 | |
| | 256 | | 10 | `WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | FD16 | |
| | 257 | |
| | 258 | The closure contains all 68 attributes, so the set is a superkey. |
| | 259 | |
| | 260 | Removing any one of the ten identifier attributes leaves an entire independent |
| | 261 | cluster unreachable. Therefore the key is minimal and is a candidate key. |
| | 262 | |
| | 263 | === Other candidate keys |
| | 264 | |
| | 265 | The chosen primary key is not the only candidate key. |
| | 266 | |
| | 267 | Any determinant that uniquely identifies one cluster can replace that cluster's |
| | 268 | surrogate identifier: |
| | 269 | |
| | 270 | * `U_USERNAME` or `U_EMAIL` can replace `U_ID`. |
| | 271 | * `C_SYMBOL` can replace `C_ID`. |
| | 272 | * `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` can replace `M_ID`. |
| | 273 | * `{H_USER_ID, H_CRYPTO_ID}` can replace `H_ID`. |
| | 274 | * `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` can replace `MC_ID`. |
| | 275 | * `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` can replace `WI_ID`. |
| | 276 | |
| | 277 | This produces: |
| | 278 | |
| | 279 | `3 x 2 x 2 x 2 x 1 x 1 x 1 x 2 x 1 x 2 = 96` |
| | 280 | |
| | 281 | candidate keys. |
| | 282 | |
| | 283 | The all-surrogate-ID combination is chosen as the primary key because the |
| | 284 | surrogate identifiers are opaque, stable and independent of user-facing values |
| | 285 | such as usernames, e-mail addresses and symbols. |
| | 286 | |
| | 287 | === Normal form before decomposition |
| | 288 | |
| | 289 | `R_EDUBERZA` is in 1NF only. |
| | 290 | |
| | 291 | It is not in 2NF because non-prime attributes depend on proper subsets of its |
| | 292 | ten-attribute candidate key. Since 2NF is a precondition for both 3NF and BCNF, |
| | 293 | the relation is also not in 3NF or BCNF. |
| | 294 | |
| | 295 | == 1NF decomposition |
| | 296 | |
| | 297 | No decomposition is necessary for 1NF. |
| | 298 | |
| | 299 | `R_EDUBERZA` already contains only atomic, single-valued attributes and has no |
| | 300 | repeating groups. |
| | 301 | |
| | 302 | The real decomposition therefore begins with the 2NF analysis. |
| | 303 | |
| | 304 | == 2NF decomposition |
| | 305 | |
| | 306 | === Relation analyzed |
| | 307 | |
| | 308 | `R_EDUBERZA`, containing all 68 attributes, with primary key |
| | 309 | |
| | 310 | `{U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID}`. |
| | 311 | |
| | 312 | FD1-FD17 are in force. |
| | 313 | |
| | 314 | === 2NF violations |
| | 315 | |
| | 316 | 2NF forbids a non-prime attribute from depending on a proper subset of a |
| | 317 | candidate key. |
| | 318 | |
| | 319 | Every dependency in the canonical cover has a determinant smaller than the |
| | 320 | ten-attribute key: |
| | 321 | |
| | 322 | * `U_ID` |
| | 323 | * `C_ID` |
| | 324 | * `M_ID` |
| | 325 | * `H_ID` |
| | 326 | * `O_ID` |
| | 327 | * `T_ID` |
| | 328 | * `MT_ID` |
| | 329 | * `MC_ID` |
| | 330 | * `W_ID` |
| | 331 | * `WI_ID` |
| | 332 | * or one of the corresponding composite candidate keys. |
| | 333 | |
| | 334 | Therefore every non-prime attribute in `R_EDUBERZA` is involved in a partial |
| | 335 | dependency. |
| | 336 | |
| | 337 | This is not a small collection of isolated violations. The entire flat relation |
| | 338 | violates 2NF because ten independent record types were artificially combined |
| | 339 | under one composite key. |
| | 340 | |
| | 341 | === Decomposition by 3NF/BCNF synthesis |
| | 342 | |
| | 343 | Since a canonical cover is already available, the decomposition uses the |
| | 344 | standard synthesis approach rather than repeatedly applying binary |
| | 345 | decomposition. |
| | 346 | |
| | 347 | Grouping the functional dependencies by determinant produces ten relations: |
| | 348 | |
| | 349 | | New relation | Attributes | Candidate keys | Source FDs | |
| | 350 | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | ---------- | |
| | 351 | | `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-FD3 | |
| | 352 | | `R_CRYPTO` | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | `C_ID`, `C_SYMBOL` | FD4-FD5 | |
| | 353 | | `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 | |
| | 354 | | `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 | |
| | 355 | | `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 | |
| | 356 | | `R_TRANSACTIONS` | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | `T_ID` | FD11 | |
| | 357 | | `R_MARKET_TRADES` | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | `MT_ID` | FD12 | |
| | 358 | | `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 | |
| | 359 | | `R_WATCHLISTS` | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | `W_ID` | FD15 | |
| | 360 | | `R_WATCHLIST_ITEMS` | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | FD16-FD17 | |
| | 361 | |
| | 362 | Every non-prime attribute now depends on the whole key of its own relation, so |
| | 363 | the 2NF violations have been removed. |
| | 364 | |
| | 365 | === Dependency preservation |
| | 366 | |
| | 367 | FD1-FD17 form the canonical cover of `R_EDUBERZA`. |
| | 368 | |
| | 369 | Every dependency is contained completely within one of the ten new relations. |
| | 370 | Therefore the union of the dependencies preserved by the decomposed relations |
| | 371 | is again FD1-FD17. |
| | 372 | |
| | 373 | No functional dependency from the canonical cover is lost. |
| | 374 | |
| | 375 | === Lossless join |
| | 376 | |
| | 377 | The resulting relations are connected through foreign-key/primary-key pairs. |
| | 378 | |
| | 379 | The important joins are: |
| | 380 | |
| | 381 | * `R_MARKETS.M_CRYPTO_ID -> R_CRYPTO.C_ID` |
| | 382 | * `R_HOLDINGS.H_USER_ID -> R_USERS.U_ID` |
| | 383 | * `R_HOLDINGS.H_CRYPTO_ID -> R_CRYPTO.C_ID` |
| | 384 | * `R_ORDERS.O_USER_ID -> R_USERS.U_ID` |
| | 385 | * `R_ORDERS.O_MARKET_ID -> R_MARKETS.M_ID` |
| | 386 | * `R_TRANSACTIONS.T_USER_ID -> R_USERS.U_ID` |
| | 387 | * `R_TRANSACTIONS.T_RELATED_ORDER -> R_ORDERS.O_ID` |
| | 388 | * `R_MARKET_TRADES.MT_MARKET_ID -> R_MARKETS.M_ID` |
| | 389 | * `R_MARKET_CANDLES.MC_MARKET_ID -> R_MARKETS.M_ID` |
| | 390 | * `R_WATCHLISTS.W_USER_ID -> R_USERS.U_ID` |
| | 391 | * `R_WATCHLIST_ITEMS.WI_WATCHLIST_ID -> R_WATCHLISTS.W_ID` |
| | 392 | * `R_WATCHLIST_ITEMS.WI_CRYPTO_ID -> R_CRYPTO.C_ID` |
| | 393 | |
| | 394 | For each such join, the shared attribute on the referenced side is a candidate |
| | 395 | key of that relation. |
| | 396 | |
| | 397 | Consequently, joining the relations through these foreign-key/primary-key |
| | 398 | pairs is lossless: no original tuples are lost and no spurious tuples are |
| | 399 | introduced. |
| | 400 | |
| | 401 | == 3NF decomposition |
| | 402 | |
| | 403 | === Relations analyzed |
| | 404 | |
| | 405 | The ten relations produced by the 2NF synthesis are checked individually for |
| | 406 | 3NF. |
| | 407 | |
| | 408 | The main question is whether a non-prime attribute is transitively dependent on |
| | 409 | a candidate key through another non-prime attribute. |
| | 410 | |
| | 411 | The foreign-key-carried dependencies from |
| | 412 | [Dependencies carried by foreign keys](#dependencies-carried-by-foreign-keys) |
| | 413 | are considered here because they are exactly the dependencies that could create |
| | 414 | transitive relationships. |
| | 415 | |
| | 416 | === Worked example: `R_MARKETS` |
| | 417 | |
| | 418 | `R_MARKETS` contains: |
| | 419 | |
| | 420 | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` |
| | 421 | |
| | 422 | The relation has: |
| | 423 | |
| | 424 | `M_ID -> M_CRYPTO_ID` |
| | 425 | |
| | 426 | and, through the foreign-key dependency: |
| | 427 | |
| | 428 | `M_CRYPTO_ID -> C_SYMBOL, C_NAME, C_CREATED_AT` |
| | 429 | |
| | 430 | Therefore, if the crypto attributes were also stored inside `R_MARKETS`, the |
| | 431 | following transitive dependency would exist: |
| | 432 | |
| | 433 | `M_ID -> M_CRYPTO_ID -> C_SYMBOL` |
| | 434 | |
| | 435 | That would violate 3NF. |
| | 436 | |
| | 437 | The crypto attributes are not stored there. They are already isolated in |
| | 438 | `R_CRYPTO`, where: |
| | 439 | |
| | 440 | `C_ID -> C_SYMBOL, C_NAME, C_CREATED_AT` |
| | 441 | |
| | 442 | Therefore the transitive dependency points to another relation rather than |
| | 443 | existing between attributes of `R_MARKETS` itself. |
| | 444 | |
| | 445 | === Remaining relations |
| | 446 | |
| | 447 | The same reasoning applies to all other foreign keys: |
| | 448 | |
| | 449 | * `H_USER_ID` and `H_CRYPTO_ID` |
| | 450 | * `O_USER_ID` and `O_MARKET_ID` |
| | 451 | * `T_USER_ID` and `T_RELATED_ORDER` |
| | 452 | * `MT_MARKET_ID` |
| | 453 | * `MC_MARKET_ID` |
| | 454 | * `W_USER_ID` |
| | 455 | * `WI_WATCHLIST_ID` and `WI_CRYPTO_ID` |
| | 456 | |
| | 457 | In every case, the referenced attributes are stored in their own relation. |
| | 458 | |
| | 459 | Within each individual relation, there is no non-prime attribute that determines |
| | 460 | another non-prime attribute. |
| | 461 | |
| | 462 | Therefore no further decomposition is required for 3NF. |
| | 463 | |
| | 464 | === Conclusion |
| | 465 | |
| | 466 | The ten relations produced by the 2NF synthesis are already in 3NF. |
| | 467 | |
| | 468 | No additional decomposition is necessary. |
| | 469 | |
| | 470 | == BCNF if possible |
| | 471 | |
| | 472 | === BCNF criterion |
| | 473 | |
| | 474 | BCNF is stricter than 3NF. |
| | 475 | |
| | 476 | For every non-trivial functional dependency `X -> Y` that holds in a relation, |
| | 477 | `X` must be a candidate key of that relation. |
| | 478 | |
| | 479 | The ten relations are therefore checked against all dependencies that hold within |
| | 480 | each relation. |
| | 481 | |
| | 482 | | Relation | Functional dependencies | Determinant | Candidate key? | |
| | 483 | | ------------------- | ----------------------- | ------------------------------------------------------- | --------------- | |
| | 484 | | `R_USERS` | FD1, FD2, FD3 | `U_ID`, `U_USERNAME`, `U_EMAIL` | Yes — all three | |
| | 485 | | `R_CRYPTO` | FD4, FD5 | `C_ID`, `C_SYMBOL` | Yes — both | |
| | 486 | | `R_MARKETS` | FD6, FD7 | `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` | Yes — both | |
| | 487 | | `R_HOLDINGS` | FD8, FD9 | `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` | Yes — both | |
| | 488 | | `R_ORDERS` | FD10 | `O_ID` | Yes | |
| | 489 | | `R_TRANSACTIONS` | FD11 | `T_ID` | Yes | |
| | 490 | | `R_MARKET_TRADES` | FD12 | `MT_ID` | Yes | |
| | 491 | | `R_MARKET_CANDLES` | FD13, FD14 | `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | Yes — both | |
| | 492 | | `R_WATCHLISTS` | FD15 | `W_ID` | Yes | |
| | 493 | | `R_WATCHLIST_ITEMS` | FD16, FD17 | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | Yes — both | |
| | 494 | |
| | 495 | Every determinant is therefore a candidate key of its own relation. |
| | 496 | |
| | 497 | === Conclusion |
| | 498 | |
| | 499 | All ten relations satisfy BCNF. |
| | 500 | |
| | 501 | No further decomposition is necessary. |
| | 502 | |
| | 503 | Splitting any of these relations further would not resolve a normal-form |
| | 504 | violation because every determinant already identifies a complete candidate key. |
| | 505 | |
| | 506 | The result is therefore stronger than a 3NF-only decomposition: the final |
| | 507 | relations are in BCNF. |
| | 508 | |
| | 509 | == Final result and discussion |
| | 510 | |
| | 511 | === Normalized relational model |
| | 512 | |
| | 513 | The normalized schema is: |
| | 514 | |
| | 515 | ## [source,text] |
| | 516 | |
| | 517 | R_USERS( |
| | 518 | U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, |
| | 519 | U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT |
| | 520 | ) |
| | 521 | |
| | 522 | R_CRYPTO( |
| | 523 | C_ID, C_SYMBOL, C_NAME, C_CREATED_AT |
| | 524 | ) |
| | 525 | |
| | 526 | R_MARKETS( |
| | 527 | M_ID, |
| | 528 | M_CRYPTO_ID -> R_CRYPTO, |
| | 529 | M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT |
| | 530 | ) |
| | 531 | |
| | 532 | R_HOLDINGS( |
| | 533 | H_ID, |
| | 534 | H_USER_ID -> R_USERS, |
| | 535 | H_CRYPTO_ID -> R_CRYPTO, |
| | 536 | H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, |
| | 537 | H_CREATED_AT, H_UPDATED_AT |
| | 538 | ) |
| | 539 | |
| | 540 | R_ORDERS( |
| | 541 | O_ID, |
| | 542 | O_USER_ID -> R_USERS, |
| | 543 | O_MARKET_ID -> R_MARKETS, |
| | 544 | O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, |
| | 545 | O_PLACED_AT, O_EXECUTED_AT |
| | 546 | ) |
| | 547 | |
| | 548 | R_TRANSACTIONS( |
| | 549 | T_ID, |
| | 550 | T_USER_ID -> R_USERS, |
| | 551 | T_TYPE, T_AMOUNT, T_CURRENCY, |
| | 552 | T_RELATED_ORDER -> R_ORDERS, |
| | 553 | T_CREATED_AT, T_DESCRIPTION |
| | 554 | ) |
| | 555 | |
| | 556 | R_MARKET_TRADES( |
| | 557 | MT_ID, |
| | 558 | MT_MARKET_ID -> R_MARKETS, |
| | 559 | MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE |
| | 560 | ) |
| | 561 | |
| | 562 | R_MARKET_CANDLES( |
| | 563 | MC_ID, |
| | 564 | MC_MARKET_ID -> R_MARKETS, |
| | 565 | MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, |
| | 566 | MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME |
| | 567 | ) |
| | 568 | |
| | 569 | R_WATCHLISTS( |
| | 570 | W_ID, |
| | 571 | W_USER_ID -> R_USERS, |
| | 572 | W_NAME, W_CREATED_AT |
| | 573 | ) |
| | 574 | |
| | 575 | R_WATCHLIST_ITEMS( |
| | 576 | WI_ID, |
| | 577 | WI_WATCHLIST_ID -> R_WATCHLISTS, |
| | 578 | WI_CRYPTO_ID -> R_CRYPTO, |
| | 579 | WI_ADDED_AT |
| | 580 | ) |
| | 581 | - |
| | 582 | |
| | 583 | The result contains ten relations connected through eleven foreign keys. |
| | 584 | Every relation satisfies BCNF. |
| | 585 | |
| | 586 | === Comparison with P2 |
| | 587 | |
| | 588 | This normalized result is the same relational structure as the P2 design. |
| | 589 | |
| | 590 | After removing the `U_`, `C_`, `M_`, `H_`, `O_`, `T_`, `MT_`, `MC_`, `W_` and |
| | 591 | `WI_` prefixes, the relations correspond directly to: |
| | 592 | |
| | 593 | * `users` |
| | 594 | * `crypto` |
| | 595 | * `markets` |
| | 596 | * `holdings` |
| | 597 | * `orders` |
| | 598 | * `transactions` |
| | 599 | * `market_trades` |
| | 600 | * `market_candles` |
| | 601 | * `watchlists` |
| | 602 | * `watchlist_items` |
| | 603 | |
| | 604 | The candidate keys also match, including the composite keys: |
| | 605 | |
| | 606 | * `{user_id, crypto_id}` on `holdings` |
| | 607 | * `{crypto_id, quote_currency}` on `markets` |
| | 608 | * `{market_id, timeframe, candle_time}` on `market_candles` |
| | 609 | * `{watchlist_id, crypto_id}` on `watchlist_items` |
| | 610 | |
| | 611 | The foreign-key structure is also identical. |
| | 612 | |
| | 613 | P2 arrived at this structure through the standard ER-to-relational |
| | 614 | transformation: |
| | 615 | |
| | 616 | * each entity becomes a relation keyed by its identifier; |
| | 617 | * each attributed M:N relationship becomes a relation of its own; |
| | 618 | * each 1:N relationship is represented by a foreign key on the N side. |
| | 619 | |
| | 620 | This normalization phase reaches the same result independently by starting from a |
| | 621 | single flat relation and applying functional-dependency-based normalization. |
| | 622 | |
| | 623 | The agreement between the two methods therefore provides an independent check |
| | 624 | of the relational design. |
| | 625 | |
| | 626 | === The `holdings.avg_price` exception |
| | 627 | |
| | 628 | The one important distinction is `holdings.avg_price`. |
| | 629 | |
| | 630 | P2 identifies `avg_price` as a derived attribute: it represents the running |
| | 631 | weighted-average buy price and can be recomputed from the relevant transaction |
| | 632 | history. It is nevertheless stored for performance reasons. |
| | 633 | |
| | 634 | The functional-dependency analysis performed in this phase does not classify |
| | 635 | that attribute as a normal-form violation. |
| | 636 | |
| | 637 | Within the relation, the dependency |
| | 638 | |
| | 639 | `H_ID -> H_AVG_PRICE` |
| | 640 | |
| | 641 | is still a valid functional dependency. |
| | 642 | |
| | 643 | The fact that `H_AVG_PRICE` can also be recomputed from rows in another relation |
| | 644 | is a different issue from whether a functional dependency inside one relation |
| | 645 | violates 2NF, 3NF or BCNF. |
| | 646 | |
| | 647 | Therefore normalization does not require `H_AVG_PRICE` to be removed. |
| | 648 | |
| | 649 | The stored value remains an intentional denormalisation for performance. |
| | 650 | |
| | 651 | === Design used going forward |
| | 652 | |
| | 653 | The P2 design is retained unchanged. |
| | 654 | |
| | 655 | The normalization phase confirms that there is no need to restructure the |
| | 656 | database objects: the independent normalization process produces the same ten |
| | 657 | relations already implemented by the project. |
| | 658 | |
| | 659 | The existing |
| | 660 | [`server/db/schema_creation.sql`](../../server/db/schema_creation.sql) |
| | 661 | therefore remains the authoritative physical schema. |
| | 662 | |
| | 663 | The `reserved_quantity` attribute added to `holdings` is naturally part of |
| | 664 | `R_HOLDINGS` because it depends on `H_ID`, just like `H_QUANTITY` and |
| | 665 | `H_AVG_PRICE`; it does not introduce a separate relation or a new normalization |
| | 666 | requirement. |
| | 667 | |
| | 668 | P4's prototype can therefore continue using the same database structure without |
| | 669 | schema changes. |
| | 670 | |
| | 671 | === Normalization history |
| | 672 | |
| | 673 | * **v01** — First complete normalization analysis. Started from a single |
| | 674 | 68-attribute relation reconstructed from the P1 model rather than from the P2 |
| | 675 | tables. |
| | 676 | * **2NF decomposition** — Applied canonical-cover synthesis to separate the ten |
| | 677 | independent entity/relationship clusters. |
| | 678 | * **3NF verification** — Checked foreign-key-induced transitive dependencies and |
| | 679 | confirmed that none remains inside an individual relation. |
| | 680 | * **BCNF verification** — Checked every determinant and established that each is |
| | 681 | a candidate key of its relation. |
| | 682 | * **Final result** — The independently derived schema matches P2 relation for |
| | 683 | relation, including candidate keys and foreign keys. The only documented |
| | 684 | denormalisation remains the stored `holdings.avg_price`. |
| | 685 | |
| | 686 | The formal normalization analysis therefore validates the existing relational |
| | 687 | design rather than requiring a new one. |