sql server free tempdb - sql-server

We are running an import stored procedure in SQL Server 2008R2-SP1 that loads thousand of rows into several tables at a time. We are having problems with tempDB and transaction log size.
Something like this:
CREATE PROCEDURE spReallyHugeImportDataProcedure
#id int
AS
BEGIN
CREATE TABLE #temp(...)
INSERT INTO #temp
SELECT *
FROM AlotOfJoins
INSERT INTO FinalTable
SELECT * FROM AlotOfJoins
DROP #tempTable
INSERT INTO #temp
SELECT *
FROM AlotOfJoins
INSERT INTO FinalTable
SELECT * FROM AlotOfJoins
DROP #tempTable
INSERT INTO #temp
SELECT *
FROM AlotOfJoins
INSERT INTO FinalTable
SELECT * FROM AlotOfJoins
DROP #tempTable
-- And so on....
END
We are trying to split the whole process and run several times for a small set of data.
Like this:
CREATE PROCEDURE spReallyHugeImportDataProcedure
#id int
AS
BEGIN
DECLARE #SomeSortOfCounter int = 100
WHILE(#SomeSortOfCounter <> 0)
BEGIN
-- TRY TO START A NEW TRANSACTION
BEGIN TRAN
CREATE TABLE #temp(...)
INSERT INTO #temp
SELECT *
FROM AlotOfJoins
WHERE SomeFileterWorkinWithTheCounter = #SomeSortOfCounter
INSERT INTO FinalTable
SELECT * FROM AlotOfJoins
DROP #tempTable
INSERT INTO #temp
SELECT *
FROM AlotOfJoins
WHERE SomeFileterWorkinWithTheCounter = #SomeSortOfCounter
INSERT INTO FinalTable
SELECT * FROM AlotOfJoins
DROP #tempTable
INSERT INTO #temp
SELECT *
FROM AlotOfJoins
WHERE SomeFileterWorkinWithTheCounter = #SomeSortOfCounter
INSERT INTO FinalTable
SELECT * FROM AlotOfJoins
DROP #tempTable
-- And so on....
-- TRY TO RELASE TEMP OBJECTS,
-- OR GIVE TO THE SERVER THE OPORTUNITY TO DO IT
COMMIT
SET #SomeSortOfCounter = #SomeSortOfCounter - 1
END
END
Is it possible for the SQL Server engine to work between those internal transactions?

Option 1: Using a table in a user database
If you really need to store the data in a temporary table, build that table in a user database (ImportTemp for example or in your destination DB) then use it instead of the tempdb. In this case, the SQL Server should not use as much space in TempDB and your data will be stored persistently -> you can reuse it and you can split your loader queries into multiple batches.
Optionally you can move this table to a different filegroup to prevent concurent writing and lower the chance of interferring with other processes.
In this case, the steps are:
Drop the 'temp' table if it exists
Create the 'temp' table (in a user database)
Fill the 'temp' table with the necessary data
Load the destination tables from the persisted 'temp' table
Drop the 'temp' table to free up space in data files
Optionally shrink the data and log files which are related to your 'temp' table
If you are using this import table relatively frequently, you can only truncate it before and after use instead of dropping and recreating it.
Option 2: Using an ETL tool
Use an ETL tool which can handle data in batches / buffers. If you are using SQL Server Standard or above you have the option to use SSIS (SQL Server Integration Services).
DBCC SHRINKFILE
You can release unused space from data and log files using the DBCC SHRINKFILE command:
USE [YourDatabase]
DBCC SHRINKFILE
(
{ file_name | file_id }
{ [ , EMPTYFILE ]
| [ [ , target_size ] [ , { NOTRUNCATE | TRUNCATEONLY } ] ]
}
)
[ WITH NO_INFOMSGS ]
Example
USE [tempdb]
DBCC SHRINKFILE (tempdb_data, TRUNCATEONLY)
Optionally
You can spread the TempDB files accross drives by adding additional data files to the TempDB:
ALTER DATABASE tempdb
ADD FILE (NAME = tempdev2, FILENAME = 'W:\tempdb2.mdf', SIZE = 256);
A related question: How to spread tempdb over multiple files?

simple answer is yes, as long as there is no outer transaction outside of the stored proc. also, there's no reason to add in explicit transactions. simply creating a loop and only working with a chunk of records per statement will let your tlog space get reused and you won't be forcing all of the records at one time into a temp table.

Related

how to trace SQL table drop create events

I have some mysterious problem where every day one table in DB (SQL Server 2016) is being recreated (I suppose dropped and created) with old data. I checked various options to try to find what process is doing this, however was unable to do that.
Scheduled Tasks - nothing
SQL Agent Jobs - nothing
How to trace what user/application/anythingelse is doing this ?
I tried launching SQL Profiler and starting manual trace, but after some time (half a day or so) it just stopped.
The default trace captures schema changes. Review the Schema Change History report or run the query below to retrieve the info in T-SQL. Note that the default trace rollover files are limited to 5 files of up to 20MB each so older events may have rolled off.
--select object created and deleted information from default trace
SELECT
trace_table.StartTime
, te.name
, trace_table.ObjectName
, trace_table.ApplicationName
, trace_table.LoginName
FROM (
SELECT REVERSE(SUBSTRING(REVERSE(path), CHARINDEX('\', REVERSE(path)) , 255)) + 'log.trc'
FROM sys.traces
WHERE
is_default = 1
) AS trace(path)
CROSS APPLY sys.fn_trace_gettable(trace.path, DEFAULT) AS trace_table
JOIN sys.trace_events AS te ON
te.trace_event_id = trace_table.EventClass
WHERE
EventSubClass = 0
AND name IN('Object:Created', 'Object:Deleted')
ORDER BY StartTime;
create a database trigger and log the create/drop table events:
create table dbo.traceTabledropcreate(EventDataXML xml, LogDatetime datetime default(getdate()));
go
create or alter trigger dbtrigger_traceTabledropcreate
on database
with execute as 'dbo'
for CREATE_TABLE, DROP_TABLE
as
begin
set nocount on;
--insert into dbo.traceTabledropcreate(EventDataXML)
--values (EVENTDATA());
declare #sessionxml xml =
(
select EVENTDATA(),
(
select *
from sys.dm_exec_sessions
where session_id = ##spid
for xml path('sessioninfo'), type
)
for xml path('')
);
insert into dbo.traceTabledropcreate(EventDataXML)
values (#sessionxml);
end
go
---..... and wait....
--..test
create table dbo.testtable(id int)
go
select *
from dbo.traceTabledropcreate
go
drop table dbo.testtable
go
select *
from dbo.traceTabledropcreate
go

The ALTER DATABASE statement is not allowed within a trigger

I want to create the file group dynamically when user want to insert data into the table, but SQL Server throws an exception.
I know that I can handle this with SQL Server Agent, but if my approach isn't correct please tell me the correct way.
Kind regards.
ALTER TRIGGER [AuditTrigger]
ON [Audit]
INSTEAD OF INSERT
AS
BEGIN
DECLARE #DateInserted DATETIME = (SELECT DateInserted FROM inserted);
DECLARE #NextRange DATETIME;
DECLARE #currentFileGroup NVARCHAR(MAX)= ('APP_PT_' + CAST(YEAR(#DateInserted) AS NVARCHAR(4)) +'_'+ CAST(MONTH(#DateInserted) AS NVARCHAR(2)))
--print #currentFileGroup;
DECLARE #fileExsits BIT = (SELECT (CASE WHEN EXISTS(SELECT NULL AS [EMPTY] FROM SYS.FILEGROUPS WHERE name LIKE #currentFileGroup) THEN 1 ELSE 0 END))
IF #fileExsits = 0
BEGIN
SET #NextRange = (SELECT Replace(CONVERT(VARCHAR(10), #DateInserted, 111),'/','-'))
DECLARE #filefullname VARCHAR(MAX) = (SELECT physical_name FROM SYS.DATABASE_FILES WHERE name = 'DB_Test')
DECLARE #fgFullName VARCHAR(MAX) = (SELECT (LEFT(#filefullname, LEN(#filefullname) - CHARINDEX('\', REVERSE(#filefullname))) + '.ndf'))
-- The exception occurs here --
ALTER DATABASE DB_TEST
ADD FILE (NAME = [#currentFileGroup],
FILENAME = [#fgFullName],
SIZE = 5MB,
MAXSIZE = 100MB,
FILEGROWTH = 1MB)
TO FILEGROUP Audit_2017
ALTER PARTITION FUNCTION [PF]()
SPLIT RANGE (#NextRange);
ALTER PARTITION SCHEME [PS]
NEXT USED [#currentFileGroup];
END
INSERT INTO LogTable VALUES (#currentFileGroup)
INSERT INTO [Audit]
SELECT DateInserted, Title
FROM inserted;
END
Result:
Msg 287, Level 16, State 2, Procedure AuditTrigger, Line 24
The ALTER DATABASE statement is not allowed within a trigger.
Instead of a trigger, you could use a stored procedure for the Audit table inserts and include the filegroup/file/partition maintenance code there. Note that this trigger will fail on multi-row inserts due to the subquery.
That said, I think the scheduled daily job approach for partition maintenance is cleaner. Not sure why you are bothering to create a new file and filegroup for each partition. Unless you have a special use case, you could simply place each partition on the same filegroup. Make sure the partition function is RANGE RIGHT to avoid excessive data movement and logging during SPLIT.

MSSQL Stored Procedure Creating A Temp Table Dynamically

We're trying to write some automated reports to execute SQL statements we have stored in a table. The table data is normally used in a stored procedure called by the triggers and uses data passed in via temp tables (created in the trigger statements), and has a table name, then an SQL statement that works on #TempInserted and #TempDeleted, which correspond to the Inserted and Deleted objects from the trigger and then some e-mail columns that determine where to send the output.
This all works fine from the trigger statements, as each creates each temp table once, during execution:-
SELECT * INTO #TempInserted FROM INSERTED
SELECT * INTO #TempDeleted FROM DELETED
Then the trigger calls the TriggerHandler stored procedure, passing the table name through as a pararmeter.
..
However, when I try to create these dynamically from a general stored procedure in order to fire off these statements as reports (so we don't duplicate the statements), in a batch, I'm hitting a problem:-
SELECT * INTO #TempInserted FROM ...
works fine from a defined table, or object (e.g. "FROM INSERTED"), but I've found that it can't get it's schema from a dynamic query.
For example, I can do
SELECT TOP 1 * INTO #Test FROM TableA
SELECT * FROM #Test
DROP TABLE #Test
But I can't then do
EXECUTE sp_executesql N'SELECT TOP 1 * INTO #Test FROM TableA'
SELECT * FROM #Test
DROP TABLE #Test
because then #Test is local to the EXECUTE context, and not its parent.
I can, however, do the insert in the EXECUTE (or a stored procedure) because the temp table is in scope, if I've already created the table schema:-
SELECT * INTO #Test FROM TableA WHERE 1 = 2 -- create an empty schema
EXECUTE sp_executesql N'INSERT INTO #Test SELECT TOP 10 * FROM TableA'
SELECT * FROM #Test
DROP TABLE #Test
So, that's OK, but my problem comes when I want to dynamically create that schema, depending on the table name were running the reports for. The INSERT works:-
SELECT * INTO #Test FROM TableA WHERE 1 = 2 -- create an empty schema
DECLARE #Table NVARCHAR(20) = 'TableA'
DECLARE #SQL NVARCHAR(200) = N'INSERT INTO #Test SELECT TOP 10 * FROM ' + #Table
EXECUTE sp_executesql #SQL
SELECT * FROM #Test
DROP TABLE #Test
But only if the temp table already has a schema. If I try to conditionally create the schema, depending on the table selected, I get a parsing error:-
DECLARE #Table NVARCHAR(20) = 'TableA'
IF #Table = 'TableA'
SELECT * INTO #Test FROM TableA WHERE 1 = 2 -- create an empty schema
IF #Table = 'TableB'
SELECT * INTO #Test FROM TableB WHERE 1 = 2 -- create an empty schema
DECLARE #SQL NVARCHAR(200) = N'INSERT INTO #Test SELECT TOP 10 * FROM ' + #Table
EXECUTE sp_executesql #SQL
SELECT * FROM #Test
DROP TABLE #Test
gives "There is already an object named '#Test' in the database." - so the query parser isn't following the structure of the query, which only actually creates the temp table once. This also holds true if you do
SELECT * INTO #Test FROM ....
DROP TABLE #Test
SELECT * INTO #Test FROM ....
So, is there a way in SQL Server 2012, of either being able to do
SELECT * INTO #Test FROM (dynamic SQL statement)
or to bypass the parser thinking you're creating the object twice
DECLARE #Table NVARCHAR(20) = 'TableA'
IF #Table = 'TableA'
SELECT * INTO #Test FROM TableA WHERE 1 = 2 -- create an empty schema
IF #Table = 'TableB'
SELECT * INTO #Test FROM TableB WHERE 1 = 2 -- create an empty schema
or to dynamically create the locally scoped temp table, from an existing database table's schema, where the table name is stored in a variable (all the examples I've found of this use the "SELECT * INTO #Test" code, which as I mentioned requires a statically defined object to create from)?
-------edit--------
For a bit of context, here's an example of why we're doing this:-
A trigger may fire producing a warning e-mail if a certain item type is transacted into a certain location. This works with our current triggers. The reason we're doing this is so that we can, in future, write a UI so the users can add other item types to this list themselves, rather than us having to update the trigger - this also means that we can control/validate the SQL being generated, behind the scenes of a point-and-click interface so that our users don't need to know any SQL and that we can be sure that nothing malicious or that will cause errors will be used.
We also can't do this in the BLL because it's from our ERP system and this would then mean we'd have to make changes to base objects, which is obviously undesirable if it can be avoided.
There is the potential for some of these e-mails to be missed/ignored/forgotten/not-actioned, so the users requested the same information on a periodic basis, as well as as-at the transaction occurring:-
So, next, we want to produce, for some of these trigger statements, daily/weekly/monthly reports. Now, obviously, it would be ideal if we could use the existing SQL trigger statements we have set up as then if one were changed it would then automatically affect the periodical reports - stay DRY. It would also mean that if we set up a new trigger, we could automatically include it in the reports by merely inserting a reference to the trigger code, along with the table name, frequency, etc, into the table that drives the periodical reports stored procedure. Again, in future, we could then write a UI, so that users can then request and schedule these reports themselves, with no intervention required from us.
I suspect I'm stuck in a catch-22 situation here. However, I've found a way around it that isn't too messy. I extract the item processing code into another stored procedure, and then compound execution of that onto the dynamic "SELECT INTO" statement - that way it runs in the same execution instance and thus has access to the temp table created in, and local to, that instance:-
SET #SQL = 'SELECT * INTO #TestTable FROM ' + #Table + ' WHERE ' + #WhereClause
SET #SQL = #SQL + '; EXEC ReportProcess'
EXECUTE sp_executeSQL #SQL
the ReportProcess stored procedure then has access to the temporary table and can process it, accordingly

Should I drop temp table in this scenario?

I have a stored procedure called in a .Net webservice that works like this (pseudo-code):
CREATE PROC SomeProc AS
BEGIN TRY
BEGIN TRAN
IF EXISTS (SELECT * FROM sys.tables WHERE name LIKE '#temp%')
DROP TABLE #temp;
CREATE TABLE #temp (...);
/* lots of logic here */
-- clear up
IF EXISTS (SELECT * FROM sys.tables WHERE name LIKE '#temp%')
DROP TABLE #temp;
COMMIT TRAN
END TRY
BEGIN CATCH
IF EXISTS (SELECT * FROM sys.tables WHERE name LIKE '#temp%')
DROP TABLE #temp;
ROLLBACK TRAN;
END CATCH
The proc is always accessed via the same connection to the database (as defined in a config file).
Somebody has raised a concern that if the webservice, and thus the procedure gets called twice in quick succession, the there is a danger that the temp table for the second call would be deleted by the first call.
Is this correct? I thought SQL Server was synchronous, so that two procedures couldn't be called at the same time and SQL would queue the requests? This post seems to suggest I am doing the right thing but the multi-thread answer concerns me. Any clarification would be helpful please.
Local temporary (#) tables are session scoped, there is no way that some other session could interfere with a temp table created in your session.
If you do a select on sys.tables in tempdb, you will see that every temp table is suffixed with a session identificator.
Also, there is no need to explicitely drop a temp table in stored procedure, SQL Server will do the automatic cleanup, and also cache the metadata for possible performance benefit.
It is possible to use TRUNCATE TABLE.
TRUNCATE TABLE #temp;

Linked Server Insert-Select Performance

Assume that I have a table on my local which is Local_Table and I have another server and another db and table, which is Remote_Table (table structures are the same).
Local_Table has data, Remote_Table doesn't. I want to transfer data from Local_Table to Remote_Table with this query:
Insert into RemoteServer.RemoteDb..Remote_Table
select * from Local_Table (nolock)
But the performance is quite slow.
However, when I use SQL Server import-export wizard, transfer is really fast.
What am I doing wrong? Why is it fast with Import-Export wizard and slow with insert-select statement? Any ideas?
The fastest way is to pull the data rather than push it. When the tables are pushed, every row requires a connection, an insert, and a disconnect.
If you can't pull the data, because you have a one way trust relationship between the servers, the work around is to construct the entire table as a giant T-SQL statement and run it all at once.
DECLARE #xml XML
SET #xml = (
SELECT 'insert Remote_Table values (' + '''' + isnull(first_col, 'NULL') + ''',' +
-- repeat for each col
'''' + isnull(last_col, 'NULL') + '''' + ');'
FROM Local_Table
FOR XML path('')
) --This concatenates all the rows into a single xml object, the empty path keeps it from having <colname> </colname> wrapped arround each value
DECLARE #sql AS VARCHAR(max)
SET #sql = 'set nocount on;' + cast(#xml AS VARCHAR(max)) + 'set nocount off;' --Converts XML back to a long string
EXEC ('use RemoteDb;' + #sql) AT RemoteServer
It seems like it's much faster to pull data from a linked server than to push data to a linked server: Which one is more efficient: select from linked server or insert into linked server?
Update: My own, recent experience confirms this. Pull if possible -- it will be much, much faster.
Try this on the other server:
INSERT INTO Local_Table
SELECT * FROM RemoteServer.RemoteDb.Remote_Table
The Import/Export wizard will be essentially doing this as a bulk insert, where as your code is not.
Assuming that you have a Clustered Index on the remote table, make sure that you have the same Clustered index on the local table, set Trace flag 610 globally on your remote server and make sure remote is in Simple or bulk logged recovery mode.
If you're remote table is a Heap (which will speed things up anyway), make sure your remote database is in simple or bulk logged mode change your code to read as follows:
INSERT INTO RemoteServer.RemoteDb..Remote_Table WITH(TABLOCK)
SELECT * FROM Local_Table WITH (nolock)
The reason why it's so slow to insert into the remote table from the local table is because it inserts a row, checks that it inserted, and then inserts the next row, checks that it inserted, etc.
Don't know if you figured this out or not, but here's how I solved this problem using linked servers.
First, I have a LocalDB.dbo.Table with several columns:
IDColumn (int, PK, Auto Increment)
TextColumn (varchar(30))
IntColumn (int)
And I have a RemoteDB.dbo.Table that is almost the same:
IDColumn (int)
TextColumn (varchar(30))
IntColumn (int)
The main difference is that remote IDColumn isn't set up as as an ID column, so that I can do inserts into it.
Then I set up a trigger on remote table that happens on Delete
Create Trigger Table_Del
On Table
After Delete
AS
Begin
Set NOCOUNT ON;
Insert Into Table (IDColumn, TextColumn, IntColumn)
Select IDColumn, TextColumn, IntColumn from MainServer.LocalDB.dbo.table L
Where not exists (Select * from Table R WHere L.IDColumn = R.IDColumn)
END
Then when I want to do an insert, I do it like this from the local server:
Insert Into LocalDB.dbo.Table (TextColumn, IntColumn) Values ('textvalue', 123);
Delete From RemoteServer.RemoteDB.dbo.Table Where IDColumn = 0;
--And if I want to clean the table out and make sure it has all the most up to date data:
Delete From RemoteServer.RemoteDB.dbo.Table
By triggering the remote server to pull the data from the local server and then do the insert, I was able to turn a job that took 30 minutes to insert 1258 lines into a job that took 8 seconds to do the same insert.
This does require a linked server connection on both sides, but after that's set up it works pretty good.
Update:
So in the last few years I've made some changes, and have moved away from the delete trigger as a way to sync the remote table.
Instead I have a stored procedure on the remote server that has all the steps to pull the data from the local server:
CREATE PROCEDURE [dbo].[UpdateTable]
-- Add the parameters for the stored procedure here
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
--Fill Temp table
Insert Into WebFileNamesTemp Select * From MAINSERVER.LocalDB.dbo.WebFileNames
--Fill normal table from temp table
Delete From WebFileNames
Insert Into WebFileNames Select * From WebFileNamesTemp
--empty temp table
Delete From WebFileNamesTemp
END
And on the local server I have a scheduled job that does some processing on the local tables, and then triggers the update through the stored procedure:
EXEC sp_serveroption #server='REMOTESERVER', #optname='rpc', #optvalue='true'
EXEC sp_serveroption #server='REMOTESERVER', #optname='rpc out', #optvalue='true'
EXEC REMOTESERVER.RemoteDB.dbo.UpdateTable
EXEC sp_serveroption #server='REMOTESERVER', #optname='rpc', #optvalue='false'
EXEC sp_serveroption #server='REMOTESERVER', #optname='rpc out', #optvalue='false'
If you must push data from the source to the target (e.g., for firewall or other permissions reasons), you can do the following:
In the source database, convert the recordset to a single XML string (i.e., multiple rows and columns combined into a single XML string).
Then push that XML over as a single row (as a varchar(max), since XML isn't allowed over linked databases in SQL Server).
DECLARE #xml XML
SET #xml = (select * from SourceTable FOR XML path('row'))
Insert into TempTargetTable values (cast(#xml AS VARCHAR(max)))
In the target database, cast the varchar(max) as XML and then use XML parsing to turn that single row and column back into a normal recordset.
DECLARE #X XML = (select '<toplevel>' + ImportString + '</toplevel>' from TempTargetTable)
DECLARE #iX INT
EXEC sp_xml_preparedocument #ix output, #x
insert into TargetTable
SELECT [col1],
[col2]
FROM OPENXML(#iX, '//row', 2)
WITH ([col1] [int],
[col2] [varchar](128)
)
EXEC sp_xml_removedocument #iX
I've found a workaround. Since I'm not a big fun of GUI tools like SSIS, I've reused a bcp script to load table into csv and vice versa. Yeah, it's an odd case to have the bulk operation support for files, but tables. Feel free to edit the following script to fit your needs:
exec xp_cmdshell 'bcp "select * from YourLocalTable" queryout C:\CSVFolder\Load.csv -w -T -S .'
exec xp_cmdshell 'bcp YourAzureDBName.dbo.YourAzureTable in C:\CSVFolder\Load.csv -S yourdb.database.windows.net -U youruser#yourdb.database.windows.net -P yourpass -q -w'
Pros:
No need to define table structures every time.
I've tested and it worked way faster than inserting directly through
the LinkedServer.
It's easier to manage than XML (which is limited to
varchar(max) length anyway).
No need of an extra layout of abstraction (tools like SSIS).
Cons:
Using the external tool bcp through the xp_cmdshell interface.
Table properties will be lost after ex/im-poring csv (i.e. datatype, nulls,length, separator within value, etc).

Resources