When you add a primary key constraint to a table in SQL Server, an index will be created automatically. When you add a foreign key constraint no index will be created. This might cause issues if you don't know that this is the behavior in SQL Server. Maybe there should be an option to automatically index the foreign keys in SQL Server, what do you think?
The other day some deletes on a newer table in the test environment became really slow. We had a primary table with a couple of hundred rows, we loaded up between 200 and 300 million rows into the child table. Then we deleted the child rows, this was fast. After this, we deleted one row from the primary table and this took several seconds.
When I looked at this I noticed something interesting, the most time during the delete was spent doing a lookup at the child table. Then I noticed that the foreign key was not indexed. After we added the index the delete became thousands of times faster
Let's try to replicate that behavior here
First create these primary table, we will add 2048 rows to this table
--Table that will have 2048 rows CREATE TABLE Store(StoreID int not null, DateCreated datetime not null, StoreName varchar(500), constraint pk_store primary key (StoreID)) GO --insert 2048 rows INSERT Store SELECT ROW_NUMBER() OVER(ORDER BY t1.number) AS StoreID,DATEADD(dd,t1.number,'20161101')
AS datecreated, NEWID() FROM master..spt_values t1 WHERE t1.type = 'p'
Now create the child table and add 500K rows, this might take up to 1 minute to run since the rows are pretty wide.
-- table that will also have 500000 rows, fk will be indexed CREATE TABLE GoodsSold (TransactionID int not null, StoreID int not null, DateCreated datetime not null, SomeValue char(5000), constraint pk_transaction primary key (TransactionID)) INSERT GoodsSold SELECT top 500000 ROW_NUMBER() OVER(ORDER BY t1.number) AS TransactionID, t2.StoreID,
DATEADD(dd,t1.number,'20161101') AS datecreated, REPLICATE('A',5000) FROM master..spt_values t1 CROSS JOIN Store t2 WHERE t1.type = 'p'
Now it is time to add the foreign key constraint and index this foreign key constraint
-- adding the foreign key ALTER TABLE GoodsSold WITH CHECK ADD CONSTRAINT FK_StoreID FOREIGN KEY(StoreID) REFERENCES Store(StoreID) GO ALTER TABLE GoodsSold CHECK CONSTRAINT FK_StoreID GO -- index the foreign key CREATE index ix_StoreID on GoodsSold(StoreID) GO
We will create another set of tables, let's start with the primary table, we will just insert into this table all the rows from the primary table we created earlier
-- create another primary table CREATE TABLE StoreFK(StoreID int not null, DateCreated datetime not null, StoreName varchar(500), constraint pk_storefk primary key (StoreID)) GO -- add the same 2048 rows from the primary table with indexed FK INSERT StoreFK SELECT * FROM Store GO
For the child table, it is the same deal, we will add all the rows from the child table we created earlier into this table
-- Add another FK table CREATE TABLE GoodsSoldFKNoIndex (TransactionID int not null, StoreID int not null, DateCreated datetime not null, SomeValue char(5000), constraint pk_transactionfk primary key (TransactionID)) -- add same 500K rows from table with FK index INSERT GoodsSoldFKNoIndex SELECT * FROM GoodsSold
Let's add the foreign key constraint, but this time we are not indexing the foreign key constraint
-- add the FK but do not index this ALTER TABLE GoodsSoldFKNoIndex WITH CHECK ADD CONSTRAINT FK_StoreID_FK
FOREIGN KEY(StoreID) REFERENCES StoreFK(StoreID) GO ALTER TABLE GoodsSoldFKNoIndex CHECK CONSTRAINT FK_StoreID_FK GO
Let make sure that the tables have the same number of rows
-- check that the tables have the same rows exec sp_spaceused 'GoodsSold' exec sp_spaceused 'GoodsSoldFKNoIndex'
name | rows | reserved | data | index_size | unused |
---|---|---|---|---|---|
GoodsSold | 500000 | 4024976 KB | 4000000 KB | 22520 KB | 2456 KB |
GoodsSoldFKNoIndex | 500000 | 4015440 KB | 4000000 KB | 14936 KB | 504 KB |
Now that we are setup, let's wipe out all the rows from the child table for a specific StoreID, the SELECT statements should return 0 rows
DELETE GoodsSoldFKNoIndex WHERE StoreID = 507 DELETE GoodsSold WHERE StoreID = 507 SELECT * FROM GoodsSoldFKNoIndex WHERE StoreID = 507 SELECT * FROM GoodsSold WHERE StoreID = 507
Now we are getting to the interesting part, turn on Include Actual Execution Plan, run statistics IO or run this in Plan Explorer
DELETE Store WHERE StoreID = 507 DELETE StoreFK WHERE StoreID = 507
You will see something like this
So 75% compared to 25%, not good but doesn't look catastrophic, if you have statistics time on, you will see the following
SQL Server Execution Times:
CPU time = 16 ms, elapsed time = 16 ms.
SQL Server Execution Times:
CPU time = 561 ms, elapsed time = 575 ms.
Now it looks much worse
What about statistics io?
Table 'GoodsSold'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Store'. Scan count 0, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'GoodsSoldFKNoIndex'. Scan count 1, logical reads 501373, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'StoreFK'. Scan count 0, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
That is terrible....
Here is also the view from Plan Explorer, look at Est. CPU Cost and Reads
There you have it, not indexing foreign keys can have a big impact even though the child table might not have any data at all
See also When did SQL Server stop putting indexes on Foreign Key columns? by Kimberly Tripp
No comments:
Post a Comment