SQL Server 2008 FILESTREAM and ADO.NET

August 6, 2010 11:45 by wjchristenson2

In my last post, I gave a tutorial of SQL Server 2008’s new FILESTREAM feature.  I described what it is, why it should be used for BLOB data storage, and released some TSQL scripts to enable FILESTREAM and create a table to hold some media.  In this post, I will show how to insert and read FILESTREAM data from a SQL 2008 Server using ADO.NET.

ADO.NET comes with the SqlClient data provider (System.Data.SqlClient).  It supports FILESTREAM operations by using the SqlFileStream class which resides in the System.Data.SqlTypes namespace.  As its name suggests, the SqlFileStream is a stream and thus inherits from System.IO.Stream.  The SqlFileStream will provide methods to allow us to read and write streams of data to the SQL Server.  I will show 2 examples: inserting and reading FILESTREAM data with ADO.NET (the SqlFileStream object).  The program I created allows the user to save and view .jpg images from the SQL Server using FILESTREAM.  Here is a picture of the final example program.


Overview:
The SqlFileStream requires 2 arguments which are supplied from the SQL Server: the read/write path to the server’s file and the transactional context/token.  These 2 items are acquired via a preliminary SQL query before the the SqlFileStream is employed to read or write to the NTFS file system.

Inserting FILESTREAM Data
I first INSERT a record with a NULL value for the varbinary(max) field.  I use this blank record to acquire the server file path and also return the current FILESTREAM transactional context/token.  After this is complete, I can employ the SqlFileStream to write to the server file from my local file.  Below is my stored proc I used along with the .NET code.

 

   1:  IF EXISTS (SELECT * FROM sysobjects WHERE type = 'P' AND name = 'usp_InsertDocument')
   2:      BEGIN
   3:          DROP PROCEDURE dbo.usp_InsertDocument
   4:      END
   5:  GO
   6:   
   7:  CREATE PROCEDURE dbo.usp_InsertDocument 
   8:      WITH ENCRYPTION
   9:  AS
  10:      SET NOCOUNT ON;
  11:   
  12:      DECLARE @DocumentID INT;
  13:   
  14:      --insert NULL first to acquire ID & the transaction context values
  15:      INSERT INTO dbo.Document (Document) VALUES (0x00);
  16:   
  17:      SELECT @DocumentID = SCOPE_IDENTITY();
  18:   
  19:      SELECT 
  20:          doc.DocumentID,
  21:          doc.DocumentGUID,
  22:          doc.Document.PathName(0) AS [PathName], 
  23:          GET_FILESTREAM_TRANSACTION_CONTEXT() AS [TransactionContext] 
  24:      FROM dbo.Document AS doc WITH (NOLOCK) 
  25:      WHERE doc.DocumentID = @DocumentID;
  26:   
  27:      SET NOCOUNT OFF;
  28:  GO

 

   1:      Private Sub InsertDocument(ByVal clientFilePath As String)
   2:          Dim dt As DataTable = New DataTable()
   3:   
   4:          Using conn As SqlConnection = New SqlConnection(_connectionString)
   5:              Dim reader As SqlDataReader = Nothing
   6:              Dim context() As Byte = Nothing
   7:              Dim serverFilePath As String = ""
   8:   
   9:              conn.Open()
  10:   
  11:              Dim transaction As SqlTransaction = conn.BeginTransaction()
  12:   
  13:              Try
  14:                  Dim cmd As SqlCommand = conn.CreateCommand()
  15:                  With cmd
  16:                      .Transaction = transaction
  17:                      .CommandType = CommandType.StoredProcedure
  18:                      .CommandText = "dbo.usp_InsertDocument"
  19:                  End With
  20:   
  21:                  reader = cmd.ExecuteReader()
  22:                  dt.Load(reader)
  23:   
  24:                  If dt.Rows.Count > 0 Then
  25:                      serverFilePath = dt.Rows(0)("PathName").ToString()
  26:                      context = DirectCast(dt.Rows(0)("TransactionContext"), Byte())
  27:   
  28:                      Using serverStream As SqlFileStream = New SqlFileStream(serverFilePath, context, FileAccess.Write)
  29:                          Using clientStream As FileStream = New FileStream(clientFilePath, FileMode.Open, FileAccess.Read)
  30:                              Dim bytesRead As Integer = 0, bufferSize As Integer = 4096
  31:                              Dim buffer As Byte() = New Byte(bufferSize - 1) {}
  32:   
  33:                              Do
  34:                                  bytesRead = clientStream.Read(buffer, 0, bufferSize)
  35:                                  If bytesRead = 0 Then
  36:                                      Exit Do
  37:                                  End If
  38:   
  39:                                  serverStream.Write(buffer, 0, bytesRead)
  40:                                  serverStream.Flush()
  41:                              Loop While True
  42:   
  43:                              clientStream.Close()
  44:                          End Using
  45:   
  46:                          serverStream.Close()
  47:                      End Using
  48:                  End If
  49:   
  50:                  transaction.Commit()
  51:              Catch ex As Exception
  52:                  Try
  53:                      'attempt to rollback the transaction
  54:                      transaction.Rollback()
  55:                  Catch ex2 As Exception
  56:                      'rollback was unsuccessful (connection may be closed)
  57:                  End Try
  58:   
  59:                  MessageBox.Show(ex.Message)
  60:              Finally
  61:                  If reader IsNot Nothing AndAlso Not reader.IsClosed Then reader.Close()
  62:              End Try
  63:          End Using
  64:   
  65:          LoadDocuments()
  66:      End Sub

 

Reading FILESTREAM Data
Similar to the insertion of a file, reading a file requires getting the server file path and then current FILESTREAM transactional context/token.  After those items are gathered from the SQL Server, we can connect to and stream the data anyway we like.  Below I streamed .jpg data from the SQL Server and loaded them into a WPF Image control for viewing purposes.

 

   1:  IF EXISTS (SELECT * FROM sysobjects WHERE type = 'P' AND name = 'usp_SelectDocument')
   2:      BEGIN
   3:          DROP PROCEDURE dbo.usp_SelectDocument
   4:      END
   5:  GO
   6:   
   7:  CREATE PROCEDURE dbo.usp_SelectDocument (
   8:      @DocumentID INT
   9:  )
  10:      WITH ENCRYPTION
  11:  AS
  12:      SET NOCOUNT ON;
  13:   
  14:      SELECT 
  15:          doc.DocumentID,
  16:          doc.DocumentGUID,
  17:          doc.Document.PathName(0) AS [PathName], 
  18:          GET_FILESTREAM_TRANSACTION_CONTEXT() AS [TransactionContext] 
  19:      FROM dbo.Document AS doc WITH (NOLOCK)
  20:      WHERE doc.DocumentID = @DocumentID;
  21:   
  22:      SET NOCOUNT OFF;
  23:  GO

 

   1:      Private Sub LoadDocument(ByVal documentID As Integer)
   2:          Dim dt As DataTable = New DataTable()
   3:   
   4:          Using conn As SqlConnection = New SqlConnection(_connectionString)
   5:              Dim reader As SqlDataReader = Nothing
   6:              Dim context() As Byte = Nothing
   7:              Dim serverFilePath As String = ""
   8:   
   9:              conn.Open()
  10:   
  11:              Dim transaction As SqlTransaction = conn.BeginTransaction()
  12:   
  13:              Try
  14:                  Dim cmd As SqlCommand = conn.CreateCommand()
  15:                  With cmd
  16:                      .Transaction = transaction
  17:                      .CommandType = CommandType.StoredProcedure
  18:                      .CommandText = "dbo.usp_SelectDocument"
  19:                      .Parameters.Add(New SqlParameter("@DocumentID", documentID))
  20:                  End With
  21:   
  22:                  reader = cmd.ExecuteReader()
  23:                  dt.Load(reader)
  24:   
  25:                  If dt.Rows.Count > 0 Then
  26:                      serverFilePath = dt.Rows(0)("PathName").ToString()
  27:                      context = DirectCast(dt.Rows(0)("TransactionContext"), Byte())
  28:   
  29:                      Using memStream As MemoryStream = New MemoryStream()
  30:                          Using serverStream As SqlFileStream = New SqlFileStream(serverFilePath, context, FileAccess.Read)
  31:   
  32:                              Dim bytesRead As Integer = 0, bufferSize As Integer = 4096
  33:                              Dim buffer As Byte() = New Byte(bufferSize - 1) {}
  34:   
  35:                              Do
  36:                                  bytesRead = serverStream.Read(buffer, 0, bufferSize)
  37:                                  If bytesRead = 0 Then
  38:                                      Exit Do
  39:                                  End If
  40:   
  41:                                  memStream.Write(buffer, 0, bytesRead)
  42:                                  memStream.Flush()
  43:                              Loop While True
  44:   
  45:                              serverStream.Close()
  46:                          End Using
  47:   
  48:                          memStream.Seek(0, SeekOrigin.Begin)
  49:   
  50:                          Dim bmpImage As BitmapImage = New BitmapImage()
  51:                          With bmpImage
  52:                              .BeginInit()
  53:                              .StreamSource = memStream
  54:                              .CreateOptions = BitmapCreateOptions.None
  55:                              .CacheOption = BitmapCacheOption.OnLoad
  56:                              .EndInit()
  57:                              .Freeze()
  58:                          End With
  59:   
  60:                          Me.imgView.Source = bmpImage
  61:   
  62:                          memStream.Close()
  63:                      End Using
  64:                  End If
  65:   
  66:                  transaction.Commit()
  67:              Catch ex As Exception
  68:              Try
  69:                  'attempt to rollback the transaction
  70:                  transaction.Rollback()
  71:              Catch ex2 As Exception
  72:                  'rollback was unsuccessful (connection may be closed)
  73:              End Try
  74:   
  75:              MessageBox.Show(ex.Message)
  76:          Finally
  77:              If reader IsNot Nothing AndAlso Not reader.IsClosed Then reader.Close()
  78:          End Try
  79:          End Using
  80:      End Sub

So there you have it.  The basics of writing and reading to a FILESTREAM on a SQL 2008 Server using ADO.NET.  Below you’ll find my scripts and .NET source code.

SqlFileStreamExample_Soln.zip (559.27 kb)


SQL 2008 FILESTREAM

July 24, 2010 10:25 by wjchristenson2

The FILESTREAM feature of SQL 2008 allows more effective storage and retrieval to BLOB data.  It accomplishes this by utilizing both SQL 2008 and the NTFS file system.  In the past, most developers stored BLOB data in the SQL Server using varbinary(max).  This has some drawbacks including:  you had a 2GB max, streaming performance was awful, and recovery from fragmentation was poor.  The FILESTREAM can hold up to the size of the NTFS volume size, streaming performance is excellent, and it can recover from fragmentation on disk nicely.

The one drawback that I can see is when the client needs to perform small & frequent BLOB updates.  Rather than writing to SQL Server data files, FILESTREAM writes to the file system.  Locating, creating, updating, deleting files outside of the SQL Server data files do come at a cost.  So some consideration as to the size & frequency of BLOB data would need to be made.

There are a few ways to enable FILESTREAM (SQL Server Configuration Manager, SQL Server Management Studio properties of the connected instance, and via TSQL scripting).  Personally I was only able to get it to work by using the SQL Server Configuration Manager.  Apparently installing Visual Studio/SQL 2005 can screw up FILESTREAM features of SQL 2008…  Anyway, here’s the basic FILESTREAM access levels which you’ll need to understand when enabling it.

FILESTREAM access levels:
a) 0 = Disables FILESTREAM support for this instance.
b) 1 = Enables FILESTREAM for Transact-SQL access.
c) 2 = Enables FILESTREAM for Transact-SQL and Win32 streaming access.

I created a script which enables FILESTREAM for the SQL Instance, then it enables it for my “testdb” database.  It auto-detects where your database’s data/log files reside and then creates a directory in there for FILESTREAM objects.

 

   1:  USE [master];
   2:  GO
   3:  DECLARE @filestream_access_level SMALLINT;
   4:  SELECT @filestream_access_level = CONVERT(SMALLINT, SERVERPROPERTY('FilestreamEffectiveLevel'));
   5:   
   6:  IF @filestream_access_level <> 2
   7:  BEGIN
   8:      EXEC sp_configure filestream_access_level, 2;
   9:      RECONFIGURE WITH OVERRIDE;
  10:  END;
  11:  GO
  12:   
  13:  USE [testdb];
  14:  GO
  15:  IF NOT EXISTS (SELECT * FROM sys.filegroups AS fg WITH (NOLOCK) WHERE name = 'testdb_FILESTREAM')
  16:  BEGIN
  17:      --add a file group and indicate that it will contain FILESTREAM objects
  18:      ALTER DATABASE testdb ADD FILEGROUP testdb_FILESTREAM CONTAINS FILESTREAM;
  19:  END;
  20:  GO
  21:  IF NOT EXISTS (SELECT * 
  22:      FROM sys.filegroups AS fg WITH (NOLOCK) 
  23:          INNER JOIN sys.database_files AS df WITH (NOLOCK) ON fg.data_space_id = df.data_space_id 
  24:      WHERE fg.name = 'testdb_FILESTREAM'
  25:          AND df.name = 'testdb_FILESTREAM')
  26:  BEGIN
  27:      DECLARE @path NVARCHAR(MAX), @sql_statement NVARCHAR(MAX);
  28:      SELECT @path = mf.physical_name FROM sys.master_files AS mf WITH (NOLOCK) WHERE mf.name = 'testdb' AND mf.type_desc = 'ROWS';
  29:      SELECT @path = SUBSTRING(@path, 1, LEN(@path) - CHARINDEX('\', REVERSE(@path))) + '\testdb_FILESTREAM';
  30:   
  31:      --tell the file group where the FILESTREAM objects should be stored
  32:      SELECT @sql_statement = N'ALTER DATABASE testdb ADD FILE (NAME = ''testdb_FILESTREAM'', FILENAME = ''' + @path + ''') TO FILEGROUP testdb_FILESTREAM;';
  33:   
  34:      EXEC sp_executesql @statement = @sql_statement;
  35:  END;
  36:  GO

 

At this point I was ready to design a table in SQL Management Studio with a FILESTREAM column.  A FILESTREAM column is actually a varbinary(max) column with the FILESTREAM attribute.  Well, the table designer in SQL Management Studio does not support specifying the FILESTREAM attribute.  You have to script it.

Here’s some rules which you’ll need to adhere to when creating FILESTREAM columns.
1) The table can have multiple FILESTREAM columns, but they must all be in the same FILEGROUP.
2) If you don’t specify the FILESTREAM_ON clause, the default FILEGROUP will be used.
3) The table must have a uniqueidentifier column with the ROWGUIDCOL attribute.  It cannot contain NULL values and must have either a UNIQUE or PRIMARY KEY single column constraint.

 

   1:  USE [testdb];
   2:  GO
   3:  IF NOT EXISTS (SELECT * FROM sys.tables AS t WITH (NOLOCK) WHERE t.name = 'Document' AND t.type = 'U')
   4:  BEGIN
   5:      CREATE TABLE dbo.Document (
   6:          DocumentID INT IDENTITY PRIMARY KEY,
   7:          DocumentGUID UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL UNIQUE DEFAULT NEWID(),
   8:          Document VARBINARY(MAX) FILESTREAM NULL
   9:      )  ON [PRIMARY];
  10:  END;
  11:  GO

In my next post, I will show .NET streaming capabilities with the new FILESTREAM features in SQL 2008.


Introduction to SQL Server Change Tracking

October 3, 2009 12:39 by wjchristenson2

An all too familiar challenge of today’s applications is to support the mobile user.  A mobile user is not always guaranteed to have a connection to the home office.  They need an application that can support both offline and online connection states.  An application that is occasionally connected is often referred to as an “Occasionally Connected Application” (OCA).  OCAs will often work offline and when brought online will execute synchronization processes.  In this blog, I’ll briefly introduce some nice features included with SQL Server 2008 Change Tracking.

SQL Server has had replication features for quite some time now.  I’m by no means an expert when it comes to SQL Server Transactional replication, but personally I’ve found it difficult to setup, troubleshoot, customize, and unable to easily communicate across N-Tier application environments.  In the past, the next option from replication was to create our own SQL synchronization engines utilizing triggers, timestamp columns, tombstone tables, etc to track database changes on the client and server and then synchronize up the differences.  There are some problems with this solution.  First glaring issue to me is the use of triggers.  Triggers cause transactions to take longer to commit and cause blocking issues.  So basically performance and storage issues result from this solution prior to SQL 2008 Change Tracking.  SQL 2008 now provides a new feature which is available in all versions: SQL Change Tracking. 

SQL Server Change Tracking Advantages/Features:

1)  Easy to Setup.  It does not require timestamp columns, tombstone tables, triggers, etc.  You can simply script or use SQL Management Studio to turn on SQL Change Tracking for a database and then what tables you want to track changes on.

2)  Better Performance.  Changes are tracked at commit time rather than when DML operations occur.  What this basically means is transactions run quicker and this also helps with blocking issues.

3)  Minimal Disk Space Costs.  Change Tracking stores changes in SQL system tables and the disk space cost is minimal.

4)  Integrates with the .NET Sync Framework.  If you are using the .NET Sync Framework to develop your OCA, then .NET/Visual Studio has some nice features that are at the developer’s disposal.

5)  Synchronize with other DB Platforms.  SQL Change Tracking runs independently from other databases.  So if your server is running an Oracle DB and you want to run SQL Compact on your client with Change Tracking enabled, it will support that just fine.  Changes on the client are tracked independently of the server and obviously vice versa.

6)  Packaged Functions.  SQL Server comes with packaged functions that are used to query the SQL Change Tracking system tables to acquire incremental changes.

7)  Auto Clean Up.  When SQL Change Tracking is enabled for a database, you can specify when change history will be purged automatically for you.

8)  Column or Entire Row.  You can enable change tracking to record that an entire row/record had something changed in it or you can even track down to what column was changed to limit the amount of data changes returned when querying for incremental changes.

Example 1 - Turn on SQL Change Tracking for a Database:


   1:  ALTER DATABASE [AdventureWorks2008] SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 365 DAYS, AUTO_CLEANUP = ON);
   2:  GO

Example 2 – Turn on SQL Change Tracking for a Table:


   1:  ALTER TABLE HumanResources.[Department] ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF);
   2:  GO

Example 3 – Return Incremental Table Inserts:


   1:  DECLARE @sync_last_received_anchor BIGINT, @sync_new_received_anchor BIGINT;
   2:   
   3:  SELECT @sync_last_received_anchor = CHANGE_TRACKING_CURRENT_VERSION();
   4:   
   5:  INSERT INTO [HumanResources].[Department] (Name, GroupName, ModifiedDate) VALUES ('Test1', 'My Group', GETDATE())
   6:  INSERT INTO [HumanResources].[Department] (Name, GroupName, ModifiedDate) VALUES ('Test2', 'My Group', GETDATE())
   7:  INSERT INTO [HumanResources].[Department] (Name, GroupName, ModifiedDate) VALUES ('Test3', 'My Group', GETDATE())
   8:   
   9:  SELECT @sync_new_received_anchor = CHANGE_TRACKING_CURRENT_VERSION();
  10:   
  11:  --return inserts
  12:  SELECT dept.*
  13:  FROM [HumanResources].[Department] AS dept 
  14:      INNER JOIN CHANGETABLE(CHANGES [HumanResources].[Department], @sync_last_received_anchor) CT ON CT.[DepartmentID] = dept.[DepartmentID]
  15:  WHERE (CT.SYS_CHANGE_OPERATION = 'I' AND CT.SYS_CHANGE_CREATION_VERSION <= @sync_new_received_anchor)

ExecuteNonQuery with “GO” Separators

September 25, 2009 08:26 by wjchristenson2

If you’ve ever tried to run a collection of SQL batches via the .NET DbCommand.ExecuteNonQuery method, you probably received an error similar to the following: “Incorrect Syntax near ‘GO’ …”.  DbCommand.ExecuteNonQuery is intended to execute a single SQL batch statement against a database.  The “GO” separator is a command that is used/understood by SQL Server Query Analyzer and Query Editor utilities to separate out SQL batches when ran.

There has been some workarounds that I’ve seen that involve regular expressions and string manipulation/splitting to break out the script with “GO” separators into a collection of SQL statements and run them each individually.  This can work, but you will run into situations where commented areas will contain the “GO” statement and other problems can/will arise.  Such situations would cause the workaround to fail.

The proper way to run scripts with the “GO” separator is to employ SQL Server Management Objects (SMOs).  Microsoft’s SQL Server 2008 Feature Pack contains links to download/install the SMO assemblies (.NET Framework object model) .  Developers can use SMOs to perform SQL Server management routines through their .NET application code.

Steps to Use SMOs:

1) Download and Install SMOs - Download the SQL Server Management Objects found on this page.  Run the installation package.  If you are running SQL Server 2005, you may need to search for the 2005 version as the link I gave is for 2008.

2) Add references - After the SMOs are installed,  we’ll need to add references to them in our project.  The SMO assemblies should appear under your .NET tab when adding references to your project.  If you can’t  find them there, the assemblies were installed to “C:\Program Files\Microsoft SQL Server\100\SDK\Assemblies” for me.  To run scripts with “GO” statement, we’ll need references to the following DLLs:
  a) Microsoft.SqlServer.ConnectionInfo.dll
  b) Microsoft.SqlServer.Management.Sdk.Sfc.dll
  c)  Microsoft.SqlServer.Smo.dll

3) Import the required namespaces and execute your SQL script with “GO” statements like below:


   1:  Imports System.Data.SqlClient
   2:  Imports Microsoft.SqlServer.Management.Smo
   3:  Imports Microsoft.SqlServer.Management.Common
   4:   
   5:  Class Window1
   6:   
   7:      Public Sub ExecuteSqlStatements(ByVal connectionString As String, ByVal sqlStatements As String)
   8:          If Not String.IsNullOrEmpty(sqlStatements) Then
   9:              Using mySqlConnection As SqlConnection = New SqlConnection(connectionString)
  10:                  Dim mySqlServer As Server = New Server(New ServerConnection(mySqlConnection))
  11:                  mySqlServer.ConnectionContext.ExecuteNonQuery(sqlStatements)
  12:              End Using
  13:          End If
  14:      End Sub
  15:   
  16:  End Class

SqlBatchExecNonQuery_Soln.zip (965.09 kb)


Common Table Expressions – What are They?

May 26, 2009 13:48 by wjchristenson2

Microsoft SQL 2005 introduced a new construct called CTEs (common table expressions).  CTEs provide a way to break down complex queries into simpler chunks and offer more readable queries.  UDFs, derived tables, and temp tables are all constructs that can be employed to help break up complex queries, however they often muddy up your SQL script.  They also exist beyond the context of the one SQL statement you may have desired to use it for.  CTEs also provide the ability for recursion without the need for recursive stored procedures.

Therefore, at a high level CTEs are used for:

1) Improving readability of your SQL scripts.

2)  Simplifying complex queries.

3)  Recursion.

A Simple CTE Example:


   1:  WITH OrderTotals (OrderID, OrderTotal) AS (
   2:      SELECT OrderDetails.OrderID, SUM(((OrderDetails.UnitPrice * OrderDetails.Quantity) - OrderDetails.Discount)) AS OrderTotal 
   3:      FROM dbo.[Order Details] AS OrderDetails WITH (NOLOCK)
   4:      GROUP BY OrderDetails.OrderID
   5:  )
   6:  SELECT * FROM dbo.Orders WITH (NOLOCK)
   7:      INNER JOIN OrderTotals ON OrderTotals.OrderID = Orders.OrderID
   8:  WHERE OrderTotals.OrderTotal > 100

In this first simple example, I used the Northwind database to find all orders which exceeded $100.  The first step is to use the keyword WITH and then name my CTE, define my columns, then enclose what SQL makes up the CTE within parenthesis.  I then proceed to use the CTE in the final query.  Notice how the final query is much simpler to read without the GROUP BY, SUM, etc logic in the CTE.

Two CTEs Example:


   1:  WITH OrderTotals (OrderID, OrderTotal) AS (
   2:      SELECT OrderDetails.OrderID, SUM(((OrderDetails.UnitPrice * OrderDetails.Quantity) - OrderDetails.Discount)) AS OrderTotal 
   3:      FROM dbo.[Order Details] AS OrderDetails WITH (NOLOCK)
   4:      GROUP BY OrderDetails.OrderID
   5:  ), OrdersMoreThan100 (OrderID, OrderTotal) AS (
   6:      SELECT OrderTotals.* FROM OrderTotals WHERE OrderTotals.OrderTotal > 100
   7:  )
   8:  SELECT * FROM dbo.Orders WITH (NOLOCK)
   9:      INNER JOIN OrdersMoreThan100 ON OrdersMoreThan100.OrderID = Orders.OrderID;

In this example, we get a little more complex with our CTEs.  Here we can see that you can comma delimit as many CTEs as you want.  I also reference the first CTE within my second CTE and simplify our final SQL statement even further.

CTE Gotchas/Notes:

1) Always terminate SQL with a “;” before you declare a CTE.

2) You can use CTEs with SELECT, UPDATE, INSERT, DELETE queries.

3) CTEs can only be used by the final SQL statement (the statement following the CTE definitions).  Subsequent SQL queries will not be able to use your CTE definitions.