0

I have question about performance:

I have table Employees

id name
1 name surname 1
2 name surname 2

And table plan

emp_id shift_date shift_begin shift_end
1 2022-01-01 08:00 17:00
1 2022-01-02 08:00 17:00
1 2022-01-04 08:00 17:00
2 2022-01-01 08:00 17:00
2 2022-01-02 08:00 17:00
2 2022-01-03 08:00 17:00

Note: emp_id is a id of employee

and I have following query:

SELECT id,name,
(select concat_ws('-',to_char(shift_begin ,'HH24:MI'),to_char(shift_end ,'HH24:MI'),typ) from plan where plan.emp_id = employee.id and shift_date =  '2022-01-01') as d0,
(select concat_ws('-',to_char(shift_begin ,'HH24:MI'),to_char(shift_end ,'HH24:MI'),typ) from plan where plan.emp_id = employee.id and shift_date = '2022-01-02') as d1,
(select concat_ws('-',to_char(shift_begin ,'HH24:MI'),to_char(shift_end ,'HH24:MI'),typ) from plan where plan.emp_id = employee.id and shift_date = '2022-01-03') as d2,
(select concat_ws('-',to_char(shift_begin ,'HH24:MI'),to_char(shift_end ,'HH24:MI'),typ) from plan where plan.emp_id = employee.id and shift_date = '2022-01-04') as d3,
(select concat_ws('-',to_char(shift_begin ,'HH24:MI'),to_char(shift_end ,'HH24:MI'),typ) from plan where plan.emp_id = employee.id and shift_date = '2022-01-05') as d4
-- continues to end of month
from employee;

Result is pretty good...

id name d0 d1 d2 d3 d4 ---> d30
1 name surname 1 08:00-17:00 08:00-17:00 08:00-17:00 ---> d30
2 name surname 2 08:00-17:00 08:00-17:00 08:00-17:00 ---> d30

...but when I have for example 50 employees which I would display in table (50 emp * 31 days) it drops in performance (after insert, delete)...

I have a crosstab too but it doesn't show me any results

Now back to the topic: Its a good option ? or use crosstab ? (Fyi in crosstab I have selection from table plan, but I want to select each of table employee).

I appreciate any help.

Stu
  • 30,392
  • 6
  • 14
  • 33
Jorns 90
  • 1
  • 1
  • 2

3 Answers3

0

An alternative method to pivot that works in most databases is conditional aggregation.

SELECT emp.id, emp.name
, STRING_AGG(CASE EXTRACT(DAY FROM plan.shift_date) WHEN 1 THEN concat_ws('-',to_char(plan.shift_begin,'HH24:MI'),to_char(plan.shift_end,'HH24:MI'),typ) END, ';') AS d1
, STRING_AGG(CASE EXTRACT(DAY FROM plan.shift_date) WHEN 2 THEN concat_ws('-',to_char(plan.shift_begin,'HH24:MI'),to_char(plan.shift_end,'HH24:MI'),typ) END, ';') AS d2
, STRING_AGG(CASE EXTRACT(DAY FROM plan.shift_date) WHEN 3 THEN concat_ws('-',to_char(plan.shift_begin,'HH24:MI'),to_char(plan.shift_end,'HH24:MI'),typ) END, ';') AS d3
-- continues to end of month
FROM employee emp
LEFT JOIN plan 
  ON plan.emp_id = emp.id
 AND plan.shift_date >= '2022-01-01' 
 AND plan.shift_date  < '2022-02-01' 
GROUP BY emp.id, emp.name
ORDER BY emp.id, emp.name;

If it's certain that they only have 1 shift per day.
Then MAX will do just fine.

, MAX(CASE EXTRACT(DAY FROM plan.shift_date) WHEN 31 THEN concat_ws('-',to_char(plan.shift_begin,'HH24:MI'),to_char(plan.shift_end,'HH24:MI'),typ) ELSE '' END) AS d31
LukStorms
  • 28,916
  • 5
  • 31
  • 45
0

For lots of result columns, crosstab() is typically shortest and fastest:

SELECT *
FROM   crosstab(
   $$
   SELECT p.emp_id, e.name, p.shift_date
        , concat_ws('-', to_char(p.shift_begin, 'HH24:MI'), to_char(p.shift_end,'HH24:MI'))
   FROM   employees e
   LEFT   JOIN plan p ON p.emp_id = e.id
                     AND p.shift_date >= '2022-01-01' 
                     AND p.shift_date <= '2022-01-31' 
   ORDER  BY e.id, p.shift_date;
   $$
 , $$SELECT generate_series (timestamp '2022-01-01'
                           , timestamp '2022-01-31'
                           , interval '1 day')::date$$
   ) AS ct (
      id int, name text
    , d1  text, d2  text, d3  text, d4  text, d5  text, d6  text, d7  text, d8  text, d9  text, d10 text
    , d11 text, d12 text, d13 text, d14 text, d15 text, d16 text, d17 text, d18 text, d19 text, d20 text
    , d21 text, d22 text, d23 text, d24 text, d25 text, d26 text, d27 text, d28 text, d29 text, d30 text
    , d31 text);

db<>fiddle here

See:

Why generate_series (timestamp '2022-01-01', ...? See:

I would generate above query dynamically for any given date range. Related examples:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
0

Another option is to aggregate the shift per employee before doing the "pivot", e.g. into a JSON value:

SELECT emp.id, 
       emp.name,
       shifts ->> '1' as d1,
       shifts ->> '2' as d2,
       shifts ->> '3' as d3,
       shifts ->> '4' as d4,
       shifts ->> '5' as d5,
       shifts ->> '6' as d6,
       shifts ->> '7' as d7,
       shifts ->> '8' as d8,
       shifts ->> '9' as d9,
       ... 
from employees emp
  left join (
    select emp_id, 
           jsonb_object_agg(extract(day from shift_date),
                            concat_ws('-', to_char(shift_begin, 'hh24:mi'), to_char(shift_end, 'hh24:mi'))) as shifts
    from plan p
     where p.shift_date >= '2022-01-01' 
       and p.shift_date  < '2022-02-01' 
    group by emp_id
  ) p on p.emp_id = emp.id
order by emp.id;
;

Depending on how you use the result, you might not even need to extract each day into a separate column if you can use the JSON value directly in your frontend.