This isn't trivial. A mix of crosstab with overlapping and intersecting ranges, plus corner cases (merge same start / end dates) on top. Very hard to solve with set-based operations i.e. pure SQL.
I suggest a procedural solution in PL/pgSQL instead. Should perform nicely, too, as it only needs a single (bitmap-index) scan over the table:
CREATE OR REPLACE FUNCTION f_student_xtab(VARIADIC _student_ids int[])
RETURNS TABLE (
student_id int
, "primary" text
, secondary text
, start_date date
, end_date date) AS
$func$
DECLARE
r record;
BEGIN
student_id := -1; -- init with impossible value
FOR r IN
SELECT t.student_id, t.site, t.primary_or_secondary = 'Primary' AS prim, l.range_end, l.date
FROM tbl t
CROSS JOIN LATERAL (
VALUES (false, t.start_date)
, (true , t.end_date)
) AS l(range_end, date)
WHERE t.student_id = ANY (_student_ids)
ORDER BY t.student_id, l.date, range_end -- start of range first
LOOP
IF r.student_id <> student_id THEN
student_id := r.student_id;
IF r.prim THEN "primary" := r.site;
ELSE secondary := r.site;
END IF;
start_date := r.date;
ELSIF r.range_end THEN
IF r.date < start_date THEN
-- range already reported
IF r.prim THEN "primary" := NULL;
ELSE secondary := NULL;
END IF;
start_date := NULL;
ELSE
end_date := r.date;
RETURN NEXT;
IF r.prim THEN
"primary" := NULL;
IF secondary IS NULL THEN start_date := NULL;
ELSE start_date := r.date + 1;
END IF;
ELSE
secondary := NULL;
IF "primary" IS NULL THEN start_date := NULL;
ELSE start_date := r.date + 1;
END IF;
END IF;
end_date := NULL;
END IF;
ELSE -- range starts
IF r.date > start_date THEN
-- range already running
end_date := r.date - 1;
RETURN NEXT;
END IF;
start_date := r.date;
end_date := NULL;
IF r.prim THEN "primary" := r.site;
ELSE secondary := r.site;
END IF;
END IF;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM f_student_xtab(1,2,3);
Or:
SELECT * FROM f_student_xtab(VARIADIC '{1,2,3}');
db<>fiddle here - with extended test case
About VARIADIC
: