I am working on SQL Server 2008. I have to create json from two tables, which have a one-to-many relationship. The tables are Customer and Orders.
Each customer may have one or many orders. The json is constructed by first getting data from the customer table and then appending all the purchases they have made.
The following is my query. I have also enclosed the json output from the query. It works and creates valid jsons. The problem is that it's too slow as I am using a cursor to loop through the Customer table. I have managed to avoid cursor to get data from the Orders table by using for xml path. Since I have to handle millions of rows, I have to replace the cursor with some other mechanism.
DECLARE #PaymentType VARCHAR(50),
#Email VARCHAR(100),
#OrderId INT
DECLARE CustomerCursor CURSOR FAST_FORWARD FOR
SELECT TOP 10
PaymentType, Email, OrderId
FROM
CUSTOMER
OPEN CustomerCursor
FETCH NEXT FROM CustomerCursor INTO #PaymentType, #Email, #OrderId
WHILE (##FETCH_STATUS = 0)
BEGIN
DECLARE #customer VARCHAR(MAX)
DECLARE #order VARCHAR(MAX)
DECLARE #customer_with_order VARCHAR(MAX)
-- construct order json
SET #order = '[' + STUFF((SELECT ',{"orderProductID":' + CAST(orderProductID AS VARCHAR) +
',"productType":"' + ProductType + '"' +
',"productName":"' + ProductName + '"' +
',"categoryName":"' + CategoryName + '"' + '}'
FROM ORDERS
WHERE orderid = #OrderId
FOR XML PAT(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') + ']'
-- construct customer json
SET #customer = '{"email":"' + CASE WHEN #Email IS NULL THEN '' ELSE
#Email END + '"'
+ ',"eventName": "ChristmasSale", "dataFields": {'
+ '"orderId":' + CAST(CASE WHEN #OrderId IS NULL THEN 0 ELSE
#OrderId END AS VARCHAR)
+ ',"paymentType":"' + CASE WHEN #PaymentType IS NULL THEN
'' ELSE #PaymentType END + '"'
+ ',"products": '
-- combine these two
SET #customer_with_order = #customer + #order + '}}'
-- insert into CUSTOMER_ORDER_DATA
INSERT INTO CUSTOMER_ORDER_DATA(email, order_id, orders)
VALUES (#Email, #OrderId, #customer_with_order)
FETCH NEXT FROM CustomerCursor INTO #PaymentType, #Email, #OrderId
END
CLOSE CustomerCursor
DEALLOCATE CustomerCursor
I can't test this, but I suspect you could rewrite the above as a set based method as below (as I have no way of testing this, there is no way I can be certain this'll work, if it doesn't, you may need to troubleshoot it a little):
INSERT INTO CUSTOMER_ORDER_DATA(email, order_id, orders)
SELECT C.Email,
C.orderid,
'{"email":"' + CASE WHEN #Email IS NULL THEN '' ELSE
#Email END + '"'
+ ',"eventName": "ChristmasSale", "dataFields": {'
+ '"orderId":' + CAST(CASE WHEN #OrderId IS NULL THEN 0 ELSE
#OrderId END AS varchar)
+ ',"paymentType":"' + CASE WHEN #PaymentType IS NULL THEN
'' ELSE #PaymentType END + '"'
+ ',"products": ' +
('[' + STUFF((
SELECT
',{"orderProductID":' + CAST(orderProductID AS varchar)
+ ',"productType":"' + ProductType + '"'
+ ',"productName":"' + ProductName + '"'
+ ',"categoryName":"' + CategoryName + '"'
+'}'
FROM ORDERS AS O
WHERE O.orderid = C.orderid
FOR XML PATH(''),TYPE).value('.', 'varchar(max)'), 1, 1, '') + ']')
FROM CUSTOMER AS C
Considering the OP has 5 millions rows, then this would likely be a bit much for one batch. Seperating it into batching of say 10,000 may be better for performance over all. Unfortunately the OP is still using 2008, so they don't have access to the OFFSET clause.
Related
I need the column alias to be named based on a scenario
declare #testing as varchar(max)
set #testing = 'choice'
select 1 as case when #testing = 'choice' then 'chose' else 'didntChoose' end
So if #testing = 'choice', the results would look like this:
chose
1
else:
didntChoose
1
Is it possible to do this without dynamic SQL?
No, you cannot change the name of the alias based on the value unless you use dynamic SQL.
When you are selecting the columns, you can only have one name/alias for each column.
If you want different column names, then you could use some like the following which uses different select statements:
IF #testing = 'choice'
select 1 as 'Chose'
ELSE
select 1 as 'didntChoose'
Or you could return two separate columns:
select
case when #testing = 'choice' then 1 else 0 end Chose,
case when #testing <> 'choice' then 1 else 0 end DidNotChose
Here is something I wrote that kind of achieves the goal, but it is not the most elegant piece of work I have ever done.
Various customers want to display different values for attributes associated with their resources. So a generic table exists that lets each customer assign different values to each of the resources.
Let's create the structures first and populate them. Nothing too fancy:
create table CustResource (
CustId int,
Attr1 varchar(50),
Attr2 varchar(50),
Attr3 varchar(50),
Attr4 varchar(50),
Attr5 varchar(50))
insert into CustResource (CustId, attr1, attr2, attr3, attr4) values (1, 'Div','Dept','Machine Type','Main Usage')
/* What just happened above is that the customer assigned display values to the first 4 attributes only */
create table PortalResource (
ResourceId int,
custId int,
ResourceName varchar(50),
Attr1 varchar(50),
Attr2 varchar(50),
Attr3 varchar(50),
Attr4 varchar(50),
Attr5 varchar(50))
insert into PortalResource (ResourceId, CustId, ResourceName, attr1, attr2, attr3, attr4)
values (10,1,'abcd1234','Local Government','State Emergency Services','File Server','Production')
insert into PortalResource (ResourceId, CustId, ResourceName, attr1, attr2, attr3, attr4)
values (11,1,'bcde2345','Local Government','State Emergency Services','Database Server','Production')
insert into PortalResource (ResourceId, CustId, ResourceName, attr1, attr2, attr3, attr4)
values (12,1,'bcde2346','Local Government','Department of Education','Domain Controller','Production')
/* Notice in the above that attr5 is not populated. This is deliberate! */
/* OK, now we want to accept the customer Id (I have hard-coded it here for quick reference, but you get the point) */
declare #SQLString varchar(1000)
, #attr1 varchar (50)
, #attr2 varchar(50)
, #attr3 varchar(50)
, #attr4 varchar(50)
, #attr5 varchar(50)
, #CustId varchar(10)
set #CustId = 1
select #attr1 = upper(attr1)
, #attr2 = upper(attr2)
, #attr3 = upper(attr3)
, #attr4 = upper(attr4 )
, #attr5 = UPPER(attr5)
, #CustId = convert(varchar,custId)
from CustResource where custid = #CustId
set #SQLString = 'Select ' + #CustId + 'as CustomerID'
If #attr1 is not null set #SQLString = #SQLString +
' , attr1 as ' + '"' + #attr1 + '"'
If #attr2 is not null set #SQLString = #SQLString +
' , attr2 as ' + '"' + #attr2 + '"'
If #attr3 is not null set #SQLString = #SQLString +
' , attr3 as ' + '"' + #attr3 + '"'
If #attr4 is not null set #SQLString = #SQLString +
' , attr4 as ' + '"' + #attr4 + '"'
If #attr5 is not null set #SQLString = #SQLString +
' , attr5 as ' + '"' + #attr5 + '"'
Set #SQLString = #SQLString + ' from PortalResource where CustId = ' + #CustId
print #SQLString
exec (#SQLString)
This works a charm, but it is super-ugleeeeee!!!!
I'll just leave this here http://www.sommarskog.se/dynamic_sql.html#columnalias
You first get the data into a temp table, and then you use sp_rename
to rename the column along your needs. (You need to qualify sp_rename
with tempdb to have it to operate in that database.)
Do have a read of his site if you're dealing a lot with dynamic SQL, there's a lot of ways to shoot yourself in the foot if you're not careful...
I have numerous tables I that I want to have created and populated dynamically based on views.
I want to perform something like a combination of these two posts:
Create Table from View
Is there a way to loop through a table variable in TSQL without using a cursor?
Select *
Into dbo.##tblTemp
From databasename.sys.views
Declare #TableName NVARCHAR(128)
While (Select COUNT(*) From #Temp) > 0
Begin
Select Top 1 #TableName = name from databasename.sys.views
Select * into #TableName from databasename.sys.views
Delete databasename.sys.views Where name = #TableName
End
Am I better off with a stored procedure that dynamically creates the sql statement to create the table?
EDIT:
Per Sebastian, I am running the below code to accomplish this:
DECLARE #cmd NVARCHAR(MAX) = ( SELECT TOP 10 'exec sp_rename '
+ '#objname =''' + OBJECT_SCHEMA_NAME(object_id)
+ '.'
+ OBJECT_NAME(object_id) + ''
+ ''', #newname = '
+ '''v_' + name + ''
+ ''';'
+ 'SELECT * INTO '
+ OBJECT_SCHEMA_NAME(object_id)
+ '.'
+ OBJECT_NAME(object_id)
+ ' FROM '
+ OBJECT_SCHEMA_NAME(object_id)
+ '.v_'
+ OBJECT_NAME(object_id)
+ ';'
+ 'DROP VIEW '
+ OBJECT_SCHEMA_NAME(object_id)
+ '.v_'
+ OBJECT_NAME(object_id)
+ ';'
FROM db.sys.views
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)');
EXEC (#cmd)
--Select #cmd
To copy all the data seems to be the wrong approach to solve your problem. If your problem is poor performance do to too many reads caused by views there are two things to look out for first.
1) Check that you tables have appropriate indexes. You could even add indexes to your views. There are many resources out there that tell you how to go about index tuning. Or you could hire a consultant (like me) to help you out with that.
2) If your queries join views, it often happens that unnecessary tables make it into the mix. For example if view v1 joins table a and b and view v2 joins table b and c and your query then joins v1 with v2, it effectively joins a with b with b with c. Such a query often can be rewritten to join to b only once helping tremendously with performance. So if you have queries joining views with views you should review those.
If after all that you still want to go forward with copying the data, you can use this:
DECLARE #cmd NVARCHAR(MAX) = ( SELECT 'SELECT * INTO '
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.'
+ QUOTENAME('tbl_'+OBJECT_NAME(object_id))
+ ' FROM '
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.' + QUOTENAME(OBJECT_NAME(object_id))
+ ';'
FROM sys.views
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)');
EXEC (#cmd);
It creates a command that uses SELECT INTO to create a table for each view in the database. Because SQL Server does not allow name collisions even for objects of differing type, I prefixed the names of the tables with "tbl_".
If you need to create the tables in a different database, you need to prefix the table names with "dbname.". In that case you can remove the "tbl_"prefix.
EDIT:
You had a few missing quotes in your version. Try this:
DECLARE #cmd NVARCHAR(MAX) = ( SELECT TOP 1 'exec sp_rename '''
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.'
+ QUOTENAME(OBJECT_NAME(object_id))
+ ''', '''
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.'
+ QUOTENAME('v_' +OBJECT_NAME(object_id))
+ ''';'
+ 'SELECT * INTO '
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.'
+ QUOTENAME(OBJECT_NAME(object_id))
+ ' FROM '
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.'
+ QUOTENAME('v_' +OBJECT_NAME(object_id))
+ ';'
+ 'DROP VIEW '
+ QUOTENAME(OBJECT_SCHEMA_NAME(object_id))
+ '.'
+ QUOTENAME('v_' +OBJECT_NAME(object_id))
+ ';'
FROM sys.views
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)');
You also can use PRINT #cmd instead of EXEC(#cmd) to see if the command put together makes sense.
A few minutes ago I was only searching for a simple syntax (SQL server) query that will copy a table Row .
This is usually done from time to time, when working on a ASP.net project, testing data with queries
inside the SQL SERVER management studio . so one of the routine actions is copying a row, altering the required columns to be different from each other, then testing data with queries
So I've encountered - this stored procedure- ,as answer by Dan Atkinson
but adding it to where all non testing purpose are stored lead me to think
is it possible to store them in sorted order so I could Distinguish
'utils' or 'testingPurpose' ones from those used in projects
(default folder inside managment treeview is Programmabilty) could this be another folder too
or this is not an option ?
if not , I thought of Utils. prefix like that (if no other way exist)
dbo.Utils.CopyTableRow
dbo.Utils.OtherRoutineActions ....
Or there's a designated way to achieve what I was thinking of.
this is a first "Util" stored procedure i've made , found it's only solution
prefexing it via Util_
ALTER PROCEDURE [dbo].[Utils_TableRowCopy](
#TableName VARCHAR(50) ,
#RowNumberToCopy INT
)
AS
BEGIN
declare #RowIdentity sysname =
(SELECT name FROM sys.identity_columns WHERE object_id = object_id(#TableName)
)
DECLARE #columns VARCHAR(5000), #query VARCHAR(8000);
SET #query = '' ;
SELECT #columns =
CASE
WHEN #columns IS NULL THEN column_name
ELSE #columns + ',' + column_name
END
FROM INFORMATION_SCHEMA.COLUMNS
WHERE (
TABLE_NAME = LTRIM(RTRIM(#TableName))
AND
column_name <> LTRIM(RTRIM(#RowIdentity))
);
SET #query = 'INSERT INTO ' + #TableName + ' (' + #columns + ') SELECT ' + #columns + ' FROM ' + #TableName + ' WHERE ' + #RowIdentity + ' = ' + CAST(#RowNumberToCopy AS VARCHAR);
--SELECT SCOPE_IDENTITY();
declare #query2 VARCHAR(100) = ' Select Top 1 * FROM '+ #TableName +' Order BY ' + #RowIdentity + ' desc' ;
EXEC (#query);
EXEC (#query2);
END
EDIT: Database names have been modified for simplicity
I'm trying to get some dynamic sql in place to update static copies of some key production tables into another database (sql2008r2). The aim here is to allow consistent dissemination of data (from the 'static' database) for a certain period of time as our production databases are updated almost daily.
I am using a CURSOR to loop through a table that contains the objects that are to be copied into the 'static' database.
The prod tables don't change that frequently, but I'd like to make this somewhat "future proof" (if possible!) and extract the columns names from INFORMATION_SCHEMA.COLUMNS for each object (instead of using SELECT * FROM ...)
1) From what I have read in other posts, EXEC() seems limiting, so I believe that I'll need to use EXEC sp_executesql but I'm having a little trouble getting my head around it all.
2) As an added extra, if at all possible, i'd also like to exclude some columns for particular tables (structures vary slightly in the 'static' database)
here's what i have so far.
when executed, #colnames returns NULL and therefore #sql returns NULL...
could someone guide me to where i might find a solution?
any advice or help with this code is much appreciated.
CREATE PROCEDURE sp_UpdateRefTables
#debug bit = 0
AS
declare #proddbname varchar(50),
#schemaname varchar(50),
#objname varchar(150),
#wherecond varchar(150),
#colnames varchar(max),
#sql varchar(max),
#CRLF varchar(2)
set #wherecond = NULL;
set #CRLF = CHAR(10) + CHAR(13);
declare ObjectCursor cursor for
select databasename,schemaname,objectname
from Prod.dbo.ObjectsToUpdate
OPEN ObjectCursor ;
FETCH NEXT FROM ObjectCursor
INTO #proddbname,#schemaname,#objname ;
while ##FETCH_STATUS=0
begin
if #objname = 'TableXx'
set #wherecond = ' AND COLUMN_NAME != ''ExcludeCol1'''
if #objname = 'TableYy'
set #wherecond = ' AND COLUMN_NAME != ''ExcludeCol2'''
--extract column names for current object
select #colnames = coalesce(#colnames + ',', '') + QUOTENAME(column_name)
from Prod.INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = + QUOTENAME(#objname,'') + isnull(#wherecond,'')
if #debug=1 PRINT '#colnames= ' + isnull(#colnames,'null')
--replace all data for #objname
--#proddbname is used as schema name in Static database
SELECT #sql = 'TRUNCATE TABLE ' + #proddbname + '.' + #objname + '; ' + #CRLF
SELECT #sql = #sql + 'INSERT INTO ' + #proddbname + '.' + #objname + ' ' + #CRLF
SELECT #sql = #sql + 'SELECT ' + #colnames + ' FROM ' + #proddbname + '.' + #schemaname + '.' + #objname + '; '
if #debug=1 PRINT '#sql= ' + isnull(#sql,'null')
EXEC sp_executesql #sql
FETCH NEXT FROM ObjectCursor
INTO #proddbname,#schemaname,#objname ;
end
CLOSE ObjectCursor ;
DEALLOCATE ObjectCursor ;
P.S. i have read about sql injection, but as this is an internal admin task, i'm guessing i'm safe here!? any advice on this is also appreciated.
many thanks in advance.
You have a mix of SQL and dynamic SQL in your query against information_schema. Also QUOTENAME isn't necessary in the where clause and will actually prevent a match at all, since SQL Server stores column_name, not [column_name], in the metadata. Finally, I'm going to change it to sys.columns since this is the way we should be deriving metadata in SQL Server. Try:
SELECT #colnames += ',' + name
FROM Prod.sys.columns
WHERE OBJECT_NAME([object_id]) = #objname
AND name <> CASE WHEN #objname = 'TableXx' THEN 'ExcludeCol1' ELSE '' END
AND name <> CASE WHEN #objname = 'TableYy' THEN 'ExcludeCol2' ELSE '' END;
SET #colnames = STUFF(#colnames, 1, 1, '');
Using SQL Server 2008 i have two tables: [Source] and [Target]
I want to check all columns of Source table and see if they exist in Target table.
If they dont exist then i want to add the columns.
I have created a procedure to fix this. The only thing im struggeling with is how to set the datatypes correctly.
I mean, if it is a varchar field i must specify the length. If its an int field i dont need to and so on. Is there a way to do this without creating seperate handeling of every datatype available?
This is incomplete... you'll have to handle the other datatypes that I forgot (I don't think I forgot any). However I did test it and it works.
So to answer your question, no, you have to handle the datatypes.
DECLARE #MasterTable SYSNAME,
#SlaveTable SYSNAME,
#txtSQL VARCHAR(max)
SELECT #MasterTable = 'orderheader',
#SlaveTable = 'orderheader2'
DECLARE #myTable TABLE
(
txtSQL VARCHAR(MAX)
)
INSERT INTO #myTable
(
[txtSQL]
)
SELECT 'ALTER TABLE [dbo].[' +
#SlaveTable +
'] ADD [' +
a.[name] +
'] [' +
typ.[name] +
']' +
CASE typ.[name]
WHEN 'decimal' THEN '(' + CAST(a.[precision] AS VARCHAR(20)) + ',' + CAST(a.[scale] AS VARCHAR(20)) + ')'
WHEN 'numeric' THEN '(' + CAST(a.[precision] AS VARCHAR(20)) + ',' + CAST(a.[scale] AS VARCHAR(20)) + ')'
WHEN 'varchar' THEN '(' + CAST(a.[max_length] AS VARCHAR(20)) + ')'
WHEN 'char' THEN '(' + CAST(a.[max_length] AS VARCHAR(20)) + ')'
WHEN 'nvarchar' THEN '(' + CAST(a.[max_length] AS VARCHAR(20)) + ')'
WHEN 'nchar' THEN '(' + CAST(a.[max_length] AS VARCHAR(20)) + ')'
WHEN 'binary' THEN '(' + CAST(a.[max_length] AS VARCHAR(20)) + ')'
WHEN 'varbinary' THEN '(' + CAST(a.[max_length] AS VARCHAR(20)) + ')'
ELSE ''
END
FROM (
SELECT col.*
FROM sys.tables tbl
INNER JOIN sys.columns col
ON tbl.[object_id] = col.[object_id]
WHERE tbl.[name] = #MasterTable
) a
LEFT JOIN (
SELECT col.*
FROM sys.tables tbl
INNER JOIN sys.columns col
ON tbl.[object_id] = col.[object_id]
WHERE tbl.[name] = #SlaveTable
) b
ON a.[name] = b.[name]
INNER JOIN sys.types typ
ON a.[system_type_id] = typ.[system_type_id]
WHERE b.name IS NULL
WHILE EXISTS
(
SELECT TOP 1 1
FROM #myTable
)
BEGIN
SELECT TOP 1 #txtSQL = txtSQL FROM #myTable
DELETE FROM #myTable WHERE [txtSQL] = #txtSQL
EXEC (#txtSQL)
END
Is using SQL Compare from Red Gate out of the question?