О единственном другом способе, который я могу придумать, - это иметь базовую таблицу сообщений с Id и TypeId. Ваши подтаблицы (QuoteMessage и JobMessage) затем ссылаются на базовую таблицу как на MessageId, так и на TypeId, но также имеют CHECK CONSTRAINTS для принудительного применения только соответствующего MessageTypeId.
Table: Message
Fields: Id, MessageTypeId, Text, ...
Primary Key: Id, MessageTypeId
Unique: Id
Table: MessageType
Fields: Id, Name
Values: 1, "Quote" : 2, "Job"
Table: QuoteMessage
Fields: Id, MessageId, MessageTypeId, QuoteId
Constraints: MessageTypeId = 1
References: (MessageId, MessageTypeId) = (Message.Id, Message.MessageTypeId)
QuoteId = Quote.QuoteId
Table: JobMessage
Fields: Id, MessageId, MessageTypeId, JobId
Constraints: MessageTypeId = 2
References: (MessageId, MessageTypeId) = (Message.Id, Message.MessageTypeId)
JobId = Job.QuoteId
Что это дает вам по сравнению с таблицами JobMesssage и QuoteMessage? Он поднимает Сообщение для первоклассного гражданина, так что вы можете читать все Сообщения из одной таблицы. В свою очередь, ваш путь запроса от сообщения к его соответствующей цитате или заданию - еще 1 присоединение. Это зависит от вашего потока приложений, является ли это хорошим компромиссом или нет.
Что касается двух идентичных таблиц, нарушающих СУХОЙ - я бы не стал зацикливаться на этом. В дизайне БД речь идет не о СУХОЙ, а о нормализации. Если две вещи, которые вы моделируете, имеют одинаковые атрибуты (столбцы), но на самом деле разные вещи (таблицы) - тогда разумно иметь несколько таблиц с одинаковыми схемами. Гораздо лучше, чем перебивать разные вещи вместе.