By Brian, 1 year and 10 months ago

Scripting Around Poor Table Design

We've all seen it. Some 3rd party database you've inherited from someone else was simply never designed with scalability and portability in mind. There's no hope to change it, as it will break the original design and thus the original application that it is supporting.

In a recent case for me, I had the problem of dealing with a 'Quantity Discount' table. ie, Buy one and its normal price, buy 2 or more and it's a dollar less per item. The table I had to work with was a disaster. There were no data pairs for pricing and quantity. Instead, it was one row per item number with 9 qty fields and 9 corresponding pricing fields similar to this:

  1. itemno, offno, qty1, qty2, ... qty9, pri1, pri2, ... pri9

  • qty1 matched with pri1, qty2 matched with pri2 and so forth through the 9 iterations.
  • itemno is the item number/sku in the database inventory table
  • offno is relating to the offer code, or promo code. Depending on the promo, certain products are priced differently.
  • the max discount field in the qty ranges was denoted by 999999 in the last field.
  • any non-used qty ranges after the max were filled in with 0's.
  • Prices are stored in whole numbers (x100) so as to eliminate decimal points. ($19.99 was stored as 1999)

The current setup simply printed out every price, line item by line item, until the 999999 item was reached, and then 'or more' was added on to the end of it.

What the client wanted to display, was Only the upsale prices. Ie, if 1 and 2 items were regular price, but 3 or more were at a discount, all they wanted to show was the main price as usual, along with «Buy 3 or more, $XX.XX». this posed even a more difficult problem when there were ranges in the prices, ie, Buy 1-2 at normal price (no display - same as regular price), Buy 3-4, $XX.XX, Buy 5 or more, $XX.XX

One stored procedure, one temp db table, and one two-dimensional indexed array later, I came up with the solution (with the help of a couple fine members on a dev mailing list I might add)

So, I waved goodbye to Function multiprice, and created Function multiprice_new :)

Here's the code I used to pull this off using Classic ASP as the scripting language, and MS SQL2000 as the DBMS.

This function takes on the item number (itemno), as well as the generic price of the single item cost (sPrice) from the main script.

  1. Function multiprice_new(itemno, sPrice)
  2.  
  3.   Dim myqty()
  4.   Dim myprice()
  5.  
  6.   Set cn = Server.CreateObject("ADODB.Connection")
  7.   Set cmd = Server.CreateObject("ADODB.Command")
  8.   
  9.   cn.Open DB
  10.   Set cmd.ActiveConnection = cn
  11.   cmd.CommandText = "multiprice_new"
  12.   cmd.CommandType = adCmdStoredproc
  13.   Set objRs = Server.CreateObject("ADODB.Recordset")
  14.  
  15.   xitemno2 = trim(itemno)
  16.   xitemno2 = Replace(Space(8-len(xitemno2))," ","0") & xitemno2     ' this fixes some formatting issues on the itemno
  17.   src = trim(session("src"))    ' this passes in the source code for the correct promo pricing
  18.  
  19.   With cmd
  20.     .Parameters.Append cmd.CreateParameter("@itemno", adVarChar, adParamInput, 25, xitemno2)
  21.     .Parameters.Append cmd.CreateParameter("@src", adVarChar, adParamInput, 25, src)
  22.     .Parameters.Append cmd.CreateParameter("@qty", adInteger, adParamOutput, , 0)
  23.     .Parameters.Append cmd.CreateParameter("@price", adInteger, adParamOutput, , 0)
  24.     
  25.   End with
  26.  
  27.   
  28.   Set objRS = cmd.Execute
  29.   x= 0
  30.   Do While Not objRs.EOF
  31.  
  32.     ReDim Preserve  myqty(x)
  33.     ReDIm Preserve  myprice(x)
  34.  
  35.     myqty(x) = objRs(0)
  36.     myprice(x) = objRs(1)/100    ' remember, the prices were stored with no cents, so this generates the cents portion of the price
  37.  
  38.         objRs.MoveNext
  39.     x = x+1
  40.     Loop
  41.  
  42.   
  43.   Dim qtyranges(2,9), price, rangecount
  44.     
  45.   price = 0
  46.   rangecount = 0
  47.  
  48.   limiter = UBound(myprice)
  49.  
  50.   for j = 0 to limiter
  51.     price = myprice(j)
  52.     i = j + 1
  53.   
  54.     If i<limiter then
  55.       Do While price = myprice(i)
  56.         i = i+1        
  57.       Loop
  58.     End if
  59.  
  60.     qtyranges(0,rangecount) = j
  61.     qtyranges(1,rangecount) = i
  62.     qtyranges(2,rangecount) = price
  63.  
  64.     rangecount = rangecount+1
  65.     j = i
  66.   Next
  67.  
  68.   
  69.   for j = 0 to rangecount-1
  70.     If qtyranges(0,j) = 0 Then
  71.       qtyranges(0,j) = 1
  72.     End If
  73.     
  74.     If sPrice < (CDbl(qtyranges(2,j)) + CDbl(.01)) Then
  75.       ' skip...  this is not a cheaper price than normal.  They only want to show the discounted qty ranges.
  76.     else
  77.       response.write "Buy " & qtyranges(0,j)
  78.       
  79.       If qtyranges(1,j) <> "" Then
  80.         If qtyranges(1,j) = limiter+1 Then
  81.           response.write " or more"
  82.         Else
  83.           If qtyranges(0,j) = qtyranges(1,j) then
  84.             ' DO nothing (avoids Buy 1-1,)
  85.           Else
  86.             response.write "-" & qtyranges(1,j)
  87.           End if
  88.         End if
  89.       End If
  90.       
  91.       response.write ", Pay $"& qtyranges(2,j) & " each<br />"
  92.     End if
  93.   next
  94.  
  95.   response.write "<br /><br />"
  96.  
  97.     objRS.Close  
  98.   cn.Close()
  99.   Set cn = Nothing
  100.  
  101.   multiprice_new=Success
  102.  
  103. End function
  104. </limiter>

This function called the following stored procedure:

  1. SET QUOTED_IDENTIFIER ON
  2. GO
  3. SET ANSI_NULLS ON
  4. GO
  5.  
  6. ALTER  procedure multiprice_new
  7. (
  8. @itemno varchar(25),
  9. @src varchar(25),
  10. @qty int output,
  11. @price int output
  12. )
  13.  
  14. as
  15.  
  16. set nocount on
  17.  
  18. begin
  19.  
  20. declare @t table
  21. (
  22. itemno varchar(25),
  23. qty int,
  24. price int
  25. )
  26.  
  27. insert into @t
  28.  
  29. select itemno, cast(qty1 as int), pri1
  30. from multiprice
  31. where itemno = '' + @itemno + ''
  32. and offno = '' + @src + ''
  33. UNION ------------
  34. select itemno, cast(qty2 as int), pri2
  35. from multiprice
  36. where itemno = '' + @itemno + ''
  37. and offno = '' + @src + ''
  38. UNION ------------
  39. select itemno, cast(qty3 as int), pri3
  40. from multiprice
  41. where itemno = '' + @itemno + ''
  42. and offno = '' + @src + ''
  43. UNION ------------
  44. select itemno, cast(qty4 as int), pri4
  45. from multiprice
  46. where itemno = '' + @itemno + ''
  47. and offno = '' + @src + ''
  48. UNION ------------
  49. select itemno, cast(qty5 as int), pri5
  50. from multiprice
  51. where itemno = '' + @itemno + ''
  52. and offno = '' + @src + ''
  53. UNION ------------
  54. select itemno, cast(qty6 as int), pri6
  55. from multiprice
  56. where itemno = '' + @itemno + ''
  57. and offno = '' + @src + ''
  58. UNION ------------
  59. select itemno, cast(qty7 as int), pri7
  60. from multiprice
  61. where itemno = '' + @itemno + ''
  62. and offno = '' + @src + ''
  63. UNION ------------
  64. select itemno, cast(qty8 as int), pri8
  65. from multiprice
  66. where itemno = '' + @itemno + ''
  67. and offno = '' + @src + ''
  68. UNION ------------
  69. select itemno, cast(qty9 as int), pri9
  70. from multiprice
  71. where itemno = '' + @itemno + ''
  72. and offno = '' + @src + ''
  73.  
  74. delete from @t
  75. where price = 0
  76.  
  77. select qty, price
  78. from @t
  79. order by qty asc
  80.  
  81. set nocount off
  82.  
  83. end
  84.  
  85. GO
  86. SET QUOTED_IDENTIFIER OFF
  87. GO
  88. SET ANSI_NULLS ON
  89. GO
  90.  

Finally, to call it from our main product display page, we simply call the function:

  1. itemno = request("itemno")
  2. sPrice = **some other function here that gets pricing from inventory table (along with the product details, etc) **
  3. Ssuccess=multiprice_new(itemno, sPrice)

And that will print out something like:

  1. Your Price: $14.98
  2. Buy 4 or more, Pay $10.98 each

assuming qty1, qty2, qty3 were priced at 14.98, and qty 4 was priced at 10.98, and qty5 field had the 999999 indicator for 'or more' in it. Confusing, yes. Poor table design at it's finest :)


So, what did we do here?

We had a problem table that would normally be pretty useless to us.
We made our own temp table that would be worthwhile to us, filling it in line by line from the bad table via unions for each of the qty/price pairs based on item number. The real problem here, is that is is impossible to group by from the original table to get the desired result. Thus, we made our own table and via the select inserts, can put the data how it can be usable to us.

Once we had a workable table, we created a loop to set up our ranges of quantities. 1,2,3; or 1-4,5-10, 11 or more. No matter what the data, our loop will create our starting integer (i) along with its matching trailing integer (j) and that ranges corresponding price (price).

Finally, we check to see if the price is less than the standard price to make sure its actually a better deal, and thus, an upsell based on quantity. If not, skip. If it is, print out the range and its price. Loop through until all fields are exhausted and the 999999 item has been reached, thus 'or more'.
We also do a check to make sure that the item range is not the same as itself («Buy 1-1, $XX.XX») as that just looks stupid and is not a range, afterall. I put a little comment in the loop above about this.

With some clever looping, and a lot of coding, the desired result has been achieved.

Next time you're handed a database table that seems impossible to get the data you need from it, don't fret. There's always a way to get what you need so long as you have access to the data at all. A little converting, kicking and punching, and you will be able to print out just about anything in any format a client desires. Even if that client is your Uncle Charlie.

Comments, thoughts, and suggestions appreciated as always.

Big thanks to Matt Warden for coming up with the qtyranges() array concept which, with some tweaking, was the key to making this successful.

-Brian

No comments

Be the first to write a comment on this post.

Write a comment

If you want to add your comment on this post, simply fill out the next form:





* Required fields

You can use these XHTML tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>.

No trackbacks

To notify a mention on this post in your blog, enable automated notification (Options > Discussion in WordPress) or specify this trackback url: http://​skeymedia.com/​scripting-around-poor-table-design/​trackback/