SQL Server does not support the equivalent of ORACLE's %TYPE and %ROWTYPE - sql-server

I writing a cursor to select data from a table into variables.
Using oracle I could simply use a single declare statement that in effect creates a variable for every column with a data type that matches the type of the underlying table (as it is currently defined at runtime)..
In SQL server it would seem you have to manually declare a variable for every column.
Am I right. Does anyone have any thoughts?
I think this is a massive reason to dislike SQL Server when compared to oracle.
I am so disappointed that SS doesn't have %TYPE and %ROWTYPE. These save hours of work when initially writing code and in the event that the type of a column needs to change, it saves having to go back and re-work all the code.
In Oracle I used to write something like:
DECLARE #MyRowVar AS ATable%ROWTYPE;
In SQL Server, I just had to write this this:
DECLARE #External_ID AS BIGINT = NULL;
DECLARE #PlatformID AS INT = NULL;
DECLARE #ActorIDOfReseller AS INT = NULL;
DECLARE #ActorIDOfClient AS INT = NULL;
DECLARE #ActorIDOfExtension AS INT = NULL;
DECLARE #CallType AS NCHAR (10) = NULL;
DECLARE #CallInitiatedDate AS DATE = NULL;
DECLARE #CallInitiatedTimeHH24MI AS TIME (0) = NULL;
DECLARE #TimePeriodID AS INT = NULL;
DECLARE #CallAnswered AS DATETIME = NULL;
DECLARE #CallAnsweredYN AS BIT = NULL;
DECLARE #CallDispositionID AS INT = NULL;
DECLARE #CountryID AS INT = NULL;
DECLARE #CallPrefixID AS INT = NULL;
DECLARE #FromNumber AS VARCHAR (32) = NULL;
DECLARE #ToNumber AS VARCHAR (80) = NULL;
DECLARE #CallDuration AS INT = NULL;
DECLARE #CallCostToExtension AS DECIMAL (10, 6) = NULL;
DECLARE #CallCostToClient AS DECIMAL (10, 6) = NULL;
DECLARE #CallCostToReseller AS DECIMAL (10, 6) = NULL;
DECLARE #CallCostToAdmin AS DECIMAL (10, 6) = NULL;
DECLARE #Flow AS VARCHAR (3) = NULL;
DECLARE #CallStart AS DATETIME = NULL;
DECLARE #MoneyUnit AS VARCHAR (32) = NULL;
DECLARE #Prefix AS VARCHAR (32) = NULL;
DECLARE #External_CallID AS VARCHAR (255) = NULL;
This is making me very sad and is a waste of my life.
I tagged this as Oracle, as I'm sure oracle developers will want to know how lucky they are to work with this professional tool.
Harvey
Continued.
From the comment below it appears there is an easier way! Thanks very much for the input. I was ranting as I got a bit knarked....sorry.
Here's some more context:
I have a csv file, with unreliable data in each column.
I BULK LOAD this csv file into tableA which has all columns declared as VARCHAR(100). (The file loads even if the data type of values aren't what was expected in the column)
I used a single statement to transfer the data from tableA into tableB.
TableB has the same columns as tableA, but tableB columns have a variety of datatypes - which are the expected datatypes that the csv data should have.
(TableB also has a few other columns not populated form the file, eg an IDENTITY column, etc..).
The statement is something like:
INSERT INTO tableB
(column list)
SELECT
( {column list for select} )
FROM tableB;
Each column in the {column list for select} above, has is wrapped with the TRY_CAST function to convert it to the same data type as the destination table or NULL.
The above step 3 worked OK, but I had no way of identifying the invalid rows.
So I decided to try to use a cursor and use the same select statement as in step 3 but without the TRY_CAST wrapping each column. I used a TRY/CATCH around the whole INSERT statement that operated on each row of the select statement to log the invalid rows when the INSERT failed.
I've now written Step 4 but boy I wish I'd posted a "how do I do this" question here first. My code looks really rubbish and I've posted it below.
As you can probably tell, I've not been using SQL Server long and I'm pretty rusty at using big databases..
Any more input you can give is very welcome. I'll re-write this tomorrow!
I like Jermey's suggestion of creating a temp table using teh SQL below, but I've not completely got my head around this yet!
Select *
INTO #tmp
From mytable
Where 1 = 0;
Any way, here it is.
(I've run this now and it took 30 seconds to process 254,000 rows, - only one of which was a failure - but this time is acceptable.)
CREATE PROCEDURE [dbo].[Call_Process_FromXoomTalk_UsingCursor]
#PlatformId int
, #StartOfMonthDate as Date = NULL
, #EndOfMonthDate as Date = NULL
, #LimitImportToRowsWithThisCallType nVarchar(max) = NULL
, #LimitImportToRowsWithThisFlow nVarchar(max) = NULL
, #Raise1Print2Both3 as tinyint = 3
AS
BEGIN
DECLARE #msg as nVarchar(max);
DECLARE #RowCount as int = 0;
DECLARE #FETCHSTATUS as int = 0;
DECLARE #CountOfRowFailures AS INT = 0;
DECLARE #CountOfRowSuccesses AS INT = 0;
--------------------------------------------------------------------------------------------------
-- The order any type of the variables declared below
-- MUST match the order of the columns of the CURSOR SELECT statement below and
-- the data type of the underlying columns of the table "Call".
--
-- CALL TABLE COLUMNS:
--
DECLARE #External_ID AS BIGINT = NULL;
DECLARE #PlatformID2 AS INT = NULL;
DECLARE #ActorIDOfReseller AS INT = NULL;
DECLARE #ActorIDOfClient AS INT = NULL;
DECLARE #ActorIDOfExtension AS INT = NULL;
DECLARE #CallType AS NCHAR (10) = NULL;
DECLARE #CallInitiatedDate AS DATE = NULL;
DECLARE #CallInitiatedTimeHH24MI AS TIME (0) = NULL;
DECLARE #TimePeriodID AS INT = NULL;
DECLARE #CallAnswered AS DATETIME = NULL;
DECLARE #CallAnsweredYN AS BIT = NULL;
DECLARE #CallDispositionID AS INT = NULL;
DECLARE #CountryID AS INT = NULL;
DECLARE #CallPrefixID AS INT = NULL;
DECLARE #FromNumber AS VARCHAR (32) = NULL;
DECLARE #ToNumber AS VARCHAR (80) = NULL;
DECLARE #CallDuration AS INT = NULL;
DECLARE #CallCostToExtension AS DECIMAL (10, 6) = NULL;
DECLARE #CallCostToClient AS DECIMAL (10, 6) = NULL;
DECLARE #CallCostToReseller AS DECIMAL (10, 6) = NULL;
DECLARE #CallCostToAdmin AS DECIMAL (10, 6) = NULL;
DECLARE #Flow AS VARCHAR (3) = NULL;
DECLARE #CallStart AS DATETIME = NULL;
DECLARE #MoneyUnit AS VARCHAR (32) = NULL;
DECLARE #Prefix AS VARCHAR (32) = NULL;
DECLARE #External_CallID AS VARCHAR (255) = NULL;
-- END OF CALL TABLE COLUMNS:
--------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
-- SETTINGS
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION
PRINT ' ';
PRINT '===================================================================================';
EXEC [PrintSystemTime] 'Starting: [Call_Process_FromXoomTalk_UsingCursor]';
PRINT '===================================================================================';
--
-- It is assumed that the file loaded into the CallData table
-- only contains calls made in the year and month provided to this procedure
--
--
-- Move rows from the staging table [CallDATA] into the main table
-- [Call]. Some simple looks and modifications are made
--
-----------------------------------------------------------------
-- Define the cursor
-- WARNING - For simplicity keep column order in step with the
-- variable declarations, select and insert use!
--
DECLARE curNewData CURSOR FAST_FORWARD
FOR
SELECT Table1.*
FROM
( SELECT
-- ID is identity field
[id] AS [ExternalID]
, #PlatformId AS [PlatformID]
, ( SELECT SubQry.[ID]
FROM [dbo].[Actor] AS SubQry
WHERE SubQry.[ExternalID] = [NewData].client_reseller_id
) AS Actor_ResellerID
, ( SELECT SubQry.[ID]
FROM [dbo].[Actor] AS SubQry
WHERE SubQry.[ExternalID] = NewData.client_client_id
) AS Actor_ClientID
, ( SELECT SubQry.[ID]
FROM [dbo].[Actor] AS SubQry
WHERE SubQry.[ExternalID] = NewData.client_extension_id
) AS Actor_ExtensionID
-- This code prevent invalid values being loaded
-- that would be rejected byt eh foreigh key
, CASE [calltype]
WHEN 'out' THEN 'out'
WHEN 'in' THEN 'in'
WHEN 'local' THEN 'local'
WHEN 'elocal' THEN 'elocal'
ELSE NULL
END AS [calltype]
-----------------------------------------------------------------------------------------------
--
-- Initiated date/time is split to produce derrived columns
--
-- , [initiated] as [initiated] -- Useful for testing
-- NOTE that CAST will convert 0000-00-00 00:00:00 to be NULL
, TRY_CAST([initiated] AS DATE) AS [CallInitiatedDate]
, TRY_CAST([initiated] AS TIME(0)) AS [CallInitiatedTimeHH24MI]
, ( SELECT isnull(GroupWithTHISID,[ID]) as ID
FROM [dbo].[DimTimePeriod] AS SubQry
WHERE SubQry.[ActorID] = NewData.[client_client_id]
AND TRY_CAST([initiated] AS TIME(0)) >= SubQry.[StartTimeHH24MI]
AND TRY_CAST([initiated] AS TIME(0)) < SubQry.[EndTimeHH24MI]
) AS [TimePeriodID]
-- What format are their dates! Do they have nano secs
, TRY_CAST([answer] as datetime2) AS [CallAnswered]
, iif([answer] is null,0,1) AS [CallAnsweredYN]
-----------------------------------------------------------------------------------------------
, ( SELECT X.[ID]
FROM [dbo].[DimCallDisposition] AS X
WHERE X.[Disposition] = NewData.[disposion]
) AS [CallDispositionID]
-----------------------------------------------------------------------------------------------
--
-- Population of Country and Area require significant processing that is done later
--
, NULL AS [CountryID]
, NULL AS [AreaID]
-----------------------------------------------------------------------------------------------
-- Useful for testing
-- , [extension_number] AS [FromClientAndNumber]
, RIGHT([extension_number],LEN([extension_number])-CHARINDEX('*',[extension_number],1)) AS [FromNumber]
, CASE LEFT([partyid],2)
WHEN '00' THEN
-- Replace 00 with a + ie for all international numbers
'+' + SUBSTRING([partyid],3,9999)
ELSE [partyid]
END AS [ToNumber]
, [duration] AS [CallDuration_SECS]
-----------------------------------------------------------------------------------------------
-- COST columns
--
-- NOTE: Some data in the cost fields in the file from the xoomtalk platform
-- was in scientific notation format
-- eg 4.2e-05 4.2e-05 7.2e-05 6.2e-05 8.3e-05 8.3e-05
-- Note that these were all very small values!
, CASE
WHEN [costext] like '%E-%' THEN LTRIM(RTRIM(TRY_CAST(TRY_CAST([costext] AS FLOAT) AS DECIMAL(10,6))))
WHEN [costext] like '%E+%' THEN NULL
ELSE TRY_CAST([costext] AS DECIMAL(10,6))
END AS [CallCostToExtension]
, CASE
WHEN NewData.[costcl] like '%E-%' THEN LTRIM(RTRIM(TRY_CAST(TRY_CAST([costcl] AS FLOAT) AS DECIMAL(10,6))))
WHEN [costcl] like '%E+%' THEN NULL
ELSE TRY_CAST([costcl] AS DECIMAL(10,6))
END AS [CallCostToClient]
, CASE
WHEN NewData.[costres] like '%E-%' THEN LTRIM(RTRIM(TRY_CAST(TRY_CAST([costres] AS FLOAT) AS DECIMAL(10,6))))
WHEN [costres] like '%E+%' THEN NULL
ELSE TRY_CAST([costres] AS DECIMAL(10,6))
END AS [CallCostToReseller]
, CASE
WHEN NewData.[costadmin] like '%E-%' THEN LTRIM(RTRIM(TRY_CAST(TRY_CAST([costadmin] AS FLOAT) AS DECIMAL(10,6))))
WHEN [costadmin] like '%E+%' THEN NULL
ELSE TRY_CAST([costadmin] AS DECIMAL(10,6))
END AS [CallCostToAdmin]
-----------------------------------------------------------------------------------------------
, [flow] AS Flow
, [start] AS [CallStart]
, [moneyunit] AS [moneyunit]
, [prefix] AS [prefix]
, [callid] AS [External_CallID]
-----------------------------------------------------------------------------------------------
FROM [dbo].[CallDATA] AS NewData
WHERE ( (#LimitImportToRowsWithThisCallType IS NULL)
OR NewData.[calltype] = #LimitImportToRowsWithThisCallType
)
AND ( (#LimitImportToRowsWithThisFlow IS NULL)
OR NewData.[flow] = #LimitImportToRowsWithThisFlow
)
AND TRY_CAST([initiated] as datetime2) >= #StartOfMonthDate
AND TRY_CAST([initiated] as datetime2) <= #EndOfMonthDate
) as Table1
ORDER BY [calltype]
, [flow]
, [CallInitiatedDate]
, [CallInitiatedTimeHH24MI]
, [CallDuration_SECS]
;
-----------------------------------------------------------------
-- Open and use the cursor
--
PRINT '------------------------------------------------------------------';
PRINT 'ABOUT TO LIST the external_ID of any ROWS that failed to insert';
OPEN curNewData;
SET #FETCHSTATUS = 0;
SET #CountOfRowFailures = 0;
SET #CountOfRowSuccesses = 0;
WHILE #FETCHSTATUS = 0
BEGIN
FETCH NEXT FROM curNewData
INTO
#External_ID
, #PlatformID2
, #ActorIDOfReseller
, #ActorIDOfClient
, #ActorIDOfExtension
, #CallType
, #CallInitiatedDate
, #CallInitiatedTimeHH24MI
, #TimePeriodID
, #CallAnswered
, #CallAnsweredYN
, #CallDispositionID
, #CountryID
, #CallPrefixID
, #FromNumber
, #ToNumber
, #CallDuration
, #CallCostToExtension
, #CallCostToClient
, #CallCostToReseller
, #CallCostToAdmin
, #Flow
, #CallStart
, #MoneyUnit
, #Prefix
, #External_CallID
;
BEGIN TRY
INSERT INTO dbo.Call
(
[External_ID]
, [PlatformID]
, [ActorID-OfReseller]
, [ActorID-OfClient]
, [ActorID-OfExtension]
, [CallType]
, [CallInitiatedDate]
, [CallInitiatedTimeHH24MI]
, [TimePeriodID]
, [CallAnswered]
, [CallAnsweredYN]
, [CallDispositionID]
, [CountryID]
, CallPrefixID
, [FromNumber]
, [ToNumber]
, [CallDuration]
, [CallCostToExtension]
, [CallCostToClient]
, [CallCostToReseller]
, [CallCostToAdmin]
, [Flow]
, [CallStart]
, [MoneyUnit]
, [Prefix]
, [External_CallID]
)
VALUES
( #External_ID
, #PlatformID2
, #ActorIDOfReseller
, #ActorIDOfClient
, #ActorIDOfExtension
, #CallType
, #CallInitiatedDate
, #CallInitiatedTimeHH24MI
, #TimePeriodID
, #CallAnswered
, #CallAnsweredYN
, #CallDispositionID
, #CountryID
, #CallPrefixID
, #FromNumber
, #ToNumber
, #CallDuration
, #CallCostToExtension
, #CallCostToClient
, #CallCostToReseller
, #CallCostToAdmin
, #Flow
, #CallStart
, #MoneyUnit
, #Prefix
, #External_CallID
)
;
SET #CountOfRowSuccesses = #CountOfRowSuccesses + 1;
END TRY
BEGIN CATCH
-- Ignore error
PRINT CONCAT(#External_ID, '|', ERROR_NUMBER(), '|', ERROR_MESSAGE());
SET #CountOfRowFailures = #CountOfRowFailures + 1;
END CATCH;
SET #FETCHSTATUS = ##FETCH_STATUS;
END;
CLOSE curNewData;
DEALLOCATE curNewData;
PRINT '------------------------------------------------------------------';
PRINT CONCAT('Count Of Row Failures =', #CountOfRowFailures)
PRINT CONCAT('Count Of Row Successes=', #CountOfRowSuccesses)
PRINT '------------------------------------------------------------------';
COMMIT;
PRINT '################################ ';
PRINT 'COMMIT DONE';
PRINT '################################ ';
EXEC [PrintSystemTime] 'Ending: [Call_Process_FromXoomTalk_UsingCursor]';
PRINT '=========================================================================';
END TRY
BEGIN CATCH
IF #Raise1Print2Both3 = 2 OR #Raise1Print2Both3 = 3
EXEC [dbo].[ErrPrintErrorDetails]
#CalledFromProcedure= '[dbo].[Call_Process_FromXoomTalk_UsingCursor]'
, #ExplainitoryErrorTextForUser = NULL
, #AddDottedLine = 1;
EXEC [PrintSystemTime] 'Ending with Error: [Call_Process_FromXoomTalk_UsingCursor]' ;
PRINT '=========================================================================';
ROLLBACK;
PRINT '################################ ';
PRINT 'ROLLBACK DONE';
PRINT '################################ ';
IF #Raise1Print2Both3 = 1 OR #Raise1Print2Both3 = 3
THROW;
END CATCH;
END

Unfortunately sometimes you have to change your tactics when you move from Oracle to SQL Server.
Back to your case, if you are processing a large data set and you want to catch the bad rows, try give your raw imported table an unique id column, start from the smallest id, copy limited rows to the new table (use cast, not try_cast) each time. When you see an error, the insert will fail and you can reduce the number of insert rows until you reach the bad row. Move it to a "bad row" and continue try other data. After all rows copied, you have one table for the good rows and one for the bad rows. Then deal with the bad ones.
It may not be the fast solution but you don't have to spend too many times perfecting your script and basically it's a one-pass solution.
Here is the concept :
Alter table imported_table add lid int identity(1,1) not null primary key;
select top 1 into bad_rows from imported_table ;
truncate table bad_rows;
Then run this code:
declare #last_id int, #r int,#c int;
select #last_id= 0; #r = 100;
while #last_id >=0 begin
begin try
insert into new_table (.....)
select top(#r) .... from imported_table where lid>#last_id order by lid;
select #c=##rowcount;
if #c<1 select #last_id=-1 --- end of loop
else select #last_id=#last_id + #c;
select #r = 100;
end try
begin catch
if #r <2 begin
insert into bad_rows(...)
select top(#r)... from imported_table
where lid>#last_id order by lid;
select #lid=#lid+1;
end
else select #r=#r / 2;
end catch;
end;

Others have stated that using temp tables is a possibility.
If you want to easily create a temp table with the same data types as your source, you can use Select INTO syntax such as
Select *
INTO #tmp
From mytable
Where 1 = 0
You can use this with only select columns, joins, etc to create a temporary working space without having to declare variables and data types

Related

SQL Server stored procedure using case statement and variables "blows up" and won't filter down to Yes or No

I feed multiple variables to a stored procedure using a case statement. Two of the variables are uniqueidentifier that should cause it to return one row always.
However, I am forced to use a top 1 to get it to avoid an error stating my sub select is wrong or it returns hundreds of thousands of rows and acts as if it is ignoring all of the and statements to force it to filter down to one row. Something is causing the case statement not to treat the WHEN statements together as they should always return either yes or no.
ALTER PROCEDURE [dbo].[test_GetUserID_Date]
(#Enterpriseid CHAR(5),
#PracticeID CHAR(4),
#person_id UNIQUEIDENTIFIER,
#pi_encounter_id UNIQUEIDENTIFIER,
#user_id INTEGER,
#encdate VARCHAR(10),
#is_valid CHAR(1) OUTPUT)
AS
BEGIN
SET #is_valid = (SELECT TOP 1
CASE
WHEN pe.enterprise_id = #Enterpriseid
AND pe.practice_id = #PracticeID
AND pe.person_id = #person_id
AND pe.enc_id = #pi_encounter_id
AND pe.created_by = #user_id
AND CONVERT(VARCHAR, GETDATE(), 112) = #encdate
THEN 'Y'
ELSE 'N'
END
FROM patient_encounter pe)
END
The purpose of a CASE expression is to manipulate the value of a particular field within each and every row returned by a query.
If you run this query, what value do you get back?
SELECT COUNT(1)
FROM Patient_Encounter PE
;
Hundreds of thousands, like you said? - at least, that's what I assume. Because your CASE expression doesn't say "Only return the row where all of these parameters match the field values". What your CASE expression (in fact, any CASE expression) actually says is : "For every row returned from Patient_Encounter, if all of these parameters match, give me a 'Y'. Otherwise, give me an 'N'."
If you want to return only the row where those parameters match, the correct way would be to use those parameter checks within the WHERE clause of the query. Your query doesn't even have a WHERE clause, which means you'll be getting every single row in Patient_Encounter.
Try this:
ALTER PROCEDURE [dbo].[test_GetUserID_Date](
#Enterpriseid CHAR(5)
,#PracticeID CHAR(4)
,#person_id UNIQUEIDENTIFIER
,#pi_encounter_id UNIQUEIDENTIFIER
,#user_id INTEGER
,#encdate VARCHAR(10)
,#is_valid CHAR(1) OUTPUT
)
AS
BEGIN
IF EXISTS (
SELECT TOP 1 1
FROM Patient_Encounter PE
WHERE PE.enterprise_id = #Enterpriseid
AND PE.practice_id = #PracticeID
AND PE.person_id = #person_id
AND PE.enc_id = #pi_encounter_id
AND PE.created_by = #user_id
AND CONVERT(VARCHAR, GETDATE(), 112) = #encdate
)
SET #is_valid = 'Y'
ELSE
SET #is_valid = 'N'
END
You wouldn't have to use TOP 1 here, but I do, because I assumed from what you wrote that the presence of 1 row, or 10, or 1,000 means we should get a 'Y'.
You can test this outside of the procedure by just running it as a regular query:
DECLARE
#Enterpriseid CHAR(5)
,#PracticeID CHAR(4)
,#person_id UNIQUEIDENTIFIER
,#pi_encounter_id UNIQUEIDENTIFIER
,#user_id INTEGER
,#encdate VARCHAR(10)
,#is_valid CHAR(1)
;
IF EXISTS (
SELECT TOP 1 1
FROM Patient_Encounter PE
WHERE PE.enterprise_id = #Enterpriseid
AND PE.practice_id = #PracticeID
AND PE.person_id = #person_id
AND PE.enc_id = #pi_encounter_id
AND PE.created_by = #user_id
AND CONVERT(VARCHAR, GETDATE(), 112) = #encdate
)
SET #is_valid = 'Y'
ELSE
SET #is_valid = 'N'
;
SELECT #is_valid
;
Thanks 3BK this answer worked.
You wouldn't have to use TOP 1 here, but I do, because I assumed from what you wrote that the presence of 1 row, or 10, or 1,000 means we should get a 'Y'.
You can test this outside of the procedure by just running it as a regular query:
DECLARE
#Enterpriseid CHAR(5)
,#PracticeID CHAR(4)
,#person_id UNIQUEIDENTIFIER
,#pi_encounter_id UNIQUEIDENTIFIER
,#user_id INTEGER
,#encdate VARCHAR(10)
,#is_valid CHAR(1)
;
IF EXISTS (
SELECT TOP 1 1
FROM Patient_Encounter PE
WHERE PE.enterprise_id = #Enterpriseid
AND PE.practice_id = #PracticeID
AND PE.person_id = #person_id
AND PE.enc_id = #pi_encounter_id
AND PE.created_by = #user_id
AND CONVERT(VARCHAR, GETDATE(), 112) = #encdate
)
SET #is_valid = 'Y'
ELSE
SET #is_valid = 'N'
;
SELECT #is_valid
;

Resume a WHILE loop from where it stopped SQL

I have a while loop query that I only want to run until 11PM everyday - I'm aware this can be achieved with a WAITFOR statement, and then just END the query.
However, on the following day, once I re-run my query, I want it to continue from where it stopped on the last run. So I'm thinking of creating a log table that will contain the last processed ID.
How can I achieve this?
DECLARE #MAX_Value BIGINT = ( SELECT MAX(ID) FROM dbo.TableA )
DECLARE #MIN_Value BIGINT = ( SELECT MIN(ID) FROM dbo.TableA )
WHILE (#MIN_Value < #MAX_Value )
BEGIN
INSERT INTO dbo.MyResults
/* Do some processing*/
….
….
….
SET #MIN_Value = MIN_Value + 1
/*I only want the above processing to run until 11PM*/
/* Once it’s 11PM, I want to save the last used #MIN_Value
into my LoggingTable (dbo.Logging) and kill the above processing.*/
/* Once I re-run the query I want my processing to restart from the
above #MIN_Value which is recorded in dbo.Logging */
END
Disclaimer: I do not recommend using WHILE loops in SQL Server but considering the comment that you want a solution in SQL, here you go:
-- First of all, I strongly recommend using a different way of assigning variable values to avoid scenarios with the variable being NULL when the table is empty, also you can do it in a single select.
-- Also, if something started running at 10:59:59 it will let the processing for the value finish and will not simply rollback at 11.
CREATE TABLE dbo.ProcessingValueLog (
LogEntryId BIGINT IDENTITY(1,1) NOT NULL,
LastUsedValue BIGINT NOT NULL,
LastUsedDateTime DATETIME NOT NULL DEFAULT(GETDATE()),
CompletedProcessing BIT NOT NULL DEFAULT(0)
)
DECLARE #MAX_Value BIGINT = 0;
DECLARE #MIN_Value BIGINT = 0;
SELECT
#MIN_Value = MIN(ID),
#MAX_Value = MAX(ID)
FROM
dbo.TableA
SELECT TOP 1
#MIN_Value = LastUsedValue
FROM
dbo.ProcessingValueLog
WHERE
CompletedProcessing = 1
ORDER BY
LastUsedDateTime DESC
DECLARE #CurrentHour TINYINT = HOUR(GETDATE());
DECLARE #LogEntryID BIGINT;
WHILE (#MIN_Value < #MAX_Value AND #CurrentHour < 23)
BEGIN
INSERT INTO dbo.ProcessingValueLog (LastUsedValue)
VALUE(#MIN_Value)
SELECT #LogEntryID = SCOPE_IDENTITY()
// Do some processing...
SET #MIN_Value = #MIN_Value + 1;
UPDATE dbo.ProcessingValueLog
SET CompletedProcessing = 1
WHERE LogEntryId = #LogEntryID
SET #CurrentHour = HOUR(GETDATE())
END

Reset the ID counter on a stored procedure in SQL Server

I'm developing a system that manages work orders for vehicles. The ID of work orders is composed as follows: OT-001-16.
Where OT- is a string, 001 is the counter, followed by - character and finally the number 16 is the current year.
Example:
If the current year is 2018, the ID should be OT-001-18.
The problem is when the year changes, the counter must restart from 001. I have a stored procedure to do that, but i think i'm doing a lot more work.
This is my stored procedure code:
CREATE PROCEDURE ot (#name varchar(100), #area varchar(100), #idate varchar(100), #edate varchar(100))
AS
BEGIN
SET NOCOUNT ON;
DECLARE #aux varchar(100);
DECLARE #aux2 varchar(100);
DECLARE #aux3 int;
DECLARE #aux4 varchar(100);
SELECT #aux = id_workorder FROM idaux;
IF (#aux IS NULL)
SET #aux = CONCAT('OT-000-', RIGHT(YEAR(GETDATE()), 2));
SET
#aux2 = SUBSTRING(
#aux, CHARINDEX('-', #aux) + 1,
LEN(#aux) - CHARINDEX('-', #aux) - CHARINDEX('-', REVERSE(#aux)));
SET #aux3 = CAST(#aux2 AS int) + 1;
SET #aux4 = #aux3;
IF #aux3 < 1000
IF #aux3 >= 10
SET #aux4 = CONCAT('0', #aux4);
ELSE
SET #aux4 = CONCAT('00', #aux4);
ELSE
SET #aux4 = #aux4;
DECLARE #f varchar(100);
DECLARE #y varchar(50);
SELECT TOP 1
#y = id_workorder
FROM workorder
WHERE (RIGHT(id_workorder, 2)) = (RIGHT(YEAR(GETDATE()), 2))
ORDER BY id_workorder DESC;
DECLARE #yy varchar(10);
SET
#yy = RIGHT(#y, 2);
DECLARE #yn varchar(10);
SET
#yn = RIGHT(YEAR(GETDATE()), 2);
BEGIN
IF #yn = #yy
BEGIN
DECLARE #laux varchar(20)
SET #f = 'OT-' + #aux4 + '-' + RIGHT(YEAR(GETDATE()), 2);
INSERT INTO workorder (id_workorder, name, area, initial_date, end_date)
VALUES (#f, #name, #area, #idate, #edate);
SELECT
#laux = id_workorder
FROM idaux
IF (#laux IS NULL)
BEGIN
INSERT idaux (id_workorder) VALUES (#f);
END
ELSE
BEGIN
UPDATE idaux SET id_workorder = #f;
END
END
ELSE
BEGIN
SET #f = CONCAT('OT-001-', (RIGHT(YEAR(GETDATE()), 2)));
INSERT INTO workorder (id_workorder, name, area, initial_date, end_date)
VALUES (#f, #name, #area, #idate, #edate);
SELECT #laux = id_workorder FROM idaux;
IF (#laux IS NULL)
BEGIN
INSERT idaux (id_workorder) VALUES (#f);
END
ELSE
BEGIN
UPDATE idaux SET id_workorder = #f;
END
END
END
END
Basically, i created an auxiliar table to save the last Work Order ID, then from this table called idaux i take the ID and i compared to new possible ID by a string handling. Then if the year of the last ID saved are equal to the current year the counter increases, but if not the counter is restarted to 001, the new ID is updated in the auxiliar table and the Work Order is inserted to the table workorder.
My stored procedure works, but i need your help to optimize the stored procedure. Any question post on comments.
Here is how I'd setup the stored procedure and the underlying table to keep track of your work orders:
create database tmpWorkOrders;
go
use tmpWorkOrders;
go
/*
The work order ID (as you wish to see it) and the
work order counter (per year) will be separated into
two separate columns (with a unique constraint).
The work order ID (you wish to see) is automatically
generated for you and stored "persisted":
http://stackoverflow.com/questions/916068/sql-server-2005-computed-column-is-persisted
*/
create table WorkOrders
(
SurrogateKey int identity(1, 1) primary key not null,
WorkOrderYear int not null,
WorkOrderCounter int not null,
WorkOrderID as
N'OT-' + right(N'000' + cast(WorkOrderCounter as nvarchar), 3)
+ N'-' + right(cast(WorkOrderYear as nvarchar), 2)persisted,
WorkOrderDescription nvarchar(200),
constraint UQ_WorkOrderIDs
unique nonclustered (WorkOrderYear, WorkOrderCounter)
);
go
create procedure newWorkOrder
(#WorkOrderYear int = null,
#WorkOderCounter int = null,
#WorkOrderDescription nvarchar(200) = null
)
as
begin
/*
If no year is given the the current year is assumed
*/
if #WorkOrderYear is null
begin
set #WorkOrderYear = year(current_timestamp);
end;
/*
If no work order counter (for the above year) is given
then the next available one will be given
*/
if #WorkOderCounter is null
begin
set #WorkOderCounter
= isnull(
(
select max(WorkOrderCounter)
from WorkOrders
where WorkOrderYear = #WorkOrderYear
) + 1,
0
);
end;
else
/*
If a work order counter has been passed to the
stored procedure then it must be validated first
*/
begin
/*
Does the work order counter (for the given year)
already exist?
*/
if exists
(
select 1
from dbo.WorkOrders as wo
where wo.WorkOrderYear = #WorkOrderYear
and wo.WorkOrderCounter = #WorkOderCounter
)
begin
/*
If the given work order counter already exists
then the next available one should be assigned.
*/
while exists
(
select 1
from dbo.WorkOrders as wo
where wo.WorkOrderYear = #WorkOrderYear
and wo.WorkOrderCounter = #WorkOderCounter
)
begin
set #WorkOderCounter = #WorkOderCounter + 1;
end;
end;
end;
/*
The actual insert of the new work order ID
*/
insert into dbo.WorkOrders
(
WorkOrderYear,
WorkOrderCounter,
WorkOrderDescription
)
values
(#WorkOrderYear,
#WorkOderCounter,
#WorkOrderDescription
);
end;
go
/*
Some test runs with the new table and stored procedure...
*/
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = null,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 3,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 0,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 0,
#WorkOrderDescription = null;
exec dbo.newWorkOrder #WorkOrderYear = null,
#WorkOderCounter = 0,
#WorkOrderDescription = null;
/*
...reviewing the result of the above.
*/
select *
from dbo.WorkOrders as wo;
Note, that the "next available" work order counter is once given (1) as the maximum + 1 and once (2) increased until it does not violate the unique key constraint on the table anymore. Like this you have two different possibilities to go about it.
There are a number of observations based on your code that you could alter to optimize and guarantee your results.
I am not aware of your Table Structure, but it seems you are using natural keys for your IDs.
Instead, use a surrogate key, such as INT/BIGINT to not only add efficiency in your table joins (no strings required), but potentially add another layer of security in your current design.
Alternatively, normalize the column into the flags they are. For example: OT-001-05 has three elements: OT is a type of work order, 001 is the ID, and 15 is the year. Presently, OT determines the ID which determines the year.
SELECT #aux = id_workorder FROM idaux;
idaux was not described. Is it a single value? If tabular, guarantee the result or it might break in the future.
Even if you add MAX(id_workorder), your result will not work as you think. Since this is a VARCHAR, the greatest value of the leftmost character not tied will return.
#aux, CHARINDEX('-', #aux) + 1,
LEN(#aux) - CHARINDEX('-', #aux) - CHARINDEX('-', REVERSE(#aux)));
This is fine, but overall you could make the code clearer and easier to debug by splitting all three of those elements into their own variable. Your still using your method, but simplified a little (personally, CHARINDEX can be a pain).
SET #aux = #Type -- 'OT'
SET #aux2 = #ID -- The result of your previous code
SET #aux3 = #Year -- your YY from GETDATE()
-- then join
SET #Work_Order = CONCAT(#aux, '-', #aux2, '-', #aux3)
Update:
Currently, your column in idaux has the ID in the MIDDLE of your column. This will produce disastrous results since any comparison of IDs will happen in the middle of the column. This means at best you might get away with PATINDEX but are still performing a table scan on the table. No index (save for FULLTEXT) will be utilized much less optimized.
I should add, if you put the ID element into its own column, you might find using BINARY collations on the column will improve its performance. Note I have not tested attempting a BINARY collation on a mixed column

How can I populate a user defined table quickly?

I'm trying to create a generic histogram function to obtain histogram data for an arbitrary table in my db.
My db has many tables, each one is roughly a few gigs in size. Only some of the columns are numeric.
I started off by trying to pass in a user defined table valued parameter. The signature for my user defined function looked something like this:
CREATE TYPE dbo.numArray AS TABLE (
number real NOT NULL
);
CREATE FUNCTION dbo.fn_numericHistogram (
#values dbo.numArray READONLY,
#numOfBreaks int = 10,
#rangeMin float = NULL,
#rangeMax float = NULL
)
That worked, but it did not meet my performance requirements because I had to insert the existing numeric column into my user defined table first. That takes a long time. This long-running insertion happened inside the calling stored procedure, and looks something like this:
DECLARE #TVP AS dbo.numArray;
-- Takes far too long
INSERT INTO #TVP (number)
SELECT myNumericColumn
FROM dbo.SomeLargeTable;
EXEC dbo.fn_numericHistogram #values = #TVP READONLY
To get around this, my next approach was to simply pass in the table name as an nvarchar but it involves a lot of string manipulation and is quite ugly.
Does that seem like a reasonable workaround? I'd much rather go with the first approach but I don't know if it's possible to populate a UDT "by reference".
Thanks
*Edit:
Assuming I can quickly populate the #values UDT with ~2gigs worth of numeric data. My function would look like this:
CREATE TYPE dbo.numArray AS TABLE (
number real NOT NULL
);
CREATE FUNCTION dbo.fn_numericHistogram (
#values dbo.numArray READONLY,
#numOfBreaks int = 10,
#rangeMin float = NULL,
#rangeMax float = NULL
)
RETURNS #output TABLE (
lowerBound float NOT NULL,
upperBound float NOT NULL,
[count] int NOT NULL
)
BEGIN;
DECLARE #intervalSize float;
IF (#rangeMin IS NULL AND #rangeMax IS NULL)
BEGIN
SELECT
#rangeMinOUT = MIN(number),
#rangeMaxOUT = MAX(number)
FROM #values;
END
SET #intervalSize = (#rangeMax - #rangeMin)/#numOfBreaks;
INSERT INTO #output (lowerBound, upperBound, [count])
SELECT #rangeMin+#intervalSize*FLOOR(number/#intervalSize) AS lowerBound,
#rangeMin+#intervalSize*FLOOR(number/#intervalSize)+#intervalSize AS upperBound,
COUNT(*) AS [count]
FROM (
-- Special Case the max values.
SELECT ISNULL(NULLIF(number, #rangeMax), #rangeMax - 0.5 * #intervalSize - #rangeMin AS number
FROM #values
) AS B
GROUP BY FLOOR(number/#intervalSize);
';
RETURN;
END;
GO
Otherwise, I'll have to pass in a table name, and the function bloats to something like this: (By the way, I'm not even sure if this would work as a function...perhaps I would need a stored procedure instead).
CREATE FUNCTION dbo.fn_numericHistogram (
#tableName nvarchar(200),
#numericColumnName nvarchar(200),
#numOfBreaks int = 10,
#rangeMin float = NULL,
#rangeMax float = NULL
)
RETURNS #output TABLE (
lowerBound float NOT NULL,
upperBound float NOT NULL,
[count] int NOT NULL
)
BEGIN;
DECLARE #intervalSize float;
IF (#rangeMin IS NULL AND #rangeMax IS NULL)
BEGIN
DECLARE #SQLQuery nvarchar(MAX);
SET #SQLQuery = N'
SELECT
#rangeMinOUT = CONVERT(float, MIN('+#numericColumnName+')),
#rangeMaxOUT = CONVERT(float, MAX('+#numericColumnName+'))
FROM '+#tableName+';
EXEC sp_executesql #SQLQuery, N'rangeMinOUT nvarchar(50) OUTPUT, rangeMaxOUT nvarchar(50) OUTPUT',
#rangeMinOUT=#rangeMin OUTPUT, #rangeMaxOUT=#rangeMax OUTPUT;
END
SET #intervalSize = (#rangeMax - #rangeMin)/#numOfBreaks;
SET #SQLQuery = N'
INSERT INTO #output (lowerBound, upperBound, [count])
SELECT '+CONVERT(nvarchar, #rangeMin)+'+'+CONVERT(nvarchar, #intervalSize)+'*FLOOR(number/'+CONVERT(nvarchar, #intervalSize)+') AS lowerBound,
'+CONVERT(nvarchar, #rangeMin)+'+'+CONVERT(nvarchar, #intervalSize)+'*FLOOR(number/'+CONVERT(nvarchar, #intervalSize)+')+'+CONVERT(nvarchar, #intervalSize)+' AS upperBound,
COUNT(*) AS [count]
FROM (
-- Special Case the max values.
SELECT ISNULL(NULLIF('+#numericColumnName+', '+CONVERT(nvarchar, #rangeMax)+'), '+CONVERT(nvarchar, #rangeMax)+' - 0.5 * '+CONVERT(nvarchar, #intervalSize)+') - '+CONVERT(nvarchar, #rangeMin)+' AS number
FROM '+#tableName+'
) AS B
GROUP BY FLOOR(number/'+CONVERT(nvarchar, #intervalSize)+');'
-- Return the results above
RETURN;
END;
GO

GP proc only executes 42 transactions - Dexterity call to a SQL Procedure contains cursor

I am having issue with calling a SQL procedure from dexterity. The procedure contains cursor. This cursor is suppose to call another procedure which has a call to Dynamics GP Procedure 'taComputerChecklineinsert'. The working is supposed to be that the overall process has to insert transactions in the payroll transaction entry. Only a fixed number of 42 transactions get inserted. I have more than 42 transactions. If i execute the same procedure from SQL server with the same parameters itself it gives the required result. the issue comes up when i call from dexterity. what could be wrong?...i have been on this for long....and cannot figure out the issue.
Resolved finally. It has got nothing to go with any of the two econnect procedures namely 'taCreatePayrollBatchHeaderInsert' and 'taComputerChecklineinsert'.
It had raised due to a select statement before the batch creation by taCreatePayrollBatchHeaderInsert. the select statement was in place to select the parameters for taCreatePayrollBatchHeaderInsert.
The code worked perfectly fine when the select statement was commented.
CREATE PROC [dbo].[GTG_PR_Create_ABS_Trx]
#CMPANYID INT
, #UPRBCHOR INT -- 1 = Computer Check , 2 = Manual Check
, #BACHNUMB CHAR(15)
, #EMPLOYID CHAR(15)
, #COMPTRTP SMALLINT -- Computer transaction type:1 = Pay code; 2 = Deduction; 3 = Benefit
, #SALCHG SMALLINT -- Salary change ; required if passing a salary pay code:1 = Reallocate dollars; 2 = Reallocate hours;3=Reduce dollars;4=Reduce hours;=5=Additional amount
, #UPRTRXCD CHAR(6) -- (OT , ABS)
, #TRXBEGDT DATETIME
, #TRXENDDT DATETIME
, #Amount NUMERIC(19 , 5) -- Amount
, #ProcessStatus INT OUT
, #ErrorState INT OUT
, #ErrString VARCHAR(255) OUT
AS
set #ErrorState = 0
set #ErrString = ''
-- Create batch if it doesn`t exist
IF NOT EXISTS( SELECT 1 FROM DYNAMICS..UPR10304 WHERE BACHNUMB = #BACHNUMB AND CMPANYID = #CMPANYID AND UPRBCHOR = #UPRBCHOR )
BEGIN
**--SELECT #BACHNUMB
-- ,#UPRBCHOR
-- ,#ErrorState
-- ,#ErrString**
EXEC taCreatePayrollBatchHeaderInsert
#I_vBACHNUMB = #BACHNUMB
, #I_vUPRBCHOR = #UPRBCHOR
, #O_iErrorState = #ErrorState OUT
, #oErrString = #ErrString OUT
-- Associate employee deduction code if association doesn`t exist
IF NOT EXISTS(SELECT 1 FROM UPR00500 WHERE EMPLOYID = #EMPLOYID AND DEDUCTON = #UPRTRXCD)
BEGIN
EXEC taCreateEmployeeDeduction
#I_vEMPLOYID = #EMPLOYID
, #I_vDEDUCTON = #UPRTRXCD
, #O_iErrorState = #ErrorState OUT
, #oErrString = #ErrString OUT
END
-- Create Transaction
EXEC taCreateComputerCheckLineInsert
#I_vBACHNUMB = #BACHNUMB
, #I_vEMPLOYID = #EMPLOYID
, #I_vCOMPTRTP = #COMPTRTP
, #I_vSALCHG = #SALCHG
, #I_vUPRTRXCD = #UPRTRXCD
, #I_vTRXBEGDT = #TRXBEGDT
, #I_vTRXENDDT = #TRXENDDT
, #I_vVARDBAMT = #Amount
, #O_iErrorState = #ErrorState OUT
, #oErrString = #ErrString OUT
END
GO

Resources