I have following stored procedure in a C# win forms application which calculates employee earnings based on attendance as follows.
Note that a shift is 12 hours and employees mark attendance for in and out of each shifts. Also salary period is from beginning to end of a month (1st to 28st / 30th / 31st).
Related tables are:
- Employee (
emp_id
,initials
,surname
,basic_sal
,budj_allowance
) - Attendance (
emp_id
,in_time
,out_time
,shift
) - Rank (
rank_id
,shift_rate
)
Calculations
Work Days - This is the number of days a particular employee has worked and this value is taken from Attendance table.
Day Offs - An employee is entitled for maximum of 4 day offs for a month and if more than four days have been taken by an employee, remaining days will be marked as "Leave days".
No of Extra Shifts - This value is taken by this formula.
[Total Shifts - total days worked]
Basic Salary - This is taken from employee master table
Budgetary Allowance - All employees are paid Rs.1,000/- as budgetary allowance
No Pay Days - This is calculated from the formula
[(No of days in the month-04) - days worked]
Less No Pay Amount - This is calculated from the formula.
[((Basic Salary + Budgetary Allowance) / (No of Days in the month-04)) x No Pay Days]
Amount for the EPF - This is calculated from the formula
[Basic Salary + Budgetary Allowance - Less No Pay Amount]
Overtime Amount - This is calculated from the formula
[Amount for the EPF - (Extra Shift Rate x Work Days)]
CREATE PROCEDURE [dbo].[sp_Earnings] @fromDate datetime, @toDate datetime
-- Add the parameters for the stored procedure here
AS
BEGIN
-- Declaring a variable to hold on of days in the month.
DECLARE @No_of_days int
SELECT @No_of_days = DATEDIFF(day,@fromDate,DATEADD(day,1,(@toDate)))
-- Declaring a constant to hold no of off days allowed in a month
DECLARE @Day_offs_allowed int
SELECT @Day_offs_allowed=4
--This is a reference to identify month and year of everyrecord. example - **"APR2014"**
DECLARE @SalRef char(20)
SELECT @SalRef= REPLACE(STUFF(CONVERT(varchar(12),CONVERT(date,@fromDate,107),106),1,3,''),' ','')
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
SELECT Employee.Emp_ID, Employee.Initials + ', ' + Employee.Surname AS Name,
COUNT(DISTINCT CONVERT(DATE, Attendance.in_time)) AS work_days,
CASE WHEN (@No_of_days - (COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))) >= @Day_offs_allowed)
THEN @Day_offs_allowed
ELSE (@No_of_days - (COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))))
END AS day_offs,
CASE WHEN (@No_of_days - (COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))) >= @Day_offs_allowed)
THEN @No_of_days - (COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))) - @Day_offs_allowed
ELSE 0
END AS leave_days,
COUNT(Attendance.shift) - COUNT(DISTINCT CONVERT(DATE, Attendance.in_time)) AS extra_shifts,
Rank.Shift_Rate,
(COUNT(Attendance.shift) - COUNT(DISTINCT CONVERT(DATE, Attendance.in_time)))* rank.Shift_Rate AS Extra_Shift_Amount,
employee.Basic_Sal,
employee.budj_allowance,
(@No_of_days-@Day_offs_allowed)- COUNT(DISTINCT CONVERT(DATE, Attendance.in_time)) AS no_pay_days,
CONVERT(DECIMAL(10,2),(((employee.basic_sal+employee.budj_allowance) / (@No_of_days-@Day_offs_allowed) )) * ((@No_of_days-@Day_offs_allowed)- COUNT(DISTINCT CONVERT(DATE, Attendance.in_time)))) AS less_no_pay_amt,
employee.basic_sal+employee.budj_allowance-CONVERT(DECIMAL(10,2),((employee.basic_sal+employee.budj_allowance) / (@No_of_days-@Day_offs_allowed) ) * ((@No_of_days-@Day_offs_allowed)- COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))))AS amt_for_epf,
CONVERT(DECIMAL(10,2),((Rank.Shift_Rate*(COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))))-((((employee.basic_sal+employee.budj_allowance)-(((employee.basic_sal+employee.budj_allowance) / (@No_of_days-@Day_offs_allowed)) * (@No_of_days-@Day_offs_allowed- COUNT(DISTINCT CONVERT(DATE, Attendance.in_time))))))))) AS over_time_amt,
@salRef AS Reference
FROM Employee INNER JOIN
Attendance ON Employee.Emp_ID = Attendance.EID INNER JOIN
Point ON Attendance.PID = Point.PID INNER JOIN
Rank ON Employee.Rank = Rank.Rank_ID
WHERE Attendance.in_time BETWEEN CONVERT(DATETIME, @fromDate, 102) AND CONVERT(DATETIME, @toDate, 102)
GROUP BY Employee.Emp_ID, Employee.Initials + ', ' + Employee.Surname, Rank.Shift_Rate, Employee.Basic_Sal, Employee.budj_allowance
ORDER BY Employee.Emp_ID
END
Questions:
- Can this be further optimized?
- Are there any notable flows?
- Is a stored procedure suitable for this requirement?
1 Answer 1
Here are my thoughts on your proc.
The Good
Good job on commenting out sections that are not so obvious to figure out. I had very little difficulty understanding it, your code is much cleaner than the average SQL post on this site. I would say a stored procedure is the correct type of database object for this, since it sounds like it will be called regularly based on its nature.
Improvements
Even though SQL engine is set up to where you don't always need to use delimiter
;
it is good practice to explicitly use them.DECLARE @No_of_days int
becomesDECLARE @No_of_days int;
etc. throughout.To avoid errors when creating a proc, it is a good idea to
DROP PROCEDURE IF EXISTS [dbo].[sp_Earnings];
for instance.This may just be personal preference, but I think
SET
is less ambiguous to set variables, soSELECT @Day_offs_allowed=4;
would becomeSET @Day_offs_allowed=4;
. This helps to differentiate them from nested queries and such.I think this is a bit odd:
SELECT @SalRef= REPLACE(STUFF(CONVERT(varchar(12),CONVERT(date,@fromDate,107),106),1,3,''),' ','')
.
I would be tempted to instead use
SET @SalRef = CONCAT( DATEPART(Mm, @fromDate), '/', DATEPART(Yy, @fromDate) )
.
This reference will look slightly different, e.g. "04/2014" but achieve the same purpose more elegantly plus you can sort them in order more easily numerically.A point on formatting. When using long nested statements it is good practice to use line break and tabs to make it easier to read. For example:
(COUNT(Attendance.shift) - COUNT(DISTINCT CONVERT(DATE, Attendance.in_time)))* rank.Shift_Rate AS Extra_Shift_Amount
Becomes:
(COUNT (Attendance.shift) - COUNT( DISTINCT CONVERT(DATE, Attendance.in_time) ) )* rank.Shift_Rate AS Extra_Shift_Amount,
Other than that I think your code is good.
Explore related questions
See similar questions with these tags.
CONVERT()
functions. Guessing your source columns areDATETME
? If you can post the execution plan this would help us look for anything that is not optimal. \$\endgroup\$