| Version 6 (modified by , 9 hours ago) ( diff ) |
|---|
Normalization
This phase does not use the relations of RelationalDesign (P2) as a starting point. It starts from the attributes of ERModel v05 (P1), put into one de-normalized relation. It states the functional dependencies that the model's rules impose on those attributes, computes the keys of that relation from the dependencies, and then decomposes it step by step through 2NF, 3NF and BCNF. Every step is checked for a lossless join and for dependency preservation. The final section compares the result with P2.
De-normalized database form
Which attributes go into the relation
The relation contains the attributes of the ER model and nothing else. In v05 all attributes belong to the 11 entity sets. None of the 15 relationships has attributes of its own.
A relationship adds no column. Foreign-key columns such as crypto_id or watchlist_id
belong to the relational model of P2, not to the ER model, so they do not appear here. What a
relationship contributes is a functional dependency between attributes that are already
in the relation. For example, Contains (Watchlists 1 : N WatchlistItems) says that every
watchlist item is on exactly one watchlist, which is the dependency WI_ID → W_ID in the
next section. It is not a column WI_WATCHLIST_ID.
Attribute names are prefixed with the entity set they come from, because several names repeat
across the model (id, created_at, quantity, type, name, price, side), and one
relation cannot contain the same name twice.
| Prefix | Entity set (P1) | Attributes |
|---|---|---|
U_ | Users | U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT
|
C_ | Cryptos | C_ID, C_SYMBOL, C_NAME, C_CREATED_AT
|
M_ | Markets | M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT
|
H_ | Holdings | H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT
|
O_ | Orders | O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT
|
T_ | Transactions | T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION
|
MT_ | MarketTrades | MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE
|
OE_ | OrderEvents | OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT
|
MC_ | MarketCandles | MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME
|
W_ | Watchlists | W_ID, W_NAME, W_CREATED_AT
|
WI_ | WatchlistItems | WI_ID, WI_ADDED_AT
|
That is 64 attributes. One case needs two more. FillsBuy and FillsSell are two
different relationships between the same two entity sets, Orders and MarketTrades. A trade
can fill one buy order and one sell order, which are two different orders. One relation
has only one O_ID column, and one column cannot hold two different orders in the same
tuple. So the order's identifier appears once per role, named after the relationship
that gives the role:
| Attribute | Meaning |
|---|---|
O_ID_FILLSBUY | the id of Orders, in its role in FillsBuy (the buy order a trade filled)
|
O_ID_FILLSSELL | the id of Orders, in its role in FillsSell (the sell order a trade filled)
|
These are not foreign keys copied from P2. They are the ER attribute Orders.id itself, once
for each of the two relationships. ERModel names these two
roles of Orders explicitly: the buy order of a trade in FillsBuy, and the sell order in
FillsSell. This is the only place where the model has two
relationships between the same pair of entity sets. Every other relationship is expressed with
the attributes above, without renaming.
This gives one relation, R_EDUBERZA, of 66 attributes:
R_EDUBERZA( U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT, C_ID, C_SYMBOL, C_NAME, C_CREATED_AT, M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, O_ID_FILLSBUY, O_ID_FILLSSELL, OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, W_ID, W_NAME, W_CREATED_AT, WI_ID, WI_ADDED_AT )
To keep the tables below readable, X_* means the non-identifier attributes of prefix
X_. For example, U_* = U_USERNAME … U_UPDATED_AT (9 attributes), and O_* =
O_SIDE … O_EXECUTED_AT (8 attributes). U_ID, O_ID, … are always written out.
Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount —
nothing here is a list or a nested record), so R_EDUBERZA satisfies 1NF as soon as it is
written down.
Functional dependencies
At this point R_EDUBERZA is just a set of attributes. It has no keys yet. U_ID,
O_ID, … are ordinary attributes of this relation, and which attribute sets are keys of
R_EDUBERZA is computed in the next section, from the
dependencies below. Each dependency is justified by a rule of the domain, as described in
the data requirements of ERModel. The rules are of four
kinds:
- (I) Identification. Every value of an identifier (
U_ID,C_ID, …) is given to exactly one real object: one user, one crypto, one order. That object has exactly one username, one balance, one price, and so on. So the identifier's value fixes those values. - (R) 1:N relationship. In a 1:N relationship, each object on the N side is linked to exactly one object on the 1 side. So the N side's identifier fixes the 1 side's identifier. Example: an order is placed by exactly one user (
Places), soO_ID → U_ID. The opposite direction does not hold: a user places many orders, soU_ID ↛ O_ID. - (U) Uniqueness rule. A rule of the form "at most one X per Y and Z" gives
Y, Z → X. - (N) Unique natural attribute. No two users share a username or an email, and no two cryptos share a symbol.
Only rules of the ER model are used. The dependencies below come from the rules stated in
ERModel v05 and nothing else. The analysis uses the
classical definitions (Armstrong's axioms), with no special treatment of NULL. Partial
relationships (Settles, FillsBuy, FillsSell) are discussed where they matter:
under Canonical cover and in the discussion.
| # | Functional dependency | Rule | Why it holds |
|---|---|---|---|
| FD1 | U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT | I | one user, one value of each |
| FD2 | U_USERNAME → U_ID | N | usernames are unique |
| FD3 | U_EMAIL → U_ID | N | emails are unique |
| FD4 | C_ID → C_SYMBOL, C_NAME, C_CREATED_AT | I | one crypto, one value of each |
| FD5 | C_SYMBOL → C_ID | N | symbols are unique |
| FD6 | M_ID → M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, C_ID | I, R | …and a market is QuotedOn exactly one crypto
|
| FD7 | C_ID, M_QUOTE_CURRENCY → M_ID | U | a crypto is quoted at most once per currency |
| FD8 | H_ID → H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, U_ID, C_ID | I, R | …and a holding belongs to one user (Holds) and is a position in one crypto (PositionIn)
|
| FD9 | U_ID, C_ID → H_ID | U | at most one holding per user and crypto |
| FD10 | O_ID → O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, U_ID, M_ID | I, R | …and an order is placed by one user (Places) on one market (PlacedOn)
|
| FD11 | T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, U_ID, O_ID | I, R | …and a ledger entry belongs to one user (Records) and to at most one order (Settles)
|
| FD12 | MT_ID → MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL | I, R | …and a trade happened on one market (Fills) and filled at most one buy order (FillsBuy) and at most one sell order (FillsSell)
|
| FD13 | OE_ID → OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, O_ID | I, R | …and an event belongs to one order (Logs)
|
| FD14 | MC_ID → MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID | I, R | …and a candle summarises one market (Aggregates)
|
| FD15 | M_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID | U | one candle per market, timeframe and bucket |
| FD16 | W_ID → W_NAME, W_CREATED_AT, U_ID | I, R | …and a watchlist is owned by one user (Owns)
|
| FD17 | WI_ID → WI_ADDED_AT, W_ID, C_ID | I, R | …and an item is on one watchlist (Contains) and names one crypto (Lists)
|
| FD18 | W_ID, C_ID → WI_ID | U | an asset appears at most once per watchlist |
Dependencies that do not hold are as important, because they are why some attributes must be combined in the key later:
- The reverse of every (R) dependency, e.g.
U_ID ↛ O_ID,M_ID ↛ MT_ID,W_ID ↛ WI_ID. These are 1:N, not 1:1. M_ID, MT_EXECUTED_AT ↛ MT_ID. Two trades on a market can share a timestamp.U_ID, W_NAME ↛ W_ID. The model does not require list names to be unique per user.O_ID_FILLSBUYandO_ID_FILLSSELLdetermine no other attribute ofR_EDUBERZAby any rule of the ER model. The order data (O_SIDE,O_PRICE, …) describes the order in theO_IDcolumn, not the order in a role column. (The database also has a rule that a trade and the orders it fills are on the same market. That rule is a trigger in P7 relating several entity sets, not a rule of the ER model, so it is not used here.)
Canonical cover
A canonical (minimal) cover is obtained in three steps.
Step 1 — single attribute on the right. Each FD above is read as one dependency per
right-side attribute, e.g. FD6 is M_ID → M_QUOTE_CURRENCY, M_ID → M_IS_ACTIVE,
M_ID → M_CREATED_AT, M_ID → C_ID.
Step 2 — no extraneous attribute on the left. Only FD7, FD9, FD15 and FD18 have more than one attribute on the left. For each one, dropping any attribute makes the rule false:
| FD | Drop | Counter-example (the smaller left side does not determine the right side) |
|---|---|---|
| FD7 | M_QUOTE_CURRENCY | BTC is quoted in USD and in EUR: one C_ID, two markets
|
C_ID | USD is the quote currency of many markets | |
| FD9 | C_ID | one user holds several cryptos |
U_ID | one crypto is held by several users | |
| FD15 | M_ID | every market has a 1h candle starting at 10:00
|
MC_TIMEFRAME | a market has a 1m and a 1h candle both starting at 10:00
| |
MC_CANDLE_TIME | a market has many 1h candles
| |
| FD18 | C_ID | a watchlist has several items |
W_ID | a crypto is on several watchlists |
Step 3 — no redundant dependency. A dependency is redundant if it follows from the others. For
almost every dependency, its right-side attribute appears on the right of no other
dependency with a different left side (e.g. nothing but U_ID determines
U_AVAILABLE_BALANCE), so it cannot be derived. The candidates worth checking are the
identifiers that are reached from several places:
T_ID → U_IDis redundant. It follows by transitivity fromT_ID → O_ID(FD11) andO_ID → U_ID(FD10): a ledger entry's user is the user of the order it settles. It is therefore removed from FD11. The derivation is valid only for an entry that has an order. Every tuple ofR_EDUBERZAdoes have one (see the discussion), so in the de-normalized relation the removal is correct. The consequence for deposits, which have no order, is taken up in the discussion.H_ID → U_ID,H_ID → C_ID,WI_ID → W_ID,WI_ID → C_ID,M_ID → C_ID,MT_ID → M_ID,MC_ID → M_ID,OE_ID → O_ID,W_ID → U_IDandO_ID → U_ID,O_ID → M_ID: for each, no other dependency with a different left side has that attribute on its right side and a left side reachable from this one, so none can be derived.- The four (U) and three (N) dependencies go "backwards" from a non-identifier to an identifier. Nothing else produces an identifier from those attributes, so they are not derivable either.
Grouping the single-attribute dependencies back by left side gives FD1–FD18 as listed,
except that FD11 loses U_ID:
| # | Functional dependency (canonical cover) |
|---|---|
| FD11 | T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, O_ID
|
FD1–FD18, with this FD11, is the canonical cover. From here on, "FD11" means this reduced form.
Candidate keys and primary key
A candidate key is a minimal set of attributes whose closure under FD1–FD18 is all 66 attributes.
Attributes that must be in every key. T_ID, OE_ID and MT_ID appear on the right side
of no dependency. Nothing determines them, so every key must contain them.
Closure of {T_ID, OE_ID, MT_ID}:
| Step | Added | Using |
|---|---|---|
| start | T_ID, OE_ID, MT_ID | — |
| 1 | T_*, O_ID | FD11 |
| 2 | OE_* | FD13 |
| 3 | MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL | FD12 |
| 4 | O_*, U_ID | FD10 |
| 5 | U_* | FD1 |
| 6 | M_*, C_ID | FD6 |
| 7 | C_* | FD4 |
| 8 | H_ID | FD9 (U_ID and C_ID are both present)
|
| 9 | H_* | FD8 |
That is 53 attributes. Still missing are all 8 MC_ attributes, the 3 W_ attributes and
the 2 WI_ attributes:
MC_: onlyMC_IDdetermines them (FD14), andMC_IDis reached only by FD15, which needsM_ID(already present),MC_TIMEFRAMEandMC_CANDLE_TIME. So the key must add eitherMC_IDor bothMC_TIMEFRAMEandMC_CANDLE_TIME. Neither of those two alone is enough.W_andWI_:WI_IDgivesW_ID(FD17), andW_IDgivesWI_IDtogether withC_ID, which is already present (FD18). So adding eitherWI_IDorW_IDgives all five.
Candidate keys (each one's closure is all 66 attributes, and removing any member breaks that, by the argument above):
| Key | Attributes |
|---|---|
| K1 | T_ID, OE_ID, MT_ID, MC_ID, WI_ID
|
| K2 | T_ID, OE_ID, MT_ID, MC_ID, W_ID
|
| K3 | T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, WI_ID
|
| K4 | T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID
|
Primary key: K1. It consists only of identifiers, and it is the key that remains at the end of the decomposition below.
Prime attributes (in at least one candidate key): `T_ID, OE_ID, MT_ID, MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID`. The other 58 attributes are non-prime. The difference matters: 2NF and 3NF only restrict dependencies of non-prime attributes, and BCNF restricts all of them.
In words, a tuple of R_EDUBERZA puts together one ledger entry, one order event, one
trade, one candle and one watchlist item. Everything else in the tuple (the user, the order,
the market, the crypto, the holding, the watchlist) follows from those five.
Normal form of R_EDUBERZA: 1NF only. It is not in 2NF, because, for example, T_AMOUNT
depends on T_ID alone, a proper part of K1.
1NF decomposition
No decomposition is needed. Every attribute of R_EDUBERZA is atomic and single-valued, and
the relation has no repeating groups (see
De-normalized database form).
2NF decomposition
How every step is described and checked
Each step of 2NF, 3NF and BCNF below lists, in this order: the relation analyzed, its dependencies, its candidate keys and primary key, and its normal form; the dependency that violates the next normal form and is used for the split; the two resulting relations, each with its dependencies, keys and normal form; and the dependency-preservation and lossless-join checks.
Every step splits one relation R into two: the extracted relation Ri and the
residual relation R' (what is left of R). The same two checks are made each time:
- Lossless join. The split of
RintoRiandR'is lossless if the common attributes determine one of the two sides:(Ri ∩ R') → Rior(Ri ∩ R') → R'. Every step below extractsRi = X ∪ (what X determines)for some determinantXthat stays inR'. SoX ⊆ Ri ∩ R'andX → Ri, and the first condition holds. - Dependency preservation. Every dependency of the canonical cover must end up with all its attributes inside one relation. So an attribute is removed from the residual only when no dependency still waiting in the residual needs it. Otherwise it is extracted and kept.
Relation analyzed first: R_EDUBERZA (66 attributes), dependencies FD1–FD18, candidate
keys K1–K4, primary key K1. Normal form: 1NF.
Dependencies that violate 2NF. 2NF forbids a non-prime attribute from depending on a proper part of a candidate key. There are six such partial dependencies:
| Part of a key | Non-prime attributes that depend on it | Through |
|---|---|---|
T_ID (K1–K4) | T_*, O_ID, and through them O_*, U_ID, U_*, M_ID, M_*, C_ID, C_*, H_ID, H_* | FD11, then FD10, FD1, FD6, FD4, FD9, FD8 |
OE_ID (K1–K4) | OE_*, O_ID | FD13 |
MT_ID (K1–K4) | MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL | FD12 |
MC_ID (K1, K2) | MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID | FD14 |
W_ID (K2, K4) | W_*, U_ID | FD16 |
WI_ID (K1, K3) | WI_ADDED_AT, C_ID | FD17 |
The table lists the part of a key that each group depends on most directly. It is not the
only one: under K3/K4, for example, MC_OPEN … MC_VOLUME also depend on
{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}, and under K1/K3 W_* depend on WI_ID through
W_ID. These lead to the same relations, so they need no extra steps. MC_TIMEFRAME,
MC_CANDLE_TIME and W_ID also depend on parts of keys, but they are prime, so 2NF does not
restrict them. They are handled under BCNF.
Each step below removes one row of this table, splitting the current relation into two. The
order is chosen so that no dependency is lost. T_ID goes first, because its group is the
largest and carries FD1–FD11 with it. Each later step handles a group whose determinant is
still in the residual relation.
Step 2NF-1 — partial dependency on T_ID
- Relation analyzed:
R_EDUBERZA(66 attributes). - Dependencies: FD1–FD18. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF.
- 2NF violations: all six rows of the table above. Split first on
T_ID, the largest group (see the order explained above). - Decomposition dependency:
T_ID → T_*, O_ID(FD11), together with everything it determines transitively (FD10, FD1, FD6, FD4, FD9, FD8).T_IDis a proper part of K1, andT_AMOUNT, for example, is non-prime, so this violates 2NF. - New relation
R_A={ T_ID, T_*, O_ID, O_*, U_ID, U_*, M_ID, M_*, C_ID, C_*, H_ID, H_* }(39 attributes). Dependencies: FD1–FD11. Candidate key and primary key:T_ID. Normal form: 2NF (it has a one-attribute key), but not 3NF (see 3NF). - Residual relation
S1=R_EDUBERZA − { T_*, O_*, U_*, M_*, C_*, H_ID, H_* }={ T_ID, O_ID, U_ID, M_ID, C_ID, OE_ID, OE_*, MT_ID, MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL, MC_ID, MC_*, W_ID, W_*, WI_ID, WI_ADDED_AT }(32 attributes).O_ID,U_ID,M_IDandC_IDstay, because FD13, FD16, FD12/FD14/FD15 and FD17/FD18 still need them. Dependencies: FD12–FD18, plus the projected dependencies between the identifiers kept here:T_ID → O_ID, U_ID, M_ID, C_ID,O_ID → U_ID, M_ID, C_ID,M_ID → C_ID,OE_ID → U_ID, M_ID, C_ID,MT_ID → C_ID,MC_ID → C_ID,WI_ID → U_ID. Candidate keys: K1–K4 (all their attributes are still here). Normal form: 1NF. - Dependency preservation: FD1–FD11 lie entirely in
R_A, and FD12–FD18 entirely inS1. ✓ - Lossless join:
R_A ∩ S1 = { T_ID, O_ID, U_ID, M_ID, C_ID }containsT_ID, andT_ID → R_A, so(R_A ∩ S1) → R_A. ✓
Step 2NF-2 — partial dependency on OE_ID
- Relation analyzed:
S1(32 attributes). Dependencies: as listed forS1in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. - Remaining 2NF violations: the partial dependencies on
OE_ID,MT_ID,MC_ID,W_IDandWI_ID(table above), and the partial dependencies of the kept identifiersO_ID,U_ID,M_ID,C_IDonT_ID. The kept identifiers cannot leave yet, because other groups still need them. Each one leaves with the last group that needs it (O_IDin 2NF-2,M_IDin 2NF-4,U_IDin 2NF-5,C_IDin 2NF-6). Split first onOE_ID, because after it no group needsO_IDany more. - Decomposition dependency:
OE_ID → OE_*, O_ID(FD13).OE_IDis a proper part of K1 andOE_*are non-prime. - New relation
R_B={ OE_ID, OE_*, O_ID }(7 attributes). Dependencies: FD13. Candidate key:OE_ID. Normal form: BCNF. - Residual relation
S2=S1 − { OE_*, O_ID }(26 attributes). No dependency still needed in the residual usesO_ID. Dependencies: FD12, FD14–FD18, plus the projectedT_ID → U_ID, M_ID, C_ID,OE_ID → U_ID, M_ID, C_ID,M_ID → C_ID,MT_ID → C_ID,MC_ID → C_ID,WI_ID → U_ID. Candidate keys: K1–K4. Normal form: 1NF. - Dependency preservation: FD13 is in
R_B, and the others are inS2.T_ID → O_IDis already kept inR_A. ✓ - Lossless join:
R_B ∩ S2 = { OE_ID }, andOE_ID → R_B(FD13). ✓
Step 2NF-3 — partial dependency on MT_ID
- Relation analyzed:
S2(26 attributes). Dependencies: as listed forS2in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. - Remaining 2NF violations: the groups of
MT_ID,MC_ID,W_ID,WI_ID, and the kept identifiersU_ID,M_ID,C_ID. Split first onMT_ID, the next group.M_IDmust still stay forMC_ID. - Decomposition dependency:
MT_ID → MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL(FD12). - New relation
R_C={ MT_ID, MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL }(9 attributes). Dependencies: FD12. Candidate key:MT_ID. Normal form: BCNF. - Residual relation
S3=S2 − { MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL }(19 attributes).M_IDstays, because FD14/FD15 need it. Dependencies: FD14–FD18, plus the projectedT_ID → U_ID, M_ID, C_ID,OE_ID → U_ID, M_ID, C_ID,MT_ID → M_ID, C_ID,M_ID → C_ID,MC_ID → C_ID,WI_ID → U_ID. Candidate keys: K1–K4. Normal form: 1NF. - Dependency preservation: FD12 is in
R_C, and FD14–FD18 are inS3. ✓ - Lossless join:
R_C ∩ S3 = { MT_ID, M_ID }containsMT_ID, andMT_ID → R_C(FD12). ✓
Step 2NF-4 — partial dependency on MC_ID
- Relation analyzed:
S3(19 attributes). Dependencies: as listed forS3in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. - Remaining 2NF violations: the groups of
MC_ID,W_ID,WI_ID, and the kept identifiersU_ID,M_ID,C_ID. Split first onMC_ID, the last group that needsM_ID, soM_IDcan leave with it. - Decomposition dependency:
MC_ID → MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID(FD14).MC_IDis a proper part of K1. The primeMC_TIMEFRAMEandMC_CANDLE_TIMEalso go into the new relation, so that FD15, which needs them withM_IDandMC_ID, is preserved. - New relation
R_D={ MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID }(9 attributes). Dependencies: FD14, FD15. Candidate keys:MC_IDand{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}. Normal form: BCNF. - Residual relation
S4=S3 − { MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID }(13 attributes).MC_TIMEFRAMEandMC_CANDLE_TIMEare prime and stay. Dependencies: FD16–FD18, plus the projectedT_ID → U_ID, C_ID,OE_ID → U_ID, C_ID,MT_ID → C_ID,MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID,WI_ID → U_ID. Candidate keys: K1–K4. Normal form: 1NF. - Dependency preservation: FD14 and FD15 are in
R_D, and FD16–FD18 are inS4. ✓ - Lossless join:
R_D ∩ S4 = { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }containsMC_ID, andMC_ID → R_D(FD14). ✓
Step 2NF-5 — partial dependency on W_ID
- Relation analyzed:
S4(13 attributes). Dependencies: as listed forS4in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. - Remaining 2NF violations: the groups of
W_IDandWI_ID, and the kept identifiersU_ID,C_ID. Split first onW_ID, the last group that needsU_ID. - Decomposition dependency:
W_ID → W_NAME, W_CREATED_AT, U_ID(FD16).W_IDis a proper part of K2. - New relation
R_E={ W_ID, W_NAME, W_CREATED_AT, U_ID }(4 attributes). Dependencies: FD16. Candidate key:W_ID. Normal form: BCNF. - Residual relation
S5=S4 − { W_NAME, W_CREATED_AT, U_ID }(10 attributes). Dependencies: FD17, FD18, plus the projectedT_ID → C_ID,OE_ID → C_ID,MT_ID → C_ID,MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID. Candidate keys: K1–K4. Normal form: 1NF. - Dependency preservation: FD16 is in
R_E, and FD17 and FD18 are inS5. ✓ - Lossless join:
R_E ∩ S5 = { W_ID }, andW_ID → R_E(FD16). ✓
Step 2NF-6 — partial dependency on WI_ID
- Relation analyzed:
S5(10 attributes). Dependencies: as listed forS5in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. - Remaining 2NF violations: the group of
WI_ID, and the kept identifierC_ID. Split onWI_ID, the last group that needsC_ID. - Decomposition dependency:
WI_ID → WI_ADDED_AT, W_ID, C_ID(FD17).WI_IDis a proper part of K1, andWI_ADDED_ATandC_IDare non-prime. - New relation
R_F={ WI_ID, WI_ADDED_AT, W_ID, C_ID }(4 attributes). Dependencies: FD17, FD18. Candidate keys:WI_IDand{W_ID, C_ID}. Normal form: BCNF. - Residual relation
S6=S5 − { WI_ADDED_AT, C_ID }={ T_ID, OE_ID, MT_ID, MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID }(8 attributes).W_IDis prime and stays. Dependencies: no dependency of the cover lies entirely insideS6. The projected ones areMC_ID → MC_TIMEFRAME, MC_CANDLE_TIMEandWI_ID → W_ID, plus derived ones such asMT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_IDandMT_ID, W_ID → WI_ID. Candidate keys: K1–K4. Normal form: 3NF, because every attribute is prime (and so 2NF). - Dependency preservation: FD17 and FD18 are in
R_F. ✓ - Lossless join:
R_F ∩ S6 = { WI_ID, W_ID }containsWI_ID, andWI_ID → R_F(FD17). ✓
Result of 2NF: R_A, R_B, R_C, R_D, R_E, R_F, S6. All seven are in 2NF (R_A
only 2NF, S6 3NF, the rest BCNF). All 18 dependencies are preserved: FD1–FD11 in R_A,
FD13 in R_B, FD12 in R_C, FD14–FD15 in R_D, FD16 in R_E, FD17–FD18 in R_F.
3NF decomposition
Only R_A is not in 3NF. R_B–R_F are already in BCNF, and S6 is in 3NF (all its
attributes are prime).
Dependencies that violate 3NF in R_A. 3NF forbids a non-prime attribute from depending on
a key only transitively, through a determinant that is not a superkey. The only key of
R_A is T_ID, but inside R_A:
U_ID → U_*(FD1),U_USERNAME → U_ID(FD2),U_EMAIL → U_ID(FD3)C_ID → C_*(FD4),C_SYMBOL → C_ID(FD5)U_ID, C_ID → H_ID(FD9),H_ID → H_*, U_ID, C_ID(FD8)M_ID → M_*, C_ID(FD6),C_ID, M_QUOTE_CURRENCY → M_ID(FD7)O_ID → O_*, U_ID, M_ID(FD10)
None of these determinants is a superkey of R_A. For example, T_ID → O_ID → O_PRICE is a
transitive dependency of the non-prime O_PRICE on the key.
Order of the steps. An attribute can leave the residual only after every dependency that
needs it has been extracted. FD9 needs U_ID and C_ID together, and extracting Markets
takes C_ID out of the residual, so Holdings must come before Markets. Extracting
Orders takes M_ID and U_ID out, so Orders comes last. The dependencies are therefore
taken from the "leaves" of the chain T_ID → O_ID → {U_ID, M_ID → C_ID} inward.
Step 3NF-1 — transitive dependency through U_ID
- Relation analyzed:
R_A(39 attributes), dependencies FD1–FD11, candidate key and primary candidate key and primary keyT_ID, normal form 2NF. - 3NF violations: all five groups listed above. Split first on
U_ID. It is a leaf of the chain: its dependents determine nothing outside its own group. - Decomposition dependency:
U_ID → U_*(FD1).U_IDis not a superkey ofR_A. - New relation
R_USERS={ U_ID, U_* }(10 attributes). Dependencies: FD1, FD2, FD3. Candidate keys:U_ID,U_USERNAME,U_EMAIL. Primary key:U_ID. Normal form: BCNF. - Residual relation
R_A1=R_A − U_*(30 attributes). Dependencies: FD4–FD11, which also implyT_ID → H_IDandO_ID → H_ID(throughU_ID, C_ID). Candidate key and primary key:T_ID. Normal form: 2NF. - Dependency preservation: FD1–FD3 are in
R_USERS, and FD4–FD11 are inR_A1. ✓ - Lossless join:
R_USERS ∩ R_A1 = { U_ID }, andU_ID → R_USERS(FD1). ✓
Step 3NF-2 — transitive dependency through C_ID
- Relation analyzed:
R_A1(30 attributes), dependencies FD4–FD11, candidate key and primary keyT_ID, normal form 2NF. - 3NF violations:
C_ID → C_*,U_ID, C_ID → H_ID → H_*,M_ID → M_*, C_ID,O_ID → O_*, U_ID, M_ID. Split first onC_ID, the next leaf. - Decomposition dependency:
C_ID → C_*(FD4). - New relation
R_CRYPTO={ C_ID, C_* }(4 attributes). Dependencies: FD4, FD5. Candidate keys:C_ID,C_SYMBOL. Primary key:C_ID. Normal form: BCNF. - Residual relation
R_A2=R_A1 − C_*(27 attributes). Dependencies: FD6–FD11. Candidate key and primary key:T_ID. Normal form: 2NF. - Dependency preservation: FD4 and FD5 are in
R_CRYPTO, and FD6–FD11 are inR_A2. ✓ - Lossless join:
R_CRYPTO ∩ R_A2 = { C_ID }, andC_ID → R_CRYPTO(FD4). ✓
Step 3NF-3 — transitive dependency through {U_ID, C_ID}
- Relation analyzed:
R_A2(27 attributes), dependencies FD6–FD11, candidate key and primary keyT_ID, normal form 2NF. - 3NF violations:
U_ID, C_ID → H_ID → H_*,M_ID → M_*, C_ID,O_ID → O_*, U_ID, M_ID. Split first on{U_ID, C_ID}, because it must come beforeMarketstakesC_IDaway. - Decomposition dependency:
U_ID, C_ID → H_ID(FD9), together withH_ID → H_*(FD8). - New relation
R_HOLDINGS={ H_ID, H_*, U_ID, C_ID }(8 attributes). Dependencies: FD8, FD9. Candidate keys:H_ID,{U_ID, C_ID}. Primary key:H_ID. Normal form: BCNF. - Residual relation
R_A3=R_A2 − { H_ID, H_* }(21 attributes). Dependencies: FD6, FD7, FD10, FD11. Candidate key and primary key:T_ID. Normal form: 2NF. - Dependency preservation: FD8 and FD9 are in
R_HOLDINGS, and the others are inR_A3. ✓ - Lossless join:
R_HOLDINGS ∩ R_A3 = { U_ID, C_ID }, andU_ID, C_ID → H_ID → H_*, so{U_ID, C_ID} → R_HOLDINGS. ✓
Step 3NF-4 — transitive dependency through M_ID
- Relation analyzed:
R_A3(21 attributes), dependencies FD6, FD7, FD10, FD11, keyT_ID, normal form 2NF. - 3NF violations:
M_ID → M_*, C_IDandO_ID → O_*, U_ID, M_ID. Split first onM_ID, becauseOrdersstill needsM_ID. - Decomposition dependency:
M_ID → M_*, C_ID(FD6). - New relation
R_MARKETS={ M_ID, M_*, C_ID }(5 attributes). Dependencies: FD6, FD7. Candidate keys:M_ID,{C_ID, M_QUOTE_CURRENCY}. Primary key:M_ID. Normal form: BCNF. - Residual relation
R_A4=R_A3 − { M_*, C_ID }(17 attributes). No dependency left needsC_ID. Dependencies: FD10, FD11. Candidate key and primary key:T_ID. Normal form: 2NF. - Dependency preservation: FD6 and FD7 are in
R_MARKETS, and FD10 and FD11 are inR_A4. ✓ - Lossless join:
R_MARKETS ∩ R_A4 = { M_ID }, andM_ID → R_MARKETS(FD6). ✓
Step 3NF-5 — transitive dependency through O_ID
- Relation analyzed:
R_A4={ T_ID, T_*, O_ID, O_*, U_ID, M_ID }(17 attributes), dependencies FD10, FD11, candidate key and primary keyT_ID, normal form 2NF. - 3NF violations: only
O_ID → O_*, U_ID, M_ID. Split onO_ID. - Decomposition dependency:
O_ID → O_*, U_ID, M_ID(FD10). - New relation
R_ORDERS={ O_ID, O_*, U_ID, M_ID }(11 attributes). Dependencies: FD10. Candidate key:O_ID. Normal form: BCNF. - Residual relation
R_TRANSACTIONS=R_A4 − { O_*, U_ID, M_ID }={ T_ID, T_*, O_ID }(7 attributes). Dependencies: FD11. Candidate key:T_ID. Normal form: BCNF. KeepingU_IDhere would have left the transitive dependencyT_ID → O_ID → U_IDinside the relation.T_ID → U_IDwas removed from the cover as redundant, so nothing is lost. - Dependency preservation: FD10 is in
R_ORDERS, and FD11 is inR_TRANSACTIONS. ✓ - Lossless join:
R_ORDERS ∩ R_TRANSACTIONS = { O_ID }, andO_ID → R_ORDERS(FD10). ✓
Result of 3NF: R_USERS, R_CRYPTO, R_HOLDINGS, R_MARKETS, R_ORDERS,
R_TRANSACTIONS (from R_A), and R_B, R_C, R_D, R_E, R_F, S6 unchanged. 12
relations, all in 3NF, and all except S6 in BCNF. All 18 dependencies are preserved.
BCNF if possible
BCNF requires every determinant of a non-trivial dependency to be a superkey, even when the dependent attribute is prime.
| Relation | Dependencies in force | Determinants | All superkeys? |
|---|---|---|---|
R_USERS | FD1, FD2, FD3 | U_ID, U_USERNAME, U_EMAIL | yes |
R_CRYPTO | FD4, FD5 | C_ID, C_SYMBOL | yes |
R_MARKETS | FD6, FD7 | M_ID, {C_ID, M_QUOTE_CURRENCY} | yes |
R_HOLDINGS | FD8, FD9 | H_ID, {U_ID, C_ID} | yes |
R_ORDERS | FD10 | O_ID | yes |
R_TRANSACTIONS | FD11 | T_ID | yes |
R_B | FD13 | OE_ID | yes |
R_C | FD12 | MT_ID | yes |
R_D | FD14, FD15 | MC_ID, {M_ID, MC_TIMEFRAME, MC_CANDLE_TIME} | yes |
R_E | FD16 | W_ID | yes |
R_F | FD17, FD18 | WI_ID, {W_ID, C_ID} | yes |
S6 | MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME; WI_ID → W_ID; derived ones such as MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID and MT_ID, W_ID → WI_ID | MC_ID, WI_ID, {MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}, {MT_ID, W_ID}, … | no |
Dependencies that violate BCNF — only in S6. MC_ID determines MC_TIMEFRAME and
MC_CANDLE_TIME, and WI_ID determines W_ID, but neither MC_ID nor WI_ID is a superkey
of S6. 3NF allowed this because the dependent attributes are prime. BCNF does not. The derived
dependencies all involve W_ID or MC_TIMEFRAME/MC_CANDLE_TIME, so they disappear once the
two steps below remove those attributes.
Step BCNF-1 — MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME
- Relation analyzed:
S6(8 attributes), dependencies as in the table above, candidate keys K1–K4, primary key K1, normal form 3NF. - BCNF violations:
MC_ID → MC_TIMEFRAME, MC_CANDLE_TIMEandWI_ID → W_ID, and the derived ones that depend on them. Split first onMC_ID. The order does not matter here, because the two violations share no attribute. - Decomposition dependency:
MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME.MC_IDis not a superkey ofS6. - New relation
{ MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }. Dependencies:MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME. Key:MC_ID. Normal form: BCNF. It is a projection ofR_D, which already contains these attributes with the same key, so it adds no information and is merged intoR_D. - Residual relation
S7={ T_ID, OE_ID, MT_ID, MC_ID, W_ID, WI_ID }(6 attributes). Dependencies:WI_ID → W_ID, and derived ones such asMT_ID, W_ID → WI_ID. Candidate keys:{T_ID, OE_ID, MT_ID, MC_ID, WI_ID}(K1) and{T_ID, OE_ID, MT_ID, MC_ID, W_ID}(K2). Normal form: 3NF. - Dependency preservation: no dependency of the cover is affected. FD14 and FD15 are in
R_D. ✓ - Lossless join: the intersection is
{ MC_ID }, andMC_ID → { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }. ✓
Step BCNF-2 — WI_ID → W_ID
- Relation analyzed:
S7(6 attributes), dependenciesWI_ID → W_IDand derived ones, candidate keys K1, K2, primary key K1, normal form 3NF. - BCNF violations: only
WI_ID → W_ID(and the derivedMT_ID, W_ID → WI_ID). Split onWI_ID. - Decomposition dependency:
WI_ID → W_ID.WI_IDis not a superkey ofS7. - New relation
{ WI_ID, W_ID }. Dependencies:WI_ID → W_ID. Key:WI_ID. Normal form: BCNF. For the same reason as in BCNF-1, it is merged intoR_F. - Residual relation
R_KEY={ T_ID, OE_ID, MT_ID, MC_ID, WI_ID }(5 attributes). No non-trivial dependency holds among these attributes. Candidate key: all five (= K1). Normal form: BCNF. - Dependency preservation: no dependency of the cover is affected. FD17 and FD18 are in
R_F. The derived dependencies ofS6/S7follow from FD12, FD15, FD17 and FD18, which are all preserved. ✓ - Lossless join: the intersection is
{ WI_ID }, andWI_ID → { WI_ID, W_ID }. ✓
Result: every relation is in BCNF. The decomposition into these 12 relations is lossless (each of the 13 binary steps passed the test) and preserves all 18 dependencies of the canonical cover.
Final result and discussion
Normalized relational model
Each relation is followed by its keys (primary key first). An attribute that is the
identifier of another relation is marked → with that relation.
R_USERS (U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH,
U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE,
U_CREATED_AT, U_UPDATED_AT)
keys: U_ID; U_USERNAME; U_EMAIL
R_CRYPTO (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT)
keys: C_ID; C_SYMBOL
R_MARKETS (M_ID, C_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT)
keys: M_ID; {C_ID, M_QUOTE_CURRENCY}
R_HOLDINGS (H_ID, U_ID → R_USERS, C_ID → R_CRYPTO, H_QUANTITY,
H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT)
keys: H_ID; {U_ID, C_ID}
R_ORDERS (O_ID, U_ID → R_USERS, M_ID → R_MARKETS, O_SIDE, O_TYPE, O_STATUS,
O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT)
key: O_ID
R_TRANSACTIONS (T_ID, O_ID → R_ORDERS, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT,
T_DESCRIPTION)
key: T_ID
R_MARKET_TRADES (MT_ID, M_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY,
MT_SIDE, MT_SOURCE, O_ID_FILLSBUY → R_ORDERS (nullable),
O_ID_FILLSSELL → R_ORDERS (nullable)) [= R_C]
key: MT_ID
R_ORDER_EVENTS (OE_ID, O_ID → R_ORDERS, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE,
OE_STATUS_AFTER, OE_CREATED_AT) [= R_B]
key: OE_ID
R_MARKET_CANDLES (MC_ID, M_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW,
MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME) [= R_D]
keys: MC_ID; {M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}
R_WATCHLISTS (W_ID, U_ID → R_USERS, W_NAME, W_CREATED_AT) [= R_E]
key: W_ID
R_WATCHLIST_ITEMS(WI_ID, W_ID → R_WATCHLISTS, C_ID → R_CRYPTO, WI_ADDED_AT) [= R_F]
keys: WI_ID; {W_ID, C_ID}
R_KEY (T_ID, OE_ID, MT_ID, MC_ID, WI_ID) [= R_KEY]
key: all five
Discussion
The eleven data relations are the P2 design, with one difference (transactions.user_id,
explained below). Each relation is one entity set of the ER model:
| P5 relation | P2 table | How the relationships appear |
|---|---|---|
R_USERS | users | — |
R_CRYPTO | crypto | — |
R_MARKETS | markets | C_ID = crypto_id (QuotedOn)
|
R_HOLDINGS | holdings | U_ID = user_id (Holds), C_ID = crypto_id (PositionIn)
|
R_ORDERS | orders | U_ID = user_id (Places), M_ID = market_id (PlacedOn)
|
R_TRANSACTIONS | transactions | O_ID = related_order (Settles); P2 also stores user_id (Records), see below
|
R_MARKET_TRADES | market_trades | M_ID = market_id (Fills), O_ID_FILLSBUY = buy_order_id, O_ID_FILLSSELL = sell_order_id
|
R_ORDER_EVENTS | order_events | O_ID = order_id (Logs)
|
R_MARKET_CANDLES | market_candles | M_ID = market_id (Aggregates)
|
R_WATCHLISTS | watchlists | U_ID = user_id (Owns)
|
R_WATCHLIST_ITEMS | watchlist_items | W_ID = watchlist_id (Contains), C_ID = crypto_id (Lists)
|
The two methods produce the foreign keys differently. In P2 they come from a transformation
rule: a 1:N relationship becomes a column on the N side. Here, each one appears because a
dependency of kind (R), for example O_ID → U_ID, keeps the other entity's identifier in the
same relation as the entity that depends on it. The candidate keys also match, including the
composite ones ({C_ID, M_QUOTE_CURRENCY}, {U_ID, C_ID}, `{M_ID, MC_TIMEFRAME,
MC_CANDLE_TIME}, {W_ID, C_ID}). They are exactly the UNIQUE` constraints in
schema_creation.sql.
The one difference: transactions.user_id. The decomposition drops U_ID from
R_TRANSACTIONS, because T_ID → U_ID follows from T_ID → O_ID and O_ID → U_ID. That is
correct for every ledger entry that settles an order. It does not work for a deposit.
Settles is partial, so a deposit has no order, and without user_id a deposit would have no
owner at all. The de-normalized relation cannot show this case. Every one of its tuples
contains an order (every key contains OE_ID, and every order event has an order), so a
ledger entry without an order cannot appear in it. P2 therefore keeps user_id (the
relationship Records) as a deliberate exception. As a result, the implemented
transactions table is in 2NF but not in 3NF (related_order → user_id is a transitive
dependency), and this is by design. For entries with an order,
transactions.user_id repeats the order's user. The only code that sets related_order (the buy
and sell inserts in advanced_db.sql) writes the user and the id of the same order row. No
database constraint enforces this.
Two order columns in market_trades. FillsBuy and FillsSell needed two role
attributes already in the de-normalized relation, and both end up in R_MARKET_TRADES.
They correspond to buy_order_id and sell_order_id.
R_KEY belongs to the formal result, but it is not implemented as a table. It is the
relation that contains a key of R_EDUBERZA, and the lossless-join result above holds for all
12 relations including it. It records no fact of the domain. It only says which ledger
entry, order event, trade, candle and watchlist item were put into the same tuple, and that
combination exists only because we started from one single relation. Not implementing it is
an implementation decision. The eleven implemented tables are not claimed to reconstruct
R_EDUBERZA on their own. They keep every attribute and every dependency of the canonical
cover, and that is what the application needs.
holdings.avg_price is shown as a derived attribute in the ER model: it can be
recomputed from the buy history. It is still stored, and that is a deliberate
denormalisation (see RelationalDesign (section "Normalisation")).
Normalisation cannot detect this. H_ID → H_AVG_PRICE is an ordinary functional dependency,
because "derivable from rows of another entity" is a property of the application logic
that maintains the value (see UseCase0004,
ON CONFLICT … DO UPDATE), not a dependency between attributes of one tuple.
Which design is used going forward: P2's, unchanged. The eleven data relations coincide
with the eleven tables of schema_creation.sql and
advanced_db.sql column for column, except for the
deliberately kept transactions.user_id explained above. So there are no database objects
to restructure, and the prototype and the reports of P6/P7 keep working against the same
schema.
The table definitions in server/db/schema_creation.sql and server/db/advanced_db.sql:
CREATE TABLE project.users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
username varchar(50) NOT NULL UNIQUE,
email varchar(255) NOT NULL UNIQUE,
full_name varchar(200),
password_hash varchar(255) NOT NULL,
available_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
invested_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (invested_balance >= 0),
-- P7: cash committed to the user's active buy orders, moved out of
-- available_balance when the order is placed and consumed as it fills.
reserved_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (reserved_balance >= 0),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz
);
CREATE TABLE project.crypto (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
symbol varchar(20) NOT NULL UNIQUE,
name varchar(255) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE project.markets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
crypto_id uuid NOT NULL REFERENCES project.crypto(id),
quote_currency char(3) NOT NULL DEFAULT 'USD',
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
);
CREATE TABLE project.holdings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
crypto_id uuid NOT NULL REFERENCES project.crypto(id),
quantity numeric(20,4) NOT NULL CHECK (quantity >= 0),
-- Committed to the user's own open sell orders, not yet removed from the
-- position. quantity - reserved_quantity is what is actually free to
-- sell — the crypto-side equivalent of users.available_balance.
reserved_quantity numeric(20,4) NOT NULL DEFAULT 0
CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity),
-- Weighted-average entry price. NOT NULL so that the P/L arithmetic in
-- v_portfolio can never silently produce NULL for an existing position.
avg_price numeric(18,6) NOT NULL DEFAULT 0 CHECK (avg_price >= 0),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz,
CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
);
CREATE TABLE project.orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
market_id uuid NOT NULL REFERENCES project.markets(id),
side varchar(4) NOT NULL CHECK (side IN ('buy', 'sell')),
type varchar(20) NOT NULL CHECK (type IN ('market', 'limit')),
status varchar(20) NOT NULL CHECK (status IN ('open', 'partially_filled', 'executed', 'cancelled')),
quantity numeric(20,4) NOT NULL CHECK (quantity > 0),
-- P7: how much of the order has been traded so far; remaining is
-- quantity - filled_quantity. Maintained from market_trades.
filled_quantity numeric(20,4) NOT NULL DEFAULT 0
CHECK (filled_quantity >= 0 AND filled_quantity <= quantity),
price numeric(18,6),
placed_at timestamptz NOT NULL DEFAULT now(),
executed_at timestamptz
);
CREATE TABLE project.transactions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
type varchar(50) NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
amount numeric(18,4) NOT NULL,
currency char(3) NOT NULL DEFAULT 'USD',
related_order uuid REFERENCES project.orders(id),
created_at timestamptz NOT NULL DEFAULT now(),
description text
);
CREATE TABLE project.market_trades (
id bigserial PRIMARY KEY,
market_id uuid NOT NULL REFERENCES project.markets(id),
executed_at timestamptz NOT NULL,
price numeric(18,6) NOT NULL CHECK (price > 0),
quantity numeric(20,6) NOT NULL CHECK (quantity > 0),
side varchar(4) CHECK (side IN ('buy', 'sell')),
source varchar(50) NOT NULL DEFAULT 'simulation',
-- P7: the orders this trade filled. NULL on a side means the counterparty
-- was the simulated market (bot ticks have both NULL).
buy_order_id uuid REFERENCES project.orders(id),
sell_order_id uuid REFERENCES project.orders(id)
);
CREATE TABLE project.market_candles (
id bigserial PRIMARY KEY,
market_id uuid NOT NULL REFERENCES project.markets(id),
timeframe varchar(5) NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
open numeric(18,6) NOT NULL,
high numeric(18,6) NOT NULL,
low numeric(18,6) NOT NULL,
close numeric(18,6) NOT NULL,
volume numeric(20,6) NOT NULL,
candle_time timestamptz NOT NULL,
CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
);
CREATE TABLE project.watchlists (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
name varchar(100) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE project.watchlist_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
watchlist_id uuid NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
crypto_id uuid NOT NULL REFERENCES project.crypto(id),
added_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
);
CREATE TABLE project.order_events (
id bigserial PRIMARY KEY,
order_id uuid NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE,
event_type varchar(20) NOT NULL
CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')),
quantity numeric(20,4) NOT NULL,
price numeric(18,6),
status_after varchar(20) NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
