1

I have Projects table. Projects can have costs, so I have a Costs table. A cost can be because of a product or beacuse of a service. So I have 4 main tables:

  • Projects (IDProject, Description...)
  • Costs(IDCost, price, quantity, IDProject...)
  • Products (IDProduct, IDProvider, Reference, Description...)
  • Services (IDService, Reference...)

Now I need to stablish the relationships between costs, products and service. I was thinking in this solution:

  • Costs(IDCost, price, quantity, IDProject, IDProduct, IDService...)

But I am not sure if this is a good solution, because I would have always a FK as null, because if it is a cost because of a product, IDService will be null. Or if a cost it is because of a service, IDProduct will be null.

Are there better solutions? Or it is not so important to allow nulls for FK?

Thanks.

7
  • 1
    Isn't cost calculated? Not sure a costs table should exist. Why isn't it just a report? Is there a one to one relationship here? Commented Aug 29, 2023 at 15:54
  • Really cost is not calculated. It is a value that the user set in each case. Anyway, calculated or not, which is the difference? At the end it is a value with the total cost. For example for a product, it is the price I pay for it to the provider. A service is the bill that I get from the person that does the service. Commented Aug 29, 2023 at 16:19
  • seems like Cost(id,causedByType,causedById) solves your problem
    – Ewan
    Commented Aug 29, 2023 at 17:12
  • 1
    There's nothing wrong with null FKs in general, any more than any other null value - a null AddressId on a user might mean "no address entered yet", a null UserId on a booking might mean "created without logging in". The particular pattern here of "exactly one of these three will always be non-null" feels like it might have a different representation, but I can't immediately picture a practical one.
    – IMSoP
    Commented Aug 29, 2023 at 17:23
  • 1
    The title does not correctly address the problem. First of all, it's not related to FKs. While your "toggled" columns happen to be FKs here, the same question would be true if there were other columns as well. Secondly, the question is more specific than just nullability; it's about having a table represent two different data structures. The fact that you use null to indicate an unused column is a side effect of that decision. The question should focus on whether you should have a table that represents two different data structures.
    – Flater
    Commented Aug 29, 2023 at 23:59

4 Answers 4

7

This design is perfectly valid. It is called Single Table Inheritance, because you merge in a single Cost table two specializations which are CostForService and CostForProduct.

The pro is that you can very easily check that one and only one of the two FK is not null.

The cons is usually a demultiplication of mostly useless fields that you have to cross-check for consistency, but in your case this does not apply.

Alternatives are possible but look to me like overengineering in your specific case.

4

I'm finding it a little difficult to follow your design without some sort of entity diagram, but I think you might have the relationship between Costs and the associated Products and Services backwards. You are structuring it such that a costs either has a product or a service. This creates a 'checkerboard' table design where every row contains at least one null and it's something I would avoid. I'm not certain but I think you can always avoid this kind of non-normalized approach in a relational model.

If you instead put a CostID FK on both the Products and Services, you eliminate the problem and you can enforce a non-null constraint on both. In this scenario, the relationship seems more natural i.e.: I would tend to think of products and services as having a cost, not the other way around. This isn't always the case, though. Sometimes eliminated nulls with this kind of approach may not seem completely intuitive. For example, we tend to talk about a shipping container having items, but in a good relational model, the items have a container (which they share with other items.)

In addition to the main question, there's an unclear relationship between Services and Projects. The reason I mention it is that you have a ProjectID on Products and Costs but you don't mention one for Services. Having a redundant FK as with Costs and Products can present a risk for inconsistent data. I would recommend either putting the ProjectID on Costs or put it on Serivces & Products but not both.

9
  • The critical thing is if a product or service always has the same cost or if the cost is set at time of sale. Commented Aug 29, 2023 at 19:24
  • @candied_orange I'm not sure I follow. Why is that critical?
    – JimmyJames
    Commented Aug 29, 2023 at 19:27
  • Because the same product ends up having many costs. Commented Aug 29, 2023 at 19:27
  • @candied_orange This is where not understanding the larger design is challenging. If it were a requirement that a single product have many costs per project, then one way you could accommodate that by making the FK point to a non-PK field on Cost. I would still avoid the OP's original approach, though.
    – JimmyJames
    Commented Aug 29, 2023 at 19:34
  • 2
    I don’t think calling it “cost” is helping. Names like “sale” or “quote” or “budget” are each better at telling a story than “cost”. Commented Aug 29, 2023 at 20:31
3

I want to explain one specific use case here, because the implementation of said use case is relevant for you to consider.

Entity Framework allows you to model inheritance in your database. What you're dealing with is very similar to an inheritance pattern. I could refactor your structure as code classes, which I'm going to since it helps study the use case I have in mind.

Your suggestion is something along the lines of:

public class Cost
{
    public Guid Id { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public Guid ProjectId { get; set; }
    public Guid? ProductId { get; set; }   // Either this is null
    public Guid? ServiceId { get; set; }   //     or this is null
}

However, in code you would avoid this, instead using inheritance to ensure that you only add the field where it will actually be used:

public class Cost
{
    public Guid Id { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public Guid ProjectId { get; set; }
}

public class ProductCost : Cost
{
    // Should never be null since this is always a product cost!
    public Guid ProductId { get; set; } 
}

public class ServiceCost : Cost
{
    // Should never be null since this is always a service cost!
    public Guid ServiceId { get; set; } 
}

Now you don't have to deal with fields that are "obviously" null; which saves you from having to constantly check for null to figure out what kind of cost this is.

If the above makes sense to you, the database equivalent of this will seem very familiar. When presented with these inherited classes, Entity Framework will generate tables that represent this relationship. However, there are a few different approaches here. All of them make sense in their own way.

More information here.


Table-per-hierarchy (TPH)

This is essentially what you're suggesting. You merge all the fields into a single table and set them to null when you're not using them. This yields the fastest querying if you intend to query across all costs, but it comes at the cost of significant waste of database storage.

Resulting tables: Cost


Table-per-type (TPT)

This represents the three classes as three separate tables. The PK for the subtypes is actually reusing the PK from the base type, to ensure that the identities work correctly. Each table contains exactly the fields that its related class contains.

This is highly efficient from an storage efficiency perspective as no column is ever left unused; but it makes it less efficient to query across all costs since you now have to deal with table joins. However, the PK uniqueness space is still shared between the subtypes, which means that there will never be a product cost and a service cost with the same PK.

Even if you only wish to fetch e.g. product costs, your query must use a table join because it must get data from both the Cost and ProductCost tables. While this join can be optimized; it's still a non-zero cost to pay.

Resulting tables: Cost, ProductCost, ServiceCost


Table-per-concrete-type (TBC)

This is a variation of TPT which can only be used when the base type is abstract and cannot be instantiated directly (i.e. you cannot have a cost which is neither a product cost nor a service cost). In this case, the base type does not get a table, and its columns are added to the subtypes (which are "concrete", i.e. non-abstract).

The benefit here (over TPT) is that when you are only querying e.g. product costs; you do not have to rely on a table join. All the information is in the ProductCost table. The drawback here (over TPT) is that the PK uniqueness is no longer shared across all costs, because there is no longer a shared table between all the cost types.

Other than that, the same pros and cons of TPT apply to TPC.

Resulting tables: ProductCost, ServiceCost


As you can see, there are several ways to represent this inheritance relationship. The choice between them is mostly a choice of which database behavior you wish to optimize, so I suggest you review the options and decide which part you want to be optimized the most.

Just to be clear here, I'm not saying you have to use Entity Framework; but you can take inspiration from the database structures that Entity Framework utilizes to model code inheritance.

3
  • Thanks for your detailed answer. But then, in TPT, if I misunderstood, if I want to know the total cost, I have to do two queries, one to know the total costs of the products and another to know the total cost of the service? This is two trips to the database, or perhaps could I use a union query? Commented Aug 31, 2023 at 16:19
  • @ÁlvaroGarcía Those are the tradeoffs you have to consider when choosing your approach.
    – Flater
    Commented Sep 1, 2023 at 0:01
  • @ÁlvaroGarcía: Speaking for EF, it allows you to ask for "all costs" and will handle the JOIN/UNION logic for you. But yes, you'd need to do something like that to fetch all the information. Query speed is not a pro for TPT, it's pro is keeping the database size lower.
    – Flater
    Commented Sep 1, 2023 at 0:19
0

nullable FK is fine, but your design is bad due to multiple nullable cols

Two solutions

  1. Add type and typeId cols and dont use a FK constraint. just enforce relationship inyour api layer

  2. Add an extra table AnyCostable with id and type use that as your FK constraint on Cost and use the same id in Products and Services with another FK constraint.

eg

Project
A

AnyCostable
A, Service
B, Product

Service
A, FK -> AnyCostable Id

Product
B, FK -> AnyCostable Id

Cost
ThingThatCostsId, A FK -> AnyCostable Id

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.