My first article on using foreign key constraints in databases included the claim that using them slows down an application. This article addresses the claim that a Foreign Key Constraint use locks and can cause Deadlocks and thus slow down applications.

Claim

Foreign key constraints can lead to lock contention or timeouts in a busy system. When a record in a referenced table is updated or deleted, the RDBMS must wait for any locks on the corresponding rows in the referencing table to be released to validate the integrity.

Foreign Key Constraint and Locks

A Foreign key constraint must use locks on the reference table when it is checked; surely no one will disagree on this point. But how do locks affect the performance of an RDBMS system?

What kind of Locks must be used?

The core statement that a Foreign Key Constraint slows down a database system by blocking requires a deeper understanding of the locking chain when a foreign key constraint is involved. To explain the locking behavior, I use a simple Foreign Key Constraint between the [demo].[customers] table and [dbo].[nations]. Each customer must be assigned to a nation that is predefined by the [dbo].[nations] table.

The figure shows a foreign key relationship in the database between the demo.customers table and dbo.nations.
1:n relationship between dbo.nations and demo.customers

To evaluate the locking chain, a new record is entered in the [demo].[customers] table. The [c_nationkey] attribute uses the [dbo].[nations] table to enforce the foreign key relationship.

/* Start a user transaction */
BEGIN TRANSACTION;
GO
	/* Insert a new record int the demo.customers table */
	INSERT INTO demo.customers
	SELECT	*
	FROM	dbo.customers
	WHERE	c_custkey = 1;

	/* We check the lock resource for the lock chaing */
	SELECT %%lockres%% AS locked_resource, * FROM demo.customers;

	/*
		What resources are locked by the transaction?
		Note: The function is an object from the demo database!	
	*/
	SELECT * FROM dbo.get_locking_status(@@SPID);

The figure shows the locks used for a new record. The table referenced by the foreign key is not in the lock chain!
Lock Chain of newly inserted row into [demo].[customers]

What is striking is that the table [dbo].[nations] – referenced by the Foreign Key Constraint – is not blocked again after the record has been inserted. Neither a shared lock nor an exclusive lock is on the reference table. At first glance, this strategy seems surprising, because SQL Server must protect the value of the foreign key from changes as long as the transaction is still open!

The figure shows how intelligently SQL Server manages the locks for an INSERT statement with a foreign key relationship.
First, the lookup tables are given a read lock and after the data record has been entered, the read locks are released again.
locking chain for a new data record

At this point you will notice how smart SQL Server (like other RDBMS systems) has implemented Foreign Key Constraints! When experienced database developers say that a Foreign Key Constraint hold locks for too long and thus the RDBMS no longer scales under high transactional workloads, this is not true!

Handling of concurrency

If SQL Server only maintains the lock on the reference table for a short period of time, the question arises as to how the database system will find a secure method to maintain the integrity rules of a Foreign Key Constraint . Since the lock on the Foreign Key Constraint is released immediately after the check, a change may have already been made in the reference table by the time a new record is next inserted. Ensuring integrity is implemented cleverly!

How long will the locks be held?

When checking a foreign key constraint, a modern RDBMS system releases the locks on the reference table as soon as the check is complete. This makes the reference table available to other processes. A maintained lock would not be a problem as long as the reference table only needs to be accessed for reading. To enable write processes to access the reference table, locks are released as quickly as possible! A typical scenario for concurrent read/write operations would be an order table that is used as a reference table for the order items!

What impact will these locks have on other processes?

During the brief moment of the SH-Lock, other processes can continue to read the Foreign Key Table. Writing processes have to wait during the action. However, since the reading process immediately releases the resources again, the delay is only marginal!

The process that wants to change a foreign key value must wait for the process that is currently using that foreign key in a transaction.
Changes on dbo.nations must wait until the INSERT process has finished

The process that wants to change a key value in the Foreign Key table must wait for the process that is currently using that Foreign Key in a transaction. Due to the nature of a Foreign Key, when the key value is changed, the detail table must always be checked to see whether this key value is being used. To do this, the process requires a SH-Lock on the individual key values. The UPDATE process waits for this lock.

If the Primary Key is changed in a reference table, this can have an impact. The Primary Key of the reference table is usually the foreign key for the detail table. When a new row is entered in the detail table, the Foreign Key constraint must be checked. If the row with the corresponding key value is itself part of a write operation, the INSERT process in the detail table must wait! Delays can occur!

Delays are possible if a key in the reference table is changed. The Transaction T1 must wait with the INSERT process until the changes on the references table are committed.
Delays are possible if a key in the reference table is changed.

A Foreign Key Constraint and Deadlocks

The use of a Foreign Key Constraint was also seen critically because of „deadlock scenarios“. Of course, a Foreign Key Constraint can lead to deadlock situations. But… – it is not the foreign key relationship that is the problem, but the way in which business processes are implemented.

The figure shows how a deadlock can occur because the foreign key constraint requests an SH lock on rows of the demo.customers table, while the process holding an X lock on the resource requests an SH lock on the dbo.nations table.
Deadlock because of behavior of Foreign Key Constraint

Transaction 1 holds an exclusive lock on the new data record to be entered, which receives the value 1 for the c_nationkey. There are NO more locks on dbo.nations!

Transaction 2 wants to delete the row with [n_nationkey] = 1 from the [dbo].[nations] table. The Foreign Key Constraint requires that the RDBMS checks rows in the detail table [demo].[customers] to prevent deletion if customers with [c_nationkey] = 1 exist.

NOTE: Up to this point, there is NO risk of a deadlock because the two transactions are each locking different resources!

Transaction 1 wants to access the table [dbo].[nations] and the row with [n_nationkey] = 1. The stalemate that now occurs cannot be resolved and Microsoft SQL Server terminates the transaction with the lower log volume.

Question: Is this a problem because of the foreign key constraint?

Answer: Yes and No.

I admit, the example will probably never happen like this. But in a high transactional system, it cannot be ruled out that locking conflicts can arise. It is the developer’s job to take these situations into account; They should know the processes better than anyone else! Technically, the Foreign Key Constraint is the cause of the deadlock, but the technical implementation is the real cause.

BEGIN TRANSACTION;
GO
	/* Insert a new record int the demo.customers table */
	SELECT	*
	FROM	dbo.nations WITH (UPDLOCK, HOLDLOCK)
	WHERE	n_nationkey = 1;

	INSERT INTO demo.customers
	SELECT	*
	FROM	dbo.customers
	WHERE	c_custkey = 1;

First, the record in [dbo].[nations] is blocked for updates and then the new record is entered. A second process that wants to change the row in [dbo].[nations] must wait until Transaction 1 has finished.

Conclusion

My personal conclusion is that locks and deadlocks should never be used as an excuse for not implementing foreign key constraints in your database. It must be the developers‘ job to deal intensively with locking behavior and concurrency. Then foreign key constraints can live peacefully with the data in a database until new requirements separate them again :)

Thank you for reading!