简体   繁体   中英

How to improve this patternless database schema?

Before jumping into the schema and tables, I would like to share what I am trying to achieve first. I am working on sort of a courier application, where I have some categories and each category has a pre-defined price .

But, determining the price is a bit ugly (absence of symmetry and patterns; at least, I can't seem to find any) . I will give you an example:

Consider the following categories: Document, Heavy Document, Laptop, Carton, Heavy Carton.

1) Document: It's for the lighter documents, which are below 0.5kg. The price is 20$, fixed.

[price stored in the prices table: 20.00]

eg For an item of 300g, the price will be 20$.

2) Heavy Document: This is for the documents that are over 0.5kg. Unlike the Document category, it doesn't have a fixed price! Rather, it has a unit price: 10$ per kg, which will be applied to each kg's except/after 0.5kg.

[price stored in the prices table: 10.00]

eg For an item of 2kg, the price will be 35$ (1.5g = 15$ + 0.5 = 20$)

3) Laptop: Straightforward, 100$. Nothing special about it, no constraint whatsoever.

[price stored in the prices table: 100.00]

eg For an item of 2kg, the price will be 35$ (1.5g = 15$ + 0.5 = 20$)

4) Carton: Here comes an interesting one. Until now there was only one dependency: weight . But this one has an additional dependency: dimension . This is somewhat similar to Document category. For the cartons that are below 3 Cubic Feet(CF), the price is 80$ per CF. The difference between Document and Carton category is that, Document has a fixed price whereas Carton has Unit Price. But wait, there's more. There is an additional constraint: dimension-weight ratio. In this case, it is 7kg per CF . And if the item's weight crosses the ratio, for each extra kg's 5$ will be charged. It's so confusing, I know. An example might help:

[price stored in the prices table: 80.00]

eg For a carton of 80kg and 2CF; the price will be 490$. Here is how:

First calculate the regular charge: 80$*2CF = 160$ Now let's figure out if it crosses Ratio : Since, 1 CF = 7kg, hence, 2CF = 14kg. But the item's weight is 80kg, so it crosses the ratio (14kg)

Since it crosses the ratio, for all the extra kgs (80-14 = 66kg), each kg will cost 5$: 66*5 = 330$. After adding it with regular charge: 330$+160$ = 490$.

5) Heavy Carton: This one is for the cartons having the dimension bigger than 3CF. The difference with Carton is the unit price. Heavy Carton is 60$ per CF.

[price stored in the prices table: 60.00]

eg For a carton of 80kg and 5CF; the price will be 525$. Here is how:

First calculate the regular charge: 60$*5CF = 300$ Now let's figure out if it crosses Ratio : Since, 1 CF = 7kg, hence, 5CF = 35kg. But the item's weight is 80kg, so it crosses the ratio (35kg)

Since it crosses the ratio, for all the extra kgs (80-35 = 45kg), each kg will cost 5$: 45*5 = 225$. After adding it with regular charge: 300$+225$ = 325$.

If you've read this far, I think I have convinced you that the business structure is really complicated. Now let's take a look at my categories schema:

+-------------------------+---------------------------------+------+-----+---------+----------------+
| Field                   | Type                            | Null | Key | Default | Extra          |
+-------------------------+---------------------------------+------+-----+---------+----------------+
| id                      | int(10) unsigned                | NO   | PRI | NULL    | auto_increment |
| name                    | varchar(191)                    | NO   |     | NULL    |                |
| created_at              | timestamp                       | YES  |     | NULL    |                |
| updated_at              | timestamp                       | YES  |     | NULL    |                |
| dim_dependency          | tinyint(1)                      | NO   |     | NULL    |                |
| weight_dependency       | tinyint(1)                      | NO   |     | NULL    |                |
| distance_dependency     | tinyint(1)                      | NO   |     | NULL    |                |
| dim_weight_ratio        | varchar(191)                    | YES  |     | NULL    |                |
| constraint_value        | decimal(8,2)                    | YES  |     | NULL    |                |
| constraint_on           | enum('weight','dim')            | YES  |     | NULL    |                |
| size                    | enum('short','regular','large') | YES  |     | regular |                |
| over_ratio_price_per_kg | decimal(8,2)                    | YES  |     | NULL    |                |
| deleted_at              | timestamp                       | YES  |     | NULL    |                |
+-------------------------+---------------------------------+------+-----+---------+----------------+

Also the schema of prices table (it's a polymorphic table, hoping to create a subcategories table someday):

+----------------+---------------------+------+-----+---------+----------------+
| Field          | Type                | Null | Key | Default | Extra          |
+----------------+---------------------+------+-----+---------+----------------+
| id             | int(10) unsigned    | NO   | PRI | NULL    | auto_increment |
| amount         | decimal(8,2)        | NO   |     | NULL    |                |
| created_at     | timestamp           | YES  |     | NULL    |                |
| updated_at     | timestamp           | YES  |     | NULL    |                |
| priceable_type | varchar(191)        | NO   | MUL | NULL    |                |
| priceable_id   | bigint(20) unsigned | NO   |     | NULL    |                |
| deleted_at     | timestamp           | YES  |     | NULL    |                |
+----------------+---------------------+------+-----+---------+----------------+

How can I improve this structure to keep things as much dynamic and coherent as possible?

So if I want to send a 10kg/2.99cf package, you charge me 240$ ( 3*80$ ) for a Carton. If I put that package in a slightly bigger box and now want to send it as a 10kg/3.01cf package, you charge me 180$ ( 3*60$ ) for a Heavy Carton. In case you round up to the next full cf , let's assume I want to send a 80kg/3CF package; you charge me 535$ ( 3*80+59*5 ). If I put the same package in a bigger box with 80kg/4CF , you charge me just 500$ ( 4*60+52*5 ).

That is indeed a good sign "that the business structure is really complicated" (even if those might be just examples values, it shows the potential to overcomplicate things).

Anyway, I would probably encode your conditions in a table like that:

category |max_kg|max_cf|is_laptop|price|p_p_kg|p_p_cf|off_kg|off_cf|off_rat
---------+------+------+---------+-----+------+------+------+------+--------
Document | 0.5  | null |    0    |  20 |   0  |   0  |  0   |  0   |  0     
Heavy Doc|  2   | null |    0    |  20 |  10  |   0  | 0.5  |  0   |  0   
Laptop   | null | null |    1    | 100 |   0  |   0  |  0   |  0   |  0  
Carton   | null |   3  |    0    |   0 |   5  |  80  |  0   |  0   |  7  
Heavy C. | null | null |    0    | 180 |   5  |  60  |  0   |  3   |  7  

There are probably some size constraints for documents too (can I eg send my 0.0kg/100cf helium-filled ballon as a document?), but you haven't specified them; listing the conditions that way should make it obvious where you have unspecific conditions though.

off_* specifies offsets, eg the amount already included in price ; p_p_kg is the price per kg for the remaining weight (reduced by the offset), analogous for p_p_cf . So a heavy carton with 80kg/4CF would be calculated as

price    -- 180
+ p_p_kg * greatest(kg - off_rat * cf - off_kg, 0)  -- 5 * (80-7*4-0) 
+ p_p_cf * greatest(cf - off_cf, 0) -- 60 * (4 - 3)

so, as expected, 180 + 5 * 52 + 60 = 500 .

The user will not come to your shop and say "I want to send this as a Heavy Carton" . He will say: "What does it cost me to send something that weights 80 kg, has 3 cf and is no laptop." And he will probably expect you not to send it as a Carton if a Heavy Carton would be cheaper.

So you take this input (and any other relevant input like distance) and check all rows that fulfill the condition with something like

select (your price formula depending on input) as cost
...
where (max_kg is null or max_kg >= 80) 
  and (max_cf is null or max_cf >= 3)
  and (is_laptop is null or is_laptop = 0)
order by cost

You should probably define that in a single place, so it's easier to add additional conditions (eg distince) and other specifications not defined in your table (eg rounding to full cf or steps of 0.1).

You will probably also need a table with additional services like express or overnight delivery, insurance for packages above 500$, delivery at a fixed time or similar.

You mentioned subcategories and "polymorphic price table", but it's not clear what you want to do with it. If you have some concrete examples that cannot be formulated in a matrix table like that, add them. But you should also be aware that simplicity is king, both for you and the customer. You would probably already lose me if I think you charge me 240$ for my 10kg/2.99cf Carton if your competition does it for 200$ , even if you would actually just charge me 180$ for a Heavy Carton.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM