2

Sorry about the title, I couldn't find anything better. Suggestions are welcome.

Say we have two tables, Suppliers and Products. The same product can come from many different suppliers, so we create a third table, let's call it SuppliersAndProducts, with this schema

SuppliersAndProducts

  - id (autoincrement)
  - supplier_id
  - product_id
  - from_date
  - to_date

from_ and to_date are there to account for the fact that a given supplier might well stop selling a given product, but could also begin selling it again at some point in the future.

Now, we want to store the price we pay when we buy a Product from a given Supplier. Prices, of course, change over time, and we want to keep a track of that too.

So we introduce a table, let's call it SuppliersAndPrices, structured like so:

SuppliersAndPrices

  - id (autoincrement)
  - supplier_id (FK to the supplier id)
  - product_id (FK to the product id)
  - price
  - from_date
  - to_date

My question about this last table is more conceptual than anything else: should this table be like I described (meaning that the association in based on both the supplier and product IDs) or instead just reference the id from SuppliersAndProducts with a column that we could call suppliers_and_products_id?

The latter design is more normalized than the former; after all, the association between a Supplier and a Product is already stated in SuppliersAndProducts, so there would be little point in repeating that information. Still, at least to me, for some reason the former feels closer to the real world.

From the point of view of query complexity, to know the price I'm paying today for a given product, with the first design I would have to write (with some pseudo-SQL to remain db-agnostic) something like:

SELECT price 
FROM   SuppliersAndPrices 
WHERE  supplier_id = X 
       AND product_id = Y 
       AND today is between from_date and to_date

whereas the second design would entail a join, thus making the query look like:

SELECT price 
FROM   SuppliersAndPrices 
       INNER JOIN SuppliersAndProducts 
               ON SuppliersAndPrices.suppliers_and_products_id = SuppliersAndProducts.id 
WHERE  SuppliersAndProducts.supplier_id = X 
       AND SuppliersAndProducts.product_id = Y 
       AND today is between SuppliersAndPrices.from_date and SuppliersAndPrices.to_date

I'm writing this off the top of my head, sorry about my SQL.

Also, now that I think about it, with the second design I would have to add another condition to the WHERE clause to check the dates in SuppliersAndProducts too, to handle the case in which a Supplier will stop carrying a product and, at some point, begin selling it again. In that case the join condition would return more than one row, and that would be Not A Good Thing™.

So, which one would you choose? Associating on the id of what already is an association in the name of greater normalization but at the price of increasing query complexity? Denormalizing a tiny bit to make querying easier and, arguably, the database structure clearer for future maintainers?

The query for the second design would probably be hidden behind a view, but that's still code that has to be maintained and understood. What I really want to know is which one of the two design is "better", for some definition of "better".

1

Just to get this part out of the way, what is the intention of keeping the historical data? If it is for the app to allow users to interact with on some level, then ok, as that is still a transactional need. Else, if it is merely for reporting purposes, that could be tracked entirely in a separate server / database / schema as you are really talking about a Slowly Changing Dimension (SCD). If it only exists for reporting then no need to complicate your transactional model with anything but the current info.

That being said, I will assume the data is needed for transactional purposes, in which case there is no question (in my mind, at least :) that Option 2 (i.e. relating the Price info with the Supplier and Product relationship table) is the only way to go. If you relate the Price properties back to their respective sources then you allow for invalid combinations of Suppliers and Products that don't exist in the SuppliersAndProducts table. And while performance and maintainability are very important considerations when designing, they are secondary to data integrity as that is the primary responsibility of the database.

Some notes:

  • It is better to use the table name + "id" rather than the generic "id". This will make writing queries a lot easier as the field name will be the same between tables. Queries in general will be more readable.

  • While I also like naming tables as plural (it just sounds better), it does make things easier to use the singular. This way the "id" field of the table includes the table name without sounding odd, such as "SuppliersID" (as opposed to "SupplierID"). If you ever have to code automated processes against the tables, it is much easier to assume that the ID field is simply "{TableName}ID", else you might need to hit the database to do a look up. And for new people being trained it is just easier to know that any table will have a standard ID field name, which makes writing queries faster. This is why I no longer use plural table names :). So, for example, SuppliersAndProduct would be SupplierAndProduct or maybe even SupplierXProduct.

  • If it weren't for the historical values then SupplierAndProduct wouldn't need an auto-incrementing ID as the combination of SupplierID and ProductID would be the composite PK.

  • But since we do have that history we can do two things with that relationship table:

    1. PK is the SupplierAndProductID field
    2. Unique Index (representing an "Alternate Key") on the combination of the SupplierAndProductID, SupplierID, and ProductID fields
  • For the SupplierAndProductPrice (or SupplierXProductPrice) table we would have (just in terms of the Key fields here; no changes to the "price" and date, etc fields):

    - supplier_and_product_price_id (autoincrement) -- I will stop PascalCasing now ;-)
    - supplier_id (FK to supplier_and_product; could also FK to supplier)
    - product_id (FK to supplier_and_product; could also FK to product)
    - supplier_and_product_id (FK to supplier_and_product)
    

    In this model, since the supplier_and_product_id field lacks any real meaning, we bring along the other two fields and the combination of all 3 will relate back to the Unique Index on supplier_and_product. I generally try to avoid FKs to Unique Indexes/Constraints but this is one situation where it makes sense to do it.

    Putting all 3 fields together in a Unique Index in order to reference the FK to it allows for bringing over the two needed fields (supplier_id and product_id) while guaranteeing that the combination of those two is always a valid combination (as it is enforced by the FK).

  • I would recommend against combining these into a single table as it duplicates relationship info of Product and Supplier when the Price changes during the from and to dates of the Product-to-Supplier relationship. But, that is just for the transactional side. On the reporting side it is definitely a good idea to combine them as suggested by @JonofAllTrades.

  • Regarding the need for the extra WHERE condition to check the date against the from and to dates in supplier_and_product: this is not a bad idea, but is technically just a safe guard, assuming that the app doesn't allow a given Price entry to have dates that fall outside of the time-frame that the Product is being supplied by that Supplier. But if the dates are somewhat validate against such oddities, then you can now get away with the simplified query of Option 1 (since the product_id and supplier_id fields are there) while having the integrity of Option 2 (since those fields also FK back to the supplier_and_product relationship table). Which means you really should FK the product_id and supplier_id fields in the Price table back to both the relationship table and their respective parent tables since it will help with JOINs between Price and the two parent tables.

  • 1
    Thanks for your reply. While you're busy writing the rest, I'll answer your question: upon receiving a communication of a price change from the supplier, a user might want want to setup things such that, starting from January 1st, 2015, the price of a given product will be Y instead of X, so that the new price will be applied automaticall and he won't have to remember to do it at the right time. So you can say the reason behind this is one of scheduling, not reporting (in which case I agree that there would be better ways). BTW, thank you for mentioning SCD, I didn't know about that :) – s.m. Nov 21 '14 at 16:19
  • 1
    @s.m. So it is written, so it shall be :) – Solomon Rutzky Nov 21 '14 at 16:32
  • 1
    I fully agree on the first three notes you wrote. For ease of typing I didn't include the table name as part of the id column name (although I did so for the last table), but I normally do. Also, good point on the plural. I'm all for conventions when they make life easier. And it is of course true that the combination of supplier and product on SupplierAndProduct is itself a PK. It didn't occurred to me, simply because I usually I choose the second design (which however always felt a bit awkward, hence the question). – s.m. Nov 21 '14 at 16:35
  • 1
    ...aaaand I also agree with the rest. I do have one question, though. You said you tend to avoid FKs on unique constraints. May I ask you why? I'm eager to learn more on best practices :) – s.m. Nov 21 '14 at 16:52
  • 1
    @s.m. now I have to justify an emotionally-driven technical rule? ;-) I think I have always just found it to be a little bit "icky" since it seems more obscured. I think I formed the opinion when I a) was less experienced and hadn't run into any valid reasons for doing it, and b) inherited projects that were sloppily done and relationships were burried in the mess and this was used as a work-around for a flawed design. But I guess I am now finding more valid reasons so I guess they are fine: "Primary" doesn't imply "only", and "Alternate" does imply "valid". Oh well :). – Solomon Rutzky Nov 21 '14 at 16:59
1

I'd combine the two. Add Price to your SuppliersAndProducts table (and maybe call it something else, like Catalog or Offerings), and when the price changes end the first record and start another.

CREATE TABLE Catalog
    (
    CatalogID     INT NOT NULL IDENTITY,
    SupplierID    INT NOT NULL REFERENCES Suppliers (SupplierID),
    ProductID     INT NOT NULL REFERENCES Products (ProductID),
    Price         DECIMAL(19, 2),  -- Nullable
    EffectiveDate DATE WITH OFFSET NOT NULL DEFAULT NOW(),
    EndDate       DATE WITH OFFSET  -- Nullable 
    )

PK on CatalogID, biz key on { SupplierID, ProductID, EffectiveDate }.

For comparison. In most of the sales-like databases I've seen, the price is stored only in the OrderDetails table, which is simpler still but obviously does not let you get the historical price of a product if you did not order it from a particular supplier in the date range in question.


To illustrate srutsky's point about granularity: this model will create more supplier/product records than the OP's second model, where prices are tracked in a separate table from when the products are sold. If we start with a simple example, my model is simpler; if there are two suppliers for a single product, that's two records:

Catalog
Supplier #1, Product A, 2014/07 to NULL, $100
Supplier #2, Product A, 2014/11 to NULL, $200

...whereas the other model has four records:

Offerings
Supplier #1, Product A, 2014/07 to NULL
Supplier #2, Product A, 2014/11 to NULL

Pricing
Reference { Supplier #1, Product A }, 2014/07 to NULL, $100
Reference { Supplier #2, Product A }, 2014/11 to NULL, $200

However, my model leads to more records in its (only) table once prices start to change. Let's say both suppliers give a 25% discount for the holidays and then revert it:

Catalog
Supplier #1, Product A, 2014/07 to 2014/12, $100
Supplier #1, Product A, 2014/12 to 2015/01, $75
Supplier #1, Product A, 2015/01 to NULL, $100
Supplier #2, Product A, 2014/11 to 2014/12, $200
Supplier #2, Product A, 2014/12 to 2015/01, $150
Supplier #2, Product A, 2015/01 to NULL, $200

...vs:

Offerings
Supplier #1, Product A, 2014/07 to NULL
Supplier #2, Product A, 2014/11 to NULL

Pricing
Reference { Supplier #1, Product A }, 2014/07 to 2014/12, $100
Reference { Supplier #2, Product A }, 2014/11 to 2015/01, $200
Reference { Supplier #1, Product A }, 2015/01 to NULL, $100
Reference { Supplier #1, Product A }, 2014/11 to 2014/12, $75
Reference { Supplier #2, Product A }, 2014/12 to 2015/01, $150
Reference { Supplier #1, Product A }, 2015/01 to NULL, $75

In the Offerings + Pricing model, only the Pricing table grows when prices change. The Offerings table does not, so queries which don't need pricing are not burdened with extra rows. Querying Offerings is very clean, at the cost that querying Pricing is more complicated.

For this example, the difference is small. However, if there are many more attributes of an offering, like manufacturer's SKU or credit terms, the single Catalog table will get more rows as well as more columns. If the supplier changes their SKU, that's a new record; if they change their terms a month later, that's another new record. In extremis, it might get to the point that some attribute changes every day, and your Catalog table would degenerate into "what was true for this supplier, for this product, on this specific day."

Now, this is only a problem to the extent that the attributes in Catalog are volatile. I suspect that, in practice, a supplier's price, SKU, and terms will rarely change more than once or twice a year, so we're talking about tens of thousands of rows, not millions. One would need to know more about your industry, the size of your product and supplier bases, and the kind of additional attributes you might want, to judge for sure. Srutsky's model will scale better, so if you're thinking big that's the right path. If your need is small- or even medium-scale, I suspect you're better off keeping it simple.

  • Thank you Jon, that makes a lot of sense. I would avoid storing the price in OrderDetails, as having the ability to figure out when prices changed over time, in this case, would be a nice-to-have. – s.m. Nov 21 '14 at 16:40
  • 1
    I'm a stats junkie myself, so it looks like I would get along pretty well with you DW guys :) Better to collect some additional data for future uses than to find yourself needing data you didn't record. YAGNI doesn't fully apply to database design, methinks. – s.m. Nov 21 '14 at 16:51
  • 1
    @s.m. and Jon: This is a great idea for the BI / reporting side of things, but potentially disastrous on the transactional side. The problem is that Price is a) a property of a Product being offered by a Supplier, and b) a more granular property at that. By combining them you force all properties of the Supplier+Product relationship to be at that same, finer granularity. Any properties that should not be as granular need to be copied upon each Price change. Changing one of those needs to be copied back to the prior rows with consecutive dates, because a relationship period can span 2+ rows. – Solomon Rutzky Nov 21 '14 at 19:17
  • 1
    @s.m. and Jon: just to be clear since text-only writing can easily come off as more negative than intended. I am not meaning to be harsh. I just wanted to point out a flaw that could, over the years, end up costing a lot of time/money trying to correct for. The only way this model works (i.e. doesn't cost a ton in maintenance) is if there are never, ever any properties added to the relationship and hence it is just the from and to dates. But even then I would still prefer the separate tables on the transactional side, for clarity. – Solomon Rutzky Nov 21 '14 at 19:30
  • 1
    @srutzky no worries, you didn't sound harsh at all to me. I too tend to ask myself "what happens if tomorrow we have to do X", so I very much appreciate your insight. One can never think of everything that might happen, so again thank you for voicing your concerns :) – s.m. Nov 21 '14 at 19:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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