There are for basic techniques for identifying records in table A whic aren't in table B. Each has its virtues, and each may perform well or badly, depending on data distribution and volume. Also the presence of nulls in the filtering columns can lead to different results (although as it seems unlikely this is a consideration in the specific scenario described in the question). Finally, not every flavour of SQL will support all of these syntaxes.
The most straigthforward is probably the NOT IN:
select *
from classes
where id not in
( select class_id
from registrations
)
/
Next is the related NOT EXISTS:
select *
from classes
where not exists
( select null
from registrations
where registrations.class_id = classes.id
)
/
Then there is the ANTI-JOIN:
select *
from classes
full outer join registrations
on (classes.id = registrations.class_id )
where registrations.class_id is null
/
Lastly there is the MINUS operator:
select id
from classes
minus
select class_id
from registrations
/
This final syntax is quite speedy to write, but obviously falls down in the details. If we want more columns from CLASSES, columns which aren't in REGISTRATIONS, then we need to feed it as a sub-query to a select on CLASSES. So, it is not as widely applicable as the other syntaxes but useful on occasions.