Pra quem for seguir pro caso de duração variável, dois detalhes que costumam travar na primeira tentativa. A restrição de exclusão com o profissional junto (profissional_id WITH =, periodo WITH &&) precisa da extensão btree_gist, senão o Postgres reclama que bigint não tem operator class pra gist. E vale usar o intervalo no formato [), que já é o padrão do tstzrange, assim uma reserva das 14h às 15h não conflita com a das 15h às 16h.
O EXCLUDE também aceita WHERE (cancelada_em IS NULL), igual ao índice parcial, então a regra de cancelamento do primeiro exemplo continua valendo.