Problem Description / Requirement
This – simplified – problem arises from software that allows for individual customization to meet customer needs.
A customer uses the software as a grading system for students at their university. A separate column has been implemented in the grade table for each subject. Students use a web interface where they can access their grades. For each exam day, the BEST grade and WORST grade should be displayed in addition to the actual grades. For this purpose, the software company has created a view [dbo].[exam_grades] that is called by the web server. This view returns for each student for each exam day the best and the worst grades.
Challenge
Very high CPU usage is observed, especially in the days following the publication of grades. The problem has been identified in the [dbo].[exam_grades] view.
The dean of the university is contacting you to rewrite the view so that CPU load does not increase significantly with a high number of calls and other program modules can continue to be used as usual.
Condition
- Rewrite the view
- The metadata of the table must NOT be changed!
Technical Information
The challenge creates three objects in your database:
- Table [dbo].[students] with 1,000 rows
- Table [dbo].[student_exam_grades] with ~350,000 rows
- View [dbo].[exam_grades
How to start the challenge
When you are connected to the SQL Server instance, run the following commands:
EXECUTE sqlchallenge.dbo.create_challenge 'list'; |
will give you a list of open challenges |
EXECUTE sqlchallenge.dbo.create_challenge 'SQL Challenge #7 - Analysis in denormalized table(s)'; |
will create the challenge for you |
customer expectations
The customer wants the view [dbo].[exam_grades] to be rewritten so that the loads on the day the grades are published do not burden other modules of the system.
Connect to Microsoft SQL Server
SQL Server: | sqlchallenges.germanywestcentral.cloudapp.azure.com |
Port: | 1433 |
Version: |
16.0.4165.4 |
Edition: | Developer Edition |
Authentication Type: | SQL Server Authentication |
Login Name: | sqlchallenge |
Password: | sqlchallenge#2025! |
Encryption | Mandatory |
Trust Server Certificate: | True |
I’m hesitant to reply – because i have a feeling i haven’t seen the catch.
If changing the view is the only option and minimizing cpu is the goal – this is my suggestion – and a bit more readable then the original
Reduces cpu time by approx half
CREATE VIEW [dbo].[exam_grades]
AS
SELECT ssg.s_id
,s.s_name
,ssg.exam_date
,best_grade = LEAST(ssg.mathematics, ssg.english, ssg.biology, ssg.geographics, ssg.philosophy, ssg.sports)
,worst_grade = GREATEST(ssg.mathematics, ssg.english, ssg.biology, ssg.geographics, ssg.philosophy, ssg.sports)
FROM dbo.student_school_grades ssg
JOIN dbo.students s
ON s.s_id = ssg.s_id;
Using an indexed view will perform better – but is not my preferred weapon of choice:
Reduces cpu time to 1/4
CREATE VIEW [dbo].[exam_grades] WITH SCHEMABINDING
AS
SELECT ssg.s_id
,s.s_name
,ssg.exam_date
,best_grade = LEAST(ssg.mathematics, ssg.english, ssg.biology, ssg.geographics, ssg.philosophy, ssg.sports)
,worst_grade = GREATEST(ssg.mathematics, ssg.english, ssg.biology, ssg.geographics, ssg.philosophy, ssg.sports)
FROM dbo.student_school_grades ssg
JOIN dbo.students s
ON s.s_id = ssg.s_id;
CREATE UNIQUE CLUSTERED INDEX someviewname ON [dbo].[exam_grades3] (s_id, exam_date)
Dear Peter,
thank you so much for solving the sqlchallenge.
To be honest – I didn’t had your solution in my mind but it is absolutely great.
I will give it a try when I write about the solutions.
Joa,
also Peter hat ja schon den Königsweg gezeigt. Ich habe somit weiter versucht, die Werte einzeln wie im Original zu behandeln, da die View schon sehr weh tat im Auge. Ich habe die Auswertung auf ein gesamtheitliche Zeile ausgewertet und nicht mehr jeden Wert gegen jeden Wert. Das ganze in einen CTE. Die eigentliche Abfrage war somit bei 42ms CPU. Dann kam aber der Join und wir waren wieder bei 110ms. Also statt CTE einen TempTable genommen, damit wir einen statischen Datensatz haben, der schneller gejoined werden kann. Hatt funktioniert, immerhin 80ms im Schnit. Aber die Aufgabe ist trotzdem failed, da ich in einer View keine #Table maintainen kann 😉 Aber ich wollts trotzdem mal zeigen
SET STATISTICS TIME ON;
DROP TABLE IF EXISTS #GradeData;
SELECT
[s_id],
[exam_date],
(
SELECT
MIN(grade)
FROM
(VALUES
(COALESCE([mathematics], ‘F’)),
(COALESCE([english], ‘F’)),
(COALESCE([biology], ‘F’)),
(COALESCE([geographics], ‘F’)),
(COALESCE([philosophy], ‘F’)),
(COALESCE([sports], ‘F’))
) AS Grades(grade)
) AS [best_grade],
(
SELECT
MAX(grade)
FROM
(VALUES
(COALESCE([mathematics], ‘F’)),
(COALESCE([english], ‘F’)),
(COALESCE([biology], ‘F’)),
(COALESCE([geographics], ‘F’)),
(COALESCE([philosophy], ‘F’)),
(COALESCE([sports], ‘F’))
) AS Grades(grade)
) AS [worst_grade]
INTO
#GradeData
FROM
[88FA498A-C65E-4BFD-8C79-B5E9F81DE10B].[dbo].[student_school_grades];
SELECT
st.s_name,
gd.exam_date,
gd.best_grade,
gd.worst_grade
FROM
#GradeData as gd
JOIN
[88FA498A-C65E-4BFD-8C79-B5E9F81DE10B].[dbo].[students] as st ON gd.s_id = st.s_id;
DROP TABLE IF EXISTS #GradeData;
Dann habe ich noch mal ein bisschen gegoogled.
https://www.brentozar.com/archive/2022/12/should-you-use-sql-server-2022s-greatest-and-least/
Und es ist eigentlich wie immer, diese Neuerungen sind größtenteils QoL Changes aber nicht performanter. Zumal bei Brent auch noch Indexe und Zahlen genutzt werden. Aber ich habe dann noch mal nachgedacht und Funktionen in in Queries sind eh immer so kleiner Killer. Also mal richtig OldSchool Sql probiert und siehe da, 40-60 ms CPU Zeit und dazu in einer View nutzbar 🙂
SET STATISTICS TIME ON;
SELECT
st.s_name,
ssg.exam_date,
CASE
WHEN ‘A’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘A’
WHEN ‘B’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘B’
WHEN ‘C’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘C’
WHEN ‘D’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘D’
WHEN ‘E’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘E’
ELSE ‘F’
END AS best_grade,
CASE
WHEN ‘F’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘F’
WHEN ‘E’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘E’
WHEN ‘D’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘D’
WHEN ‘C’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘C’
WHEN ‘B’ IN (ssg.[mathematics], ssg.[english], ssg.[biology], ssg.[geographics], ssg.[philosophy], ssg.[sports]) THEN ‘B’
ELSE ‘A’
END AS worst_grade
FROM
[88FA498A-C65E-4BFD-8C79-B5E9F81DE10B].[dbo].[student_school_grades] as ssg
JOIN
[88FA498A-C65E-4BFD-8C79-B5E9F81DE10B].[dbo].[students] as st ON ssg.s_id = st.s_id
Hat Spaß gemahct und gelernt 🙂
Vielen Dank, lieber Daniel.
Deine Lösung ist sehr interessant.