I want to create reports like below picture's report on SSRS.
Yellow parts mean SET_PHASE,
Green parts mean PROD_PHASE
And my query result like this:
I want to show for per line, all order and I want to show for per order, SETUP and PRODUCTION depends on duratıon time.
SET_PHASE's duration time is SET_DURATION,
PROD_PHASE's duration time is PROD_DURATION
I hope so my query is clear :) Could you help me about issue?
Answer:
Hello Alan,
Current situation I have just these data:
PROD100059335 SETUP PRODUCTION 1 14 LINE 4
PROD100058991 SETUP PRODUCTION 1 5 LINE 6
PROD100059259 SETUP PRODUCTION 2 24 LINE 4
PROD100059188 SETUP PRODUCTION 1 3 LINE 2
PROD100059248 SETUP PRODUCTION 1 15 LINE 2
PROD100059055 SETUP PRODUCTION 2 23 LINE 2
PROD100058754 SETUP PRODUCTION 5 18 LINE 6
And If I use your query I just show "PROD100058754", "PROD100059259", "PROD100059055" these order. I don't understand why other data lost.
until "DECLARE #n TABLE(n int)" part I can show other data. but after that I can not show.
And I applied procedure on SSRS my report shows like this:
I couldn't do correctly and I don't know how can I fix them:(
for example "PROD100059259" this order normally has setup phase but on the report I don't have yellow field.
Do you have any suggestions for me?
OK, here is an attempt to give you what you want but there are a few caveats:
The durations are scaled and no operation can take less than 1 time slot so the setup vs production duration is only approximate
I haven't found a good way of labelling each bar so I've used tooltips
First the code... I've added lots of comments so hopefully you can follow it thru, it's based on your sample data.
NOTE: I've update the table as it now seems like you are using integer durations rather than the 00:00 format from your first example.
-- CREATE A TEST TABLE AND POPULATE IT
DECLARE #data TABLE(STR_ORDER_ID varchar(20), SET_DURATION varchar(10), PROD_DURATION varchar(10), Set_decimal int, Prod_Decimal int, Line varchar(10))
INSERT INTO #data
VALUES
('PROD100059335', NULL, NULL, 1, 14, 'LINE 4'),
('PROD100058991', NULL, NULL,1, 5, 'LINE 6'),
('PROD100059259', NULL, NULL,2, 24, 'LINE 4'),
('PROD100059188', NULL, NULL,1, 3, 'LINE 2'),
('PROD100059248', NULL, NULL,1, 15, 'LINE 2'),
('PROD100059055', NULL, NULL,2, 23, 'LINE 2'),
('PROD100058754', NULL, NULL,5, 18, 'LINE 6')
DECLARE #Gap int = 2 -- determines how many columns we use to separate each order
-- ASSUME durations are in hours/minutes or minutes/seconds and convert them to decimal minutes or decimal seconds respectively
-- COMMENTED THIS AS WE NO LONGER NEED IT. No longer required as durations are now integer values.
--UPDATE d
-- SET
-- Set_decimal = (CAST(LEFT(d.SET_DURATION, len(d.SET_DURATION)-3) AS INT) * 60) + CAST(RIGHT(d.SET_DURATION, 2) AS INT) ,
-- Prod_Decimal = (CAST(LEFT(d.PROD_DURATION, len(d.PROD_DURATION)-3) AS INT) * 60) + CAST(RIGHT(d.PROD_DURATION, 2) AS INT)
--FROM #data d
-- CREATE A NORMALISED TABLE, this will just help to make the next steps simpler
DECLARE #normData TABLE(RowId INT IDENTITY (1,1), Line varchar(10), STR_ORDER_ID varchar(20), OperationOrder int, Operation varchar(10), Duration int)
INSERT INTO #normData (Line, STR_ORDER_ID, OperationOrder, Operation, Duration)
SELECT * FROM (
SELECT Line, STR_ORDER_ID, 1 as OperationOrder , 'SET' as Operation , Set_decimal FROM #data
UNION
SELECT Line, STR_ORDER_ID, 2 , 'PROD' , Prod_decimal FROM #data
UNION
SELECT Line, STR_ORDER_ID, 3 , 'GAP' , #Gap FROM #data ) u -- this adds dummy data that will act as gaps in hte timeline. Change 5 to whatever value suits you best
ORDER BY Line, STR_ORDER_ID, OperationOrder
-- find the largest line running total duration per line and scale it to fit to 240 (so we dont go over 256 column limit in SSRS)
DECLARE #MaxDur INT = (SELECT MAX(rt) FROM (
select *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData) mRt)
-- Now scale the values back so they fit but don't let any value become less than 1
IF #MaxDur > 240
BEGIN
UPDATE nd
SET Duration = CASE WHEN nd.Duration / (#MaxDur/240) <1 THEN 1 ELSE nd.Duration / (#MaxDur/240) END
FROM #normData nd
END
/* check what we have so far by uncommenting this bit
select *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData
--*/
-- ================================================================ --
-- At this point you 'may' have enough data to plot a bar chart. == --
-- ================================================================ --
-- CREATE A SIMPLE NUMBERS TABLE, we'll need this to act as our time series
DECLARE #n TABLE(n int)
DECLARE #i int = 0
DECLARE #t int = #MaxDur --(SELECT max(Duration) +5 FROM #normData) -- simple loop counter target set to slightly bigger than our highest duration
WHILE #i<#t
BEGIN
INSERT INTO #n SELECT #i
SET #i = #i +1
END
-- Join our numbers table to our real data
-- This will give us at least 1 row per time slot and associated activity during that time slot.
-- We can plot this driectly as a matrix.
SELECT *
FROM #n n
LEFT JOIN (
-- Sub queries below give use a runnintg total, we then join this back to itself to get the previous
-- running total and this will give us the 'time range' for each operation.
SELECT
a.*
, ISNULL(b.Rt,0)+1 AS TimeStart
, a.RT AS TimeEnd
FROM
(SELECT *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData
) a
LEFT JOIN
(SELECT *
, SUM(Duration) OVER(PARTITION BY Line ORDER BY Line, STR_ORDER_ID, OperationOrder) AS Rt
from #normData
) b
ON a.RowId = b.RowId + 1 and a.Line = b.Line
) d
ON n.n between d.TimeStart and d.TimeEnd
ORDER BY Line, STR_ORDER_ID, OperationOrder, n, TimeStart, TimeEnd
You can use the code above in your dataset.
The report design:
The report is very simple. It's a matrix with a single row group based on Line and a single column group based on n which is our time slot number.
I've added a blank row to act as a spacer between the 'bars'.
The expression of the cell background is
=SWITCH(
Fields!OperationOrder.Value = 1, "Yellow",
Fields!OperationOrder.Value = 2, "Green",
Fields!OperationOrder.Value = 3, Nothing,
True, Nothing
)
There is also a tooltip which displays STR_ORDER_ID and the operation name.
You get the following output.
Related
For example, there is a table
int type
int number
int value
How to make that when inserting a value into a table
indexing started from 1 for different types.
type 1 => number 1,2,3...
type 2 => number 1,2,3...
That is, it will look like this.
type
number
value
1
1
-
1
2
-
1
3
-
2
1
-
1
4
-
2
2
-
3
1
-
6
1
-
1
5
-
2
3
-
6
2
-
Special thanks to #Larnu.
As a result, in my case, the best solution would be to create a table for each type.
As I mentioned in the comments, neither IDENTITY nor SEQUENCE support the use of another column to denote what "identity set" they should use. You can have multiple SEQUENCEs which you could use for a single table, however, this doesn't scale. If you are specific limited to 2 or 3 types, for example, you might choose to create 3 SEQUENCE objects, and then use a stored procedure to handle your INSERT statements. Then, when a user/application wants to INSERT data, they call the procedure and that procedure has logic to use the SEQUENCE based on the value of the parameter for the type column.
As mentioned, however, this doesn't scale well. If you have an undeterminate number of values of type then you can't easily handle getting the right SEQUENCE and handling new values for type would be difficult too. In this case, you would be better off using a IDENTITY and then a VIEW. The VIEW will use ROW_NUMBER to create your identifier, while IDENTITY gives you your always incrementing value.
CREATE TABLE dbo.YourTable (id int IDENTITY(1,1),
[type] int NOT NULL,
number int NULL,
[value] int NOT NULL);
GO
CREATE VIEW dbo.YourTableView AS
SELECT ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id ASC) AS Identifier,
[type],
number,
[value]
FROM dbo.YourTable;
Then, instead, you query the VIEW, not the TABLE.
If you need consistency of the column (I name identifier) you'll need to also ensure row(s) can't be DELETEd from the table. Most likely by adding an IsDeleted column to the table defined as a bit (with 0 for no deleted, and 1 for deleted), and then you can filter to those rows in the VIEW:
CREATE VIEW dbo.YourTableView AS
WITH CTE AS(
SELECT id,
ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id ASC) AS Identifier,
[type],
number,
[value],
IsDeleted
FROM dbo.YourTable)
SELECT id,
Identifier,
[type],
number,
[value]
FROM CTE
WHERE IsDeleted = 0;
You could, if you wanted, even handle the DELETEs on the VIEW (the INSERT and UPDATEs would be handled implicitly, as it's an updatable VIEW):
CREATE TRIGGER trg_YourTableView_Delete ON dbo.YourTableView
INSTEAD OF DELETE AS
BEGIN
SET NOCOUNT ON;
UPDATE YT
SET IsDeleted = 1
FROM dbo.YourTable YT
JOIN deleted d ON d.id = YT.id;
END;
GO
db<>fiddle
For completion, if you wanted to use different SEQUENCE object, it would look like this. Notice that this does not scale easily. I have to CREATE a SEQUENCE for every value of Type. As such, for a small, and known, range of values this would be a solution, but if you are going to end up with more value for type or already have a large range, this ends up not being feasible pretty quickly:
CREATE TABLE dbo.YourTable (identifier int NOT NULL,
[type] int NOT NULL,
number int NULL,
[value] int NOT NULL);
CREATE SEQUENCE dbo.YourTable_Type1
START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dbo.YourTable_Type2
START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dbo.YourTable_Type3
START WITH 1 INCREMENT BY 1;
GO
CREATE PROC dbo.Insert_YourTable #Type int, #Number int = NULL, #Value int AS
BEGIN
DECLARE #Identifier int;
IF #Type = 1
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type1;
IF #Type = 2
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type2;
IF #Type = 3
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type3;
INSERT INTO dbo.YourTable (identifier,[type],number,[value])
VALUES(#Identifier, #Type, #Number, #Value);
END;
I have a varchar(250) ParameterValue that I would like to check the number of decimal places in.
This is the line of code that I cannot get working:
where RIGHT(CAST(ParameterValue as DECIMAL(10,5)), ParameterValue), 1) != 0
The code below is where the line of code is used:
select *
INTO #ParamPrecision
from Data_table
where RIGHT(CAST(ParameterValue as DECIMAL(10,5)), ParameterValue), 1) != 0
AND ParameterBindStatus = 0
UPDATE a
SET a.ParameterBindStatus = 5
FROM Data_table, #ParamPrecision b
WHERE a.SQLParameterId = b.SQLParameterId
INSERT Log_table
(
SQLBatchId,
SQLProcess,
Error,
SQLError_Message,
ParametersSent
)
SELECT SQLBatchId,
'sp_ReadParametersToBindData',
1,
'Invalid parameter value sent from MES',
'some parameter info'
FROM #ParamPrecision
SELECT *
INTO #UnBoundPrompt
FROM Data_table
WHERE DATEADD(DAY, 1, SQLTimeStamp) < GETDATE()
AND ParameterBindStatus = 0
UPDATE a
SET a.ParameterBindStatus = 99
FROM Data_tablea, #UnBoundPrompt b
WHERE a.SQLParameterId = b.SQLParameterId
INSERT Log_table
(
SQLBatchId,
SQLProcess,
Error,
SQLError_Message,
ParametersSent
)
SELECT SQLBatchId,
'sp_ReadParametersToBindData',
1,
'Parameter download timeout',
'some parameter info'
FROM #UnBoundPrompt
If the check for decimal places is not satisfied, the next select statement checks if the parameter timestamp is active for more than 1 day. If this is satisfied, a log entry is made.
If the number of decimal places exceeds 4, then I want to set the ParameterBindStatus = 5 and update the log table.
I have changed the code as follows to allow me to confirm the rest of the code and that works but the code does not execute when trying to detect number of decimal places.
select *
INTO #ParamPrecision
from Data_table
where ParameterValue > '1500'
AND ParameterBindStatus = 0
this may help with your precision problem - I've laid it out as a table so you can see each step of the transformation but you can easily see the pattern :) essentially you just reverse the string and truncate it. All steps included (can be done faster) - you may/may not need to add a bit for the case that there is no decimal point.
--setup
create table test
(stringVal varchar(250));
insert into test values
('12.3456'),
('1.2345678'),
('12'),
('0.123')
--query
SELECT stringVal,
Reverse(CONVERT(VARCHAR(50), stringVal, 128)) as reversedText
, Cast(Reverse(CONVERT(VARCHAR(50), stringVal, 128)) as float) as float
, Cast(Cast(Reverse(CONVERT(VARCHAR(50), stringVal, 128)) as float) as bigint) as bigint
, len(Cast(Cast(Reverse(CONVERT(VARCHAR(50), stringVal, 128)) as float) as bigint)) as decimalPrecision
FROM test
I have an nvarchar(200) called ColumnA in Table1 that contains, for example, the value:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
I want to extract every 7 characters into Table2, ColumnB and end up with all of these values below.
ABCDEFG
BCDEFGH
CDEFGHI
DEFGHIJ
EFGHIJK
FGHIJKL
GHIJKLM
HIJKLMN
IJKLMNO
JKLMNOP
KLMNOPQ
LMNOPQR
MNOPQRS
NOPQRST
OPQRSTU
PQRSTUV
QRSTUVW
RSTUVWX
STUVWXY
TUVWXYZ
[Not the real table and column names.]
The data is being loaded to Table1 and Table2 in an SSIS Package, and I'm puzzling whether it is better to do the string handling in TSQL in a SQL Task or parse out the string in a VB Script Component.
[Yes, I think we're the last four on the planet using VB in Script Components. I cannot persuade the other three that this C# thing is here to stay. Although, maybe it is a perfect time to go rogue.]
You can use a recursive CTE calculating the offsets step by step and substring().
WITH
cte
AS
(
SELECT 1 n
UNION ALL
SELECT n + 1 n
FROM cte
WHERE n + 1 <= len('ABCDEFGHIJKLMNOPQRSTUVWXYZ') - 7 + 1
)
SELECT substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ', n, 7)
FROM cte;
db<>fiddle
If you have a physical numbers table, this is easy. If not, you can create a tally-on-the-fly:
DECLARE #string VARCHAR(100)='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
--We create the tally using ROW_NUMBER against any table with enough rows.
WITH Tally(Nmbr) AS
(SELECT TOP(LEN(#string)-6) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values)
SELECT Nmbr
,SUBSTRING(#string,Nmbr,7) AS FragmentOf7
FROM Tally
ORDER BY Nmbr;
The idea in short:
The tally returns a list of numbers from 1 to n (n=LEN(#string)-6). This Number is used in SUBSTRING to define the starting position.
You can do it with T-SQL like this:
DECLARE C CURSOR LOCAL FOR SELECT [ColumnA] FROM [Table1]
OPEN C
DECLARE #Val nvarchar(200);
FETCH NEXT FROM C into #Val
WHILE ##FETCH_STATUS = 0 BEGIN
DECLARE #I INTEGER;
SELECT #I = 1;
WHILE #I <= LEN(#vAL)-6 BEGIN
PRINT SUBSTRING(#Val, #I, 7)
SELECT #I = #I + 1
END
FETCH NEXT FROM C into #Val
END
CLOSE C
Script Component solution
Assuming that the input Column name is Column1
Add a script component
Open the script component configuration form
Go to Inputs and Outputs Tab
Click on the Output icon and set the Synchronous Input property to None
Add an Output column (example outColumn1)
In the Script editor, use a similar code in the row processing function:
Dim idx as integer = 0
While Row.Column1.length > idx + 7
Output0Buffer.AddRow()
Output0Buffer.outColumn1 = Row.
Column1.Substring(idx,7)
idx +=1
End While
I have a table for the actions.
The table has several slots for the same time on the same day. Same action can't be booked for the same time twice. I'm trying to come up with the way to list all the IDs for an action 'A', such as every available time is listed only once, even if there are both slots available, but if 'A' is book for some time already and another slot for this time is empty, that slot wouldn't be showing.
And it comes to me that I don't know T-SQL that good.
I overcame this by selecting all the rows where 'A' is booked, selecting all distinct (date, time start and time end) which are not booked and doing check whether 'A' is already booked for this time. But all this checking is done on the software level, and those multiple requests to the server and looping in the program to perform the same job as one LIKELY SIMPLE sql request don't look very efficient to me.
If there a way to do something like:
SELECT ID FROM mytable
WHERE Action IS NULL AND (date, time_start, time_end **'ALL TOGETHER IN ONE ROW'**)
NOT IN (SELECT date, time_start, time_end FROM mytable
WHERE Action = 'A')
HAVING 'THOSE THREE BEING DISTINCT'
By other words can I select rows which partially match other rows? It would be simple if I had only one column to compare, but there are three.
In SQL Server we generally use WHILE instead of FOR. I believe what you're trying to do could be fulfilled as follows if you want to loop through the table (ideally your ID field would be the PRIMARY KEY as well). This is just inserting it into a temp table for now, but potentially it should give you the results you want:
-- DECLARE and set counters
DECLARE #curr INT, #prev INT, #max INT
SELECT #curr = 0, #prev = 0, #max = MAX(ID) FROM myTable
-- Make a simple temp table
CREATE TABLE #temp (ID INT)
-- Start looping
WHILE (#curr < #max)
BEGIN
-- Set our counter for the next row
SELECT #curr = MIN(ID) FROM myTable WHERE ID > #prev
-- Populate temp table with a self-join to compare slots
-- Slot must match on date + time but NOT have equal SLOT value
-- Will only INSERT if we meet our criteria i.e. neither slot booked
INSERT INTO #temp
SELECT DISTINCT A.ID
FROM myTable A
JOIN myTable B ON B.[Date] = A.[date] AND B.time_start = A.time_start AND B.time_end = A.time_end
WHERE A.[Action] IS NULL -- Indicates NO booking
AND B.[Action] IS NULL -- Indicates NO booking
AND A.SLOT <> B.SLOT
AND A.ID = #curr
-- Update our counter
SET #prev = #curr
END
-- Get all our records
SELECT * FROM #temp
-- Remove the sleeping dog ;)
DROP TABLE #temp
There is a little bit of redundancy here because it checks ALL rows, even if a condition has been found in the first row of that time slot, but you can tweak it from here if you need to.
You should really avoid using field names like "Date" and "Action" because these are reserved words in SQL.
You question is a bit unclear, but I think this will point you in a productive direction. SQL is designed to perform operation on sets of rows, not to loop through processing one row at a time. The following code will correlate your data into one row for each pair of slots at each date/time. You can use a CASE expression, as shown, to add a column that indicates the status of the row, and you can then add a WHERE clause, not shown, to perform any additional filtering.
-- Sample data.
declare #Samples as Table ( SampleId Int, Slot Int, EventDate Date, StartTime Time(0), EndTime Time(0), Action VarChar(10) );
insert into #Samples ( SampleId, Slot, EventDate, StartTime, EndTime, Action ) values
( 200, 1, '20150501', '00:00:00', '00:30:00', NULL ),
( 201, 2, '20150501', '00:00:00', '00:30:00', NULL ),
( 202, 1, '20150501', '00:30:00', '01:00:00', 'A' ),
( 203, 2, '20150501', '00:30:00', '01:00:00', NULL ),
( 204, 1, '20150501', '01:00:00', '01:30:00', NULL ),
( 205, 2, '20150501', '01:00:00', '01:30:00', 'A' ),
( 206, 1, '20150501', '01:30:00', '02:00:00', 'B' ),
( 207, 2, '20150501', '01:30:00', '02:00:00', 'B' );
select * from #Samples;
-- Data correleated for each date/time.
select Slot1.EventDate, Slot1.StartTime, Slot1.EndTime,
Slot1.Action as Action1, Slot2.Action as Action2,
Coalesce( Slot1.Action, Slot2.Action ) as SummaryAction,
case when Slot1.Action = Slot2.Action then 'ERROR!' else 'Okay.' end as Status
from #Samples as Slot1 inner join
#Samples as Slot2 on Slot2.EventDate = Slot1.EventDate and Slot2.StartTime = Slot1.StartTime and
Slot1.Slot = 1 and Slot2.Slot = 2;
I have a periodic check of a certain query (which by the way includes multiple tables) to add informational messages to the user if something has changed since the last check (once a day).
I tried to make it work with checksum_agg(binary_checksum(*)), but it does not help, so this question doesn't help much, because I have a following case (oversimplified):
select checksum_agg(binary_checksum(*))
from
(
select 1 as id,
1 as status
union all
select 2 as id,
0 as status
) data
and
select checksum_agg(binary_checksum(*))
from
(
select 1 as id,
0 as status
union all
select 2 as id,
1 as status
) data
Both of the above cases result in the same check-sum, 49, and it is clear that the data has been changed.
This doesn't have to be a simple function or a simple solution, but I need some way to uniquely identify the difference like these in SQL server 2000.
checksum_agg appears to simply add the results of binary_checksum together for all rows. Although each row has changed, the sum of the two checksums has not (i.e. 17+32 = 16+33). This is not really the norm for checking for updates, but the recommendations I can come up with are as follows:
Instead of using checksum_agg, concatenate the checksums into a delimited string, and compare strings, along the lines of SELECT binary_checksum(*) + ',' FROM MyTable FOR XML PATH(''). Much longer string to check and to store, but there will be much less chance of a false positive comparison.
Instead of using the built-in checksum routine, use HASHBYTES to calculate MD5 checksums in 8000 byte blocks, and xor the results together. This will give you a much more resilient checksum, although still not bullet-proof (i.e. it is still possible to get false matches, but very much less likely). I'll paste the HASHBYTES demo code that I wrote below.
The last option, and absolute last resort, is to actually store the table table in XML format, and compare that. This is really the only way you can be absolutely certain of no false matches, but is not scalable and involves storing and comparing large amounts of data.
Every approach, including the one you started with, has pros and cons, with varying degrees of data size and processing requirements against accuracy. Depending on what level of accuracy you require, use the appropriate option. The only way to get 100% accuracy is to store all of the table data.
Alternatively, you can add a date_modified field to each table, which is set to GetDate() using after insert and update triggers. You can do SELECT COUNT(*) FROM #test WHERE date_modified > #date_last_checked. This is a more common way of checking for updates. The downside of this one is that deletions cannot be tracked.
Another approach is to create a modified table, with table_name (VARCHAR) and is_modified (BIT) fields, containing one row for each table you wish to track. Using insert, update and delete triggers, the flag against the relevant table is set to True. When you run your schedule, you check and reset the is_modified flag (in the same transaction) - along the lines of SELECT #is_modified = is_modified, is_modified = 0 FROM tblModified
The following script generates three result sets, each corresponding with the numbered list earlier in this response. I have commented which output correspond with which option, just before the SELECT statement. To see how the output was derived, you can work backwards through the code.
-- Create the test table and populate it
CREATE TABLE #Test (
f1 INT,
f2 INT
)
INSERT INTO #Test VALUES(1, 1)
INSERT INTO #Test VALUES(2, 0)
INSERT INTO #Test VALUES(2, 1)
/*******************
OPTION 1
*******************/
SELECT CAST(binary_checksum(*) AS VARCHAR) + ',' FROM #test FOR XML PATH('')
-- Declaration: Input and output MD5 checksums (#in and #out), input string (#input), and counter (#i)
DECLARE #in VARBINARY(16), #out VARBINARY(16), #input VARCHAR(MAX), #i INT
-- Initialize #input string as the XML dump of the table
-- Use this as your comparison string if you choose to not use the MD5 checksum
SET #input = (SELECT * FROM #Test FOR XML RAW)
/*******************
OPTION 3
*******************/
SELECT #input
-- Initialise counter and output MD5.
SET #i = 1
SET #out = 0x00000000000000000000000000000000
WHILE #i <= LEN(#input)
BEGIN
-- calculate MD5 for this batch
SET #in = HASHBYTES('MD5', SUBSTRING(#input, #i, CASE WHEN LEN(#input) - #i > 8000 THEN 8000 ELSE LEN(#input) - #i END))
-- xor the results with the output
SET #out = CAST(CAST(SUBSTRING(#in, 1, 4) AS INT) ^ CAST(SUBSTRING(#out, 1, 4) AS INT) AS VARBINARY(4)) +
CAST(CAST(SUBSTRING(#in, 5, 4) AS INT) ^ CAST(SUBSTRING(#out, 5, 4) AS INT) AS VARBINARY(4)) +
CAST(CAST(SUBSTRING(#in, 9, 4) AS INT) ^ CAST(SUBSTRING(#out, 9, 4) AS INT) AS VARBINARY(4)) +
CAST(CAST(SUBSTRING(#in, 13, 4) AS INT) ^ CAST(SUBSTRING(#out, 13, 4) AS INT) AS VARBINARY(4))
SET #i = #i + 8000
END
/*******************
OPTION 2
*******************/
SELECT #out