Roman Ali
Roman Ali

Reputation: 65

Mysql joining multiple totals - making query efficient

I am creating a clock-in / clock-out system for employees.

There is a tbl_clockins which contains records of each clock-in/clock-out session with information on whether each session is paid, how late the employee was for that session or how much overtime they did, etc.

There is another table called tbl_user_work_settings where the manager can set which days employees are on holiday, or have taken off on sickness etc.

I am creating some reports where I need totals for each employee, e.g. total days taken as holiday by each employee wihin a given date range. I have a very long query which actually gets all the required information, but it is huge and somewhat inefficient. Is there any way to make it smaller/more efficient? Any help is appreciated.

// get total days worked, unpaid days, bank holidays, holidays, sicknesses
// and absences within given date range for given users            
$sql = "SELECT us.username, daysWorked, secondsWorked,
            unpaidDays, bankHolidays, holidays, sicknesses, absences
FROM
  (SELECT username FROM users WHERE clockin_valid='1') us
  LEFT JOIN (
    SELECT   username, selectedDate, count(isUnpaid) AS unpaidDays
    FROM     tbl_user_work_settings
    WHERE    isUnpaid = '1'
         AND selectedDate>='$startDate'
         AND selectedDate<='$endDate'
    GROUP BY username
  ) u ON us.username=u.username
  LEFT JOIN (
    SELECT   username, count(isBankHoliday) AS bankHolidays
    FROM     tbl_user_work_settings
    WHERE    isBankHoliday='1'
         AND selectedDate>='$startDate'
         AND selectedDate<='$endDate'
    GROUP BY username
  ) bh ON us.username=bh.username
  LEFT JOIN (
    SELECT   username, count(isHoliday) AS holidays
    FROM     tbl_user_work_settings
    WHERE    isHoliday='1'
         AND selectedDate>='$startDate'
         AND selectedDate<='$endDate'
    GROUP BY username
  ) h ON us.username=h.username
  LEFT JOIN (
    SELECT   username, count(isSickness) AS sicknesses
    FROM     tbl_user_work_settings
    WHERE    isSickness='1'
         AND selectedDate>='$startDate'
         AND selectedDate<='$endDate'
    GROUP BY username
  ) s ON us.username=s.username
  LEFT JOIN (
    SELECT   username, count(isOtherAbsence) AS absences
    FROM     tbl_user_work_settings
    WHERE    isOtherAbsence='1'
         AND selectedDate>='$startDate'
         AND selectedDate<='$endDate'
    GROUP BY username
  ) a ON us.username=a.username
  LEFT JOIN (
    SELECT   username, count(DISTINCT DATE(in_time)) AS daysWorked,
                SUM(seconds_duration) AS secondsWorked
    FROM     tbl_clockins
    WHERE    DATE(in_time)>='$startDate'
         AND DATE(in_time)<='$endDate'
    GROUP BY username
  ) dw ON us.username=dw.username";

if(count($selectedUsers)>0)
  $sql .= " WHERE (us.username='"
       .  implode("' OR us.username='", $selectedUsers)."')";

$sql .= " ORDER BY us.username ASC";

Upvotes: 2

Views: 266

Answers (2)

eggyal
eggyal

Reputation: 125855

You can use SUM(condition) on a single use of the tbl_user_work_settings table:

// get total days worked, unpaid days, bank holidays, holidays, sicknesses
// and absences within given date range for given users            
$sql = "
  SELECT      users.username,
              SUM(ws.isUnpaid      ='1')       AS unpaidDays,
              SUM(ws.isBankHoliday ='1')       AS bankHolidays,
              SUM(ws.isHoliday     ='1')       AS holidays,
              SUM(ws.isSickness    ='1')       AS sicknesses,
              SUM(ws.isOtherAbsence='1')       AS absences,
              COUNT(DISTINCT DATE(cl.in_time)) AS daysWorked,
              SUM(cl.seconds_duration)         AS secondsWorked
  FROM        users
    LEFT JOIN tbl_user_work_settings           AS ws
           ON ws.username = users.username
          AND ws.selectedDate  BETWEEN '$startDate' AND '$endDate'
    LEFT JOIN tbl_clockins                     AS cl
           ON cl.username = users.username
          AND DATE(cl.in_time) BETWEEN '$startDate' AND '$endDate'
  WHERE       users.clockin_valid='1'";

if(count($selectedUsers)>0) $sql .= "
          AND users.username IN ('" . implode("','", $selectedUsers) . "')";

$sql .= "
  GROUP BY    users.username
  ORDER BY    users.username ASC";

By the way (and perhaps more for the benefit of other readers), I really hope that you are avoiding SQL injection attacks by properly escaping your PHP variables before inserting them into your SQL. Ideally, you shouldn't do that at all, but instead pass such variables to MySQL as the parameters of a prepared statement (which don't get evaluated for SQL): read more about Bobby Tables.

Also, as an aside, why are you handling integer types as strings (by enclosing them in single quote characters)? That's needless and a waste of resource in MySQL having to perform unnecessary type conversion. Indeed, if the various isUnpaid etc. columns are all 0/1, you can change the above to remove the equality test and just use SUM(ws.isUnpaid) etc. directly.

Upvotes: 2

purplesoft
purplesoft

Reputation: 526

Put each table that would join in a temp table... then create indexes on joinable fields of temp tables... and make your query with temp tables.

Example:

SELECT   username, selectedDate, count(isUnpaid) AS unpaidDays
INTO     #TempTable1
FROM     tbl_user_work_settings
WHERE    isUnpaid = '1'
     AND selectedDate>='$startDate'
     AND selectedDate<='$endDate'
GROUP BY username
create clustered index ix1 on #TempTable1 (username)

Upvotes: 0

Related Questions