It has been a while since we have had a puzzle on this blog post, so today we are going to see a very simple but brain teaser puzzle. This puzzle is about Deleting Qualified Rows From Multiple Tables. This question was actually asked to me by my client while working together on the Comprehensive Database Performance Health Check.
Everyone who gives correct answers to the puzzle will get a 50% discount on SQL Server Performance Tuning Practical Workshop – Recorded Classes and One person will get FREE Access to it as well.
Puzzle Setup – Multiple Tables
The problem which I am going to describe happened with my one of the largest client who is working in the financial technology area (a private bank). However, due to privacy issues, I can’t write here the details about their table structure, I am going to use a sample database of the students and class.
The setup is very simple. We have three tables.
Table 1: Student – Containing student names
Table 2: Class – Containing the class name
Table 3: StudentClass- Containing the relationship between student and class
Here is the script to set up that up:
USE TempDB GO -- Set up environment CREATE TABLE Student (ID INT, StudentName VARCHAR(100)) GO CREATE TABLE Class (ID INT, ClassName VARCHAR(100)) GO CREATE TABLE StudentClass (ID INT, ClassID INT, StudentID INT) GO -- Populate Data INSERT INTO Student (ID, StudentName) SELECT 1, 'Mark' UNION SELECT 2, 'John' UNION SELECT 3, 'Thomas' GO -- Populate Data INSERT INTO Class (ID, ClassName) SELECT 1, 'Maths' UNION SELECT 2, 'English' UNION SELECT 3, 'Science' GO -- Populate Data INSERT INTO StudentClass (ID, ClassID, StudentID) SELECT 1, 1, 1 UNION SELECT 2, 2, 2 UNION SELECT 3, 1, 3 UNION SELECT 4, 2, 1 UNION SELECT 5, 2, 3 UNION SELECT 6, 3, 1 GO -- Query to List all Students and Classes SELECT DISTINCT s.StudentName, c.ClassName FROM StudentClass sc INNER JOIN Class c ON c.ID = sc.ClassID INNER JOIN Student s ON s.ID = sc.StudentID GO
Here is the output of the query which we just ran. It lists the name of the students with the class they are enrolled in.
Puzzle Statement – Delete from Multiple Tables
Due to some reason, in the school, the class of English was canceled during the summer. All the students who had enrolled in the English class were affected. Here are two scenarios for us for the students who had taken English class.
Scenario 1: If a student has enrolled in a single class of English – Remove from the Student’s list and Remove from the StudentClass list.
Scenario 2: If a student has enrolled in multiple class with English class – Remove from the StudentClass list.
Here is the initial script was written which satisfies both the scenarios.
SET STATISTICS IO ON -- Delete from Students Table DELETE s FROM Student s INNER JOIN StudentClass sc ON s.ID = sc.StudentID INNER JOIN Class c ON c.ID = sc.ClassID WHERE c.ClassName = 'English' AND sc.StudentID IN (SELECT sc2.StudentID FROM StudentClass sc2 GROUP BY sc2.StudentID HAVING COUNT(*) = 1) GO -- Delete from StudentClass Table DELETE sc FROM StudentClass sc INNER JOIN Class c ON c.ID = sc.ClassID WHERE c.ClassName = 'English' GO
If you run the above query, you will get the following output.
Additionally, when I go to messages, it shows the following output for multiple tables:
Table ‘Student’. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘Class’. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘StudentClass’. Scan count 7, logical reads 9, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row affected)
Table ‘StudentClass’. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘Class’. Scan count 1, logical reads 6, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(3 rows affected)
Your Task:
Optimize the above DELETE statement. That’s it!
How to Participate
Once you figure out how to optimize the DELETE statement, post your answer in the comments section. Your answer will be invisible until July 20th. I will publish all the answers on July 20th.
I will check the performance of the DELETE statement by running above DELETE statement and your modified suggestion. If your suggestion takes less amount of total logical reads, you will be the winner.
You have complete freedom to do anything to optimize the DELETE statement. Here are a few ideas:
- Re-write DELETE statement
- Combine DELETE operations
- USE CTE
- USE TempTables
- Create Indexes
- Use Subqueries
Pretty much anything you can think of it, you can try. However, your answer should be correct and logical reads should be less. I will also test by passing the different subject in the WHERE clause and will see that your query follows both the scenarios mentioned above.
Everyone Gets to Win
Everyone who gives correct answers to the puzzle will get a 50% discount on SQL Server Performance Tuning Practical Workshop – Recorded Classes and One person will get FREE Access to it as well.
Reference:Â Pinal Dave (https://blog.sqlauthority.com)
48 Comments. Leave new
SET STATISTICS IO ON
–select *
delete sc
from dbo.StudentClass as sc
inner join dbo.Class as c
on sc.ClassID = c.ID
where c.ClassName = ‘english’
–select s.*
delete s
from dbo.Student as s
where not exists (select *
from dbo.StudentClass as sc
inner join dbo.Class as c
on sc.ClassID = c.ID
where sc.StudentID = s.ID
and c.ClassName ‘english’)
This deletes everything from both the tables.
SET STATISTICS IO ON
DECLARE @ClassID AS INT
SELECT @ClassID=ID FROM Class WHERE ClassName=’English’
DELETE s
FROM Student s
INNER JOIN StudentClass sc ON s.ID = sc.StudentID
WHERE sc.ClassID = @ClassID AND
sc.StudentID IN
(SELECT sc2.StudentID
FROM StudentClass sc2
GROUP BY sc2.StudentID
HAVING COUNT(*) = 1)
DELETE sc
FROM StudentClass sc
WHERE sc.ClassID = @ClassID
GO
This is so far the most efficient answer 5 logical reads.
–select *
delete sc
from dbo.StudentClass as sc
inner join dbo.Class as c
on sc.ClassID = c.ID
where c.ClassName = ‘english’;
–select s.*
delete s
from dbo.Student as s
outer apply(select top(1) sc.StudentID
from dbo.StudentClass as sc
where sc.StudentID = s.ID
order by sc.ID
)oa
where oa.StudentID is null
11 Logical read. Very simple solution.
Hi Pinal.
I’m not sure where it is allowed to use variables and whether we should optimize for general case…. So my version for this particular case:
create unique clustered index PK_Student on Student(ID);
declare @id int = (select top 1 ID from Class c WHERE c.ClassName = ‘English’);
delete s
from Student s
where ID in (select StudentID
from StudentClass
group by StudentID
having count(case when ClassID=@id then 1 end)=1)
option(merge join);
delete StudentClass where ClassID = @id;
(5 logical reads for deletes + 1 logical read for create clustered index)
This query deletes everything from the student class table.
Here is my solution. I believe by using table variable and OUTPUT clause, I was about to bring the page read to 5 page reads only.
— Declare Variable for @ClassID
DECLARE @ClassID AS INT
SELECT @ClassID=ID
FROM Class WHERE
ClassName=’English’;
— Delete from StudentClass Table
DELETE sc
FROM StudentClass sc
WHERE sc.ClassID = @ClassID;
— Delete from Students Table
DELETE s
FROM dbo.Student s
WHERE NOT EXISTS (SELECT StudentID
FROM StudentClass sc WHERE sc.StudentID = s.ID)
Here is the output I receive where it says there are only 6 logical reads.
Table ‘Class’. Scan count 1, logical reads 1,
Table ‘StudentClass’. Scan count 1, logical reads 1
Table ‘Student’. Scan count 1, logical reads 1
Table ‘StudentClass’. Scan count 1, logical reads 3
Btw, I have already attended your class 3 years ago in the UK if you remember me. (Hint: You pronounced my name wrong till the end of the class) I used the two techniques which you had taught in the class in this solution. I hope that I can win this puzzle. If I do not win, I will still purchase the recorded class.
Hi Calunoth,
I do remember you – great solution indeed.
I am happy that you remember the improve the query performance.
SET STATISTICS IO OFF
USE TempDB
GO
— Set up environment
SET NOCOUNT ON
GO
DROP TABLE IF EXISTS Student
CREATE TABLE Student (ID INT, StudentName VARCHAR(100))
GO
DROP TABLE IF EXISTS Class
CREATE TABLE Class (ID INT , ClassName VARCHAR(100))
GO
DROP TABLE IF EXISTS StudentClass
CREATE TABLE StudentClass (ID INT, ClassID INT, StudentID INT)
GO
— Populate Data
INSERT INTO Student (ID, StudentName)
SELECT 1, ‘Mark’
UNION
SELECT 2, ‘John’
UNION
SELECT 3, ‘Thomas’
GO
— Populate Data
INSERT INTO Class (ID, ClassName)
SELECT 1, ‘Maths’
UNION
SELECT 2, ‘English’
UNION
SELECT 3, ‘Science’
GO
— Populate Data
INSERT INTO StudentClass (ID, ClassID, StudentID)
SELECT 1, 1, 1
UNION
SELECT 2, 2, 2
UNION
SELECT 3, 1, 3
UNION
SELECT 4, 2, 1
UNION
SELECT 5, 2, 3
UNION
SELECT 6, 3, 1
GO
— Query to List all Students and Classes
SELECT DISTINCT s.StudentName,
c.ClassName
FROM StudentClass sc
INNER JOIN Class c ON c.ID = sc.ClassID
INNER JOIN Student s ON s.ID = sc.StudentID
GO
/* My solution */
SET STATISTICS IO ON
— Class ID we want to get rid of
DECLARE @CLASSid integer
SELECT @CLASSid = ID
FROM Class
WHERE ClassName = ‘English’
— Delete from StudentClass Table
DELETE sc
FROM StudentClass sc
WHERE SC.ClassID = @CLASSid
— Delete any students that now dont have any class to go to — could also use not exists
DELETE FROM Student
WHERE ID NOT IN (
SELECT StudentID
FROM StudentClass
)
/* 6 LOGICAL READS */
GO
SET STATISTICS IO OFF
go
SELECT DISTINCT s.StudentName,
c.ClassName
FROM StudentClass sc
INNER JOIN Class c ON c.ID = sc.ClassID
INNER JOIN Student s ON s.ID = sc.StudentID
6 Logical reads indeed. Amazing solution.
1) Create Foreign key relationship with “On Cascade Delete”. So ID in the student table, will reference the StudentID in StudentClass Table.
2) While executing delete statement on Student table, it will automatically deletes the record on StudentClass table also
Would love to see a working example if you can post it, please.
SET STATISTICS IO ON;
— Delete from StudentClass Table
DELETE sc
FROM StudentClass sc
INNER JOIN Class c ON c.ID = sc.ClassID
WHERE c.ClassName = ‘English’;
GO
— Delete from Students Table
DELETE FROM dbo.Student
WHERE ID NOT IN (SELECT StudentID
FROM dbo.StudentClass);
Simple and effective.
So far 11 page reads.
SET STATISTICS IO ON
Select s.ID,S.StudentID INto #temp from
StudentClass s
Where StudentID In(Select StudentID from StudentClass Group BY StudentID Having Count(ClassID) = 1)
AND EXISTS(Select ID from Class c Where c.ID=s.classID AND c.ClassName=’ENglish’)
— Delete from StudentClass Table
Delete From StudentClass
Where ID IN (Select ID from #temp)
— Delete from Students Table
Delete From Student
Where ID IN (Select StudentID from #temp)
DROP Table #temp
Select s.ID,S.StudentID INto #temp1 from
StudentClass s
Where StudentID In(Select StudentID from StudentClass Group BY StudentID Having Count(ClassID) > 1)
AND EXISTS(Select ID from Class c Where c.ID=s.classID AND c.ClassName=’ENglish’)
— Delete from StudentClass Table
Delete From StudentClass
Where ID IN (Select ID from #temp1)
— Delete from Students Table
Delete From Student
Where ID IN (Select StudentID from #temp1)
DROP Table #temp1
A great attempt but the logical reads are way higher compared to original my logical reads. In your case, it is 39 total logical reads.
Additionally, it deletes everything from StudentClass table.
Hi:
I will able to achieve that by using cursor
Create proc DeleteStudent @className nvarchar(max)
as
Begin
Declare @ClassId int
select @classId=c.ID from Class as c where c.className=@className
DECLARE Details_Cursor CURSOR FAST_FORWARD LOCAL FOR
select studentid from studentclass as sc where sc.ClassID=@classId
declare @studentId int,@stdCount int
OPEN Details2_Cursor
fetch next from Details_Cursor into @studentId
while @@FETCH_STATUS=0
BEGIN
select @stdCount=COUNT(*) from studentclass where studentid= @studentId and classid!=@classId
if(@stdCount=0)
Begin
delete from studentclass where studentid= @studentId and classid=@classId
delete from student where id= @studentId
End
Else
Begin
delete from studentclass where studentid= @studentId and classid=@classId
End
delete from class where classid=@ClassId
fetch next from Details_Cursor into @studentId
END
CLOSE Details_Cursor
DEALLOCATE Details_Cursor
end
exec DeleteStudent ‘english’
This gives an error.
;WITH CTE1 AS
(
select * FROM StudentClass
),CTE2 AS
(
select * FROM Class where ClassName = ‘English’
),CTE3 AS
(
select * FROM Student
)delete from Student where id in (select CTE1.id
–select top 1 CTE1.id
from CTE1 inner join CTE2 on CTE1.id= CTE2.ID
inner join CTE3 on CTE1.id= CTE2.ID
;WITH CTE1 AS
(
select * FROM StudentClass
),CTE2 AS
(
select * FROM Class where ClassName = ‘English’
),CTE3 AS
(
select * FROM Student
)delete from StudentClass where id in (select CTE2.id
from CTE1 inner join CTE2 on CTE1.id= CTE2.ID
inner join CTE3 on CTE1.id= CTE2.ID)
Ia m not able to run as it keep on giving me some error.
;WITH CTE1 AS
(
select * FROM StudentClass
),CTE2 AS
(
select * FROM Class where ClassName = ‘English’
),CTE3 AS
(
select * FROM Student
)delete from Student where id in (select CTE1.id
–select top 1 CTE1.id
from CTE1 inner join CTE2 on CTE1.id= CTE2.ID
inner join CTE3 on CTE1.id= CTE2.ID ) — ONE BRACKET WAS MISSING
;WITH CTE1 AS
(
select * FROM StudentClass
),CTE2 AS
(
select * FROM Class where ClassName = ‘English’
),CTE3 AS
(
select * FROM Student
)DELETE from StudentClass where id in (select CTE2.id
from CTE1 inner join CTE2 on CTE1.id= CTE2.ID
inner join CTE3 on CTE1.id= CTE2.ID)
Hi The contest is over.
Hi Sir, i
this is just 1 try without indexing
–option 1 -use table variable
declare @del_Stud TABLE (ID INT)
INSERT INTO @del_Stud
select s.ID FROM Student s
INNER JOIN (select StudentID,MAX(ClassID) ClassID from StudentClass sc group by StudentID having count(*)=1)t ON s.ID = t.StudentID
INNER JOIN Class c ON c.ID = t.ClassID AND c.ClassName=’English’
select * from @del_Stud
delete from Student where id in (select ID from @del_Stud)
delete from StudentClass where id in (select ID from @del_Stud)
–option 2 –using student to delete stud class
DELETE s
FROM Student s
INNER JOIN (select StudentID,MAX(ClassID) ClassID from StudentClass sc group by StudentID having count(*)=1)t ON s.ID = t.StudentID
INNER JOIN Class c ON c.ID = t.ClassID AND c.ClassName=’English’
DELETE sc
FROM StudentClass sc LEFT OUTER JOIN Student s ON s.ID=sc.StudentID
WHERE s.StudentName is null
Great attempt 16 logical reads.
DELETE sc FROM StudentClass sc
INNER JOIN class c ON c.ID=sc.ClassID
WHERE c.ClassName=’English’
DELETE FROM Student WHERE ID NOT IN
(SELECT DISTINCT StudentID FROM StudentClass )
Good work.
11 Page Reads
Second solution from my side:
SET STATISTICS IO ON
DECLARE @ClassID AS INT
SELECT @ClassID=ID FROM Class WHERE ClassName=’English’
DECLARE @StudentID AS VARCHAR(MAX)
SELECT @StudentID= COALESCE(@StudentID , ‘,’) + CAST(StudentID AS VARCHAR(20)) + ‘,’
FROM StudentClass
GROUP BY StudentID
HAVING COUNT(*) = 1 AND MIN(ClassID)=@ClassID
DELETE FROM Student WHERE @StudentID LIKE ‘%,’ + CAST(ID AS VARCHAR(20)) + ‘,%’
DELETE FROM StudentClass WHERE ClassID=@ClassID
GO
Only 4 logical reads and passes all the tests. You are awesome! You win the free access to the class!
Thank you! I have sent email …
Hi Harsh,
Would you please resend it. I have yet not received it. pinal @ sqlauthority.com
I have sent email again.
When/Where are the answers going to published?
Very soon. I am going through each answer carefully.
Bit late for the Quiz participation . But below script gives the logical read of 2
SET STATISTICS IO ON;
DEClare @classID INT
SELECT @classID=id from class with(nolock) WHERE className=’English’
DELETE FROM studentclass where classID= @ClassID
delete S
FROM student S
LEFT JOIN StudentClass SC WITH(NOLOCK) ON SC.StudentID=S.id
WHERE SC.StudentID IS NULL AND SC.classID = @classID
Table ‘Class’. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘StudentClass’. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(3 rows affected)
Table ‘Student’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(0 rows affected)
This is an awesome answer and worth mentioning. Great work.
Select @classid=Id from classs where classname =‘English’ will always pick one record , infact it should not run without using top 1. And I’m next query it is using Min(classid), seems not useful
Declare @Students Table(StudentID Int Primary Key,StudentName VarChar(500))
Declare @StudentClass Table(ClassID Int Identity(1,1),StudentID Int,ClassName VarChar(500));
Declare @CLassName VarChar(500);
Insert Into @Students
Values(1,’John’),(2,’Mark’),(3,’Thomas’)
Insert Into @StudentClass
(StudentID,ClassName)
Values(1,’English’),(2,’English’),(2,’Maths’),(2,’Science’),(3,’English’),(3,’Maths’)
Set @ClassName=’English’
–Method-1
;
WITH CTE
As
(Select S.StudentID,Count(ClassID) As Cnt
From @Students S Inner JOin @StudentClass SC On S.StudentID=SC.StudentID
Where S.StudentID In(Select Distinct StudentID From @StudentClass Where ClassName=@CLassName)
Group By S.StudentID)
Delete S From @Students S Inner JOin CTE E On E.StudentID=S.StudentID
Where Cnt=1
Delete From @StudentClass Where ClassName=@ClassName
Select S.StudentName,ClassName
From @Students S Inner Join @StudentClass SC On S.StudentID=SC.StudentID
–Method-1
;
WITH CTE
As
(Select Distinct StudentID
From @StudentClass Where ClassName=@ClassName),
N
As
(Select S.StudentID,Count(*) As NoOfClass
From @Students S Inner JOin @StudentClass SC On SC.StudentID=S.StudentID
Inner JOin CTE E On E.StudentID=S.StudentID
Group By S.StudentID)
Delete S From @Students S Inner JOin N On N.StudentID=S.StudentID
Where N.NoOfClass=1
Delete From @StudentClass Where ClassName=@ClassName
Select S.StudentName,ClassName
From @Students S Inner JOin @StudentClass SC On SC.StudentID=S.STudentID
Sorry, I am not able to run this one. It just gives an error.
USE TempDB
GO
—
SET STATISTICS IO ON
—
DECLARE @ClassID INT
—
SELECT @ClassID = ID
FROM Class
WHERE ClassName = ‘English’
—
DELETE Student
WHERE ID IN
(SELECT sc2.StudentID
FROM StudentClass sc2
WHERE sc2.ClassID = @ClassID
GROUP BY sc2.StudentID
HAVING COUNT(*) = 1)
—
DELETE StudentClass
WHERE ClassID = @ClassID
—