The algorithm starts by representing the original relation R with attributes A, B, C, D as the root node in the tree. Functional dependencies are noted but not yet applied.
💡 Initialization sets the base schema from which decomposition will proceed, making clear what attributes and dependencies are involved.
Line:R = Relation(['A','B','C','D']) # STEP 1
💡 The root node contains all attributes, showing the starting point before decomposition.
traverse
Identify First Functional Dependency to Decompose
The algorithm selects the first functional dependency A -> B to check if decomposition is needed to preserve dependencies and ensure lossless join.
💡 Choosing the first dependency focuses the algorithm on one dependency at a time, simplifying the process.
Line:fd = FunctionalDependency('A', 'B') # STEP 2
💡 Focusing on one FD at a time helps in stepwise decomposition and verification.
compare
Check if Decomposition Needed for A -> B
The algorithm checks if the current schema contains both A and B to decide if decomposition is necessary to preserve the dependency.
💡 This check determines if the dependency is already preserved or if splitting is required.
Line:if 'A' in R.keys and 'B' in R.keys: # STEP 3
💡 Presence of both attributes in one schema means dependency can be preserved by decomposition.
split
Split Node for Dependency A -> B
The root node is split into two child nodes: one with attributes {A, B} and another with remaining attributes {C, D}, to preserve the dependency A -> B.
💡 Splitting creates smaller schemas that isolate dependencies, essential for normalization.
💡 Intersections of decomposed schemas contain keys ensuring lossless join.
reconstruct
Output Final Decomposition Result
The algorithm outputs the final decomposition schemas {R1(A,B), R2(B,C), R3(C,D)} confirming lossless join and dependency preservation.
💡 Final output shows the successful decomposition meeting all criteria.
Line:return [R1, R2, R3] # STEP 11
💡 Final decomposition schemas are lossless join and dependency preserving.
class Relation:
def __init__(self, keys):
self.keys = set(keys)
class FunctionalDependency:
def __init__(self, lhs, rhs):
self.lhs = lhs
self.rhs = rhs
# STEP 1
R = Relation(['A','B','C','D'])
# STEP 2
fd1 = FunctionalDependency('A', 'B')
# STEP 3
if 'A' in R.keys and 'B' in R.keys:
# STEP 4
R1 = Relation(['A','B'])
R2 = Relation(['C','D'])
# STEP 5
fd2 = FunctionalDependency('B', 'C')
# STEP 6
if 'B' in R1.keys and 'C' in R2.keys:
# STEP 7
R3 = Relation(['C'])
R4 = Relation(['D'])
# STEP 8
fd3 = FunctionalDependency('C', 'D')
# STEP 9
if 'C' in R3.keys and 'D' in R4.keys:
pass # Dependency preserved
# STEP 10
if R1.keys.intersection(R2.keys) or R2.keys.intersection(R3.keys):
lossless = True
# STEP 11
result = [R1, Relation(['B','C']), Relation(['C','D'])]
return result
📊
Lossless Join & Dependency Preservation in Decomposition - Watch the Algorithm Execute, Step by Step
Watching this step-by-step visualization helps you understand the subtle conditions that guarantee lossless join and dependency preservation, which are often abstract in theory.
Step 1/11
·Active fill★Answer cell
setup
ABCD
leaf
Initialize root node with all attributes of relation R.
traverseKey: A -> B
ABCD
leaf
Select functional dependency A -> B for processing.
compareKey: A -> B
ABCD
leaf
Check if schema contains both A and B to decide on decomposition.
splitKey: A -> B↑ promoted: A
ABCD
AB
leaf
CD
leaf
Split root node into R1(A,B) and R2(C,D) to preserve A -> B.
traverseKey: B -> C
ABCD
AB
leaf
CD
leaf
Focus on node with B to check dependency B -> C.
compareKey: B -> C
ABCD
AB
leaf
CD
leaf
Check if B and C are in the same node to preserve B -> C.
splitKey: B -> C↑ promoted: C
ABCD
AB
leaf
CD
C
leaf
D
leaf
Split node {C,D} into {C} and {D} to preserve B -> C.
traverseKey: C -> D
ABCD
AB
leaf
CD
C
leaf
D
leaf
Focus on node with C to check dependency C -> D.
compareKey: C -> D
ABCD
AB
leaf
CD
C
leaf
D
leaf
Check if C and D are in the same or connected nodes to preserve C -> D.
compare
ABCD
AB
leaf
CD
C
leaf
D
leaf
Verify intersections of decomposed schemas ensure lossless join.
reconstruct
ABCD
AB
leaf
BC
C
leaf
D
leaf
Output final decomposition preserving lossless join and dependencies.
Key Takeaways
✓ Decomposition must isolate dependencies to preserve them while ensuring lossless join.
This is hard to see from code alone because the interplay between dependencies and schema splits is subtle.
✓ Lossless join depends on intersections of decomposed schemas containing keys that enable correct joins.
Visualizing the tree clarifies how intersections guarantee no data loss.
✓ Sequentially processing each functional dependency guides the decomposition steps and decisions.
The trace shows how each dependency influences the tree structure and decomposition.
Practice
(1/5)
1. Trace the sequence of events when two transactions concurrently update the same row under pessimistic locking with row-level locks. What happens if the second transaction tries to acquire the lock while the first holds it?
easy
A. The second transaction immediately aborts with a deadlock error
B. The second transaction acquires a shared lock and reads the row but cannot update
C. The second transaction proceeds and overwrites the first transaction's changes
D. The second transaction waits until the first transaction commits or rolls back
Solution
Step 1: Understand pessimistic locking behavior
Pessimistic locking blocks conflicting operations to prevent concurrency issues.
Step 2: Analyze lock acquisition
When the first transaction holds an exclusive row-level lock, the second transaction must wait to acquire the same exclusive lock.
Step 3: Outcome of concurrent update attempts
The second transaction is blocked until the first commits or rolls back, releasing the lock.
Final Answer:
Option D -> Option D
Quick Check:
Waiting prevents dirty writes and ensures serializability under pessimistic concurrency control.
Hint: Pessimistic locks cause waiting, not immediate aborts or overwrites.
Common Mistakes:
Thinking the second transaction aborts immediately
Assuming the second transaction can overwrite without waiting
Confusing shared and exclusive locks in updates
2. Consider a 1:N relationship between entities Department and Employee, where one department has many employees. When a new employee is added, what is the sequence of steps to maintain referential integrity in the database?
easy
A. Insert the employee record without any reference to the department.
B. Insert the employee record with a foreign key referencing the department's primary key; verify the department exists.
C. Insert the department record with a foreign key referencing the employee's primary key.
D. Insert the employee record first, then create the department record if missing.
Solution
Step 1: Understand the 1:N relationship
Each employee belongs to one department; department is the 'one' side.
Employee table must have a foreign key referencing Department's primary key.
Step 3: Insert employee with valid department reference
Before inserting employee, ensure the department exists to avoid foreign key violation.
Step 4: Evaluate other options
Insert the employee record first, then create the department record if missing inserts employee before department, risking integrity violation. Insert the department record with a foreign key referencing the employee's primary key reverses foreign key direction, which is incorrect. Insert the employee record without any reference to the department omits foreign key, breaking relationship.
Final Answer:
Option B -> Option B
Quick Check:
Foreign keys must reference existing primary keys to maintain integrity.
Hint: In 1:N, the 'many' side holds the foreign key referencing the 'one' side.
Common Mistakes:
Inserting dependent records before referenced records
Misplacing foreign keys on the 'one' side instead of 'many' side
3. Given the following SQL query executed on the Employee table with rows (1,100), (2,200), (3,300), (4,300), what is the output printed by the Python code below?
import sqlite3
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
cursor.execute('CREATE TABLE Employee(Id INTEGER, Salary INTEGER)')
cursor.executemany('INSERT INTO Employee VALUES (?, ?)', [(1, 100), (2, 200), (3, 300), (4, 300)])
query = '''
SELECT Salary FROM (
SELECT DISTINCT Salary FROM Employee
ORDER BY Salary DESC
LIMIT 1 OFFSET 1
)
'''
cursor.execute(query)
result = cursor.fetchone()
print(result[0] if result else 'NULL')
conn.close()
easy
A. 300
B. NULL
C. 100
D. 200
Solution
Step 1: Identify distinct salaries and order descending
Distinct salaries are [300, 200, 100] ordered descending.
Step 2: Apply LIMIT 1 OFFSET 1
OFFSET 1 skips the highest (300), LIMIT 1 picks the next, which is 200.
Final Answer:
Option D -> Option D
Quick Check:
Distinct ordering and offset correctly select second highest salary [OK]
Not using DISTINCT leads to duplicate highest salary
4. Given the following Python code simulating a JOIN to find customers with orders, what is the output of the print statement?
easy
A. ['Alice', 'Bob', 'Charlie']
B. ['Bob', 'Charlie']
C. ['Alice']
D. ['Alice', 'Bob']
Solution
Step 1: Trace order_customer_ids set
Extract customer IDs from orders: {1, 2}.
Step 2: Filter customers by IDs in order_customer_ids
Customers with id 1 and 2 are Alice and Bob; Charlie (id 3) is excluded.
Final Answer:
Option D -> Option D
Quick Check:
Only Alice and Bob have orders, matching output [OK]
Hint: Set membership filters customers with orders correctly [OK]
Common Mistakes:
Including customers without orders
Off-by-one errors in filtering
Confusing customer IDs with order IDs
5. In a geo-distributed NoSQL system designed for high availability and partition tolerance, an application requires strict consistency for a subset of critical transactions. Which approach best addresses this requirement without sacrificing overall system availability?
hard
A. Switch the entire system to a strongly consistent SQL database cluster
B. Accept eventual consistency for all transactions to maintain availability
C. Implement a hybrid approach using multi-model databases or selective consistency zones for critical data
D. Use synchronous replication across all nodes for all data to guarantee consistency
Solution
Step 1: Understand system constraints
The system is geo-distributed, prioritizing availability and partition tolerance, but needs strict consistency for some transactions.
Step 2: Evaluate options
Switch the entire system to a strongly consistent SQL database cluster sacrifices availability globally, which is undesirable. Accept eventual consistency for all transactions to maintain availability ignores strict consistency needs. Use synchronous replication across all nodes for all data to guarantee consistency sacrifices availability due to synchronous replication delays.
Step 3: Hybrid solution
Implement a hybrid approach using multi-model databases or selective consistency zones for critical data proposes selective consistency zones or multi-model databases, allowing critical transactions to be strongly consistent while maintaining availability elsewhere.
Final Answer:
Option C -> Option C
Quick Check:
Hybrid approaches balance consistency and availability per data criticality [OK]
Hint: Hybrid selective consistency zones enable strict consistency without losing availability globally
Common Mistakes:
Thinking entire system must switch to SQL for consistency
Assuming eventual consistency suffices for critical transactions
Believing synchronous replication is always feasible in geo-distributed setups
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.