views:

76

answers:

3

The following are my sql server 2005 table structures:

Products (productID INT PK, ...)

ProductBids (productID INT, userID INT, Created DATETIME)

Users(UserID INT PK, TOTALBIDS INT)

Each day a user can bid as many times as they want on all the products. There is a sql job that is run periodically, that counts the total bids a user has performed and puts the total in the TOTALBIDS field.

The catch is, our business rules require that we only count up to 10 bids for any given day.

So the update query has to group by day, and if the total bids for a user on the products goes over 10, we just use 10 for that day.

e.g. day#1 bids 5 times in total day#2 bids 15 times in total day#3 bids 10 times

(assuming on 3 days in total) The bidCount for the user will be 5 + 10 + 10 = 25 (not 30).

Is this possible in a single query?

A: 

I think you can use an aggregate + a case statement to do this. Something like:

declare @t table (a int, b int)

insert into @t values(1, 5)
insert into @t values(1, 15)
insert into @t values(1, 10)


select a, sum( case when b>10 then 10 else b end) 
from @t
group by a

The case statement is ensuring that you never add more than 10 if the value is greater than 10

marshall
+1  A: 

You don't say what you want to do with the results, but you can certainly SELECT the user's earliest ten bids of each day:

with ProductBidsRanked(productID, userID, Created, rk) as (
  select
    productID, userID, Created,
    row_number() over (
      partition by userID, dateadd(datediff(day,0,Created),0)
      order by Created
    )
)
  select productID, userID, Created
  from ProductBidsRanked
  where rk <= 10

Of course, if you only need the total, and want to replace the total with 10 when it exceeds 10, that's easier:

with PartialAgg(userID,countOr10) as (
  select
    userID,
    case when count(*) > 10 then 10 else count(*) end
  from ProductsBids
  group by userID, dateadd(datediff(day,0,Created),0)
)
  select
    userID, sum(countOr10) as BidsAdjusted
  from PartialAgg
  group by userID;

Response to comment:

You say you want to add it to the user's bidcount, but bidcount isn't a column name in any of your tables. Perhaps you meant TOTALBIDS, so for example, if the second query is the one that works for you, you could do something like this:

with PartialAgg(userID,countOr10) as (
  select
    userID,
    case when count(*) > 10 then 10 else count(*) end
  from ProductsBids
  group by userID, dateadd(datediff(day,0,Created),0)
), FullAgg(userID,BidsAdjusted) as (
  select
    userID, sum(countOr10) as BidsAdjusted
  from PartialAgg
  group by userID
)
  update users set
    TOTALBIDS = TOTALBIDS + BidsAdjusted
  from users join FullAgg
  on FullAgg.userID = users.userID

FYI, there's some SQL Server specific stuff here - ANSI doesn't allow UPDATE with a CTE, and I didn't confirm that T-SQL's quirky UPDATE .. FROM can be used in combination with a CTE.

In any case, given that this seems like the kind of update you would run only infrequently, and never concurrently, it could be wisest to insert the results of my first suggestion (whichever serves your purpose) into a temporary table and base your update on that.

Steve Kass
WIth the result, I want to add it to the users current bidcount.
mrblah
I've added to my answer.
Steve Kass
+1  A: 
CREATE TABLE dbo.ProductBids(ProductID INT, UserID INT, Created DATETIME);

CREATE TABLE dbo.Users(UserID INT, TotalBids INT);

INSERT dbo.Users(UserID) SELECT 1 UNION ALL SELECT 2;

INSERT dbo.ProductBids 
           SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()-1
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 1, GETDATE()
UNION ALL  SELECT 1, 2, GETDATE()
UNION ALL  SELECT 1, 2, GETDATE()
UNION ALL  SELECT 1, 2, GETDATE();

UPDATE u 
SET TotalBids = x.TotalBids
FROM
(
    SELECT
     UserID, 
     TotalBids = SUM(CASE WHEN c > 10 THEN 10 ELSE c END)
    FROM
    (
     SELECT
      UserID,
      c = COUNT(*)
     FROM
      dbo.ProductBids
     GROUP BY
      UserID,
      DATEADD(DAY, 0, DATEDIFF(DAY, 0, Created)) 
    ) AS y
    GROUP BY UserID
) AS x
INNER JOIN dbo.Users AS u
ON x.UserID = u.UserID;

GO

SELECT UserID, TotalBids FROM dbo.Users;

GO

DROP TABLE dbo.Users, dbo.ProductBids;

However in general I frown upon storing this total, when you can derive the information from existing data. The problem is that the data in the Users table is only guaranteed to be accurate between the time you run the UPDATE statement and the next time any DML operation happens againt the ProductBids table.

Aaron Bertrand
Should be implemented as a view++
OMG Ponies
its an experensive query, and I don't want to run in on the fly everytime I list 10 users on a page etc.
mrblah
But it's ok if it isn't accurate? Maybe consider an indexed view.
Aaron Bertrand