Phil Factor SQL Speed Phreak Competition: No 2
This competition is now over, but the winner, Dave, got an Amazon Voucher for $60, and the privilege of being able to display the 'Phil Factor SQL Speed Phreak' award on their own siteIt was quite a struggle with some close competition from many of those who participated in this competition. However, Dave came up with a clever solution that produced an FIFO calculation from a million rows in a just a few seconds. The third Phil Factor Speed Phreak competition will soon be held on here. Watch out! (here is the original preamble.) This competition is to calculate current items in stock and current stock value in FIFO order. I have seen many different algorithms to do this and most of them involve a cursor.
Can it be done more quickly without, or even with, a cursor? In other words, what is the fastest way in SQL Server (any version) to provide this stock inventory report? Beware that the sample file includes the two extra columns; CurrentItems (INT) and CurrentValue (MONEY) ! The table is in this form (simplified from the way we'd do it in a real system of course).
You are welcome to change the two nonclustered indexes to suit your solution. You can download the complete sample data here. I have an idea of my own of the way to do this but I don’t know if it is the fastest. Explanation of FIFO rules (example, abbreviated)
a) First IN add 738 items (each $245.94) to the stock, for a total of $181,503.72 As mentioned before, the CurrentItems and CurrentValue columns in the sample data are only included for you to validate your routines. Here are some guidelines for your entries:1) Include a header in your suggestion. Make sure your name and date is present. I will allow you to use an existing tally number table (make sure it starts with 0). You can use any kind of object for this competition, except SQLCLR. If you are using a fixed tally number table, it has to be named dbo.TallyNumbers and the column named Number. The winner will be amongst the tied fastest entrants (generally there is a group of these) and it will be the one with the highest number of votes. We'll announce the winner in three week's time on 16th November. For a starter, here is a common cursor based solution that you will find in production in many places.
The above is already done. Here is the code to produce the final resultset.
Good luck to you all! Peter Larsson
This question is marked "community wiki".
showing 5 of 24
show all
|
|
Dave Ballantyne - Phil Factor Challenge Entry 3.d Peso had adding some index hints and shuffled a calculation to my initial 3.a. So this is 3.a(+) with comments with 3.d doing the TotalStock sum in one operation shaves some more time off “A problem well stated is a problem half solved” - Charles F. Kettering Let us think about what has been asked within the challenge. We have a warehouse which starts empty , the data contains the stock movements in (including returns) and out. The stock movements ALWAYS happen in a first in first out basis. With this we know that if the present stock level (sum(in's) - sum(out's) is 50 that will be the last 50 that have entered the warehouse. The key to this query is to efficiently find the cost of those 50 items.
This answer is marked "community wiki".
|
RBarryYoung_1b: Naive, Pure Set-Based SolutionOK, just to get things rolling, here is a straight-forward Set-based solution, with no T-SQL tricks (though there is a very important algorithmic trick in cteOverlapValue).
Oops! I got those numbers completely wrong,... In reality, performance is OK, but not great. it takes 9 seconds on 100,000 rows (about 1500 Articles) and 90 seconds on the full set. Not bad for virtually no optimizations. 1b: OK, I have made the corrections Requested by Peso (ORDER BY and Fixed NULLS on Article 25001). My current timings for it are 9sec on 100k rows and 95sec on 1M rows.
This answer is marked "community wiki".
Excellent suggestion Barry! Great to see a pure set-based solution. However, there are two things I miss. 1) Articles need to be sorted by ArticleID. 2) Last ArticleID 25001 reports correct Items but wrong Value.
(Oct 25 '09 at 07:43)
Peso
OK, thanks Peso. Obviously I can fix (1) right now ... (:-)), (2) is going to take some investigation though.
(Oct 25 '09 at 12:58)
RBarryYoung
The procedure header says 1a, but the code (I think) is 1b?
(Oct 26 '09 at 11:42)
Peso
Right. I'm not very good with directions (that's why I need computers :-)). Fixed now.
(Oct 26 '09 at 14:26)
RBarryYoung
Ok, sorry for the really slow comment, but this was the one showing incorrect values for me - first article, value shows as 0.0, should be 15446.39. Sorry for the delay!
(Oct 30 '09 at 21:23)
Matt Whitfield ♦
|
|
Well, I know this answer will be disqualified, but I thought it might be interesting to see how a CLR Type would help with this. Here is my code, which runs in about 45 seconds on my box. I also think the idea of a quirky update using a CLR type is funny!
The source code for the assembly type is (no comments as yet - I might come back and fix this):
This answer is marked "community wiki".
It's a great entry! However, have you disabled parallelism manually?
(Oct 24 '09 at 20:48)
Peso
I did the MAXDOP 1 because it was a typical quirky update safety addition - I did remove it at one point but didn't find it make any difference to the performance overall...
(Oct 24 '09 at 20:56)
Matt Whitfield ♦
It's a great learning experience for me! Thank you very much. Until now, I have only dealt with aggregate SQLCLR. Take a look here http://www.developerworkshop.net/software.html
(Oct 24 '09 at 21:17)
Peso
|
|
Not finished yet , i think there is some more to come. Could you tell me what the performance is like against the 'test' system ? Just for reference i stopped Barry's running after 5 Minutes , this completes in a little over a minute.
This answer is marked "community wiki".
Dave: Five Minutes?!? are you sure that you have Peso's indexes on the table? My sProc uses them pretty heavily...
(Oct 26 '09 at 17:14)
RBarryYoung
Dave - 51 seconds on my box, but the result set isn't in the right format, and the values shown in that result set don't match the provided data...
(Oct 26 '09 at 17:27)
Matt Whitfield ♦
Dave: It faster than mine on my machine (82 sec vs. 95 sec) but it is not producing the correct output for me. Instead of a summary by ArticleID returning (ArticleID, CurrentItems, CurrentValues) sorted by ArticleID, it's just returning the entire stock table with the columns RollingCount and RollingBalance appended. Is it possible that you posted the wrong version of your script?
(Oct 26 '09 at 17:29)
RBarryYoung
@Barry - yours runs at 1:24 on my box... although that doesn't seem to get the correct values either?
(Oct 26 '09 at 17:34)
Matt Whitfield ♦
Dave, I can't parse the query. Put a comma before CteAudit. Also, I get 1,000,001 records in return, where I should get 15,002 records instead.
(Oct 26 '09 at 21:35)
Peso
Matt, can you post on mine what is wrong?
(Oct 27 '09 at 00:04)
RBarryYoung
Dave, will you edit the suggestion and include proper header with edition and version for easier track of timings?
(Oct 27 '09 at 21:37)
Peso
showing 5 of 7
show all
|
RBarryYoung_2d: Pure Set-Based, summary calcsOK, similar to my first entry, but using a smarter way to calculate the final CurrentValues: New version, 2c (don't ask about 2b :-)), primarily leveraging a new index.... OK, now we're really cooking with gas! Andriy's version spurred me to rethink certain parts of the query, resulting in a smaller and even fast version that finally uses the indexes the way that I always wanted it to. First the new index to create before running:
Andthe new version:
This is twice as fast as v2c (7 sec) on my machine.
This answer is marked "community wiki".
It is getting better and better!
(Oct 27 '09 at 18:34)
Peso
Barry - this is excellent - 21.952 seconds on my machine (excluding the index creation)... Results are A-OK on this one for me - did you still want me to post what was wrong with the other one (sorry only just saw that...)
(Oct 27 '09 at 18:51)
Matt Whitfield ♦
Matt: yes, please do. Make sure to not which version it is. -thnx!
(Oct 27 '09 at 19:05)
RBarryYoung
Barry, you are getting closer!
(Oct 27 '09 at 19:17)
Peso
thanks folks. :-) But I am not sure that I can squeeze anymore out of it...
(Oct 27 '09 at 19:50)
RBarryYoung
Yes you could! See new timings...
(Oct 28 '09 at 11:46)
Peso
Well, I couldn't let a Cursor win, now could I?
(Oct 28 '09 at 11:58)
RBarryYoung
The index you have in your code, is that to replace all non-clustered index, or is it an additional index?
(Oct 28 '09 at 11:59)
Peso
Peso: its additional, I am using all 3 NC indexes.
(Oct 28 '09 at 14:23)
RBarryYoung
This is faster for me now - 7.112 seconds - results still bang on :)
(Oct 30 '09 at 21:24)
Matt Whitfield ♦
showing 5 of 10
show all
|
|
For anyone having trouble loading the data file, here's the XML for a format file and a BULK INSERT statement that uses it. 1) Save this XML as fifo_format.xml:
2) Change the paths for the data file and format file as needed and execute this statement:
This answer is marked "community wiki".
Nice collateral benefit to following the challenge. Thanks!
(Oct 27 '09 at 21:43)
KenJ
|
|
/* Dave Ballantyne - 20091027 / / Ver 1a */
This answer is marked "community wiki".
Please follow the guidelines when naming your suggestions. Is this a better edition 1? or a new edition?
(Oct 27 '09 at 12:45)
Peso
@Peso : Slighty more streamlined than before. Dont think it'll get anywhere near Barrys best at the moment
(Oct 27 '09 at 13:14)
dave ballantyne
|
|
Here will the preliminary timings be presented. I write preliminary since Phil is (will be) away for a few days, so I will do the preliminary timings on a similar machine as Phil has. Current standings
This answer is marked "community wiki".
Barry's timings on his solution varies due to a lot of tempdb space. But it is still a great suggestion.
(Oct 26 '09 at 11:51)
Peso
What configuration are you testing on, Peso? I would think that my solution #2 would do a lot better than that on a multi-core environment. (Or is MAXDOP set down?)
(Oct 26 '09 at 23:58)
RBarryYoung
It's probably my tempDB going through the roof. I'll try the suggestions on a better tempdb configuration. Your queries generates a lot of activity on the tempdbs.
(Oct 27 '09 at 12:44)
Peso
|
|
Dave Ballantyne ver 2.b -- Had a bit of a think and a eureka moment --- --- 2.a Had a bug with 0 valued articles not being shown
This answer is marked "community wiki".
Excellent. I just had to change "on TallyNumbers.Number <= Items" to "on TallyNumbers.Number between 1 and Items" because the TallyNumbers table starts with 0.
(Oct 28 '09 at 11:50)
Peso
|
|
Essentially the same but added some comments , tidied it up , added an index
This answer is marked "community wiki".
Now, this baby is flying!
(Oct 28 '09 at 17:21)
Peso
2.873 seconds on my box, correct results too - excellent!
(Oct 28 '09 at 18:03)
Matt Whitfield ♦
+1: Wow! Got the StockID subsumes TranDate trick to work, I see.
(Oct 28 '09 at 18:26)
RBarryYoung
A little note however. The sample system depicted in the competition DOES have StockID in same order as TranDate. If your system doesn't follow same order, this will return wrong value! If your system allows to register future planned INs this will not work. For example you know you are getting 500 items next wednesday and you register them already today with wednesday's date.
(Oct 28 '09 at 20:12)
Peso
I'd suggest you change all "StockID" to "TranDate". It will in fact run faster. With StockID you average at 2.0 seconds and uses 438k reads. With TranDate you average at 1.8 seconds and uses 243k reads.
(Oct 28 '09 at 20:24)
Peso
When I created some additional indexes, I now average the "Dave 3a" on 1.5 seconds with 242k reads.
(Oct 28 '09 at 22:25)
Peso
Accept your point on the TranDate , but i have in the past had issues with collissions (which could leave you with a negative stock level) in the past. The stockid seems more 'chronological' to me. I think im done now :)
(Oct 29 '09 at 09:26)
dave ballantyne
3a is currently faster than 3b on the preliminary test machine. We have to wait for Phil to get back from vacation to get normalized timings.
(Oct 29 '09 at 19:17)
Peso
Odd, im getting exactly the same cpu and io in profiler on both statements.
(Oct 30 '09 at 08:27)
dave ballantyne
Send me an email, and I'll send you the index I am using for 3a.
(Oct 30 '09 at 09:40)
Peso
showing 5 of 10
show all
|


Was there any particular reason you disallowed CLR functionality?
On an initial look, this would be the ideal scenario in which to use a CLR type...
Two things had us made this decision: 1) We have tried CLR before and there was no performance gain. 2) To test and verify, poster need to disclode the source code for the suggestion. But if you have a CLR routine that runs on 15 seconds or less, I think Phil will be interested in testing it.
Questions: 1) Is the Stock Transaction List referred to the one that will be used for Final Evaluation? If not, can you describe the differences? 2) What is the configuration that entries will be (finally) evaluated on?
Yes, the downloadable file is the data against all suggestions will be measured. Phil will drop or rename the two last columns so that no suggestion can use them. I guess Phil will be using same test harness as last time with 'Subscription List' competition.
More Questions: I am hazy on what the entries are supposed to produce. Must they 1) Update the CurrentItems and CurrentValues columns in the Stock table?, or 2) Return to the Client connection the table (ArticleID, CurrentItems, CurrentValue) in ArticleID, TranDate order?, or 3) Both?
Hmm, looking at the examples: or 4) just return the final CurrentItems, CurrentValue for each ArticleID?
The precalculated CurrentItems and CurrentValue is only there for demonstration purpose only, for easier verification of your solution. They do not exist in the actual table, as per DDL above. There will be only one record per ArticleID, the final values. For Article 10000 the records should read "10000 109 15446.39". Three columns named ArticleID, CurrentItems, CurrentValue, and in that left-to-right order.
Great! Thanks, Peso.
Those filtered indexes will only work on SQL Server 2008, right?
Yes. Luckily you are allowed to edit the non-clustered indexes. The clustered index stays.
Just out of curiousity, did the accounting people sign off on returns being treated as LIFO, when everything else is FIFO?
Also, in the example given, if there were a subsequent "Out" transaction, would the 5 returns be taken out first and if so at what price? The return price?
Go on, you know you want to allow CLR really!! :)
@Bob; the returns are treated as the other IN, in respect to number of items. For the value, we don't known the original IN price and those 5 returns could essentially be one customer only who bought 5 articles either on five different times, or at one time. Even then, we can't derive of the five articles (since we send the order file to the outsourced warehouse with a shipping file containing orders and addresses) were take out at the same price, or 5 different prices. So since it is a disguised IN (without a price), the price has to be the latest known IN-price for that article. Make sense?
It might be an idea just to put a comment on those filtered indexes that explains that we don't want to discourage entries from people with only access to 2005 or 2000.- maybe even a commented out suggestion for SQL Server 2000 indexes.
For those who may be having trouble loading some sample data for this (I sure did), here is a script to create all of the rows (5061) for 75 of the 15000 ArticleIDs: (http://www.movingsql.com/dnn/LinkClick.aspx?fileticket=Vvt21qZze7o%3d&tabid=125&mid=911). It loads it into a second table (StockX) with the extra columns, after creating both tables (Stock & StockX).
Peso: I need some clarification on how RET (Step e) interacts with the other actions. The INs and OUTs are described as FIFO, however, RET is described as "just using the last price", does that RET then deplete stock available at that price? If so, is it LIFO, the way that OUT is FIFO? In other words, with the following actions: {IN 1@3.0}, {RET 1(@3.0)}, {IN 1@5.0}; will the next OUT be at $3.0 or $5.0?
Nevermond, I figured it out (got my RETs reversed), Sorry.
The next will first look at the oldest insert (either IN or RET; both are treated as insert) still having items left, which is {IN 1@3.0}. If more OUTs are done it will find the now oldest item which is $3.0 again (deriving from the RET insert). If more OUT is done, the now oldest IN still having items left is the IN@5.0
Did we ever disallow CLR routines? I don't remember doing that! Actually, I rather like CLR entries as well as Cursor entries as it is interesting to see how they perform.
Matt posted a CLR routine with code below, which you can test. I'll post my cursor code too. – Peso 10 hours ago
The bit about disallowing is in the main text - 'You can use any kind of object for this competition, except SQLCLR.'
I think that is a residue from Phil's and mine early discussion.
OK - well it's still in the text there :) - so if they are allowed... :)