Is there script/procedure/function that can generate a CREATE TABLE statement from an arbitrary SQL query?
When building procedures, I'd like to have a quick way to generate a temporary table, rather than having to review the table definitions of all of the tables referenced in the query.
Simple example:
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
Would generate (columns' data definition is retrieved from the system table):
-- randomly-generated table name
CREATE TABLE #random_name (
PAT_ID VARCHAR(18) NOT NULL,
PAT_NAME VARCHAR(200),
ENC_ID NUMERIC(18,0) NOT NULL,
ADMSN_TIME DATE,
DISCH_TIME DATE
)
SSMS workflow:
select the text
right click, select Generate CREATE TABLE statement (function/script/proc called; result placed on clipboard)
place cursor in desired location
paste
This is a bit of a hack, but you could try selecting into a table (see line #3):
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
INTO delete_me
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
Then you can highlight the delete_me table in SSMS, right-click, and generate the CREATE TABLE script.
Finally, you'd want to DROP TABLE delete_me to clean up.
use
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
into #randomtable
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
and your table will be created when you execute the statement.
First of all, I would make a stored procedure of the query. (It keeps me from forgetting it later)
Secondly, I would write a query to generate the table for me:
DECLARE #CREATE_TABLE_QUERY NVARCHAR(MAX) = N'';
SELECT
#CREATE_TABLE_QUERY += ', ' + name + ' ' + UPPER(system_type_name) + CHAR(13) + CHAR(10) + CHAR(9)
FROM
sys.dm_exec_describe_first_result_set('YOUR_PROCEDURE_NAME_HERE', NULL, 1);
SELECT
#CREATE_TABLE_QUERY = N'CREATE TABLE TABLE_NAME_HERE(' + CHAR(13) + CHAR(10) + CHAR(9) + STUFF(#CREATE_TABLE_QUERY, 1, 1, N'') + ');';
PRINT #CREATE_TABLE_QUERY;
Note: Replace 'YOUR_PROCEDURE_NAME_HERE' with the name of your own stored procedure.
Note: Replace TABLE_NAME_HERE with the table name of your choice.
The above will generate something like this:
CREATE TABLE TABLE_NAME_HERE(
WeekName VARCHAR(40)
, Line Name VARCHAR(50)
, TheDate DATETIME
, ReceivedAll INT
, Answered INT
, Abandoned INT
, Call Length INT
, WaitTimeAnswer INT
, WaitTimeAbandon INT
, PeriodName VARCHAR(10)
, Week SMALLINT
, Period SMALLINT
, Year SMALLINT
, WeekInPeriod SMALLINT
, NumWeeksInPeriod SMALLINT
, WeekendDate DATETIME
, CRCOperative VARCHAR(100)
, CallType VARCHAR(20)
, Charge Time INT
, SourceNumber VARCHAR(80)
, DestinationNumber VARCHAR(80)
, CallStart DATETIME
, Out of Hours VARCHAR(12)
, IsWorkingDay BIT
);
Related
I am attempting to Pivot a table passed in as a UDT.. Column 1 will have duplicate values and Column 2 will have different values (example below). I am hoping for the Column Name to be set as Phone Number and the two Values under the Phone Number column.
DECLARE #query AS NVARCHAR(MAX) = '';
--Temp Table to act as UDT for ease of testing
DECLARE #udt TABLE (DatabaseFieldName nvarchar(50), Value nvarchar(50))
INSERT INTO #udt VALUES('PhoneNumber','01234567890')
INSERT INTO #udt VALUES('PhoneNumber','09876543210')
--Preview of table before Pivot
select * from #udt
CREATE TABLE #temp
(
DatabaseFieldName nvarchar(50),
Value nvarchar(50)
)
INSERT INTO #temp
SELECT DatabaseFieldName, Value
FROM #udt
SELECT #cols = #cols + QUOTENAME(DatabaseFieldName) + ',' FROM (select distinct DatabaseFieldName from #temp) as temp
SELECT #cols = substring(#cols, 0, len(#cols)) -- Trims ',' at the end
SET #query =
'
SELECT * FROM
(
SELECT DatabaseFieldName, Value
FROM #temp
) AS SRC
PIVOT
(
MIN(Value) for DatabaseFieldName in (' + #cols + ')
) AS PivotTable';
execute(#query)
DROP TABLE #temp
Example of current & desired results
Data as it comes in:
DatabaseFieldName | Value
--------------------------------
PhoneNumber | 01234567890
PhoneNumber | 09876543210
Outcome I am hoping to get:
PhoneNumber
------------
01234567890
09876543210
What I am currently getting:
PhoneNumber
------------
01234567890
At the moment the second number is being ignored due to the use of distinct in the select statement, however an error is thrown if distinct is not used.
Are all the values that you want in the pivoted column PhoneNumbers? If so, you can just select the phone numbers themselves with an alias. E.g
SELECT Value As PhoneNumber
FROM (either #UDT or #temp)
WHERE DatabaseFieldName = 'PhoneNumber'
Unless there is a requirement I'm missing here, this would do everything you want it to.
It is not due to DISTINCT, it is because of using MIN(Value) , it gets the first one.
Let's say you had another set of rows
INSERT INTO #udt VALUES('Name','Hello')
INSERT INTO #udt VALUES('Name','Test')
Now the result set of your dynamic SQL will look like this.
Name PhoneNumber
Hello 01234567890
Having said that PIVOT can be used only with aggregate functions, you will get error if MIN is removed. You might have to think of other options for getting desired results such as using case if needed for multiple fields
SELECT CASE WHEN DatabaseFieldName = 'PhoneNumber' THEN Value END AS PhoneNumber,
CASE WHEN DatabaseFieldName = 'Name' THEN Value END AS Name
FROM #udt
Ok I have been requested to add a logging process inside the procedure (I didn't come up with this, I don't think its the best thing to do but I am expected to do this at work). I will try to explain with a very simplified example:
CREATE TABLE testLog (
results varchar(200)
);
CREATE TABLE Person (
PersonID int,
name varchar(50),
pAddress varchar(50),
pPhone varchar(10)
);
INSERT INTO Person
VALUES (1, 'Anne', '123 St', '1111111111'),
(2, 'Peter', 'XYZ St', '222222222'),
(3, 'Jason', '890 St', '3333333333');
-------------------------------------------------
CREATE PROCEDURE SpcDetailsList #inPersonID int
AS
BEGIN
SELECT TOP 1
PersonID,
name,
pAddress,
pPhone
FROM Person p
WHERE p.PersonID = #inPersonID;
END;
Ok, this is what I have - you pass in the parameter values and get something (the actual procedure is confirmed to return always a single row or nothing). But now additionally I have to make the procedure log itself. And this is how I am planning to do it -
ALTER PROCEDURE SpcDetailsList #inPersonID int
AS
BEGIN
DECLARE #PersonID int,
#name varchar(50),
#pAddress varchar(50),
#pPhone varchar(10);
SELECT
#PersonID = PersonID,
#name = name,
#pAddress = pAddress,
#pPhone = pPhone
FROM Person p
WHERE p.PersonID = #inPersonID;
INSERT INTO testLog (results)
VALUES (CAST(#PersonID AS varchar(1)) + ' ' + #name + ' ' + #pAddress + ' ' + #pPhone);
SELECT
#PersonID AS PersonID,
#name AS name,
#pAddress AS pAddress,
#pPhone AS pPhone
END
The actual procedure can return about 10 to 15 fields. I have to cast them, and replace null for each of them (which I am not doing in this example) then concatenate them (the results needs to be inserted into single field as one string) to insert to log. This doesn't look nice to me. Are there better options to do this exact same thing with good performance?
Edit: I didn't mention here that I have about 10 procedures with different number of outputs which will have to be inserted to the same log table. And the actual log table has more columns including the SP name, parameter values passed in, etc.
If you have some latitude on what your log looks like, I'd recommend outputting it to XML. It does all the formatting, casting, etc. for you. It's also parse-able. You can also use a temporary table to eliminate your need to declare variables for all the fields. Something like this:
SELECT TOP 1
PersonID,
name,
pAddress,
pPhone
into #temp
FROM Person p
WHERE p.PersonID = #inPersonID
INSERT INTO testLog VALUES((
SELECT *
FROM #temp
FOR XML AUTO
))
select *
from #temp
I have a log table has xml column which contains log contents
There is also table called logType which is the type of log
I need to create query descripes the xml contents as readable string
I added a column with name logXPath to logtype table
and i created the following query
SELECT contents.value(LogXPath, 'nvarchar(max)')
FROM dbo.Log
JOIN dbo.LogType ON dbo.Log.logTypeID = dbo.LogType.logTypeID
and I got the following error
The argument 1 of the XML data type method "value" must be a string literal
and I searched for a way to do this with no results!!
Is there any do dynamic xpath in Sql Server XML Column?
Edit
for example assume the following schema and data
CREATE TABLE [dbo].[logType]
(
[logTypeID] [int] NOT NULL ,
[logTypeName] [nvarchar](50) NOT NULL ,
[xPath] [nvarchar](MAX) NOT NULL ,
CONSTRAINT [PK_logType] PRIMARY KEY CLUSTERED ( [logTypeID] ASC )
)
GO
INSERT [dbo].[logType]
( [logTypeID] ,
[logTypeName] ,
[xPath]
)
VALUES ( 1 ,
N'Patient Data' ,
N'(/Patient/PatientName)[1]'
)
INSERT [dbo].[logType]
( [logTypeID] ,
[logTypeName] ,
[xPath]
)
VALUES ( 2 ,
N'Clinic Data' ,
N'(/Clinic/ClinicName)[1]'
)
/****** Object: Table [dbo].[log] Script Date: 02/04/2015 13:58:47 ******/
GO
CREATE TABLE [dbo].[log]
(
[logID] [int] NOT NULL ,
[logTypeID] [int] NOT NULL ,
[Contents] [xml] NULL ,
CONSTRAINT [PK_log] PRIMARY KEY CLUSTERED ( [logID] ASC )
)
GO
INSERT [dbo].[log]
( [logID] ,
[logTypeID] ,
[Contents]
)
VALUES ( 1 ,
1 ,
N'<Patient><PatientID>1</PatientID><PatientName>john</PatientName></Patient>'
)
INSERT [dbo].[log]
( [logID] ,
[logTypeID] ,
[Contents]
)
VALUES ( 2 ,
2 ,
N'<Clinic><ClinicID>1</ClinicID><ClinicName>Clinic 1</ClinicName></Clinic>'
)
When I make query like the following ,it gives me the error
SELECT logTypeName ,
[Contents].value(dbo.logType.xPath, 'nvarchar(max)') AS data
FROM dbo.[log]
JOIN dbo.logType ON dbo.[log].logTypeID = dbo.logType.logTypeID
You can build a query dynamically using the table LogType.
declare #SQL nvarchar(max);
set #SQL = 'select case L.logTypeID'+
(
select ' when '+cast(LT.logTypeID as varchar(11))+
' then L.Contents.value('''+LT.xPath+''', ''nvarchar(max)'')'
from LogType as LT
for xml path('')
)+' end as Name from dbo.[Log] as L;';
exec (#SQL);
It will give you a query that looks like this:
select case L.logTypeID
when 1 then L.Contents.value('(/Patient/PatientName)[1]', 'nvarchar(max)')
when 2 then L.Contents.value('(/Clinic/ClinicName)[1]', 'nvarchar(max)')
end as Name
from dbo.[Log] as L;
Okay, so this has been here a while (a year), but this might be helpful...
A VERY helpful TABLE function: http://beyondrelational.com/modules/2/blogs/28/posts/10495/xquery-lab-58-select-from-xml.aspx
Using that function, you can get the values you're after something like this:
Select l.Contents, t.XPath, x.Value
From [log] l With (NoLock)
Inner Join [LogType] t With (NoLock)
on t.LogTypeID=l.LogTypeID
CROSS APPLY XMLTable(l.Contents) AS x
Where REPLACE(REPLACE(REPLACE(t.XPath,'[1]',''),'(',''),')','')=REPLACE('/'+x.XPath,'[1]','')
SQL Server does not allow replacing entire XPath expression with a variable, but you can use sql:variable and sql:column extensions inside the expression (I can't say how exactly without seeing your xml structure and what information you want to query from XML column).
Or, as mentioned above, you can use dynamic SQL:
DECLARE #xpath NVARCHAR(MAX);
SET #xpath = ... //Calculate xpath expression here
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'SELECT contents.value(''' + #xpath + ''', ''NVARCHAR(MAX)''
FROM dbo.Log
JOIN dbo.LogType ON dbo.Log.logTypeID = dbo.LogType.logTypeID';
EXEC sp_executesql #sql;
I can't find an easy/generic way to register to an audit table the columns changed on some tables.
I tried to do it using a Trigger on after update in this way:
First of all the Audit Table definition:
CREATE TABLE [Audit](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Date] [datetime] NOT NULL default GETDATE(),
[IdTypeAudit] [int] NOT NULL, --2 for Modify
[UserName] [varchar](50) NULL,
[TableName] [varchar](50) NOT NULL,
[ColumnName] [varchar](50) NULL,
[OldData] [varchar](50) NULL,
[NewData] [varchar](50) NULL )
Next a trigger on AFTER UPDATE in any table:
DECLARE
#sql varchar(8000),
#col int,
#colcount int
select #colcount = count(*) from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'MyTable'
set #col = 1
while(#col < #colcount )
begin
set #sql=
'INSERT INTO Audit
SELECT 2, UserNameLastModif, ''MyTable'', COL_NAME(Object_id(''MyTable''), '+ convert(varchar,#col) +'), Deleted.'
+ COL_NAME(Object_id('MyTable'), #col) + ', Inserted.' + COL_NAME(Object_id('MyTable'), #col) + '
FROM Inserted LEFT JOIN Deleted ON Inserted.[MyTableId] = Deleted.[MyTableId]
WHERE COALESCE(Deleted.' + COL_NAME(Object_id('MyTable'), #col) + ', '''') <> COALESCE(Inserted.' + COL_NAME(Object_id('MyTable'), #col) + ', '''')'
--UserNameLastModif is an optional column on MyTable
exec(#sql)
set #col = #col + 1
end
The problems
Inserted and Deleted lost the context when I use the exec function
Seems that colnumber it isn't always a correlative number, seems if you create a table with 20 columns and you delete one and create another, the last one have a number > #colcount
I was looking for a solution for all over the net but I couln't figure out
Any Idea?
Thanks!
This highlights a greater problem with structural choice. Try to write a set-based solution. Remove the loop and dynamic SQL and write a single statement that inserts the Audit rows. It is possible but to make it easier consider a different table layout, like keeping all columns on 1 row instead of splitting them.
In SQL 2000 use syscolumns. In SQL 2005+ use sys.columns. i.e.
SELECT column_id FROM sys.columns WHERE object_id = OBJECT_ID(DB_NAME()+'.dbo.Table');
#Santiago : If you still want to write it in dynamic SQL, you should prepare all of the statements first then execute them.
8000 characters may not be enough for all the statements. A good solution is to use a table to store them.
IF NOT OBJECT_ID('tempdb..#stmt') IS NULL
DROP TABLE #stmt;
CREATE TABLE #stmt (ID int NOT NULL IDENTITY(1,1), SQL varchar(8000) NOT NULL);
Then replace the line exec(#sql) with INSERT INTO #stmt (SQL) VALUES (#sql);
Then exec each row.
WHILE EXISTS (SELECT TOP 1 * FROM #stmt)
BEGIN
BEGIN TRANSACTION;
EXEC (SELECT TOP 1 SQL FROM #stmt ORDER BY ID);
DELETE FROM #stmt WHERE ID = (SELECT MIN(ID) FROM #stmt);
COMMIT TRANSACTION;
END
Remember to use sys.columns for the column loop (I shall assume you use SQL 2005/2008).
SET #col = 0;
WHILE EXISTS (SELECT TOP 1 * FROM sys.columns WHERE object_id = OBJECT_ID('MyTable') AND column_id > #col)
BEGIN
SELECT TOP 1 #col = column_id FROM sys.columns
WHERE object_id = OBJECT_ID('MyTable') AND column_id > #col ORDER BY column_id ASC;
SET #sql ....
INSERT INTO #stmt ....
END
Remove line 4 #colcount int and the proceeding comma. Remove Information schema select.
DO not ever use any kind of looping a trigger. Do not use dynamic SQl or call a stored proc or send an email.All of these things are exretemly inappropriate in a trigger.
If tyou want to use dynamic sql use it to create the script to create the trigger. And create an audit table for every table you want audited (we actually have two for every table) or you will have performance problems due to locking on the "one table to rule them all".
i've got around 10 tables in my sql 2008 server.
Currently, my mdf is around 3.5Gig. (I also have some binary data in some of the tables). So, I'm wondering if there's a way i could see which tables are the biggest in size.
Is this possible?
Maybe it's an index or FTS catalog instead?
run this:
/******************************************************************************
** File: “GetTableSpaceUseage.sql”
** Name: Get Table Space Useage for a specific schema
** Auth: Robert C. Cain
** Date: 01/27/2008
**
** Desc: Calls the sp_spaceused proc for each table in a schema and returns
** the Table Name, Number of Rows, and space used for each table.
**
** Called by:
** n/a – As needed
**
** Input Parameters:
** In the code check the value of #schemaname, if you need it for a
** schema other than dbo be sure to change it.
**
** Output Parameters:
** NA
*******************************************************************************/
/*—————————————————————————*/
/* Drop the temp table if it's there from a previous run */
/*—————————————————————————*/
if object_id(N'tempdb..[#TableSizes]') is not null
drop table #TableSizes ;
go
/*—————————————————————————*/
/* Create the temp table */
/*—————————————————————————*/
create table #TableSizes
(
[Table Name] nvarchar(128) /* Name of the table */
, [Number of Rows] char(11) /* Number of rows existing in the table. */
, [Reserved Space] varchar(18) /* Reserved space for table. */
, [Data Space] varchar(18) /* Amount of space used by data in table. */
, [Index Size] varchar(18) /* Amount of space used by indexes in table. */
, [Unused Space] varchar(18) /* Amount of space reserved but not used. */
) ;
go
/*—————————————————————————*/
/* Load the temp table */
/*—————————————————————————*/
declare #schemaname varchar(256) ;
-- Make sure to set next line to the Schema name you want!
set #schemaname = 'dbo' ;
-- Create a cursor to cycle through the names of each table in the schema
declare curSchemaTable cursor
for select sys.schemas.name + '.' + sys.objects.name
from sys.objects
, sys.schemas
where object_id > 100
and sys.schemas.name = #schemaname
/* For a specific table uncomment next line and supply name */
--and sys.objects.name = 'specific-table-name-here'
and type_desc = 'USER_TABLE'
and sys.objects.schema_id = sys.schemas.schema_id ;
open curSchemaTable ;
declare #name varchar(256) ; /* This holds the name of the current table*/
-- Now loop thru the cursor, calling the sp_spaceused for each table
fetch curSchemaTable into #name ;
while ( ##FETCH_STATUS = 0 )
begin
insert into #TableSizes
exec sp_spaceused #objname = #name ;
fetch curSchemaTable into #name ;
end
/* Important to both close and deallocate! */
close curSchemaTable ;
deallocate curSchemaTable ;
/*—————————————————————————*/
/* Feed the results back */
/*—————————————————————————*/
select [Table Name]
, [Number of Rows]
, [Reserved Space]
, [Data Space]
, [Index Size]
, [Unused Space]
from [#TableSizes]
order by [Table Name] ;
/*—————————————————————————*/
/* Remove the temp table */
/*—————————————————————————*/
drop table #TableSizes ;
taken from Robert Caine blog
Edited the code to parse, several chars that were in single quote used a special single quote, as well the -- sign.
This code is for Microsoft SQL 2005+
exec sp_spaceused [tablename]
sys.allocations_units has the information you need. You join with sys.partitions to group all allocation units of a partition together and also to obtain the more usable object_id rather than the esoteric allocation_unit_id.
select object_name(p.object_id),
sum(au.total_pages)*8 as [space_in_kb]
from sys.partitions p
join sys.allocation_units au on p.hobt_id = au.container_id
group by p.object_id
order by [space_in_kb] desc
And yes, all tables (heap or clustered) are 'partitions', the terms does not refer to partitioned tables. sys.partitions also has the 'rows' column that may interest you.
exec sp_spaceused <tablename>
This query shows the size of each table in the current database.
SELECT sysobjects.[name] AS [TableName],
SUM(sysindexes.reserved) * 8 AS [Size(KB)],
SUM(sysindexes.dpages) * 8 AS [Data(KB)],
(SUM(sysindexes.used) - SUM(sysindexes.dpages)) * 8 AS [Indexes(KB)],
(SUM(sysindexes.reserved) - SUM(sysindexes.dpages)) * 8 AS [Unused(KB)]
FROM dbo.sysindexes AS sysindexes
JOIN dbo.sysobjects AS sysobjects ON sysobjects.id = sysindexes.id
WHERE sysobjects.[type] = 'U'
GROUP BY sysobjects.[name]
ORDER BY [Size(KB)] DESC
In SQL 2008+: right-click on the DB name in SSMS, select Reports, then Standard Reports, then Disk Usage by Table.
Sometimes I run this... It gets all tables to temp table, loops it through and gets sizes for all tables. Result data is in #tablesizes, so you can query it how you like.
Works in Sql Server >2005
declare #tables TABLE
(
table_name nvarchar(200)
)
declare #tablesizes TABLE
(
[name] nvarchar(200),
[rows] int,
reserved nvarchar(200),
data nvarchar(200),
index_size nvarchar(200),
unused nvarchar(200),
reserved_int int,
data_int int,
index_size_int int,
unused_int int
)
declare #t nvarchar(200)
insert into #tables
select Table_Name from information_schema.tables
while exists(select * from #tables)
begin
set #t=(select top 1 table_name from #tables)
insert into #tablesizes([name],[rows],reserved,data,index_size,unused)
exec sp_spaceused #t
delete top (1) from #tables
end
update #tablesizes set
reserved_int=convert(int, replace(reserved,' KB','')),
data_int=convert(int, replace(data,' KB','')),
index_size_int=convert(int, replace(index_size,' KB','')),
unused_int=convert(int, replace(unused,' KB',''))
select * from #tablesizes order by data_int desc
You could use:
SELECT ##servername;
IF EXISTS(SELECT name FROM tempdb.sys.tables WHERE name LIKE '#spaceUsed%')
BEGIN
DROP TABLE #spaceUsed;
END;
CREATE TABLE #spaceUsed (
name VARCHAR(255) ,
rows INT ,
reserved VARCHAR(50) ,
data VARCHAR(50) ,
index_size VARCHAR(50) ,
unused VARCHAR(50));
EXEC sp_msforeachtable
#command1 ='
--
INSERT INTO #spaceUsed
exec sp_spaceused N''?'';
'
,#whereand = ' And Object_id In (Select Object_id From sys.objects
Where SCHEMA_NAME(Schema_ID) like ''%'')';
DECLARE
#spaceUsedData TABLE (
name VARCHAR(255) ,
rows INT ,
reservedMB BIGINT NULL ,
dataMB BIGINT NULL ,
index_sizeMB BIGINT NULL ,
unusedMB BIGINT NULL);
INSERT INTO INTO #spaceUsedData (name , rows , reservedMB , dataMB ,index_sizeMB ,unusedMB)
SELECT name , rows ,
Convert ( BIGINT ,Ltrim(Rtrim(Replace(reserved ,'KB' ,'')) ))/1024 ,
Convert ( BIGINT ,Ltrim(Rtrim(Replace(data ,'KB' ,'')) ))/1024 ,
Convert ( BIGINT ,Ltrim(Rtrim(Replace(index_size ,'KB' ,'')) ))/1024 ,
Convert ( BIGINT ,Ltrim(Rtrim(Replace(unused ,'KB' ,'')) ))/1024
FROM #spaceUsed;
SELECT * , reservedMB+ dataMB+index_sizeMB+unusedMB AS TotalMB FROM #spaceUsedData
ORDER BY rows DESC;