Procedure takes long time to execute query - sql-server

I have the following SP for SQL Server. Strangely the SP has weired behaviour when executing the query
Select #max_backup_session_time = Max(MachineStat.BackupSessionTime) from MachineStat where MachineStat.MachineID = #machine_id;
It takes 1 second if the MachineStat table has rows pertaining to #machine_id but if there are no rows for a #machine_id then it takes more than half a minute to execute. Can someone please help me understand this.
SET NOCOUNT ON;
DECLARE #MachineStatsMId TABLE (
MachineId INT NULL,
BackupSessiontime BIGINT NULL,
MachineGroupName NVARCHAR(128) NULL )
DECLARE #machine_id AS INT;
DECLARE #Machine_group_id AS INT;
DECLARE #machine_group_name AS NVARCHAR(128);
DECLARE #max_backup_session_time AS BIGINT;
SET #machine_id = 0;
SET #Machine_group_id = 0;
SET #machine_group_name = '';
DECLARE MachinesCursor CURSOR FOR
SELECT m.MachineId,
m.MachineGroupId,
mg.MachineGroupName
FROM Machines m,
MachineGroups mg
WHERE m.MachineGroupId = mg.MachineGroupId;
OPEN MachinesCursor;
FETCH NEXT FROM MachinesCursor INTO #machine_id, #machine_group_id, #machine_group_name;
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #max_backup_session_time = Max(MachineStat.BackupSessionTime)
FROM MachineStat
WHERE MachineStat.MachineID = #machine_id;
INSERT INTO #MachineStatsMId
VALUES (#machine_id,
#max_backup_session_time,
#machine_group_name);
FETCH NEXT FROM MachinesCursor INTO #machine_id, #machine_group_id, #machine_group_name;
END;
SELECT *
FROM #MachineStatsMId;
CLOSE MachinesCursor;
DEALLOCATE MachinesCursor;
GO

Here is an alternate version that avoids a cursor and table variable entirely, uses proper (modern) joins and schema prefixes, and should run a lot quicker than what you have. If it still runs slow in certain scenarios, please post the actual execution plan for that scenario as well as an actual execution plan for the fast scenario.
ALTER PROCEDURE dbo.procname
AS
BEGIN
SET NOCOUNT ON;
SELECT
m.MachineId,
BackupSessionTime = MAX(ms.BackupSessionTime),
mg.MachineGroupName
FROM dbo.Machines AS m
INNER JOIN dbo.MachineGroups AS mg
ON m.MachineGroupId = mg.MachineGroupId
INNER JOIN dbo.MachineStat AS ms -- you may want LEFT OUTER JOIN here, not sure
ON m.MachineId = ms.MachineID
GROUP BY m.MachineID, mg.MachineGroupName;
END
GO

Related

How to clean up all entries in a cdc table in MS SQL?

Reading Microsoft Docs this is the relevant system procedure:
sys.sp_cdc_cleanup_change_table
I tried using it like this:
DECLARE #max_lsn binary(10);
SET #max_lsn = sys.fn_cdc_get_max_lsn();
Exec sys.sp_cdc_cleanup_change_table
#capture_instance = N'dbo_mytable',
#low_water_mark = #max_lsn;
The query is executed successfully but checking the table again with the query:
DECLARE #from_lsn binary(10), #to_lsn binary(10);
SET #from_lsn = sys.fn_cdc_get_min_lsn('dbo_mytable');
SET #to_lsn = sys.fn_cdc_get_max_lsn();
SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_mytable
(#from_lsn, #to_lsn, N'all');
Still returns a non empty table. I'm not familiar with SQL much. What am I missing?
I built a little test for this, and yes, I saw the same thing. It took me a couple of minutes to figure out what was going on.
The "gotcha" is this little entry in the docs:
If other entries in cdc.lsn_time_mapping share the same commit time as the entry identified by the new low watermark, the smallest LSN associated with that group of entries is chosen as the low watermark.
In other words, if the result of sys.fn_cdc_get_max_lsn() maps to a cdc.lsn_time_mapping.tran_begin_time that also has other start_lsn values associated with it, then the cleanup proc won't actually use the value of sys.fn_cdc_get_max_lsn() as the new low water mark.
In other other words, if the max lsn currently in the change table you want to clean up has the same tran_begin_time as other LSN's, and it is not the lowest of those LSNs, you cannot get a "complete" cleanup of the change table.
The easiest way to get a complete cleanup in those cases is probably to make a minor change to the target table to advance the max lsn and force a new entry, and "hope" that the new entry isn't also associated with any other LSNs with the same tran begin time.
To make that more explicit, here's my little test. Running it over and over has a result that in some cases cleanup is predicted to fail (and fails) and in other cases it is predicted to succeed (and succeeds).
/*
one time setup:
create table t(i int primary key, c char);
create table u(i int primary key, d char);
go
exec sp_cdc_enable_db;
go
exec sys.sp_cdc_enable_table #source_schema = 'dbo',
#source_name = 't',
#supports_net_changes = 1,
#role_name = null;
exec sys.sp_cdc_enable_table #source_schema = 'dbo',
#source_name = 'u',
#supports_net_changes = 1,
#role_name = null;
*/
set nocount on;
delete from t;
delete from u;
go
insert t select 1, 'a';
insert u select 1, 'b';
waitfor delay '00:00:01';
go
declare #fail int;
select #fail = count(*)
from cdc.lsn_time_mapping
where tran_begin_time = (
select tran_begin_time
from cdc.lsn_time_mapping
where start_lsn = sys.fn_cdc_get_max_lsn()
);
print iif(#fail > 1, 'this wont do the cleanup you expect', 'this will do the cleanup');
DECLARE #max_lsn binary(10) = sys.fn_cdc_get_max_lsn();
Exec sys.sp_cdc_cleanup_change_table
#capture_instance = N'dbo_t',
#low_water_mark = #max_lsn;
go
if exists (select * from cdc.dbo_t_ct) print 'did not cleanup';
else print 'did the cleanup';

Executing dynamic sql code with sp_executesql with return value

I'm looking for a way to execute the #batchSQL dynamic SQL through sp_executesql. I'm working on synchronizing tables between two databases and due to poor performance trying to see if synchronizing data in batches could help solve the performance issues.
The batching part is just an excerpt, but it would execute all stored procedures that synchronize data by starting with the first record in a table and calculating the next batch size based on how many records it can synchronize within one minute.
There is a default set for the first run: 10 for #batchSize and 0 for #batchRow. The calculations are not really relevant to the issue so I've left that part out and just kept the important bits. In the #batchSQL variable the #batchProcess contains the name of the next stored procedure to run.
In order to run the second batching process after the first 'default' run, the #cursRows variable gets the number of rows in the cursor with ##CURSOR_ROWS. This way the second batch process will start from the first record not yet synchronized. However after the sp_executesql line runs, the value returned is null in the BatchSize column of the BatchTable table.
When I'm not executing it as dynamic sql but hardcoding the #batchSQL line with the same values as used in the default run, it returns the value from #cursRows correctly.
DECLARE
#batchRow int, -- First row number included in batch
#batchSize int, -- Calculated batch size
#batchProcess nvarchar(100), -- Batch process name (stored procedure)
#batchSql nvarchar(max); -- Batch execution statement
#cursRowsOUT int, -- Number of rows loaded into the procedure cursor
#cursRows int, -- Number of rows loaded into the procedure cursor
SELECT #batchSize = 10, #batchRow = 0, #batchProcess = 'SYNCHRONIZE_ALL_CARS';
SELECT #batchSQL = concat(N'DECLARE #cursRows int EXEC [dbo].[', #batchProcess, N'] #batchRow= ', #batchRow, N', #batchSize= ', #batchSize, N', #cursRows = #cursRows OUTPUT');
EXEC sp_executesql #batchSQL, N'#cursRowsOUT int', #cursRowsOUT = #cursRows;
SET [dbo].[BatchTable] SET BatchSize = #cursRowsOUT
Here is the procedure that I'm testing with called by the previous code snippet.
ALTER PROCEDURE [dbo].[SYNCHRONIZE_ALL_CARS]
#batchRow int,
#batchSize int,
#cursRows int OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#carId nvarchar(15),
#manufacturer nvarchar(50),
#type nvarchar(50);
DECLARE sync_cursor CURSOR STATIC FOR
SELECT CAR_ID, MANUFACTURER, TYPE
FROM [TestData].[dbo].[CAR]
ORDER BY (SELECT NULL)
OFFSET #batchRow ROWS
FETCH NEXT #batchSize ROWS ONLY;
OPEN sync_cursor;
SET #cursRows = ##CURSOR_ROWS;
FETCH NEXT FROM sync_cursor INTO
#carId,
#manufacturer,
#type;
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC [dbo].[SYNCHRONIZE_CAR]
#carIdentifier = #carId,
#manufacturerName = #manufacturer,
#typeName = #type;
FETCH NEXT FROM sync_cursor INTO
#carId,
#manufacturer,
#type;
END
CLOSE sync_cursor;
DEALLOCATE sync_cursor;
END
Does anyone have an idea how to get #cursRows back from this query?
All help is greatly appreciated!
Your dynamic SQL is not quite right, because you are not passing back the value in an OUTPUT parameter, you are just dumping it into a local variable.
But you actually don't need dynamic SQL here at all. You can directly call a stored procedure whose name is stored in a variable
SELECT #batchSize = 10, #batchRow = 0, #batchProcess = 'dbo.[SYNCHRONIZE_ALL_CARS]';
EXEC #batchProcess
#batchRow = #batchRow,
#batchSize = #batchSize,
#cursRows = #cursRows OUTPUT;
If you really wanted to use dynamic SQL, you could do it like this (note that the parameters are passed right through rather than injected)
SELECT #batchSQL = N'
EXEC [dbo].' + QUOTENAME(#batchProcess) + N'
#batchRow = #batchRow,
#batchSize = #batchSize,
#cursRows = #cursRows OUTPUT;
';
EXEC sp_executesql #batchSQL,
N'#cursRows int',
#cursRows = #cursRows;

How to get and use the value returned by a stored procedure to a INSERT INTO... SELECT... statement

I am just new in SQL language and still studying it. I'm having hard time looking for answer on how can I use the stored procedure and insert value into a table.
I have this stored procedure:
CREATE PROCEDURE TestID
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NewID VARCHAR(30),
#GenID INT,
#BrgyCode VARCHAR(5) = '23548'
SET #GenID = (SELECT TOP (1) NextID
FROM dbo.RandomIDs
WHERE IsUsed = 0
ORDER BY RowNumber)
SET #NewID = #BrgyCode + '-' + CAST(#GenID AS VARCHAR (30))
UPDATE dbo.RandomIDs
SET dbo.RandomIDs.IsUsed = 1
WHERE dbo.RandomIDs.NextID = #GenID
SELECT #NewID
END;
and what I'm trying to do is this:
INSERT INTO dbo.Residents([ResidentID], NewResidentID, [ResLogdate],
...
SELECT
[ResidentID],
EXEC TestID ,
[ResLogdate],
....
FROM
source.dbo.Resident;
There is a table dbo.RandomIDs containing random 6 digit non repeating numbers where I'm pulling out the value via the stored procedure and updating the IsUsed column of the table to 1. I'm transferring data from one database to another database and doing some processing on the data while transferring. Part of the processing is generating a new ID with the required format.
But I can't get it to work Sad I've been searching the net for hours now but I'm not getting the information that I need and that the reason for my writing. I hope someone could help me with this.
Thanks,
Darren
your question is little bit confusing, because you have not explained what you want to do. As i got your question, you want to fetch random id from randomids table and after performed some processing on nextid you want to insert it into resident table [newresidentid] and end of the procedure you fetch data from resident table. if i get anything wrong feel free to ask me.
your procedure solution is following.
CREATE PROCEDURE [TestId]
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NEWID NVARCHAR(30)
DECLARE #GENID BIGINT
DECLARE #BRGYCODE VARCHAR(5) = '23548'
DECLARE #COUNT INTEGER
DECLARE #ERR NVARCHAR(20) = 'NO IDS IN RANDOM ID'
SET #COUNT = (SELECT COUNT(NEXTID) FROM RandomIds WHERE [IsUsed] = 0)
SET #GENID = (SELECT TOP(1) [NEXTID] FROM RandomIds WHERE [IsUsed] = 0 ORDER BY [ID] ASC)
--SELECT #GENID AS ID
IF #COUNT = 0
BEGIN
SELECT #ERR AS ERROR
END
ELSE
BEGIN
SET #NEWID = #BRGYCODE + '-' + CAST(#GENID AS varchar(30))
UPDATE RandomIds SET [IsUsed] = 1 WHERE [NextId] = #GENID
INSERT INTO Residents ([NewResidentId] , [ResLogDate] ) VALUES (#NEWID , GETDATE())
SELECT * FROM Residents
END
END
this procedure will fetch data from your randomids table and perform some processing on nextid than after it directs insert it into resident table and if you want to insert some data through user you can use parameter after declaring procedure name
E.G
CREATE PROCEDURE [TESTID]
#PARAM1 DATATYPE,
#PARAM2 DATATYPE
AS
BEGIN
END
I'm not convinced that your requirement is a good one but here is a way to do it.
Bear in mind that concurrent sessions will not be able to read your update until it is committed so you have to kind of "lock" the update so you will get a block until you're going to commit or rollback. This is rubbish for concurrency, but that's a side effect of this requirement.
declare #cap table ( capturedValue int);
declare #GENID int;
update top (1) RandomIds set IsUsed=1
output inserted.NextID into #cap
where IsUsed=0;
set #GENID =(select max( capturedValue) from #cap )
A better way would be to use an IDENTITY or SEQUENCE to solve your problem. This would leave gaps but help concurrency.

Duplicate Auto Numbers generated in SQL Server

Be gentle, I'm a SQL newbie. I have a table named autonumber_settings like this:
Prefix | AutoNumber
SO | 112320
CA | 3542
A whenever a new sales line is created, a stored procedure is called that reads the current autonumber value from the 'SO' row, then increments the number, updates that same row, and return the number back from the stored procedure. The stored procedure is below:
ALTER PROCEDURE [dbo].[GetAutoNumber]
(
#type nvarchar(50) ,
#out nvarchar(50) = '' OUTPUT
)
as
set nocount on
declare #currentvalue nvarchar(50)
declare #prefix nvarchar(10)
if exists (select * from autonumber_settings where lower(autonumber_type) = lower(#type))
begin
select #prefix = isnull(autonumber_prefix,''),#currentvalue=autonumber_currentvalue
from autonumber_settings
where lower(autonumber_type) = lower(#type)
set #currentvalue = #currentvalue + 1
update dbo.autonumber_settings set autonumber_currentvalue = #currentvalue where lower(autonumber_type) = lower(#type)
set #out = cast(#prefix as nvarchar(10)) + cast(#currentvalue as nvarchar(50))
select #out as value
end
else
select '' as value
Now, there is another procedure that accesses the same table that duplicates orders, copying both the header and the lines. On occasion, the duplication results in duplicate line numbers. Here is a piece of that procedure:
BEGIN TRAN
IF exists
(
SELECT *
FROM autonumber_settings
WHERE autonumber_type = 'SalesOrderDetail'
)
BEGIN
SELECT
#prefix = ISNULL(autonumber_prefix,'')
,#current_value=CAST (autonumber_currentvalue AS INTEGER)
FROM autonumber_settings
WHERE autonumber_type = 'SalesOrderDetail'
SET #new_auto_number = #current_value + #number_of_lines
UPDATE dbo.autonumber_settings
SET autonumber_currentvalue = #new_auto_number
WHERE autonumber_type = 'SalesOrderDetail'
END
COMMIT TRAN
Any ideas on why the two procedures don't seem to play well together, occasionally giving the same line numbers created from scratch as lines created by duplication.
This is a race condition or your autonumber assignment. Two executions have the potential to read out the same value before a new one is written back to the database.
The best way to fix this is to use an identity column and let SQL server handle the autonumber assignments.
Barring that you could use sp_getapplock to serialize your access to autonumber_settings.
You could use repeatable read on the selects. That will lock the row and block the other procedure's select until you update the value and commit.
Insert WITH (REPEATABLEREAD,ROWLOCK) after the from clause for each select.

performance problem sql server 2005 update sentence

I have a table "OFICIAL3" with 500k rows. and 30 columns. and table INSIS with 150k rows and 20 columns.
OFICIAL3.NUMERO_TITULO has an index.
INSIS.NumeroDocumento has an index too.
update sentence take long time. this process will take 9 hours in my machine
my machine is a core 2 duo 2.GHZ and 2GB RAM
ALTER PROCEDURE [dbo].[CompletarDatos] AS
declare #cantidad int;
declare #CONTADOR int;
declare #NRO_TITULO VARCHAR(600);
declare #POYECTO VARCHAR(200);
DECLARE #I_PROYECTO VARCHAR(500);
DECLARE #I_AREA_INT VARCHAR(500);
SET NOCOUNT ON
BEGIN
SET #cantidad =(select count(*) from OFICIAL3)
SET #CONTADOR=1
declare CURSORITO cursor for
select NUMERO_TITULO from OFICIAL3
open CURSORITO
fetch next from CURSORITO
into #NRO_TITULO
while ##fetch_status = 0
begin
SET #CONTADOR=#CONTADOR+1
PRINT 'ROW='+CONVERT(NVARCHAR(30),#CONTADOR)+' NRO TITULO='+#NRO_TITULO
SET #I_PROYECTO = (SELECT PROYECTO FROM INSIS WHERE NumeroDocumento=#NRO_TITULO)
SET #I_AREA_INT = (SELECT I_AREA_INTERVENCION FROM INSIS WHERE NumeroDocumento=#NRO_TITULO)
UPDATE OFICIAL3 SET PROYECT=#I_PROYECTO , COD_AREA=#I_AREA_INT WHERE NUMERO_TITULO=#NRO_TITULO
fetch next from CURSORITO into #NRO_TITULO
end
-- cerramos el cursor
close CURSORITO
deallocate CURSORITO
END
Assuming OFICIAL4 is a typo, this should work as a single update:
UPDATE o
SET PROYECT = i.PROYECTO,
COD_AREA = i.I_AREA_INTERVENCION
FROM OFICIAL3 o
INNER JOIN
INSIS i
ON o.NUMERO_TITULO = i.NumeroDocumento
As others have commented, an approach that avoids the CURSOR is vastly preferable from a performance point of view. Another thought is that a covering index on `INSIS (NumeroDocumento, PROYECTO, I_AREA_INTERVENCION) would speed things up further for this query.
Is there any way you can do this without a cursor? Removing the iteration should help it considerably.

Resources