Welcome to MSDN Blogs Sign in | Join | Help

UPDATE with OUTPUT clause – Triggers – and SQLMoreResults

NOTE:  the code in this BLOG is TSQL instead of ODBC calls.  Since ODBC can be hard to understand and other API’s will have the same basic issues, I decided to use the simpler and more concise TSQL, which should also appeal to a wider audience.

 

An ISV I work with recently ran into an interesting problem; here is the description and solution.

 

PROBLEM:

Adding an unexpected trigger caused application code to fail due to incomplete SQL Syntax, and not reading through all returned results.

 

The ISV wanted to utilize the OUTPUT Clause of the UPDATE statement in their ODBC (SNAC) based application. The OUTPUT clause is very useful in providing data back to the application regarding the row, or rows, which were updated (or: inserted / deleted).  In the example I use below, the application is interested in knowing the date/time of the updated row(s).

 

This could be accomplished by issuing the following statement:

UPDATE T SET COL2 = @Pcol2, COL3 = getdate() OUTPUT CAST(INSERTED.COL3 AS varchar(30))WHERE COL1 = @Pcol1

 

The ISV coded up the application expecting a return value for number of rows affected, and if that value was greater than 0 then it also returned the value of the inserted date/time.

 

This worked well, until an external Partner application added a trigger to the table listed in the UPDATE statement.

 

Example: CREATE TRIGGER [dbo].[TTrigger1] on [dbo].[T] after update as update t2 set col3 = 0

 

Now the application failed on the UPDATE statement with the following error message:

[Microsoft][SQL Native Client][SQL Server]The target table 'T' of the DML statement cannot have any enabled triggers if the statement contains an OUTPUT clause without INTO clause.

 

The error message is self-explanatory, but was a surprise to the ISV application (and the application developer).  The developer did not expect a trigger to ever be created on the table.

There are two different methods of getting OUTPUT data from an UPDATE statement;

·       UPDATE with the OUTPUT clause only – this returns output results directly as part of the statement. This option cannot have a trigger defined on the table.

·       UPDATE with OUTPUT and INTO clauses – this returns the output a specific table, or table variable. This option must be used if there is any possibility the table will have a trigger on it at any point.

·       See the following website for complete the OUTPUT Clause documentation:

http://msdn.microsoft.com/en-us/library/ms177564.aspx

The developer then utilized the following syntax to send the same statement to SQL Server, and also to get the expected result back: declare @p165 table (col2 varchar(30));UPDATE T SET COL2 = ?, COL3 = getdate() OUTPUT CAST(INSERTED.COL3 AS varchar(30)) into @p165 WHERE COL1 = 1;select * from @p165

 

Now a subtlety occurred, can you guess what it was? If you guessed that additional results are returned you are correct.

The ODBC code returned data in a loop utilizing the following API calls:  SQLFetch, SQLNumResultCols, SQLRowCount, SQLMoreResults:

·       The first results returned were the number of rows affected by the trigger, not the number of rows affected by the UPDATE statement, which was what the application was actually expecting

·       The second set of results were the number of rows affected by the UPDATE statement

·       The third set of results were the number of rows returned by the SELECT statement reading the table variable

·       And finally, the actual data from the updated row(s) – which is what we really wanted in the first place!

So, the lessons to be learned here are:

1.   Be aware that triggers will affect your UPDATE statements if utilizing the OUTPUT clause

2.    You should utilize the INTO clause to avoid the issue

3.    Always use SQLMoreResults to read all of the result-sets that could be returned from SELECT, UPDATE, INSERT, or DELETE statements.

4.    Triggers should include the ‘SET NOCOUNT ON’ statement to avoid returning the ‘affected number of rows’.

SOLUTION:

The application was changed to utilize the INTO clause, and SQLMoreResults was used to return all the resulting data.  Using SET NOCOUNT ON in trigger logic is also a best practice that prevents additional results ‘Rows affected’ from being generated.

 

Here is a script to duplicate the issues I’ve described:

USE tempdb

GO

------You may want to run this script in steps from comment – to comment

------so you can follow along, instead of running the entire script at once

 

CREATE TABLE t(

      [col1] [int] NOT NULL,

      [col2] [varchar](30) NULL,

      [col3] [datetime] NULL

) ON [PRIMARY]

GO

insert into t values (1,'abc', getdate())

insert into t values (1,'abc', getdate())

insert into t values (1,'abc', getdate())

GO

select * from t

GO

UPDATE t SET col2 = 'Peter', col3 = getdate()

OUTPUT CAST(INSERTED.col3 AS varchar(30))WHERE col1 = 1

GO

select * from t

GO

------So far everything is good, Now let’s add the new table and the trigger

CREATE TABLE t2(

      [col1] [int] NULL,

      [col2] [datetime] NULL

) ON [PRIMARY]

GO

insert into t2 values (2, getdate())

insert into t2 values (2, getdate())

GO

select * from t2

GO

------In this example, the trigger: ttr1 will update the rows

------of a second table: t2

CREATE TRIGGER ttr1 on t after update as update t2 set col1 = 0

GO

------OK, let’s try now with the trigger on

UPDATE t SET col2 = 'Peter', col3 = getdate() OUTPUT CAST(INSERTED.col3 AS varchar(30))WHERE col1 = 1

GO

------Chances are good you got the following error message

--Msg 334, Level 16, State 1, Line 1

--The target table 't' of the DML statement cannot have any enabled triggers --if the statement contains an OUTPUT clause without INTO clause.

----- let’s fix that now.

declare @p1 varchar(30)

UPDATE t SET col2 = 'Peter', col3 = getdate() OUTPUT CAST(INSERTED.col3 AS varchar(30))into @p1 WHERE col1 = 1

GO

------Notice this failed as well with the following error message

--Msg 1087, Level 16, State 1, Line 2

--Must declare the table variable "@p1".

------We need to use a table

------for this to work correctly we must use a table or

------a table variable where the ‘INTO’ data will reside,

------and be retrieved from

declare @p1 table (col2 varchar(30))

UPDATE t SET col2 = 'Peter', col3 = getdate() OUTPUT CAST(INSERTED.col3 AS varchar(30))into @p1 WHERE col1 = 1

select * from @p1

--Now you get what we were originally looking for

--    the date/times of the rows that were updated

--Look at the results under the 'Messages' tab as well...

--you will see the number of rows affected:

--    2 for the rows inserted as part of the trigger

--    3 for the rows Updated

--    and 3 for the rows we selected from the table variable

--Now, you can see that the application must utilize SQLMoreResults if it

--wants to return all the valid results.

 

Cross Posted from http://blogs.microsoft.com/mssqlisv
Posted by mssqlisv | 1 Comments

Use SQL Server replay tools to reproduce and resolve customer issues

For many ISVs run that into issues at customer sites, it is sometimes difficult to isolate underlying problems, especially on a 24x7 production environment, where limitations apply to real time troubleshooting and live debugging. In situations like this, constructing a repro scenario in a separate environment would be ideal to minimize impact to live production system, and to speed up resolution process.

 

SQL Server Profiler

Allow me introduce SQL Profiler, which offers replay trace function.  Well, it’s not something new. First shipped in SQL Server 7.0, the feature has gone through many improvements in later releases. You can use the tool to take a captured trace as input and replay it against test database(s). It helps identify issues that could be reproduced by replaying the events in the trace. Profiler itself uses ODBC only. In SQL Server 2005 and 2008, the replay function can be configured to use multiple threads (up to 64) to replay workloads.

Advantages:

1.       SQL Server profiler is a built-in tool with full support of Microsoft product team. It works out of box.

2.       Easy to set up and run. Capture a trace using predefined replay template with all required events, and replay it against original database(s) (target machine needs to meet certain requirements http://msdn2.microsoft.com/en-us/library/ms175525.aspx)

3.       In addition to multi-threaded replay, it also provides option of step through to replay events in the order they were traced.

Disadvantages:

1.       Certain events can’t be replayed including replication, events involving GUID, session binding events, operations on Blobs using bcp, full-text, READTEXT, WRITETEXT, and etc. See BOL for more details (http://msdn2.microsoft.com/en-us/library/ms188238.aspx)

2.       The tool does not support multiple machine replay (running multiple instances of Profiler from multiple machines to replay the trace).

3.       Profiler GUI tool is client side tracing and might be intrusive and generate significant performance overhead when capturing events for replay. Be careful of what events to capture and consider using server side tracing (sp_trace_create).

 

RML Utilities

Starting in SQL Server 2000, SQL Server Customer Support Services team (CSS) started a project of similar tool, called Ostress, with higher flexibility and scalability to help troubleshoot some of the challenging SQL problems. The latest version is packaged in “Replay Markup Language(RML) Utilities” supporting both SQL 2005 and SQL 2000 (http://support.microsoft.com/kb/944837). The tool can replay multi-threaded events as profiler does but with multiple machine replay support. It can simulate up to 1000 concurrent threads. The tool has a component called Ostress (just like old version), which takes a TSQL batch file and “stress” test it by opening arbitrary number of connections and iterate the TSQL batch in each connection configurable number of loops. This is useful when workload can be characterized as same or similar batch from various number of users (connections).

 

Advantages:

1.       The tool offers both replay and stress test options.

2.       It supports multiple machine replay (multiple instances of OStress) with up to 1000 concurrent threads.

3.       OStress supports 7.0, 2000, and 2005 trace formats.

Disadvantages:

1.       The tool is provided as is, no technical support from Microsoft. But you can submit questions via contact in readme of the tool.

2.       Requires extra steps to process trace file and convert to RML format before being replayed.

3.       Does not support MARS replay.

 

Recommendation

When to use SQL profiler and when to use RML Utilities? If you have a workload that can be replayed/reproduced with no or low concurrency requirement (<64 concurrent threads), use profiler that offers flexibility of step through or multi-threaded replay options. If you need to replay a workload with high concurrency requirement (> 64 threads) or an isolated batch that can be “stress” tested for simulation, use RML Utilities. Keep in mind, for concurrency replay, full sync of ordered events is very hard to replay and no tools exist today to exactly duplicate the original trace. So the issues that happened on traced source server might not be reproduced consistently afterwards even on same environment.

 

Both tools above are for database replay. For a simulation test of multi-tier application environment, consider load-test tool of Visual Studio (Team edition) or 3rd-party vendor products.

Cross Posted from http://blogs.microsoft.com/mssqlisv
Posted by mssqlisv | 1 Comments

SQL Server Intermittent Connectivity Issue

Recently many customers of an ISV I work with, reported intermittent connectivity issues when running the ISV application on SQL Server. Some customers reported the issue to be SQL Server 2005 specific. Others stated that they are experiencing the same issue on both SQL Server 2000 and 2005. Due to the intermittent nature, and the variation of the issue, it took us quite a while  to collect all the data, (odbc trace, netmon trace, sql trace…), analyse it, and understand the exact cause.

SynAttackProtect

The first issue we found was a subtle Winsock behavior change in Window 2003 SP1. Windows 2003 SP1 introduces a configurable registry setting: SynAttackProtect, that protects the server from network Denial-Of-Service attacks.  By default the protection is on.  In a SQL Server environment, when the number of simultaneous client connection requests is more than the system can handle and SQL Server backlog queue is full, the client will receive a 'connection failed' error from SQL Server:

TCP Provider: An existing connection was forcibly closed by the remote host

The SQL Protocols team has a good Blog that explains the detailed interaction between SynAttackProtect setting and SQL Server. See http://blogs.msdn.com/sql_protocols/archive/2006/04/12/574608.aspx.

In Windows 2003, this issue could be worked-around by configuring the registry setting to disable SynAttackProtect.

1)    Launch regedit.exe

2)    Add DWORD value named SynAttackProtect under registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\

3)    Set data value to 0

See http://technet2.microsoft.com/windowsserver/en/library/8d3a9f4d-13d1-4280-ac57-30242504d8ba1033.mspx?mfr=true for additional information.

After applying the registry change, two customers reported the intermittent connection issue went away. Both customers had been stress-testing SQL Server.  SynAttackProtect is more likely to become an issue in a lab environment where SQL Server is being stress-tested with extreme load.  Customers running stress-testing in lab environments should turn off SynAttackProtect.  I am not sure I’d recommend proactively turning it off in a production system given the potential security risk.  If a production system ever runs into the SynAttackProtect issue, the where/why of the large number of connection requests should be examined first.

Windows “Scalable Networking Pack”

Windows Scalable Networking Pack was a second network stack change that was released as part of Windows 2003 SP1 + KB91222, or Windows 2003 SP2. With the Scalable Networking Pack, the TCP Chimney Offload feature is enabled by default to increase performance. However implementations on certain network cards are problematic when TCP Chimney Offload enabled, and can cause intermittent connection drop. When the connection is dropped due to incompatibility between the network card and Windows Scalable Networking Pack, typical error message is

[08S01] [Microsoft][SQL Native Client]Communication link failure

A workaround for this issue could be to disable to TCP Chimney Offload feature.

1)    Launch regedit.exe

2)    Edit DWORD EnableTCPChimney under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters, set data value to 0

3)    Edit DWORD EnableRSS under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters, set data value to 0

4)    Edit DWORD EnableTCPA under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters, set data value to 0

5)    Restart the server

Amongst others, network cards using Broadcom 5708 chipsets are known to have compatibility issues w/ TCP Chimney Offload.  Disabling TCP Chimney Offload would fix the incompatibility issue. However it would also remove the benefit of Windows TCP stack performance improvements. It should only be used as a workaround until a fix becomes available from the network card vendor. 

See support article http://support.microsoft.com/kb/942861 for additional information.

By now, the majority of customers solved the intermittent connectivity issue after applying the SynAttackProtect and/or TCP Chimney Offload changes. Some customers connecting to SQL Server through Citrix have to turn off the TCP Chimney Offload feature on the Citrix server to fix the issue.

Query Timeout

The particular ISV application runs with a configurable Query timeout, by default the timeout is set to 1 second.  When a query timeout happens, the application will retry the query for 16 times, if it still fails, the query will be submitted again with nolock hint.  How would that affect the connection?  At the first glance, it seems to be irrelevant, but it does. Here’s the sequence of what could happen.

1)    The query is submitted to SNAC for execution

2)    The execution of the query takes too long so the timeout expires

3)    Once the timeout happens, client attempts to cancel the query and sends an ATTN packet to the server. After sending ATTN, the client then waits for the response from the server. A timeout is set for this wait, the value of the timeout is obtained through SQL_ATTR_CONNECT_TIMEOUT and if not set, the default to 120 seconds.

4)    The wait for server to response also times out. This is treated as an error from the client and basically the connection is considered dead in such scenarios

5)    The client marks the connection as dead and then returns the “Query timeout expired” error message.

6)     The application code, on seeing a timeout expired message attempts to execute the request again on the same connection, but immediately hits the “communication link failure” message because the connection is deemed as dead by the client

 

During the investigation, we discovered a regression in SQL Server 2005. SQL Server 2005 may not response to a query cancel (timeout) request in a timely manner if the query requires index scan through a large number of pages. Checking for any attention requests is delayed when the SQL Server storage engine is busy bringing pages in from disk. Resulting in

[08S01] [Microsoft][SQL Native Client]Communication link failure

 

A hotfix is available to fix the regression, see details in

http://support.microsoft.com/kb/945442.

 

All the remaining customers had some long running batch queries that index scan a large table. This is just the last piece we needed to complete the puzzle. After applying the hotfix, all customers reported the problem solved.

Cross Posted from http://blogs.microsoft.com/mssqlisv

Posted by mssqlisv | 2 Comments

Using time zone data in SQL Server 2008

 

In SQL Server 2008 Microsoft has introduced a number of new date and time data types.  One of these is the datetimeoffset data type. This data type includes an offset from UTC time as well as the datetime value and ensures that the datetime can be retrieved in UTC or a particular timezone based on this offset.  There are also new functions to allow for conversions between different time zones using the new function SWITCHOFFSET(). 

 

An example from SQL Server 2008 Books On Line (BOL):

CREATE TABLE dbo.test

    (

    ColDatetimeoffset datetimeoffset

    );

GO

INSERT INTO dbo.test

VALUES ('1998-09-20 7:45:50.71345 -5:00');

GO

SELECT SWITCHOFFSET (ColDatetimeoffset, '-08:00')

FROM dbo.test;

GO

--Returns: 1998-09-20 04:45:50.7134500 -08:00

SELECT ColDatetimeoffset

FROM dbo.test;

--Returns: 1998-09-20 07:45:50.7134500 -05:00

 

One of the most common questions we are asked is why we use the offset and not a timezone name.  A timezone name is much easier to remember than an offset, and most people do not know an offset without looking it up, making queries more difficult. 

 

Unfortunately, there is no current international standard authority for timezone names and values.  Each system needs to use a system of their own choosing, and until there is an international standard, it is not feasible to try to have SQL Server provide one, and would ultimately cause more problems than it would solve.  However, there are a couple of common systems that are well recognized around the world.  One is the Dynamic timezone data that is stored in the Windows Vista registry.  This data can be read from the registry into a file, which is then imported into SQL Server.


Another cross-platform standard is the public domain Olson Timezone database (
http://www.twinsun.com/tz/tz-link.htm). There are many public domain programs for extracting a time zone from these files, but at this time most are programmatic solutions.   So a programmatic function could be written in the CLR, but to provide full database functionality and query-ability, a table is needed.

 

The programmatic solutions take a date, then apply the many different rules that determine when a zone is in daylight savings time and when it is not.  However, there are also historical changes.  Daylight savings time changed in 2007, meaning that determining what the offset for a particular zone is at a particular time is different depending on the year.  Then there are times when leap seconds need to be added.  Therefore any data-driven solution must have rows that have valid time ranges as well.

 

The approach recommended here is to take one of the DLLs found on the web and instead of providing a programmatic solution around a specific date – to write all of the rows out into a database as an offset with the valid ranges.  Currently this example uses the Windows standard naming conventions for timezones, with a mapping to the Olson timezone names, but you could easily add other names in other languages as well.

 

Working with timezones is very complex, and the following is a suggestion only for some ideas on how to use time zone data more effectively.   This is an example program (no guarantees) that uses a .NET library and writes the data from the Olson tz files in table format, and which can then be imported into SQL Server.    The .NET timezone library can be found at http://www.babiej.demon.nl/Tz4Net/main.htm and they request a small donation.   

 

Here is some sample code to write to files the timezone data (no guarantees – does not include leap seconds):

 

            StreamWriter sr = File.CreateText(@"D:\TZMapping.txt");

            StreamWriter tr = File.CreateText(@"D:\TZZones.txt");

 

            string[] zoneNames = OlsonTimeZone.AllNames;

            sr.WriteLine("ID\tDaylightName\tStandardName\tRawUtcOffset\tOffsetSeconds\tWin32Id");

            tr.WriteLine("ID\tTransitionStart\tTransitionEnd\tDeltaSeconds\tDST");

 

            for (int i = 0; i < zoneNames.Length; i++)

            {

                OlsonTimeZone tz = OlsonTimeZone.GetInstanceFromOlsonName(zoneNames[i].ToString());

                sr.Write(i.ToString() + "\t");

                sr.Write(tz.DaylightName.Trim() + "\t");

                sr.Write(tz.StandardName.Trim() + "\t");

                sr.Write(tz.RawUtcOffset.ToString() + "\t");

                sr.Write(tz.RawUtcOffset.TotalSeconds.ToString() + "\t");

                sr.WriteLine(tz.Win32Id == null ? "" : tz.Win32Id.Trim());

 

                DaylightTime[] times = tz.AllTimeChanges;

                for (int j = 0; j < times.Length; j++)

                {

                    tr.Write(i.ToString() + "\t");

                    tr.Write(times[j].Start.ToString("yyyy-MM-dd HH:mm:ss") + "\t");

                    tr.Write(times[j].End.ToString("yyyy-MM-dd HH:mm:ss") + "\t");

                    tr.Write(times[j] is StandardTime ? "0\t" :times[j].Delta.TotalSeconds.ToString() + "\t");

                    tr.WriteLine(times[j] is StandardTime ? false.ToString() : true.ToString() );

                }

            }

            tr.WriteLine();

            sr.WriteLine();

            tr.Close();

            sr.Close();

 

Import the TZMapping file, which will become the parent table, with the ID as the primary key.   Your table structure might look like this:

  

 

 

 Please note:  If you use the Flat File Datasource in the Import Data Wizard in SQL Server 2008 Management Studio, you will need to open the Advanced Tab to set the source OutPutColumnWidth to greater than the default of 50.   Then import the TZZones file, which will become the child table with the ID, TransitionStart, and TransitionEnd as the composite primary key with a foreign key reference to the TZMapping table.  The TZZones table includes historical timezone data.    Joining these new tables into your data into queries now allows for queries that include standard names, Windows IDs, etc.

 

For example, offsets can now be retrieved by a preferred name:

 

select UtcOffset from TZmapping where StandardName = 'US/Pacific (PST)'

 

The following two queries return different offset amounts for the same day in two different years.  This is because the US changed daylight savings time, and the date in March now falls into daylight savings when it did not before.

 

 

select (dbo.TZMapping.OffsetSeconds + dbo.TZZones.DeltaSeconds)/3600

from dbo.TZMapping 

join dbo.TZZones

on dbo.TZMapping.id = dbo.TZZones.id

where dbo.TZMapping.StandardName = 'America/Los_Angeles (PST)'

and '2006-03-15'

between dbo.TZZones.TransitionStart

and dbo.TZZones.TransitionEnd

 

 

 

select (dbo.TZMapping.OffsetSeconds + dbo.TZZones.DeltaSeconds)/3600

from dbo.TZMapping 

join dbo.TZZones

on dbo.TZMapping.id = dbo.TZZones.id

where dbo.TZMapping.StandardName = 'America/Los_Angeles (PST)'

and '2007-03-15'

between dbo.TZZones.TransitionStart

and dbo.TZZones.TransitionEnd

 

 

Again, timezones are a complex area and each application will need to address how you are going to handle time zone data to make programs more user friendly.  This is just one small example.

Cross Posted from http://blogs.microsoft.com/mssqlisv
Posted by mssqlisv | 5 Comments

Increase your SQL Server performance by replacing cursors with set operations

You have probably heard many times, from different sources, that as a best practice; avoid using TSQL cursors.

During a recent visit to a partner we ran into a common cursor case, which I wanted to use as an example to demonstrate why you should avoid TSQL cursors in most cases, and how to convert cursor logic to simple set join operations. Now there are certain scenarios where using a cursor makes sense. For example, a cursor is ideal for row by row processing that can’t be accomplished by set based operations. A cursor is flexible in that it provides a window, or subset, of data and that allows manipulation of the data in various ways. Study carefully what you want to achieve on case by case basis before using a cursor. Keep in mind SQL Server, as a modern RDBMS system, performs much better with set operations.

Here is simplified version of a real cursor that was used to update a big table with over 200 million rows.

DECLARE @EntityId Varchar(16)

DECLARE @PerfId Varchar(16)

DECLARE @BaseId Varchar(16)

DECLARE @UpdateStatus Int

 

DECLARE outerCursor CURSOR FOR

SELECT EntityId, BaseId

FROM outerTable

--Returns 204,000 rows

 

OPEN outerCursor

FETCH NEXT FROM outerCursor INTO @EntityId, @BaseId

 

WHILE @@FETCH_STATUS = 0

BEGIN

            DECLARE innerCursor CURSOR FOR

            SELECT PRFMR_ID

            FROM innerTable

            WHERE ENTY_ID = @BaseId

                       

            OPEN innerCursor

            FETCH NEXT FROM innerCursor INTO @PerfId

            SET @UpdateStatus = @@FETCH_STATUS

           

            WHILE @UpdateStatus = 0

            BEGIN

                  UPDATE 200MilRowTable

                  SET ENTY_ID = @EntityId

                  WHERE PRFMR_ID = @PerfId

           

                  FETCH NEXT FROM innerCursor INTO @PerfId

                  SET @UpdateStatus = @@FETCH_STATUS

            END

           

            CLOSE innerCursor

            DEALLOCATE innerCursor --clean up inner cursor

                       

            FETCH NEXT FROM outerCursor INTO @EntityId, @BaseId

END

 

CLOSE outerCursor

DEALLOCATE outerCursor –cleanup outer cursor

 

 

You might notice that this is a nested cursor with 204,000 loops in total for outerCursor. The innerTable has 10 million rows but innerCursor varies in number of loops depending on @BaseId of outerCursor. When I arrived at the customer this cursor had been running for over a day. The developer was “hoping” that it would finish soon given another day or two. The problem was nobody knew for sure how much time this thing would need to complete. Well, we can find out how much progress it has made so far to make an educated guess:

SELECT execution_count, st.text

FROM sys.dm_exec_query_stats as qs

CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as st