Uma opção é usar uma junção externa completa entre as duas tabelas no seguinte formato:
SELECT count (1)
FROM table_a a
FULL OUTER JOIN table_b b
USING (<list of columns to compare>)
WHERE a.id IS NULL
OR b.id IS NULL ;
Por exemplo:
CREATE TABLE a (id int, val text);
INSERT INTO a VALUES (1, 'foo'), (2, 'bar');
CREATE TABLE b (id int, val text);
INSERT INTO b VALUES (1, 'foo'), (3, 'bar');
SELECT count (1)
FROM a
FULL OUTER JOIN b
USING (id, val)
WHERE a.id IS NULL
OR b.id IS NULL ;
Retornará uma contagem de 2, enquanto que:
CREATE TABLE a (id int, val text);
INSERT INTO a VALUES (1, 'foo'), (2, 'bar');
CREATE TABLE b (id int, val text);
INSERT INTO b VALUES (1, 'foo'), (2, 'bar');
SELECT count (1)
FROM a
FULL OUTER JOIN b
USING (id, val)
WHERE a.id IS NULL
OR b.id IS NULL ;
retorna a contagem esperada de 0.
O que eu mais gosto nesse método é que ele só precisa ler cada tabela uma vez, em vez de ler cada tabela duas vezes ao usar EXISTS. Além disso, isso deve funcionar para qualquer banco de dados que suporte junções externas completas (não apenas o Postgresql).
Geralmente desencorajo o uso da cláusula USING, mas aqui há uma situação em que acredito que seja a melhor abordagem.
Adendo 2019-05-03:
Se houver um problema com possíveis dados nulos, (ou seja, a coluna id não pode ser anulada, mas a val é), você pode tentar o seguinte:
SELECT count (1)
FROM a
FULL OUTER JOIN b
ON ( a.id = b.id
AND a.val IS NOT DISTINCT FROM b.val )
WHERE a.id IS NULL
OR b.id IS NULL ;
EXCEPT
, verifique esta pergunta: Uma maneira eficiente de comparar dois grandes conjuntos de dados no SQL