x

The ‘FIFO Stock Inventory’ SQL Problem

Phil Factor SQL Speed Phreak Competition: No 2

alt text

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 site

It 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'll tell you the business rules for the algorithm and provide a sample cursor-based routine that generates the correct result, but slowly (about 40 minutes).

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?
It is a reasonable request. We have a stock transaction list (1,000,001 records, 17.6MB zipped) with 15,002 unique articles and we need to do a stock inventory report that gives the current breakdown of the ArticleID, the number of items in stock and the current stock value according to FIFO rules. The list should be in ArticleID order. The sample stock transaction list will include two running totals so you can check the results of your routine. They are NOT part of the competition, and you are not allowed to use them in your calculation.

Beware that the sample file includes the two extra columns; CurrentItems (INT) and CurrentValue (MONEY) !
If you want a smaller subset to test on, import all records and remove all articles but two or three. These are the samples for which all suggestions will be measured with.

The table is in this form (simplified from the way we'd do it in a real system of course).
In this made-up but realistic problem, you have an existing table and database design which you can’t do anything about. (we can all relate to that!) You have an existing cursor-based solution that is taking several minutes to run. (Yes, we all relate to that). The database is performing badly, and you need to take effective steps to remedy this. The only thing you can do is to come up with a better-performing routine. Redesigning the database isn’t an option very often, in the real world, because this requires team sign-in. This is a competition based on the real, sometimes imperfect, world: not an exposition of database design. The point is that we are faced with designs like this and we have to heal them. The competition is all about making stuff go faster.

CREATE TABLE    dbo.Stock
                (
                    StockID INT IDENTITY(1, 1) NOT NULL,
                    ArticleID SMALLINT NOT NULL,
                    TranDate DATETIME NOT NULL,
                    TranCode VARCHAR(3) NOT NULL,
                    Items INT NOT NULL,
                    Price MONEY NULL,
                    CONSTRAINT [PK_Stock] PRIMARY KEY CLUSTERED 
                    (
                        StockID ASC
                    )
                )  

CREATE NONCLUSTERED INDEX IX_Input ON dbo.Stock (TranCode, ArticleID)
--INCLUDE (TranDate, Items, Price) -- Remove comment for SQL Server 2005 and later
--WHERE TranCode IN ('IN', 'RET')   -- Remove comment for SQL Server 2008

CREATE NONCLUSTERED INDEX IX_Output ON dbo.Stock (TranCode, ArticleID)
-- INCLUDE (TranDate, Items) -- Remove comment for SQL Server 2005 and later
--WHERE TranCode = 'OUT'  -- Remove comment for SQL Server 2008

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)

StockID ArticleID TranDate TranCode Items  Price CurrentItems CurrentValue
   4567     10000 10:45:07 IN         738 245.94          738   181,503.72
  21628     10000 12:05:25 OUT        600                 138    33,939.72
  22571     10000 14:39:27 IN          62 199.95          200    46,336.62
  30263     10000 16:14:13 OUT        165                  35     6,998.25
  42090     10000 18:18:58 RET          5                  40     7,998.00
  58143     10000 20:18:54 IN         500 135.91          540    75,953.00

a) First IN add 738 items (each $245.94) to the stock, for a total of $181,503.72
b) Then we take out 600 items (each 245.94) from the stock, leaving a total of $33,939.72
c) Then we insert 62 items (each 199.95) to the stock, for a total of $46,336.62
d) Then we take out 165 items (138 each 245.94 and 27 each 199.95), leaving a total of $6,998.25
e) Then we return 5 items. We can’t track at which price we took them out; so all returns are priced at the price of the latest ones inserted before the return. Even if there should be items left for the price of 245.94, the returned items are valued for 199.95. After the return, the current stock value is $7,998.00
f) The final insert adds $67,995.00 to the stock value, for a total of $75,953.00

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.
2) Include an edition number. First edition is 1. If you later improve your current suggestion post it again as version 2. Example: “Peso 1” and if improved, “Peso 1b”, “Peso 1c” etc.
3) If you are trying a new algorithm, change the edition to “Peso 2”. If you improve this algorithm, change the version to “Peso 2b”, “Peso 2c” etc. This will save Phil hours of work in the test harness!
4) If you create a temp table, make sure you delete it in the script.
5) Keep the order of columns in output as ArticleID, CurrentItems, CurrentValue

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 time taken for their creation will not be included in the timings. The time measured is the “Main” script/procedure. If you want to call sub-procedures, go ahead.

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.

CREATE TABLE    #Work
                (
                    RowID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED,
                    Price MONEY
                )  

DECLARE @ArticleID INT = 1,
        @PrevID INT = 0,
        @TranCode VARCHAR(3),
        @Items INT,
        @Price MONEY,
        @Loop INT = 0,
        @StockID INT,
        @LatestPrice MONEY,
        @Total INT = (SELECT COUNT(*) FROM dbo.Stock)

DECLARE curYak CURSOR FORWARD_ONLY FOR
                SELECT      ArticleID,
                            TranCode,
                            Items,
                            Price,
                            StockID
                FROM        dbo.Stock
                ORDER BY    ArticleID,
                            TranDate

OPEN    curYak

FETCH   NEXT
FROM    curYak
INTO    @ArticleID,
        @TranCode,
        @Items,
        @Price,
        @StockID

WHILE @@FETCH_STATUS = 0
    BEGIN
        IF @ArticleID > @PrevID
            BEGIN
                TRUNCATE TABLE  #Work

                SET @LatestPrice = NULL
            END

        IF @TranCode = 'IN'
            BEGIN
                INSERT  #Work
                        (
                            Price
                        )
                SELECT  @Price
                FROM    dbo.TallyNumbers
                WHERE   Number < @Items

                SET     @LatestPrice = @Price
            END

        IF @TranCode = 'RET'
            INSERT  #Work
                    (
                        Price
                    )
            SELECT  @LatestPrice
            FROM    dbo.TallyNumbers
            WHERE   Number < @Items

        IF @TranCode = 'OUT'
            DELETE  w
            FROM    (
                        SELECT      TOP(@Items)
                                    RowID
                        FROM        #Work
                        ORDER BY    RowID
                    ) AS w

        UPDATE      s
        SET         s.CurrentItems = w.CurrentItems,
                    s.CurrentValue = COALESCE(w.CurrentValue, 0)
        FROM        dbo.Stock AS s
        INNER JOIN  (
                        SELECT  COUNT(*) AS CurrentItems,
                                SUM(Price) AS CurrentValue
                        FROM    #Work
                    ) AS w ON s.StockID = @StockID

        SELECT  @PrevID = @ArticleID,
                @Loop += 1

        IF @Loop % 1000 = 0
            RAISERROR('Now updating record %d of %d.', 10, 1, @Loop, @Total) WITH NOWAIT

        FETCH   NEXT
        FROM    curYak
        INTO    @ArticleID,
                @TranCode,
                @Items,
                @Price,
                @StockID
    END

DROP TABLE  #Work

CLOSE       curYak
DEALLOCATE  curYak

The above is already done. Here is the code to produce the final resultset.

SELECT      ArticleID,
            CurrentItems,
            CurrentValue
FROM        (
                SELECT  ArticleID,
                        CurrentItems,
                        CurrentValue,
                        ROW_NUMBER() OVER (PARTITION BY ArticleID ORDER BY TranDate DESC) AS recID
                FROM    dbo.Stock
            ) AS d
WHERE       recID = 1
ORDER BY    ArticleID

Good luck to you all!

Peter Larsson

more ▼

asked Oct 23, 2009 at 03:56 PM in Default

Peso gravatar image

Peso
1.6k 5 6 8

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...
Oct 23, 2009 at 04:34 PM Peso
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.
Oct 23, 2009 at 05:08 PM Peso
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?
Oct 23, 2009 at 05:09 PM RBarryYoung
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.
Oct 23, 2009 at 05:16 PM Peso
Those filtered indexes will only work on SQL Server 2008, right?
Oct 23, 2009 at 05:46 PM RBarryYoung
(comments are locked)
10|1200 characters needed characters left

28 answers: sort voted first

Timings are quite an eye-opener. They do vary quite a bit, and some solutions vary more than others.

I took the average of six timings.

It is a tie between GianLuca and Dave Ballantyne. Because I can only award the competition to one person, I'm going to use as a tie-breaker the fact that the fastest timing in the whole run was by Dave Ballantyne at an amazing 2220 ms. He therefore is awarded the Phil factor Speed Phreak award, and sets the next competition. However, I've asked Red-Gate to award a second $60 Amazon Voucher to Gianluca for his wonderful entry 1a which was so close to Dave's average timings that one couldn't put a cigarette paper between them.

Matt Whitfield's late modification to his CLR comes in at 4300 ms, and gets an honourable mention, as does Scot Hauder (4620 ms) Steve(1a) with 4416 ms

The original run gave the following figures (I've used Matt's second CLR solution)....

**Secs       Ms Entry**
     3     2560 Dave Ballantyne - Phil Factor Challenge Entry 3.d
     2     2750 Gianluca 1a
     3     3050 Gianluca 2a
     3     3190 Dave Ballantyne - Phil Factor Challenge Entry 3.b
     4     3313 Matt (waiting for fixed version!)
     4     4360 Matt Whitfield CLR solution v2
     4     4560 Steve 1a
     5     5033 Scot Hauder
     6     5830 Herman 1d
     6     5906 RBarryYoung 2d Pure Set-Based, summary calcs
    10    10173 Andriy Z. 1
    16    16300 Dave Ballantyne Ver 2b
    41    41000 Dave Ballantyne Ver 1a
    62    61673 RBarryYoung_1b: Naive, Pure Set-Based Solution
   102   101430 Peso CURSOR (not an entry)
   111   111160 Herman, 30 Oct 2009 

Just to show you the sort of variations i was seeing, here is another sample run. In future, I'm going to judge the winner over at least 20 runs, and record the variance as well as the average time.

4   4236	Dave Ballantyne - Phil Factor Challenge Entry 3.b
4   4546	Gianluca 1a
5   4703	Dave Ballantyne - Phil Factor Challenge Entry 3.d
6   5566	Steve 1a
6   6190	Scot Hauder
7   7216	RBarryYoung_2d: Pure Set-Based, summary calcs
7   7450	Herman 1d
9   8610	Gianluca 2a
10  10296	Andriy Z. 1
18  17813	Dave Ballantyne Ver 2b
28  27500	Matt Whitfield CLR solution (V1)
42  42003	Dave Ballantyne Ver 1a
63  62803	RBarryYoung_1b: Naive, Pure Set-Based Solution
114 113116	 Herman, 30 Oct 2009
198 198136	Peso CURSOR (not an entry)

Sadly, I left the harness to watch a DVD, and when I got back to do the next lot of runs, windows had decided to upgrade itself and I hadn't saved the latest version, so I'm afraid the umpire's decision is now final

Final order over eight runs:

  1. Dave Ballantyne - Phil Factor Challenge Entry 3.d
  2. Gianluca 1a
  3. Gianluca 2a
  4. Dave Ballantyne - Phil Factor Challenge Entry 3.b
  5. Steve 1a
  6. Scot Hauder
  7. Andriy Z. 1
  8. Dave Ballantyne Ver 2b
  9. RBarryYoung_2d: Pure Set-Based, summary calcs
  10. Matt Whitfield CLR solution
  11. Dave Ballantyne Ver 1a
  12. RBarryYoung_1b: Naive, Pure Set-Based Solution
  13. Herman 1d
  14. Peso CURSOR (not an entry)
  15. Herman, 30 Oct 2009
  16. Dave Ballantyne 1

(I had to disqualify Matt as I haven't got an entry that gives the right answer)

more ▼

answered Nov 11, 2009 at 06:03 AM

Phil Factor gravatar image

Phil Factor
3.9k 8 9 16

Phil - do you want any changes made to the harness, or have you not got round to unpacking that one yet? :)
Nov 11, 2009 at 06:15 AM Matt Whitfield ♦♦
Matt, do you have another version of your CLR for Phil to test?
Nov 12, 2009 at 02:45 AM Peso
It would be nice to get a CLR that is in the same region as the entrants that are taking just a handful of seconds! I don't think we'll get a cursor-based entry in the same league.
Nov 12, 2009 at 08:21 AM Phil Factor
Sounds closer than the initial tests have shown
Nov 12, 2009 at 09:52 AM dave ballantyne
For those suggestion with preliminary timings 10 seconds or less, there is slight mismatch betweeen my test machine and Phil's test machine for about +/- 1 second. But how come the CURSOR method went from 12 seconds to 102 seconds?
Nov 12, 2009 at 10:26 AM Peso
(comments are locked)
10|1200 characters needed characters left

What do you think?
Am I allowed to compete, since all rules are known?

Please comment here what you think.

more ▼

answered Oct 23, 2009 at 05:32 PM

Peso gravatar image

Peso
1.6k 5 6 8

Wow, have I been there, Peso. It's probably Phil's call, but my feeling is NO. I say this knowing that I may be in your position too in the near future (hopefully :-)). The problem is that no matter how fair you are, sooner or later someone is going to feel cheated because you were both a competitor and the Question Asker (or worse, Tester/Judge). What I would recommend is just throwing you solution in at the end, in the final measurements, as an example non-competing entry. I.e., non-eligible to win.
Oct 23, 2009 at 05:41 PM RBarryYoung
I respect that. If this is the consensus, I will post my set-based solution very last before deadline so that no-one can steal my thunder :-)
Oct 23, 2009 at 05:51 PM Peso
Oh boy! Someone already voted this as an unhelpful post grin
Oct 23, 2009 at 05:59 PM Peso
I think the series would be more fun if we disallow the current winner from entering, since it gives other people a chance! Actually, I think we should be allowed to steal Peso's thunder too at some point!
Oct 24, 2009 at 05:23 AM Phil Factor
Well, I'm upvoting it, so they should cancel out, ;-)
Oct 24, 2009 at 03:24 PM RBarryYoung
(comments are locked)
10|1200 characters needed characters left

I can't find the Dave 3.a version in answer list.

more ▼

answered Nov 04, 2009 at 08:41 PM

Mladen gravatar image

Mladen
1

3.a has been superseded , 3.d is now the latest
Nov 11, 2009 at 09:23 AM dave ballantyne
(comments are locked)
10|1200 characters needed characters left

Here is my cursor solution as counterweight to all set-based solutions :-)

-- Peso CURSOR
CREATE TABLE #Stage ( ArticleID INT, CurrentItems INT, CurrentValue MONEY )

DECLARE @ArticleID INT, @PrevID INT = 0, @Items INT, @CurrItems INT, @Price MONEY, @LatestPrice MONEY, @Away INT, @Diff INT

DECLARE curYak CURSOR LOCAL FORWARD_ONLY FOR SELECT s.ArticleID, s.Items, s.Price, COALESCE(w.Items, 0) AS Away FROM ( SELECT ArticleID, TranDate, Items, Price FROM dbo.Stock WHERE TranCode IN ('IN', 'RET') ) AS s LEFT JOIN ( SELECT ArticleID, SUM(Items) AS Items FROM dbo.Stock WHERE TranCode = 'OUT' GROUP BY ArticleID ) AS w ON w.ArticleID = s.ArticleID ORDER BY s.ArticleID, s.TranDate

OPEN curYak

FETCH NEXT FROM curYak INTO @ArticleID, @Items, @Price, @Away

WHILE @@FETCH_STATUS = 0 BEGIN SELECT @Diff = CASE WHEN @ArticleID > @PrevID THEN 0 WHEN @CurrItems > @Away THEN @CurrItems - @Away ELSE @Diff END, @CurrItems = CASE WHEN @ArticleID > @PrevID THEN @Items ELSE @CurrItems + @Items - @Diff END, @PrevID = @ArticleID, @LatestPrice = COALESCE(@Price, @LatestPrice)

    IF @CurrItems &gt;= @Away
       INSERT    #Stage
         (
          ArticleID,
          CurrentItems,
          CurrentValue
         )
       VALUES    (
          @ArticleID,
          @CurrItems - @Away,
          (@CurrItems - @Away) * @LatestPrice
         )

    FETCH  NEXT
    FROM   curYak
    INTO   @ArticleID,
       @Items,
       @Price,
       @Away
END

CLOSE curYak DEALLOCATE curYak

SELECT ArticleID, SUM(CurrentItems) AS CurrentItems, SUM(CurrentValue) AS CurrentValue FROM #Stage GROUP BY ArticleID ORDER BY ArticleID

DROP TABLE #Stage
more ▼

answered Nov 11, 2009 at 04:47 PM

Peso gravatar image

Peso
1.6k 5 6 8

(comments are locked)
10|1200 characters needed characters left

Ok, well I would post a pure set based solution, except that the site is refusing to let me post the whole thing!! Lame.

Anyway, the SQL is just

EXEC [dbo].[FIFOStock]

It runs in about 6.3 seconds on my box.

There are two source files, one for the support class (mostly the same guts as the CLR type version, except for the fact that IBinarySerialize support is removed, and it is now class, rather than struct based).

FIFOStock Procedure:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void FIFOStock()
    {
        using (SqlConnection connection = new SqlConnection("context connection=true"))
        {
            connection.Open();
            using (SqlCommand command = new SqlCommand("SELECT [ArticleID], CASE [TranCode] WHEN 'IN' THEN 1 WHEN 'OUT' THEN 2 ELSE 3 END, [Items], [Price] FROM [dbo].[Stock] ORDER BY [ArticleID], [TranDate]", connection))
            {
                short lastArticleID = -1;
                StockStack ss = new StockStack();

                // Create a record object that represents an individual row, including it's metadata.
                SqlDataRecord record =
                    new SqlDataRecord(new SqlMetaData[] {
                    new SqlMetaData("ArticleID", SqlDbType.Int),
                    new SqlMetaData("CurrentItems", SqlDbType.Int),
                    new SqlMetaData("CurrentValue", SqlDbType.Money),
                });

                SqlContext.Pipe.SendResultsStart(record);

                using (SqlDataReader reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        short articleID = reader.GetInt16(0);
                        if (articleID != lastArticleID)
                        {
                            if (lastArticleID != -1)
                            {
                                // Populate the record.
                                record.SetInt32(0, lastArticleID);
                                record.SetInt32(1, ss.TotalCount);
                                record.SetSqlMoney(2, ss.TotalPrice);

                                SqlContext.Pipe.SendResultsRow(record);
                            }
                            ss = new StockStack();
                            lastArticleID = articleID;
                        }
                        int opCode = reader.GetInt32(1);
                        switch (opCode)
                        {
                            case 1:
                            {
                                ss.AddStock(reader.GetSqlMoney(3), reader.GetInt32(2));
                            } break;
                            case 2:
                            {
                                ss.RemoveStock(reader.GetInt32(2));
                            } break;
                            case 3:
                            {
                                ss.ReturnStock(reader.GetInt32(2));
                            } break;
                        }
                    }

                    // populate and emit the final record
                    record.SetInt32(0, lastArticleID);
                    record.SetInt32(1, ss.TotalCount);
                    record.SetSqlMoney(2, ss.TotalPrice);
                    SqlContext.Pipe.SendResultsRow(record);
                }
                SqlContext.Pipe.SendResultsEnd();
            }
        }
    }
}

StockStack class:

using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; using System.Collections.Generic; using System.Text;

internal class StockStack { public override string ToString() { StringBuilder sb = new StringBuilder(1024); foreach (StockStackItem ssi in _stackItems) { sb.Append(ssi.Value); sb.Append(" x "); sb.AppendLine(ssi.Count.ToString()); } sb.Append("---"); sb.Append(TotalPrice.ToString()); sb.Append(" - "); sb.Append(TotalCount.ToString()); return sb.ToString(); }

public void ReturnStock(int Count)
{
    _stackItems[_stackItems.Count - 1].ReturnStock(Count);
}

public void AddStock(SqlMoney Price, int Count)
{
    _stackItems.Add(new StockStackItem((Decimal)Price, Count));
}

public void RemoveStock(int Count)
{
    int itemsLeft = 0;
    bool itemEmpty = false;
    while (true)
    {
        _stackItems[0].RemoveStock(Count, out itemsLeft, out itemEmpty);

        if (itemEmpty)
        {
            _stackItems.RemoveAt(0);
        }
        if (itemsLeft == 0)
        {
            return;
        }
        Count = itemsLeft;
    }
}

public int TotalCount
{
    get
    {
        int totalCount = 0;
        foreach (StockStackItem ssi in _stackItems)
        {
            totalCount += ssi.Count;
        }
        return totalCount;
    }
}


public SqlMoney TotalPrice
{
    get
    {
        Decimal totalValue = 0;
        foreach (StockStackItem ssi in _stackItems)
        {
            totalValue += ssi.TotalValue;
        }
        return totalValue;
    }
}

public static StockStack GetInitialisedStack(SqlMoney Price, int Count)
{
    StockStack ss = new StockStack();
    ss._stackItems.Add(new StockStackItem((Decimal)Price, Count));
    return ss;
}

public static StockStack Empty
{
    get
    {
        return new StockStack();
    }
}

public static StockStack Parse(SqlString s)
{
    // no implementation of from-string for this type
    return StockStack.Empty;
}

// List of items in the stock stack
private List&lt;StockStackItem&gt; _stackItems = new List&lt;StockStackItem&gt;(4);

private class StockStackItem
{
    Decimal _stackItemValue;
    int _stackItemCount;

    public void ReturnStock(int Count)
    {
        _stackItemCount += Count;
    }

    public int Count
    {
        get
        {
            return _stackItemCount;
        }
    }

    public Decimal Value
    {
        get
        {
            return _stackItemValue;
        }
    }

    public Decimal TotalValue
    {
        get
        {
            return _stackItemValue * _stackItemCount;
        }
    }

    public void RemoveStock(int Count, out int RemainingItems, out bool IsEmpty)
    {
        if (_stackItemCount &gt; Count)
        {
            RemainingItems = 0;
            IsEmpty = false;
            _stackItemCount -= Count;
        }
        else 
        {
            RemainingItems = Count - _stackItemCount;
            IsEmpty = true;
            _stackItemCount = 0;
        }
    }

    public StockStackItem(Decimal StackItemValue, int StackItemCount)
    {
        _stackItemValue = StackItemValue;
        _stackItemCount = StackItemCount;
    }
}
}
more ▼

answered Nov 12, 2009 at 11:00 AM

Matt Whitfield gravatar image

Matt Whitfield ♦♦
29.4k 61 65 87

(comments are locked)
10|1200 characters needed characters left
Your answer
toggle preview:

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

New code box

There's a new way to format code on the site - the red speech bubble logo will automatically format T-SQL for you. The original code box is still there for XML, etc. More details here.

Follow this question

By Email:

Once you sign in you will be able to subscribe for any updates here

By RSS:

Answers

Answers and Comments

SQL Server Central

Need long-form SQL discussion? SQLserverCentral.com is the place.

Topics:

x987
x344
x8
x7
x5

asked: Oct 23, 2009 at 03:56 PM

Seen: 29198 times

Last Updated: Nov 14, 2009 at 01:55 PM