این چالش در سایت برنامه نویس در چند روز پیش
مطرح شد و 2 کاربر در بحث مشارکت کرده اند که هر دو در بدست آوردن کد مناسب
سهیم بوده اند. پاسخ های مربوط به چالش را در ادامه قرار داده ام.
این چالش بسیار ساده طراحی شده تا دوستان
بیشتری قادر به حل آن باشند. چالش مرتبط به خلاصه کردن و کلاسه کردن داده هاست.
فردی که مساله را با کوتاه ترین کد بطور کامل حل کند برنده اعلام خواهد شد.
راه حل محدودیت خاصی ندارد. شما قادر هستید از هر دستوری (چه DML و چه DDL)
برای گرفتن نتیجه استفاده کنید.
جدولی داریم که نتایج آزمون های دروس مختلف
دانشجویان را نگهداری میکند. شماره دانشجو، شماره درس و نمره. یک دانشجو از یک
درس بیش از یک نمره نمی تواند داشته باشد. نمرات بین 0 تا 20 هستند.
کد زیر را برای ایجاد جدول همراه با سطرهای نمونه اجرا کنید:
CREATE
TABLE
Results
(std_nbr
INTEGER
NOT NULL ,
crs_nbr
INTEGER
NOT NULL ,
nbr
REAL NOT
NULL
CHECK
(nbr BETWEEN
0 AND
20),
PRIMARY
KEY
(std_nbr, crs_nbr));
INSERT
INTO
Results(std_nbr, crs_nbr, nbr)
VALUES
(1, 01, 16.00),
(1, 02, 16.50),
(1, 05, 15.00),
(1, 07, 17.00),
(1, 10, 16.00),
(1, 11, 19.75),
(2, 03, 09.75),
(2, 04, 07.25),
(2, 05, 10.00),
(2, 06, 14.25),
(2, 07, 13.75),
(3,
15, 20.00),
(3, 16, 19.50),
(3, 17, 19.50),
(3, 18, 19.50),
(3, 19, 17.75),
(3, 25, 16.25);
نتیجه ی مورد نظر به شکل زیر است، نتیجه
ابتدا بر اساس ستون std_nbr بصورت صعودی و بعد توسط Class آن هم بصورت صعودی
مرتب می شود:
std_nbr Class
Cnt Average
----------- ----- ----------- ----------------------
1 A 2 18.375
1 B 4 15.875
2 B 1 14.25
2 C 2 11.875
2 D 2 8.5
3 A 5 19.25
3 B 1 16.25
نمرات زیر 10 در کلاس D، نمرات بین 10 و زیر
14 در کلاس C، نمرات بین 14 و زیر 17 در کلاس B و نمرات برابر یا بزرگتر از 17
در کلاس A طبقه بندی می شوند.
هدف دسته بندی کردن نمرات هر دانشجو و بدست آوردن تعداد نمره در هر کلاس همراه
با میانگین نمرات است.
هدف همانطور که قبلا اعلام شد بدست آوردن کوتاه ترین روش است.
از راه حل های صحیح دوستان استقبال خواهد شد حتی اگر خیلی طولانی و ابتدایی
باشد.
اولین راه حل را خودم ارسال کردم که
کمی نیز طولانی بود. ابتدا بطور جداگانه برای هر کلاس یک Select
نوشتم و سپس تمام select ها را با یکدیگر اجتماع کردم تا
خروجی حاصل شود یعنی:
/*msalim 1: 366
chars*/
SELECT std_nbr, 'A' class, COUNT(*) cnt, AVG(nbr) average
FROM Results
WHERE nbr BETWEEN 17 AND 20
GROUP BY std_nbr
UNION
SELECT std_nbr, 'B', COUNT(*), AVG(nbr)
FROM Results
WHERE nbr >= 14 AND nbr < 17
GROUP BY std_nbr
UNION
SELECT std_nbr, 'C', COUNT(*), AVG(nbr)
FROM Results
WHERE nbr >= 10 AND nbr < 14
GROUP BY std_nbr
UNION
SELECT std_nbr, 'D', COUNT(*), AVG(nbr)
FROM Results
WHERE nbr < 10
GROUP BY std_nbr
ORDER BY std_nbr, class;
دومین راه حل توسط جناب
Behrouzlo (بهروزلو) پست شد:
/*behrouzlo: 231
chars*/
Select Std_nbr,Class, Count (*), Avg (nbr)
From (
Select Std_nbr, Case
When nbr
BETWEEN 17
AND 20
Then
'A'
When
nbr >= 14 AND
nbr < 17 Then
'B'
When
nbr >= 10 AND
nbr < 14 Then
'C'
When
nbr < 10 Then
'D' End
As Class,
nbr
From Results)
As List
Group By
Std_nbr,Class
Order By
Std_nbr,Class
خب، واقعا تلاش خوبی بود. ولی ظاهرا
فراموش شده بود که برای دو عبارت Countو Avg
نام مستعار طبق خروجی در نظر گرفته شود.
همچنین عبارت CASE را می توانستند بطور قابل توجهی کوتاه تر
بنویسند. که آقای رضا یاراحمدی به آن اشاره کردند در پست بعدی، یعنی:
Case
When
nbr < 10 Then
'D'
When
nbr < 14 Then
'C'
When
nbr < 17 Then
'B'
Else
'A' End
جواب سوم:
اگر قصد این باشد که کوئری ساده تر شود خب میتوانیم یک ستون محاسباتی به جدول اضافه
کنیم که این کلاس ها را برای هر سطر بدست بیاورد و سپس توسط یک کوئری فوق العاده
ساده نتیجه ی مورد نظر را تولید کنیم یعنی:
ALTER TABLE Results
ADD C AS
CASE WHEN nbr >= 17 THEN 'A'
WHEN nbr >= 14 THEN 'B'
WHEN nbr >= 10 THEN 'C'
ELSE 'D'
END C
SELECT std_nbr, C
Class, COUNT(*) Cnt, AVG(nbr) Average
FROM Results
GROUP BY std_nbr, C
ORDER BY std_nbr, c
ولی مشکلی که این روش CASE
دارد پویا و انعطاف پذیر نبودنش هست. چه میشود که کاربر بخواهد این Range
ها را از یک جدول بخواند؟
/*msalim 2: 182
chars*/
SELECT std_nbr, c class, COUNT(*) cnt, AVG(nbr) Average
FROM (VALUES ('A', 17, 20),
('B', 14, 16.99),
('C', 10, 13.99),
('D', 0, 9.99)
) D(c, i, j)
JOIN Results
ON nbr BETWEEN i AND j
GROUP BY std_nbr, c
ORDER BY std_nbr, c
البته هنوز داده ها از جدول خوانده
نشده اند. برای این منظور باید یک جدول ایجاد کنیم با در نظر گرفتن چند قید مهم و
اساسی.
هر داده ای نباید در جدول درج شود. اگر با دقت 2 رقم اعشار نمرات محاسبه می شوند.
باید بین بازه ها یک صدم فاصله وجود داشته باشد. (این بررسی، شرط تداخل هم پوشش می
دهد). کوچکترین مقدار شروع بازره باید 0 و بزرگترین 20 باشد. شروع بازه باید از
پایان بازه کوچکتر باشد.
تصویر زیر میتونه در تفهیم بحث کمک
کند:
برای بررسی بیشتر این شروط و قیود (که
قابل پیاده سازی در خود تعریف جدول نیستند) نیاز به trigger هست که من در اینجا از اینکار صرف نظر کردم و
فقط توسط کوئری این ارزیابی را انجام داده ام:
CREATE TABLE Classes
(cls_no CHAR(1) NOT NULL PRIMARY KEY,
Start_range INTEGER NOT NULL,
End_range REAL NOT NULL,
CHECK (start_range < end_range),
CHECK (start_range BETWEEN 0 AND 20
AND end_range BETWEEN 0 AND 20));
;WITH Cte(c, i, j) AS
(SELECT 'A', 17, 20 UNION ALL
SELECT 'B', 14, 16.99 UNION ALL
SELECT 'C', 10, 13.99 UNION ALL
SELECT 'D', 0, 9.99
)
INSERT INTO Classes (cls_no, start_range, end_range)
SELECT * FROM Cte A
WHERE NOT EXISTS --No Conflict
(SELECT *
FROM Cte
WHERE A.i BETWEEN i AND j
AND c <> A.c)
AND EXISTS --No Gap
(SELECT *
FROM Cte
WHERE (CHAR(ASCII(c) - 1) = A.c
AND A.i - j = .01)
OR A.c = 'D')
AND (SELECT MIN(i) FROM Cte) = 0
AND (SELECT MAX(j) FROM Cte) = 20;
بعد از ساخت جدول و درج این چهار سطر
کوئری که توسط اون نتیجه مورد نظر رو بدست میاریم:
SELECT std_nbr, cls_no,
COUNT(*) cnt, AVG(std_nbr) average
FROM Results
INNER JOIN Classes
ON nbr BETWEEN start_range AND
end_range
GROUP BY std_nbr, cls_no
ORDER BY std_nbr, cls_no;
|
|
|
|