Chapter 33

Auctions

Abstract

Auctions are a common way of resolving Personal valuations of items.

Keywords

eBay.com

Craigslist.org

Stock markets

Auction theory

Bidding

English auction

Japanese auction

Dutch auction

Vickrey auction

Buyer's remorse

LIFO

FIFO

We live in the worlds of eBay.com, craigslist.org, stock markets, and bargain shopping via search engines. Yet programmers do not understand how a market works. The first principles of a market are pretty simple.

1. Not everyone values things the same. A lobster dinner looks good to me, but deadly to someone with a shellfish allergy.

2. Personal valuations change over time. I might eat a second lobster dinner immediately after the first one, but a third lobster is too much to eat right now. Call me next week, please. People do not trade unless they feel what they gain is more valuable than what they lose. I want that lobster dinner more than I want the $25 in my pocket. But I want $50 in my pocket more than a lobster in my belly.

33.1 General Types of Bidding

Bids have to stop at some point in time, so there is usually a deadline. They can be sealed-bid, where each bidder has a fixed number of bids (usually one) that they traditionally submit in an envelope. Open bid auctions are more lively; the bidders call out their offers until the auctioneer closes the bidding. You can enter and leave the auction at will.

These days, we do it online, so we have “bid snatcher” software that automatically submits a bid that is under your preset limit. The usual strategy is to wait until the last possible minute (second? microsecond?) to get in a bid in so short a time slot that nobody can type fast enough to beat you.

33.2 Types of Auctions

Auctions come in all kinds of flavors, but the basic parts are an offering of something (I have here a first edition of CAPTAIN BILLY’S WHIZBANG, 1919! I start the bidding at $500!) and bids (I’ll pay $1000 for it!). The bid can be accepted or rejected. If the bid is accepted, the item changes ownership.

33.2.1 English Auctions

English Auctions are also known as increasing or ascending price auctions. The auctioneer asks for starting price and the bidders increase the price until the item sells or the price offered is not high enough and the auction fails. This is the form that most of us know. It is how eBay works. Sort of eBay is more complicated.

33.2.2 Japanese Auctions

The Japanese auction is an ascending price auction with levels or tiers. Once you drop out, you cannot re-enter the bidding. For example, if the starting bid is $10, every interested bidder agrees to meet it. We then go up a level or $15; anyone who fails to meet the new level is out and cannot re-enter the bidding, even if they are willing to pay a higher price later. You have a lot of information—the number of other bidders at every level and their exit prices. The auction continues until only one bidder remains.

33.2.3 Dutch Auctions

Dutch Auctions are also known as decreasing or descending price auctions. The auctioneer asks for high starting price, and then drops the price lower and lower in steps until the item clears or auction fails. This form of auction used to be popular with small retail stores decades ago. The items would be put in the store window with a flip-chart price tag and an announcement that the price would decrease $x per day until sold.

Today, the Dutch auction is hidden in online clearance sales. Instead of looking at the store window on Main Street, you get a series of email advertisements at lower and lower prices.

33.2.4 Vickrey Auctions

Obviously, a winner can have “buyer’s remorse”—the feeling that they paid too much for the item. If you have seen bidding wars on eBay or real life, you understand how emotions can get mixed up in the heat of the moment.

The solution is Vickrey or second price auctions. The highest bidder wins, but the price paid is between the highest and second highest bids. The winner cannot be unhappy; he paid less than his bid and won. The losers cannot be unhappy; they all bid less than the winner.

The eBay proxy bid system is a form of second price auction. A variant of a Vickrey auction, named generalized second-price auction, is used in Google’s and Yahoo!’s online advertisement programs. The computer runs hundreds or thousands of auctions for the on-line adverting slots they sell.

33.2.5 Auction Schema

What would the basic tables be? Let’s start with the bidders and a simple skeleton.

CREATE TABLE Bidders
(bidder_id INTEGER NOT NULL PRIMARY KEY,
..)

When we log in an item for an auction, we need to identify the item, get a starting bid amount. The minimum bid is also called the reserve amount. If there is no bid, equal or greater than that amount, the auction is void.

Initially we do not have

CREATE TABLE Items
(item_id INTEGER NOT NULL PRIMARY KEY,
item_name VARCHAR(25) NOT NULL,
initial_bid_amt DECIMAL (12,2) NOT NULL
 CHECK(initial_bid_amt >= 0.00),
minimum_bid_amt DECIMAL (12,2) NOT NULL
 CHECK(initial_bid_amt >= 0.00));

A bid has to be timestamped and the bid amounts have to increase over time.

CREATE TABLE Bids
(item_id INTEGER NOT NULL
REFERENCES Items (item_id),
bidder_id INTEGER NOT NULL
REFERENCES Bidders (bidder_id),
bid_timestamp TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL,
bid_amt DECIMAL (12,2) NOT NULL
 CHECK(bid_amt >= 0.00),
PRIMARY KEY (item_id, bidder_id, bid_timestamp)
);

I recommend using a bid insertion procedure to ensure that bids are always increasing (or decreasing in the case of a Dutch auction) rather than in the DDL. The reason is that a bid can be pulled from the bids table.

33.3 LIFO and FIFO Inventory

Imagine a very simple inventory of one kind of item, Widgets, into which we add stock once a day. This inventory is then used to fill orders that also come in once a day. Here is the table for this toy problem.

CREATE TABLE WidgetInventory
(receipt_nbr INTEGER NOT NULL PRIMARY KEY,
purchase_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
qty_on_hand INTEGER NOT NULL
 CHECK (qty_on_hand >= 0),
unit_price DECIMAL (12,4) NOT NULL);

With the following data:

WidgetInventory
Receipt_nbrPurchase_dateQty_on_handUnit_price
1'2017-08-01'1510
2'2017-08-02'2512
3'2017-08-03'4013
4'2017-08-04'3512
5'2017-08-05'4510

t0010

The business now sells 100 units on 2017-08-05. How do you calculate the value of the stock sold? There is not one right answer, but here are some options:

1. Use the current replacement cost, which is $10.00 per unit as of 2017-08-05. That would mean the sale cost us only $1000.00 because of a recent price break.

2. Use the current average price per unit. We have a total of 160 units, for which we paid a total of $1840.00; that gives us an average cost of $11.50 per unit, or $1150.00 in total inventory costs.

3. Use LIFO, which stands for, “Last In, First Out”. We start by looking at the most recent purchases and work backwards through time.

2017-08-05: 45 * $10.00 = $450.00 and 45 units
2017-08-04: 35 * $12.00 = $420.00 and 80 units
2017-08-03: 20 * $13.00 = $260.00 and 100 with 20 units left over

For a total of $1130.00 in inventory cost.

4. Use FIFO, which stands for “First In, First Out”. We start by looking at the earliest purchases and work forward through time.

2017-08-01: 15 * $10.00 = $150.00 and 15 units
2017-08-02: 25 * $12.00 = $300.00 and 40 units
2017-08-03: 40 * $13.00 = $520.00 and 80 units
2017-08-04: 20 * $12.00 = $240.00 with 15 units left over

For a total of $1210.00 in inventory costs.

The first two scenarios are trivial to program.

CREATE VIEW (current_replacement_cost)
AS
SELECT unit_price
FROM WidgetInventory
WHERE purchase_date
= (SELECT MAX(purchase_date) FROM WidgetInventory);
CREATE VIEW (average_replacement_cost)
AS
SELECT SUM(unit_price * qty_on_hand)/SUM(qty_on_hand)
FROM WidgetInventory;

The LIFO and FIFO are more interesting because they involve looking at matching the order against the blocks of inventory in a particular order.

33.3.1 LIFO Cost as a VIEW

Consider this VIEW:

CREATE VIEW LIFO (stock_date, unit_price, tot_qty_on_hand, tot_cost)
AS
SELECT W1.purchase_date, W1.unit_price, SUM(W2.qty_on_hand), SUM(W2.qty_on_hand *
W2.unit_price)
FROM WidgetInventory AS W1,
 WidgetInventory AS W2
WHERE W2.purchase_date <= W1.purchase_date
GROUP BY W1.purchase_date, W1.unit_price;

A row in this VIEW tells us the total quantity on hand, the total cost of the goods in inventory, and what we were paying for items on each date. The quantity on hand is a running total. We can get the LIFO cost with this query:

SELECT (tot_cost - ((tot_qty_on_hand - :order_qty) * unit_price))
AS cost
FROM LIFO AS L1
WHERE stock_date
 = (SELECT MIN(stock_date)
FROM LIFO AS L2
 WHERE tot_qty_on_hand >= :order_qty);

This is straight algebra and a little logic. You need to find the most recent date when we had enough (or more) quantity on hand to meet the order. If, by dumb blind luck, there is a day when the quantity on hand exactly matched the order, return the total cost as the answer. If the order was for more than we have in stock, then return nothing. If we go back to a day when we had more in stock than the order was for, look at the unit price on that day, multiply by the overage, and subtract it.

33.3.2 CASE Expressions

Alternatively, you can use a derived table and a CASE expression. The CASE expression computes the cost of units that have a running total quantity less than the :order_qty and then performs algebra on the final block of inventory, which would put the running total over the limit. The outer query does a sum on these blocks:

SELECT SUM(W3.v) AS cost
FROM (SELECT W1.unit_price
* CASE WHEN SUM(W2.qty_on_hand) <= :order_qty
 THEN W1.qty_on_hand
 ELSE :order_qty
 - (SUM(W2.qty_on_hand) - W1.qty_on_hand)
 END
FROM WidgetInventory AS W1,
 WidgetInventory AS W2
 WHERE W1.purchase_date <= W2.purchase_date
 GROUP BY W1.purchase_date, W1.qty_on_hand, W1.unit_price
HAVING (SUM(W2.qty_on_hand) - W1.qty_on_hand) <= :order_qty)
 AS W3(v);

FIFO can be found with a similar VIEW or derived table:

CREATE VIEW FIFO (stock_date, unit_price, tot_qty_on_hand, tot_cost)
AS
SELECT W1.purchase_date, W1.unit_price,
 SUM(W2.qty_on_hand), SUM(W2.qty_on_hand *
W2.unit_price)
FROM WidgetInventory AS W1,
 WidgetInventory AS W2
WHERE W2.purchase_date >= W1.purchase_date
GROUP BY W1.purchase_date, W1.unit_price;

With the corresponding query:

SELECT (tot_cost - ((tot_qty_on_hand - :order_qty) * unit_price)) AS cost
FROM FIFO AS F1
WHERE stock_date
  = (SELECT MAX (stock_date)
FROM FIFO AS F2
 WHERE tot_qty_on_hand >= :order_qty);

These queries and VIEWs only told us the value of the Widget inventory. Notice that we never actually shipped anything from the inventory.

33.3.3 Updating Inventory

How to write the UPDATE statements that let us change this simple inventory.

What we did not do in part 1 was to actually update the inventory when the Widgets were shipped out. Let’s build another VIEW that will make life easier.

CREATE VIEW StockLevels (purchase_date, previous_qty, current_qty)
AS
SELECT W1.purchase_date,
 SUM(CASE WHEN W2.purchase_date < W1.purchase_date
THEN W2.qty_on_hand ELSE 0 END),
 SUM(CASE WHEN W2.purchase_date <= W1.purchase_date
THEN W2.qty_on_hand ELSE 0 END)
FROM WidgetInventory AS W1,
 WidgetInventory AS W2
WHERE W2.purchase_date <= W1.purchase_date
GROUP BY W1.purchase_date, W1.unit_price;
StockLevels
Purchase_datePrevious_qtyCurrent_qty
'2017-08-01'  0 15
'2017-08-02' 15 40
'2017-08-03' 40 80
'2017-08-04' 80115
'2017-08-05'115160

Using CASE expressions will save you a self-join.

CREATE PROCEDURE RemoveQty (IN my_order_qty INTEGER)
LANGUAGE SQL
BEGIN
IF my_order_qty > 0
THEN
UPDATE WidgetInventory
SET qty_on_hand
= CASE
WHEN my_order_qty
 >= (SELECT current_qty
 FROM StockLevels AS L
WHERE L.purchase_date
 = WidgetInventory.purchase_date)
THEN 0
WHEN my_order_qty
 < (SELECT previous_qty
FROM StockLevels AS L
 WHERE L.purchase_date
 = WidgetInventory.purchase_date)
THEN WidgetInventory.qty_on_hand
ELSE (SELECT current_qty
FROM StockLevels AS L
 WHERE L.purchase_date = WidgetInventory.purchase_date)
- my_order_qty END;
END IF;
-- remove empty bins
DELETE FROM WidgetInventory
WHERE qty_on_hand = 0;
END;

Another inventory problem is how to fill an order with the smallest or greatest number of bins. This assumes that the bins are not in order, so you are free to fill the order as you wish. Using the fewest bins would make less work for the order pickers. Using the greatest number of bins would clean out more storage in the warehouse.

For example, with this data, you could fill an order for 80 widgets by shipping out bins (1, 2, 3) or bins (4, 5). These bins happen to be in date and bin number order in the sample data, but that is not required.

Mathematicians call it (logically enough) a bin-packing problem and it belongs to the NP-complete family of problems. This kind of problem is too hard to solve for the general case because the work requires trying all the combinations, and this increases too fast for a computer to do it.

However, there are “greedy algorithms” that are often nearly optimal. The idea is to begin by taking the “biggest bite” you can until you have met or passed your goal. In procedural languages, you can back-track when you go over the target amount and try to find an exact match or apply a rule that dictates from which bin to take a partial pick.

33.4 Bin Packing

This is not easy in SQL, because it is a declarative, set-oriented language. A procedural language can stop when it has a solution that is “good enough,” while an SQL query tends to find all of the correct answers no matter how long it takes. If you can put a limit on the number of bins you are willing to visit, you can fake an array in a table:

CREATE TABLE Picklists
(order_nbr INTEGER NOT NULL PRIMARY KEY,
goal_qty INTEGER NOT NULL
 CHECK (goal_qty > 0),
bin_nbr_1 INTEGER NOT NULL UNIQUE,
qty_on_hand_1 INTEGER DEFAULT 0 NOT NULL
CHECK (qty_on_hand_1 >= 0),
bin_nbr_2 INTEGER NOT NULL UNIQUE,
qty_on_hand_2 INTEGER DEFAULT 0 NOT NULL
CHECK (qty_on_hand_2 >= 0),
bin_nbr_3 INTEGER NOT NULL UNIQUE,
qty_on_hand_3 INTEGER DEFAULT 0 NOT NULL
CHECK (qty_on_hand_3 >= 0),
CONSTRAINT not_over_goal
CHECK (qty_on_hand_1 + qty_on_hand_2 + qty_on_hand_3
<= goal_qty)
CONSTRAINT bins_sorted
CHECK (qty_on_hand_1 >= qty_on_hand_2
 AND qty_on_hand_2 >= qty_on_hand_3));

Now you can start stuffing bins into the table. This query will give you the ways to fill or almost fill an order with three or fewer bins. The first trick is to load some empty dummy bins into the table. If you want at most (n) picks, then add (n-1) dummy bins:

INSERT INTO WidgetInventory
VALUES (-1, '1990-01-01', 0 ,0.00),
 (-2, '1990-01-02', 0 ,0.00);

The following code shows how to build a common table expression (CTE) or VIEW with the possible pick lists:

CREATE VIEW PickCombos(total_pick, bin_1, qty_on_hand_1,
bin_2, qty_on_hand_2,
bin_3, qty_on_hand_3)
AS
SELECT DISTINCT
 (W1.qty_on_hand + W2.qty_on_hand + W3.qty_on_hand) AS total_pick,
 CASE WHEN W1.receipt_nbr < 0
THEN 0 ELSE W1.receipt_nbr END AS bin_1, W1.qty_on_hand,
 CASE WHEN W2.receipt_nbr < 0
THEN 0 ELSE W2.receipt_nbr END AS bin_2, W2.qty_on_hand,
 CASE WHEN W3.receipt_nbr < 0
THEN 0 ELSE W3.receipt_nbr END AS bin_3, W3.qty_on_hand
FROM WidgetInventory AS W1,
WidgetInventory AS W2,
WidgetInventory AS W3
WHERE W1.receipt_nbr NOT IN (W2.receipt_nbr, W3.receipt_nbr)
AND W2.receipt_nbr NOT IN (W1.receipt_nbr, W3.receipt_nbr)
AND W1.qty_on_hand >= W2.qty_on_hand
AND W2.qty_on_hand >= W3.qty_on_hand;

Now you need a procedure to find the pick combination that meets or comes closest to a certain quantity.

CREATE PROCEDURE OverPick (IN goal_qty INTEGER)
LANGUAGE SQL
BEGIN
IF goal_qty > 0
THEN
SELECT goal_qty, total_pick, bin_1, qty_on_hand_1,
bin_2, qty_on_hand_2,
bin_3, qty_on_hand_3
FROM PickCombos
WHERE total_pick
= (SELECT MIN (total_pick)
 FROM PickCombos
WHERE total_pick >= goal_qty)
END IF;
END;

The VIEW could be put into a CTE and produce a query without a VIEW. With the current data and goal of 73 Widgets, you can find two picks that together equal 75, namely {3, 4} and {4, 2, 1}.

I will leave it as an exercise for the reader to find a query that under-picks a target quantity.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.198.60