0

I'm having a table representing the dealers cards and their rank. I'm now trying to make a query (as fast as possible) to set status on the game.

(As said before, only the dealer cards is shown)
W = Win
S = Stand
L = Loss
B = Blackjack (in two cards)

About the rules: The dealer wins at 21, if it's in two cards its blackjack. If the rank is between 17 and 20 it's S = stand. Over 21 is a loss.

Ranks:

1 (ACE) - 1 or 11 rank. Counted as 11.

2-10 - 2-10 rank

11-13 (knight - king) - 10 rank

╔════╦══════╦════════╗
║ Id ║ Rank ║ Status ║
╠════╬══════╬════════╣
║  1 ║    1 ║        ║
║  2 ║    5 ║        ║
║  3 ║    8 ║ L      ║  //24 = Loss
║  4 ║    3 ║        ║
║  5 ║    1 ║        ║
║  6 ║    7 ║ W      ║  //21 = Win
║  7 ║   10 ║        ║
║  8 ║    1 ║ B      ║  //21 = Blackjack
║  9 ║   10 ║        ║
╚════╩══════╩════════╝

I've tried to use a counter to check if it's blackjack and then I'm using a "RunningPoint" to check the sum of the cards.

I have now a solution bad it shows very bad performance when it's a lot of data. How would you do this and what can I do to optimize my query? When using more data I also need to use option (maxrecursion 0)

(When having 1 million rows it's not even possible to run this...)

My example: http://sqlfiddle.com/#!6/3855e/1

Community
  • 1
  • 1
MrProgram
  • 5,044
  • 13
  • 58
  • 98
  • Are you using MySQL or T-SQL? I assume T-SQL because the fiddle uses SQL Server. – Gordon Linoff Oct 11 '15 at 20:00
  • What's your SQL Server release? – dnoeth Oct 11 '15 at 20:40
  • I use T-SQL. I'm using SQL Server 2014 – MrProgram Oct 12 '15 at 08:01
  • Could you please test the different solutions and post some info about performance? I would be interested how much faster the "query update" actually is... – dnoeth Oct 12 '15 at 14:26
  • @dnoeth yes of course. As soon as I've done all the testing – MrProgram Oct 12 '15 at 14:40
  • This is the 2015 SQL Server usergroup T-SQL competition. @krillezzz, why don't you try to solve the problem yourself instead of asking others for help, then taking the credit? No cool, bro. – Daniel Hutmacher Oct 21 '15 at 11:24
  • Cheating in a contest is really mean and apparently he also changed his SO-name :-) Btw, his previous question was also based on this challenge: http://stackoverflow.com/questions/33017005/running-sum-in-t-sql – dnoeth Oct 21 '15 at 16:31

2 Answers2

1

This solution is based on quirky update. More info here.

LiveDemo

Data and structures:

CREATE TABLE #BlackJack
(
   id INT 
  ,Rank INT
  ,running_total INT
  ,result NVARCHAR(100)
);

CREATE CLUSTERED INDEX IX_ROW_NUM ON #BlackJack(id);

insert into #BlackJack (Id, Rank)
values (1, 1),(2, 5), (3, 8), (4, 3), (5, 1),
       (6, 7), (7, 10), (8, 1),(9, 10), (10, 10), (11,1);

Main query:

DECLARE @running_total       INT = 0
        ,@number_of_cards    INT = 0
        ,@prev_running_total INT = 0;

UPDATE #BlackJack
SET 
   @prev_running_total = @running_total
  ,@running_total = running_total = IIF(@running_total >= 20, 0, @running_total) 
                                    + CHOOSE(Rank,11,2,3,4,5,6,7,8,9,10,10,10,10)
  ,result        = CASE WHEN @running_total = 20 THEN 'S'
                        WHEN @running_total = 21 AND @number_of_cards = 2 THEN 'B'
                        WHEN @running_total = 21 THEN 'W'
                        WHEN @running_total > 21 THEN 'L'
                        ELSE NULL
                    END
  ,@number_of_cards  = IIF(@prev_running_total >= 20, 0, @number_of_cards) + 1
FROM #BlackJack WITH(INDEX(IX_ROW_NUM))
OPTION (MAXDOP 1);

SELECT *
FROM #BlackJack
ORDER BY id;

Warning

If you use SQL Server < 2012 you need to replace IIF and CHOOSE with CASE. I don't check all Blackjack rules, only for provided sample. If something is wrong feel free to change CASE logic.

Second I extend base table BlackJack with auxiliary columns, but you can create any new table, if needed.

The key point is to read data sequentially based on clustered key ascending and do not allow parallel execution. Before you use it in production check how it behaves with large data set.

Lukasz Szozda
  • 162,964
  • 23
  • 234
  • 275
  • [**Here**](http://www.sqlservercentral.com/articles/T-SQL/68467/) is a nice article that discusses the rules of using quirky update. [Join the discussion](http://http://www.sqlservercentral.com/Forums/Topic802558-203-1.aspx) for more information. – Felix Pamittan Oct 12 '15 at 03:38
  • @lad2025 why does your update change the order of Id ? – MrProgram Oct 12 '15 at 12:07
  • @krillezzz I don't understand. Do you mean last `SELECT * FROM #BlackJack;`? If yes add `SELECT * FROM #BlackJack ORDER BY Id;` – Lukasz Szozda Oct 12 '15 at 12:14
  • Sorry. I mean SELECT * FROM #BlackJack. Thanks. – MrProgram Oct 12 '15 at 12:19
1

There's no efficient solution using plain SQL (including Windowed Aggregate Functons), at least nobody found one, yet :-)

Your recursive query performs bad because it's way too complicated, this is a simplified version:

Edit: Fixed the calculation (Fiddle)

WITH ctePoints AS
 (
   SELECT 1 AS id
        ,rank
        ,CASE 
           WHEN rank >= 10 THEN 10
           WHEN rank = 1 THEN 11
           ELSE rank
         END AS Point
        ,1 AS Counter
   FROM dbo.BlackJack
   WHERE Id = 1

   UNION ALL

   SELECT t2.Id
        ,t2.rank
        ,CASE WHEN t1.Point < 17 THEN t1.Point ELSE 0 END 
         + CASE 
             WHEN t2.rank >= 10 THEN 10
             WHEN t2.rank = 1 THEN 11
             ELSE t2.rank
           END AS Point
        ,CASE WHEN t1.Point < 17 THEN t1.Counter + 1 ELSE 1 END AS Counter
   FROM dbo.BlackJack AS t2
   INNER JOIN ctePoints AS t1 ON t2.Id = t1.Id + 1
 ) 
SELECT ctepoints.*
     ,CASE 
        WHEN Point < 17 THEN ''
        WHEN Point < 20 THEN 'S'
        WHEN Point > 21 THEN 'L'
        WHEN Point = 21 AND Counter = 2 THEN 'B'
        ELSE 'W' 
      END AS DealerStatus            
FROM ctePoints

It's probably still too slow, because it processes row by row.

I usually use recursive SQL to replace cursor logic (because in my DBMS it's usually much faster) but a cursor update might actually be faster (Demo):

CREATE TABLE #BlackJack
(
   id INT PRIMARY KEY CLUSTERED
  ,Rank INT
  ,DealerStatus CHAR(1)
);

insert into #BlackJack (Id, Rank)
values 
(1, 1),(2, 5), (3, 8), (4, 3), (5, 1), (6, 7), (7, 10), (8, 1),(9, 10), (10, 10), (11,1);


DECLARE @Counter INT = 0
        ,@Point INT = 0
        ,@id int
        ,@Rank int
        ,@DealerStatus char(1)

DECLARE c CURSOR
FOR
SELECT id, Rank
FROM #BlackJack 
ORDER BY id FOR UPDATE OF DealerStatus

OPEN c

FETCH NEXT FROM c INTO @id, @Rank

WHILE @@FETCH_STATUS = 0
  BEGIN
    SET @counter = @counter + 1

    SET @Rank = CASE
                  WHEN @Rank >= 10 THEN 10
                  WHEN @Rank  = 1  THEN  11
                  ELSE @Rank
                END 

    SET @Point = @Point + @Rank

    SET @DealerStatus = CASE 
                          WHEN @Point < 17 THEN ''
                          WHEN @Point < 20 THEN 'S'
                          WHEN @Point > 21 THEN 'L'
                          WHEN @Point = 21 AND @Counter = 2 THEN 'B'
                          ELSE 'W' 
                        END 

    IF @Point >= 17 
    BEGIN
      UPDATE  #BlackJack 
      SET DealerStatus = @DealerStatus
      WHERE CURRENT OF c;

      SET @Point = 0

      SET @Counter = 0
    END

    FETCH NEXT FROM c INTO @id, @Rank
  END

CLOSE c
DEALLOCATE c

SELECT * FROM #BlackJack ORDER BY id

Still @lad2025's "quirky update" is the fastest way to get the expected result, but it's using an undocumented feature and if a Service Pack breaks it there's no way to complain about it :-)

dnoeth
  • 59,503
  • 4
  • 39
  • 56