Click here to monitor SSC
Av rating:
Total votes: 3
Total comments: 19


Joe Celko
Celko's SQL Stumper: The Data Warehouse Problem
25 September 2009

Joe Celko comes back with a puzzle that isn't new, but one where the answer he originally gave now seems archaic: It is a deceptively simple problem, but is it true that the new features of SQL have simplified the solution? We leave it to the readers to provide the answer!

Oldies but Not So Goodies

There was a posting a SQL Server Newsgroup a short while ago by a regular who was proud that his old T-SQL was still running today.  He saw it as a tribute to MS providing upward compatibility in their 4GL dialect and his skill.  Of course, there were problems when *= was deprecated, BIT became a numeric type and a few other things happened to the SQL engine as it moved from dialect to something closer to Standard SQL. 

I appreciate the sentiment, but I would consider the fact of my old T-SQL still running as a problem rather than a brag.  In those days, we used triggers where we now have DRI actions.  We wrote a lot of IF-THEN-ELSE logic where we now have CASE expressions.  We nested subqueries where we now have an OVER() clause.  We truncated DATETIME to zero hour where we now have a native DATE data type. The ISO-11179 naming conventions did not exist. 

I cannot go back and modernize my old procedures.  The companies that were failures or got bought up are not around.  The companies that were successful have migrated code already – which is part of why they are successful. 

But I do have my old puzzle columns to haunt me.  The clever code of a decade or more ago might still work today, but it ought to be replaced with a current release and rewritten with the new features.  Let me pull up one of these old puzzles and its old solution. I have cleaned up the code just a little bit, so the data element names are better and you can cut & paste it easily.  If you have been around for awhile, you will recognize the old design patterns we used when certain features were not available.  Your task is to write new answers with the current features available in SQL Server. 

The Data Warehouse Problem

This one came in as a data warehouse problem in 1999. You have a history table of customer daily total purchases that looks like this:

CREATE TABLE DailySalesTotals

(customer_id CHAR(10) NOT NULL,

 order_date DATE NOT NULL,

 order_amt DECIMAL(8,2) NOT NULL,

 PRIMARY KEY (customer_id, order_date));

Which we can fill with some dummy data.

INSERT INTO DailySalesTotals (customer_id, order_date, order_amt)

VALUES  ('Celko', '1999-11-28', 450.00),

 ('Curly', '1999-11-25', 400.00),

 ('Curly', '1999-11-26', 300.00),

 ('Curly', '1999-11-27', 400.00),

 ('Curly', '1999-11-28', 450.00),

 ('Larry', '1999-11-25', 400.00),

 ('Larry', '1999-11-26', 400.00),

 ('Larry', '1999-11-27', 450.00),

 ('Larry', '1999-11-28', 400.00),

 ('Moe', '1999-11-25', 400.00),

 ('Moe', '1999-11-26', 400.00),

 ('Moe', '1999-11-27', 400.00),

 ('Moe', '1999-11-28', 400.00);

The problem is to report just those  customers who decreased their purchase amounts on their most recent order placed with us.  We are trying to get an idea when people are saturated with whatever we are selling.  If their order level is holding steady we are happy with them.  I came up with this query back then:

 

SELECT H1.customer_id, ' dropped purchase amount on ',

MAX(H1.order_date)

  FROM DailySalesTotals AS H1

 WHERE H1.order_amt

       < (SELECT H2.order_amt

            FROM DailySalesTotals AS H2

           WHERE H1.customer_id = H2.customer_id

             AND H2.order_date

                 = (SELECT MAX(order_date)

                      FROM DailySalesTotals AS H3

                     WHERE H1.customer_id = H3.customer_id

                      AND H1.order_date > H3.order_date

                   )

           )

  AND H1.order_date = (SELECT MAX( order_date)

                          FROM DailySalesTotals h4

                         WHERE h4.customer_id = H1.customer_id

                        )

 GROUP BY customer_id;

The nested subquery says that the order amount has dropped and then uses another subquery within itself to ask if the date is the most recent date on file for that customer.  Nesting correlated subqueries is usually expensive, so we want to avoid that. 

What is your answer in one SQL Statement? I have included a script with some larger volumes of test data sufficient to compare the timings of both solutions. 

The best answer to each stumper will be given a prize of a $100 Amazon voucher. The stumper will be run simultaneously on SQL Server Central and Simple-Talk. To see all the comments so far, you will need to visit both sites.

 We will take entries for a week after the first Monday of publication, posted as comments to the articles.

Two weeks after the challenge is sent out, the judge's decision and comments will be sent out in the newsletter, and published on the site. Joe Celko and Phil Factor will judge the answers to this puzzle. Your answer should :
  1) Solve the problem -- Duh!
  2) Avoid proprietary features in SQL Server that will not port or be good across all releases, present and future.
  3) Use Standard features in SQL Server that will be good across all releases, present and future. Extra points for porting code.
  4) Be clever but not obscure.
  5) Explain how you got your answer.



This article has been viewed 6245 times.
Joe Celko

Author profile: Joe Celko

Joe Celko is one of the most widely read of all writers about SQL, and was the winner of the DBMS Magazine Reader's Choice Award four consecutive years. He is an independent consultant living in Austin, TX. He has taught SQL in the US, UK, the Nordic countries, South America and Africa.
He served 10 years on ANSI/ISO SQL Standards Committee and contributed to the SQL-89 and SQL-92 Standards.
He has written over 800 columns in the computer trade and academic press, mostly dealing with data and databases. He is the author of eight books on SQL for Morgan-Kaufmann, including the best selling SQL FOR SMARTIES.
Joe is a well-known figure on Newsgroups and Forums, and he is famous for his his dry wit. He is also interested in Science Fiction.

Search for other articles by Joe Celko

Rate this article:   Avg rating: from a total of 3 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.


Subject: Possible Solution
Posted by: Chris Howarth (view profile)
Posted on: Monday, September 28, 2009 at 2:15 AM
Message:

First, using a CTE and the ROW_NUMBER function, assign a row number to each row, partitioned by customer_id and ordered by order date descending.

Then use two instances of the CTE in the output query - once to represent the most recent order, and again to represent the second most recent order. The respective orders are obtained by filtering on the RowNumber column.

Then join the two instances of the CTE together on customer_id and apply a filter to return only those customers whose most recent order amount was less then the amount of their second most recent order.

Here's the code:



WITH CTE
AS
(
SELECT  ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC) AS RowNumber,
        
customer_id,
        
order_date,
        
order_amt
FROM DailySalesTotals
)
SELECT order1.customer_id
FROM CTE order1
    
INNER JOIN CTE order2
        
ON order2.customer_id = order1.customer_id
            
--The second most recent order
            
AND order2.rownumber = 2
WHERE
    
--The most recent order
    
order1.RowNumber = 1
    
--Recent order amount lower than second most recent order amount
    
AND order1.order_amt < order2.order_amt;

Subject: ...follow up to previous post...
Posted by: Chris Howarth (view profile)
Posted on: Monday, September 28, 2009 at 2:24 AM
Message:

Amending the query slightly to additionally return the order_date (as per Joe's original solution) gives:...

WITH CTE
AS
(
SELECT  ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC) AS RowNumber,
        
customer_id,
        
order_date,
        
order_amt
FROM DailySalesTotals
)
SELECT  order1.customer_id,
        
' dropped purchase amount on ',
        
order1.order_date
FROM CTE order1
    
INNER JOIN CTE order2
        
ON order2.customer_id = order1.customer_id
            
--The second most recent order
            
AND order2.rownumber = 2
WHERE
    
--The most recent order
    
order1.RowNumber = 1
    
--Recent order amount lower than second most recent order
       AND order1.order_amt < order2.order_amt;

Subject: Re: ...follow up to previous post...
Posted by: Phil Factor (view profile)
Posted on: Monday, September 28, 2009 at 3:41 AM
Message:

Ahem... As I'm helping Joe judge this, I'll start by congratulating Chris on this entry. It is very ingenious and shows the way.. Chris, you're on the right track, but beware. It is a third slower than Joe's original solution (using his test data) and it gives false positives (this has now been fixed  Ed:).


Subject: Re: ...follow up to previous post...
Posted by: Chris Howarth (view profile)
Posted on: Monday, September 28, 2009 at 4:39 AM
Message: Hi Phil

Thanks for the feedback.

Unfortunately I can't replicate the inconsistency between mine and Joe's (original) versions using Joe's test data. Both queries return 391 rows, the first four rows of each resultset are as follows:

/*
--Joe's solution:
customer_id (No column name) (No column name)
--------------------------------------------------------
Aaron dropped purchase amount on 2008-08-07
Abel dropped purchase amount on 2008-12-23
Abraham dropped purchase amount on 2008-01-03
Aimee dropped purchase amount on 2009-09-19

--My solution:
customer_id (No column name) order_date
--------------------------------------------------------
Aaron dropped purchase amount on 2008-08-07
Abel dropped purchase amount on 2008-12-23
Abraham dropped purchase amount on 2008-01-03
Aimee dropped purchase amount on 2009-09-19
*/
(Chris's code has now been fixed- looks like it got mangled during code-highlighting. Ed:)

In terms of performance, using SQL Profiler to monitor performance both queries take a mean of 100ms to complete, but the original shows ~1850 reads whereas my version shows ~24 reads.

I'm running 32-bit SQL Server 2008 SP1 CU4 (10.0.2734).

Cheers
Chris

Subject: Oops!
Posted by: Phil Factor (view profile)
Posted on: Monday, September 28, 2009 at 5:20 AM
Message: Yes, Apologies. I was working from the second version which hit a formatting problem when we prettified the code. The code gives the correct result now and is now slightly faster than Joes code.

Subject: A standard solution that runs in all versions.
Posted by: WBrewer (view profile)
Posted on: Monday, September 28, 2009 at 8:42 AM
Message: I've gone for a solution that will work in any version of SQL Server. It will also give more information, as in reality, the sales Dept will want to know by how much it dropped and when the previous order was. It runs at exactly the same speed as Chris's solution on my machine. In order to comply with the competition, I've used Joe's original result and commented out my 'improved' version.

SELECT
    
CustomersLastTwoDates.customer_ID,
  
'dropped purchase amount on',
  
lastOrderDate  
/*   CustomersLastTwoDates.customer_ID,
   'dropped purchase amount by',
    previous.order_amt-CustomersLastTwoDates.order_amt,
   'between',
   previous.order_date,
   'and',
   lastOrderDate  */
FROM
  
(SELECT
      
lastone.customer_ID,
      
lastone.order_date AS lastOrderDate,
      
lastone.order_amt,
      
DATEADD(DAY, -MIN(DATEDIFF(DAY,d.order_Date,lastone.order_date)),
            
lastone.order_date)AS penultimateOrderDate
    
FROM DailySalesTotals d INNER JOIN
      
(SELECT latest.customer_id, latest.order_date, latest.order_amt
        
FROM    DailySalesTotals latest
        
INNER JOIN
              
(SELECT  customer_ID,
                      
[order_date] = MAX(Order_Date)
              
FROM    DailySalesTotals
              
GROUP BY customer_ID) lastPurchase
      
ON latest.customer_ID= lastPurchase.customer_ID
      
AND latest.order_Date=lastPurchase.order_date) lastone
  
ON   lastone.customer_ID= d.customer_ID
  
AND lastone.order_Date> d.order_date
  
GROUP BY lastone.customer_ID, lastone.order_date,lastone.order_amt
  
HAVING MIN(lastone.order_amt-d.order_amt)<0
  
) CustomersLastTwoDates
INNER JOIN DailySalesTotals previous
ON previous.customer_ID =CustomersLastTwoDates.customer_ID
AND previous.order_Date=CustomersLastTwoDates.penultimateOrderDate
WHERE CustomersLastTwoDates.order_amt < previous. order_amt



Subject: Old vs New
Posted by: Anonymous (not signed in)
Posted on: Monday, September 28, 2009 at 10:05 AM
Message: I'm confused....

The text states that "Your task is to write new answers with the current features available in SQL Server.", but the rules require us to "Use Standard features in SQL Server that will be good across all releases, present and future"

Which should be honoured?

Subject: Old vs New
Posted by: Anonymous (not signed in)
Posted on: Monday, September 28, 2009 at 10:31 AM
Message: I'm confused....

The text states that "Your task is to write new answers with the current features available in SQL Server.", but the rules require us to "Use Standard features in SQL Server that will be good across all releases, present and future"

Which should be honoured?

Subject: Re Old vs New
Posted by: Phil Factor (view profile)
Posted on: Monday, September 28, 2009 at 11:50 AM
Message: Yes, the rules at the end were general ones for the series. what we mean is use whatever you like as long as it is not specific to SQL Server and/or a particular release. As an example, we'd probably frown on 'quirky update' as it is not SQL Standard, only the Sybase dialect. However, we'll smile on anything ingenious, simple, elegant and fast.

Subject: The 'Celko Stumper' competition continues...
Posted by: Phil Factor (view profile)
Posted on: Monday, September 28, 2009 at 3:15 PM
Message: The discussion on this competition at SQL Server Central is on
http://www.sqlservercentral.com/Forums/Topic794383-1604-3.aspx

Subject: how about denormalization
Posted by: Alex Kuznetsov (view profile)
Posted on: Monday, September 28, 2009 at 3:50 PM
Message: I am not going to compete this time, but denormalization may simplify the problem dramatically. If your table looks like this:

CREATE TABLE dbo.DailySalesTotals
(customer_id CHAR(10) NOT NULL,
order_date DATE NOT NULL,
order_amt DECIMAL(8,2) NOT NULL,
previous_order_amount DECIMAL(8,2) NULL,
CONSTRAINT PK_DailySalesTotals PRIMARY KEY (customer_id, order_date));

then your query is trivial. You can have a FK constraint ensure that previous_order_amount is either NULL or matches order_amt for some previous date.

Subject: My humble offering
Posted by: JeriHatTrick (view profile)
Posted on: Monday, September 28, 2009 at 4:33 PM
Message: -- The CTE numbers each row by order_date and partitions for each
-- customer_id. The INNER JOIN joins the CTE to itself on the
-- condition that the customer_ids are equal and that the two tuples
-- being joined to one another are for sequential row_num values.
-- The WHERE clause makes sure that we return only the most recent
-- order for each customer and that their most recent order is
-- less than their previous order. The row_num restriction in the
-- row_num = 1 predicate combined with the sequential restriction
-- enforced by the JOIN guarantees that no tuples will be returned for
-- customers with only one order.
WITH OrdersByDate AS
(
SELECT
customer_id
,order_date
,order_amt
,ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC) row_num
FROM DailySalesTotals
)
SELECT
ODB1.customer_id
,' dropped purchase amount on '
,ODB1.order_amt
FROM OrdersByDate ODB1
INNER JOIN OrdersByDate ODB2
ON ODB1.customer_id = ODB2.customer_id
AND ODB1.row_num = ODB2.row_num - 1 -- compare two most recent orders
WHERE ODB1.row_num = 1 -- most recent row for customer
AND ODB1.order_amt < ODB2.order_amt -- ensure decreasing amount

Subject: Simple Phrenetic Talk
Posted by: Anonymous (not signed in)
Posted on: Tuesday, September 29, 2009 at 9:41 AM
Message: "Index Fill (Phil) Factor
For IDIOTS"

By Joe Celco

Cost: 100$ Amazon Voucher

Subject: Re: Old vs New
Posted by: Plamen (view profile)
Posted on: Tuesday, September 29, 2009 at 12:17 PM
Message: Here is solution that is ANSI compliant and should port to other products that support the ANSI ranking functions. Regarding performance, it depends: how large is the data set, size of partitions, degree of parallelism, etc.

<pre>
SELECT customer_id, ' dropped purchase amount on ', MAX(order_date) AS order_date
FROM (
SELECT customer_id, order_date,
order_amt *
CASE ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC)
WHEN 1 THEN 1
WHEN 2 THEN -1
END AS total
FROM DailySalesTotals) AS T
GROUP BY customer_id
HAVING SUM(total) < 0;
</pre>

Subject: Timings to date...
Posted by: Phil Factor (view profile)
Posted on: Tuesday, September 29, 2009 at 4:05 PM
Message: As fast as I can test routines, more are entered.
All these are on the 96347-row full table. Paul Ireland, KevRiley, Celko and Peso (3) are all in the same league and come out in a different order every run. I've had to eliminate several candidates as they either gave the wrong customers, or didn't give the date of the last order that had dropped in value. One of the routines had to be disqualified because it took over two minutes to run!

These are the execution times, and say nothing about the quality of the solution.


46ms Paul Ireland
60ms KevRiley
63ms Celko
63ms Peso 3
173ms girish Bhat
296ms RobertFolkerts
296ms AndyM
296ms Wm Brewer
313ms Peso
326ms andriy.zabavskyy
360ms maxdemarzi
360ms Mark Marinovic
373ms John McVay
390ms Peso 2
390ms Peter Brinkhaus
406ms plamen
453ms back_of_the_fleet
470ms maxdemarzi
563ms v_paronov
623ms Steve Rowland
640ms Chris Howarth
720ms Quan_L_Nguyen
750ms Chris Howarth 2
1046ms GSquared


if ivan.yong, eemore, JeriHatTrick, egilette, PhilMond, Gianluca Sartori (too slow), and tKells can please fix their scripts, I'll put them back in the running. If I've rejected your entry unfairly and unreasonably, Oops, sorry!


Subject: how about this
Posted by: Alex Kuznetsov (view profile)
Posted on: Wednesday, September 30, 2009 at 1:07 PM
Message: I loved Plamen's solution, but wanted to tweak it a little bit:

SELECT customer_id,
DATEADD(day,
SUM(LatestDiff), '19010101')
AS LatestDate FROM(
SELECT customer_id, order_date,
CASE ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC)
WHEN 1 THEN DATEDIFF(day,'19010101',order_date) END as LatestDiff,
order_amt *
CASE ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC)
WHEN 1 THEN 1
WHEN 2 THEN -1
END AS total
FROM DailySalesTotals
) AS t
GROUP BY customer_id
HAVING SUM(total) < 0;

Of course, on RDBMS which fully implements OLAP functions, we can have a simpler solution. Will not run on 2008, but here is the idea:

WITH CTE
AS
(
SELECT ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC) AS RowNumber,
SUM(order_amt) OVER(PARTITION BY customer_id ORDER BY order_date DESC) AS RunningTotal,
SUM(DATEDIFF(day,'19010101',order_date)) OVER(PARTITION BY customer_id ORDER BY order_date DESC) AS RunningTotalDaysFrom19010101,
customer_id,
order_date,
order_amt
FROM DailySalesTotals
)
SELECT order1.customer_id,
DATEADD(day, RunningTotalDaysFrom19010101 - DATEDIFF(day,'19010101',order_date),'19010101') AS last_order_date
FROM CTE order1
WHERE
--The second recent order
order1.RowNumber = 2
--Recent order amount lower than second most recent order amount
AND order1.order_amt > RunningTotal - order1.order_amt;



Subject: Timings for all the entrants here and SSC
Posted by: Phil Factor (view profile)
Posted on: Friday, October 02, 2009 at 9:32 AM
Message: OK. This is the current state of play, timing the execution time with 10,000 rows . Of the speedy routines, we are now getting some routines that are consistently faster than Celko's. (46-50 ms). We have a bunch who are at or around 60 ms, and then a small band between 100 and 300 ms.

Joe's promised to take a look at them all at the weekend, Obviously, speed isn't everything in qualities we are looking for in this competition, so we'll both be having a careful look, rank all the entries on various qualities and then, hopefully, we can announce a winner on Monday.

46ms Peso's GianLuca-copy
46ms Gianluca Again
46ms mByther 2
50ms Gianluca Sartori
60ms KevRiley
60ms Paul Ireland
60ms Kevriley 4
60ms Celko
63ms Peso 3
63ms KevRiley 5
63ms giancula 3
106ms Eric Pratley
110ms Quan 2
173ms girish Bhat
280ms RobertFolkerts
283ms Wm Brewer
296ms Herman 2
313ms AndyM
330ms Peso
330ms ivan.yong
330ms Peso 4
340ms andriy.zabavskyy
343ms maxdemarzi
360ms Mark Marinovic
360ms mByther 1
376ms John McVay
390ms Peso 2
390ms Peter Brinkhaus
390ms plamen
423ms Alex Kuznetsov
436ms Gustavo 2
453ms back_of_the_fleet
453ms maxdemarzi
560ms v_paronov
610ms Steve Rowland
610ms eemore-571761
643ms Chris Howarth
690ms Herman van Midden
703ms jfortuny
736ms Quan_L_Nguyen
750ms Chris Howarth 2
763ms Gustavo
1030ms GSquared
10766ms Ramesh


Subject: New rules, new suggestion
Posted by: Peso (view profile)
Posted on: Saturday, October 03, 2009 at 3:21 PM
Message: SELECT Customer_ID,
' dropped purchase amount on ',
MAX(Order_Date)
FROM (
SELECT dst.Customer_ID,
dst.Order_Date,
dst.Order_Amt,
ROW_NUMBER() OVER (PARTITION BY dst.Customer_ID ORDER BY dst.Order_Date) AS recID
FROM dbo.DailySalesTotals AS dst
CROSS JOIN (
SELECT 0 AS d UNION ALL
SELECT 1
) AS x
) AS d
WHERE recID > 1
GROUP BY Customer_ID,
recID / 2
HAVING MAX(CASE WHEN recID % 2 = 1 THEN Order_Amt ELSE NULL END) < MAX(CASE WHEN recID % 2 = 0 THEN Order_Amt ELSE NULL END)


Subject: My version
Posted by: parody (view profile)
Posted on: Wednesday, October 07, 2009 at 4:09 AM
Message: CTE's are popular, here is my version:

WITH Data (Customer_ID,Order_Date,Order_Amt,RowID)
AS
(SELECT
Customer_ID
,Order_Date
,Order_Amt
,Row_Number() OVER(PARTITION BY Customer_ID ORDER BY Order_Date DESC) AS RowID
FROM DailySalesTotals)

SELECT
ltrim(rtrim(a.Customer_ID)) + ' dropped order by £' + convert(varchar(10),a.Order_Amt - b.Order_Amt) + ' on ' + convert(char(11),b.Order_Date,113)
FROM Data a
INNER JOIN Data b
ON a.Customer_ID = b.Customer_ID
WHERE a.RowID > b.RowID
AND a.RowID <= 2 AND b.RowID <= 2
AND b.Order_Amt < a.Order_Amt

And another without using CTE which I believe is better performance:

SELECT
ltrim(rtrim(Customer_ID)) + ' dropped order by £' + convert(varchar(10),SUM(Order_Amt)) + ' on ' + convert(char(11),MAX(Order_Date),113)
FROM
(SELECT
Customer_ID
,CASE
WHEN Row_Number() OVER(PARTITION BY Customer_ID ORDER BY Order_Date DESC) = 1 THEN -1*Order_Amt
WHEN Row_Number() OVER(PARTITION BY Customer_ID ORDER BY Order_Date DESC) = 2 THEN Order_Amt
ELSE NULL
END AS Order_Amt
,Order_Date
FROM DailySalesTotals) Sub
GROUP BY Customer_ID
HAVING SUM(Order_Amt) > 0

 










Phil Factor
Automated Script-generation with Powershell and SMO
 In the first of a series of articles on automating the process of building, modifying and copying SQL Server... Read more...



 View the blog
Using SQL Test Database Unit Testing with TeamCity Continuous Integration
 With database applications, the process of test and integration can be frustratingly slow because so... Read more...

SQL Source Control: The Development Story
 Often, there is a huge difference between software being easy to use, and easy to develop. When your... Read more...

How to Import Data from HTML pages
 It turns out that there are plenty of ways to get data into SQL Server from websites, whether the data... Read more...

SQL Scripts Manager: An Appreciation
 SQL Scripts Manager is Simple-Talk's present to its readers. William Brewer was an enthusiastic... Read more...

Hosted Team Foundation Server 2010 Review
 Team Foundation Server (TFS) has expanded its remit to support the whole software development process,... Read more...

Beginning SQL Server 2005 Reporting Services Part 1
 Steve Joubert begins an in-depth tour of SQL Server 2005 Reporting Services with a step-by-step guide... Read more...

Ten Common Database Design Mistakes
 Database design and implementation is the cornerstone of any data centric project (read 99.9% of... Read more...

Reading and Writing Files in SQL Server using T-SQL
 SQL Server provides several "standard" techniques by which to read and write to files but, just... Read more...

Beginning SQL Server 2005 Reporting Services Part 2
 Continuing his in-depth tour of SQL Server 2005 Reporting Services, Steve Joubert demonstrates the most... Read more...

Creating CSV Files Using BCP and Stored Procedures
 Nigel Rivett demonstrates some core techniques for extracting SQL Server data into CSV files, focussing... Read more...

Over 400,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.

Join Simple Talk