Como em qualquer consulta, o método mais eficiente é "depende". Há muitas variáveis em jogo - o número de linhas nas tabelas, os comprimentos das linhas, se existem índices, a RAM no servidor, etc etc.
A melhor maneira que consigo pensar em lidar com esse tipo de problema (pensando em manutenibilidade e uma abordagem ampla para eficiência) é usando CTEs, que permitem criar um resultado temporário e reutilizá-lo em toda a sua consulta. Os CTEs usam a palavra-chave WITH e, essencialmente, alias um resultado como uma tabela, para que você possa JOIN contra ela várias vezes:
WITH user_memberships AS (
SELECT *
FROM memberships
WHERE user_id = ${id}
), user_apps AS (
SELECT *
FROM apps
INNER JOIN user_memberships
ON user_memberships.team_id = apps.team_id
), user_collections AS (
SELECT *
FROM collections
INNER JOIN user_memberships
ON user_memberships.team_id = collections.team_id
), user_webhooks AS (
SELECT *
FROM webhooks
LEFT OUTER JOIN user_collections ON user_collections.id = webhooks.collection_id
INNER JOIN user_memberships
ON user_memberships.team_id = webhooks.team_id
OR user_memberships.team_id = user_collections.team_id
)
SELECT events.*
FROM events
WHERE app_id IN (SELECT id FROM user_apps)
OR collection_id IN (SELECT id FROM user_collections)
OR membership_id IN (SELECT id FROM user_memberships)
OR team_id IN (SELECT team_id FROM user_memberships)
OR user_id = ${id}
OR webhook_id IN (SELECT id FROM user_webhooks)
;
Os benefícios de fazer desta forma são:
- Cada CTE pode aproveitar um índice nos predicados JOIN apropriados e retornar resultados apenas para esse subconjunto mais rapidamente, em vez de fazer o planejador de execução tentar resolver uma série de predicados complexos
- Os CTEs podem ser mantidos individualmente, facilitando a solução de problemas com subconjuntos
- Você não está violando o princípio DRY
- Se o CTE tiver valor fora da consulta, você poderá movê-lo para um procedimento armazenado e fazer referência a ele