Este é um projeto OO clássico para incompatibilidade de impedância de tabelas relacionais. O design de tabela que você descreveu é conhecido como 'tabela por subclasse'. Os três designs mais comuns são todos compromissos em comparação com a aparência real de seus objetos em seu aplicativo:
- Tabela por classe concreta
- Tabela por hierarquia
- Tabela por subclasse
O design que você não gosta - "onde as tabelas têm 100 colunas e a maioria dos valores são NULL" - é 2. uma Tabela para armazenar toda a hierarquia de especialização. Este é o menos flexível por todos os tipos de motivos, incluindo - se seu aplicativo exigir uma nova subclasse, você precisará adicionar colunas. O design que você descreve acomoda a mudança muito melhor porque você pode adicionar estendê-lo adicionando uma nova tabela de subclasse descrita por um valor em product_type.
A opção restante - 1. Tabela por classe concreta - geralmente é indesejável devido à duplicação envolvida na implementação de todos os campos comuns em cada tabela de especialização. Embora, as vantagens são que você não precisará executar nenhuma junção e as tabelas de subclasse podem até estar em diferentes instâncias de banco de dados em um sistema muito grande.
O projeto que você descreveu é perfeitamente viável. A variação abaixo é como ficaria se você estivesse usando uma ferramenta ORM para fazer suas operações CRUD. Observe como o ID em cada tabela de subclasse É o valor FK para a tabela pai na hierarquia. Um bom ORM gerenciará automaticamente o CRUD correto da tabela de subclasses com base apenas no valor dos valores discriminadores em product.id e product.product_type_id. Se você está planejando usar um ORM ou não, veja a documentação da subclasse do hibernate, mesmo que seja apenas para ver as decisões de design que eles tomaram.
product
=======
id INT
product_name VARCHAR
product_type_id INT -> Foreign key to product_type.product_type_id
valid_since DATETIME
valid_to DATETIME
magazine
========
id INT -> Foreign key to product.product_id
title VARCHAR
..
web_site
========
id INT -> Foreign key to product.product_id INT
name VARCHAR
..