Changes between Version 6 and Version 7 of ErModel


Ignore:
Timestamp:
09/24/26 11:44:17 (3 days ago)
Author:
231285
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • ErModel

    v6 v7  
    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 
     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
     10Registered participants of the platform. Every action in the simulation is
     11attributed to a user, and the two balance attributes are what makes the
     12simulation work: cash that is free to trade is tracked separately from cash that
     13is currently committed to open positions, so the platform can refuse a purchase
     14without 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
     34The 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 —
     36the same asset can be quoted against several currencies, and a user's holding is
     37in 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
     49A tradeable pair: one crypto asset quoted in one currency, e.g. BTC/USD. This is
     50where prices live, and it is the thing an order is placed *on*. Modeled as its
     51own entity set rather than an attribute of `Cryptos` because a market has its own
     52lifecycle — it can be deactivated without deleting the asset — and because
     53trades, 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
     67A user's instruction to buy or sell on a market. Needed as a separate entity set
     68because an order is a record of *intent* that outlives its execution: it keeps
     69the requested quantity and price even after it has been filled, which is what
     70makes 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
     89The financial ledger: every movement of virtual cash, in one place. This exists
     90so that a balance is never just a number someone edited — it is the sum of an
     91auditable list of entries, which is also what the "explain every step" goal of
     92the 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
     105Individual executed trades on a market, from the user's own fills and from the
     106market simulator. This is the single source of truth for the current price: the
     107price of a market is the price of its most recent trade, never a column someone
     108writes 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
     125OHLCV aggregates per market and timeframe — the data a price chart is drawn
     126from. Stored rather than computed on the fly because the point of the project is
     127a chart-driven interface, and re-aggregating the whole trade history for every
     128screen 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
     142A named list of assets a user wants to monitor. A separate entity set rather than
     143a flag on the relationship between users and assets, because a user may want
     144several 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
     157Ties a market to the asset it trades. One asset can be quoted in many markets;
     158every 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
     162Records which market an order was placed on. Every order must name a market;
     163a market may have no orders yet. No attributes.
     164
     165===== Places — Users (1) : Orders (N), total on Orders
     166Records who placed an order. Every order belongs to exactly one user; a new user
     167has no orders. No attributes.
     168
     169===== Records — Users (1) : Transactions (N), total on Transactions
     170Attributes each ledger entry to a user. Every entry belongs to exactly one user.
     171No attributes.
     172
     173===== Settles — Orders (1) : Transactions (N), partial on both sides
     174Links a ledger entry to the order that caused it. Partial on the `Transactions`
     175side because deposits have no originating order, and partial on the `Orders` side
     176because an order that never executes never produces a ledger entry. This is why
     177the corresponding column is nullable in P2. No attributes.
     178
     179===== Fills — Markets (1) : MarketTrades (N), total on MarketTrades
     180Every executed trade happened on exactly one market. No attributes.
     181
     182===== Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles
     183Every candle summarises trades of exactly one market. No attributes.
     184
     185===== Owns — Users (1) : Watchlists (N), total on Watchlists
     186Every watchlist belongs to exactly one user. No attributes.
     187
     188===== Holds — Users (M) : Cryptos (N), partial on both sides, **with attributes**
     189A user's position in an asset. M:N because one user holds many assets and one
     190asset is held by many users, and partial on both sides because a user may hold
     191nothing and an asset may be held by nobody. Modeled as a relationship rather
     192than an entity set because a position has no identity of its own — it is
     193entirely 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**
     205Which assets are on which watchlist. M:N: a list holds many assets, an asset
     206appears on many lists. Partial on both sides — an empty list is valid and an
     207asset 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
     228Reasoning for the AI-assisted part of this phase, and the full interaction log,
     229are on https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md.