SQL Server - PIVOT - c#

We are working on a C# application, we've been using Linq to SQL or standard ADO (when performance needed) to work with SQL Server.
We have a table layed out like so:
Customer ID, Year/Month, Product Name, Quantity
Each customer has additional columns per product.
We need display this information in a data grid like so:
Customer, Year/Month, Product A Quantity, Product B Quantity, Product C Quantity, etc.
What query could give us these results? And how could it be dynamic no matter what products are added and removed? We will be using a ListView in WPF for displaying the data.
We would just store the information differently, but they can add/remove products all the time.
Will PIVOT work?
(PS - the product names are really in another table for normalization, I changed it a little for simplicity for you guys)

The sql pivot command can be used but it requires the columns to be hard-coded. You could either hard-code them, use dynamic sql to generate the columns, or only get the raw data from sql without a pivot and do the data massaging in c#.

You can use pivot with dynamic SQL. Following T-SQL code is taken from this article on sqlteam.com. I've tried to modify the sample for your needs. Also beware of dangers using dynamic SQL, it might lead to SQL Injection if a product name contains apostrophe.
Create a stored proc first;
CREATE PROCEDURE crosstab
#select varchar(8000),
#sumfunc varchar(100),
#pivot varchar(100),
#table varchar(100)
AS
DECLARE #sql varchar(8000), #delim varchar(1)
SET NOCOUNT ON
SET ANSI_WARNINGS OFF
EXEC ('SELECT ' + #pivot + ' AS pivot INTO ##pivot FROM ' + #table + ' WHERE 1=2')
EXEC ('INSERT INTO ##pivot SELECT DISTINCT ' + #pivot + ' FROM ' + #table + ' WHERE '
+ #pivot + ' Is Not Null')
SELECT #sql='', #sumfunc=stuff(#sumfunc, len(#sumfunc), 1, ' END)' )
SELECT #delim=CASE Sign( CharIndex('char', data_type)+CharIndex('date', data_type) )
WHEN 0 THEN '' ELSE '''' END
FROM tempdb.information_schema.columns
WHERE table_name='##pivot' AND column_name='pivot'
SELECT #sql=#sql + '''' + convert(varchar(100), pivot) + ''' = ' +
stuff(#sumfunc,charindex( '(', #sumfunc )+1, 0, ' CASE ' + #pivot + ' WHEN '
+ #delim + convert(varchar(100), pivot) + #delim + ' THEN ' ) + ', ' FROM ##pivot
DROP TABLE ##pivot
SELECT #sql=left(#sql, len(#sql)-1)
SELECT #select=stuff(#select, charindex(' FROM ', #select)+1, 0, ', ' + #sql + ' ')
EXEC (#select)
SET ANSI_WARNINGS ON
Then try the following (I haven't test it, you might need to add qty to select statement)
EXECUTE crosstab 'select ProductID,CustomerID, YearMonth from sales group by ProductId', 'sum(qty)','ProductId','sales'

If you want to try a method that doesn't involve dynamic SQL, you could go through C#.
This guy ran a test comparing the two: http://weblogs.sqlteam.com/jeffs/jeffs/archive/2005/05/12/5127.aspx

Related

SQL GroupBy for Stored Procedure caused Error

I have a quick question that why my SQL Stored Procedure did not work properly. Can someone explain what is wrong with my Stored Procedure Query?
Error: "Each GROUP BY expression must contain at least one column that is not an outer reference."
SELECT
#TabGroupBy + #TabGroupByName + ','
,SUM(Value) AS Sum
,[Unit]
,[Child_Name]
FROM (
SELECT [model_id]
,[Child_ID]
,[Property_ID]
,[DDate]
,[Hour]
,[Value]
FROM [RP_IRP].[M_PLEXOS].[dat_Generators]
where parent_ID=1 and child_ID in(9, 357,358) and Property_ID in (4,31)
) a
inner join [RP_IRP].[M_PLEXOS].[Child_Object] b on a.child_id=b.child_id
inner join [M_PLEXOS].[Property] d on d.[Property_ID] = a.[Property_ID]
inner join [M_PLEXOS].[Units] e on d.[Unit_ID]=e.[Unit_ID]
inner join [M_PLEXOS].[Model_Config] f on a.[Model_id]=f.[Model_id]
WHERE Child_Name = #SelectedChildValue AND Property = #SelectedPropertyValue
AND Unit = #SelectedUnitValue
GROUP BY Child_Name , #TabGroupBy , Unit HAVING SUM(Value) > #MinValue
It can never work the way you are trying it, because by definition, all column names in the SELECT-part, as well as all column names in the GROUP BY-clause can not come from #-variables. They must be written as plain text because they are identifiers.
The same applies to table names + column names in FROM and JOIN clauses.
The reason for this is that the query compiler is built to check all specified columns (and tables, and schemas, and more) against objects that exist in your database(s), and this needs to succeed before a single line of compiled code runs. You should always keep in mind that at compilation time, #-variables don't yet exist and can't have values (because they don't yet exist).
The solution is to use dynamic SQL. You can achieve what you want by building the actual SQL string that you want to execute in a #SQL variable of type NVARCHAR(max), and then EXEC the contents of that variable. EXEC will invoke the SQL query compiler on the contents of its parameter.
Example code, may not be 100% perfect because I can't run it due to not having your database available, but this should get you on your way:
DECLARE #SQL NVARCHAR(max) =
'SELECT ' +
QUOTENAME(#TabGroupBy) + ' AS ' + QUOTENAME(#TabGroupByName) + ', ' +
'SUM(Value) AS Sum, ' +
'[Unit], ' +
'[Child_Name] ' +
'FROM ( ' +
'SELECT [model_id],[Child_ID],[Property_ID],[DDate],[Hour],[Value] ' +
'FROM [RP_IRP].[M_PLEXOS].[dat_Generators] ' +
'where parent_ID = 1 ' +
' and child_ID in (9, 357, 358) ' +
' and Property_ID in (4, 31) ' +
') a ' +
'inner join [RP_IRP].[M_PLEXOS].[Child_Object] b on a.child_id=b.child_id ' +
'inner join [M_PLEXOS].[Property] d on d.[Property_ID] = a.[Property_ID] ' +
'inner join [M_PLEXOS].[Units] e on d.[Unit_ID]=e.[Unit_ID] ' +
'inner join [M_PLEXOS].[Model_Config] f on a.[Model_id]=f.[Model_id] ' +
'WHERE Child_Name = ''' + REPLACE(#SelectedChildValue , '''', '''''') + ''' ' +
' AND Property = ''' + REPLACE(#SelectedPropertyValue, '''', '''''') + ''' ' +
' AND Unit = ''' + REPLACE(#SelectedUnitValue , '''', '''''') + ''' ' +
'GROUP BY Child_Name , ' + QUOTENAME(#TabGroupBy) + ', Unit ' +
'HAVING SUM(Value) > ' + CAST(#MinValue AS VARCHAR(20)) -- assuming #MinValue is INT or FLOAT
EXEC (#SQL)
This code assumes there may be quotes inside the #-variables. Always use REPLACE to double embedded quotes if the #-variables represent string values, or even better: use dynamic SQL along with #-parameters, see this Q & A for how that can be done.
For cases where the #-variables represent database identifiers (column names etc.), you need to use QUOTENAME(...) as in the example code to make sure that no abuse can take place.
You cannot parameterize a GROUP BY column. Remove the parameter #TabGroupBy from the GROUP BY

SQL Server – pulling user data using dynamic query in stored procedure

I have problem, I'm having trouble viewing user information using stored procedure. The procedure accepts three parameters: table, column and searchBySomething. Every time I want to search for a user using another column, the column variable receives the column of the id and the searchBySomething variable receives specific id, the procedure is work but when I'm send another column I'm get the error message
Invalid column name (the data)
The procedure looks like this :
ALTER PROCEDURE [dbo].[userDetailsDisplay]
#table NVARCHAR(30),
#column NVARCHAR(30),
#searchBySomething NVARCHAR(30)
DECLARE #sql NVARCHAR(100)
SET #sql = 'SELECT * FROM ' + #table + ' WHERE ' + #column + ' = ' + #searchBySomething
EXECUTE sp_executesql #sql
So the specific error you're getting is because you're not checking the input to see if the string being passed into #column actually exists. You can check for it's existence against the metadata catalog view sys.columns doing something like this:
if not exists
(
select 1
from sys.columns
where object_id = object_id(#table)
and name = #column
)
begin
raiserror('Column %s does not exist in table %t', 16, 1, #column, #table)
return
end
However I would be remiss if I didn't point out two things.
First, this dynamic table dynamic where clause pattern is very bad practice. If it's for someone who already has database access, they can simply query the tables themselves. And if it's for an external user, well, you've basically given them full database read access through this procedure. Of course there are some rare occasions where this pattern is needed, so if you're dead set on using dynamic sql, that leads me to my next point.
The code you've written is vulnerable to a SQL injection attack. Any time you use dynamic SQL you must be VERY careful how it's constructed. Say I passed in the column name ; drop database [admin]-- Assuming you had such a database, my could would happily be executed and your database would disappear.
How to make dynamic SQL safe is a complex topic, but if you're serious about learning more about it, this is probably one of the best articles you can find. http://www.sommarskog.se/dynamic_sql.html
By parameterizing your query and using quotename() on the table and column, I modified it to look like this. This will still throw weird errors if someone tries to do an injection attack, but at least it wont actually execute their code.
create procedure [dbo].[userDetailsDisplay]
#table nvarchar(30),
#column nvarchar(30),
#searchBySomething nvarchar(30)
as
begin
declare
#sql nvarchar(max),
#params nvarchar(1000)
if not exists
(
select 1
from sys.columns
where object_id = object_id(#table)
and name = #column
)
begin
raiserror('Column %s does not exist in table %t', 16, 1, #column, #table)
return
end
select #sql = '
select *
from ' + quotename(#table) + ' WHERE ' + quotename(#column) + ' = #searchBySomething'
execute sp_executesql
#stmt = #sql,
#params = '#searchBySomething nvarchar(30)',
#searchBySomething = #searchBySomething
end
Just check to make sure that the column exist in the table.
for each #table called, check that the #column variable is in that table.
SET #sql = 'SELECT * FROM ' + #table + ' WHERE ' + #column + ' = ' +''' #searchBySomething +''''
Ex : select * from table where column ='value'

Joining across several tables - then applying a PIVOT to display the data differently

I am trying to figure a way to execute a seemingly complex join scenario, and am unsure as to how to go about it.
Some Background info:
-I have a 'ProjectCategory' table, which contains a foreign key 'ProjectID' and 'CategoryID' to the Project and Category tables respectively. One project could have as many assigned categories to it as there are existing (up to 10)
-I have a 'Budget' table and a 'Sponsor' Table.
-My 'Project' table is related to my 'Budget' Table in that all Projects have an associated BudgetID
-My 'Budget' Table is related to my 'Sponser' table in that all Budgets have an associated SponsorID.
-'Project' table and 'Sponsor' table are not directly related.
An example of the result set that I am trying to get is firstly:
SponsorName(Field in sponsor table) - ProjectName - Category
___________________________________ ___________ ________
A ABC categoryA
A ABC categoryB
A DEF categoryX
A DEF categoryZ
I would then like to use a PIVOT to show the data like:
SponsorName - ProjectName -categoryA - categoryB -categoryC - categoryD ...
___________ ___________ _________ _________ _________ _________
A ABC X X
A DEF X X
B EFG X X
Where the Xs mark which categories are associated with each project/sponsor combination. The filling in of the Xs is maybe something I will do in the codebehind or using other stored procedures, but this is the basic idea of what I am trying to do.
I am having trouble even figuring out how to write a query to get back the first set before I even implement a pivot to show it as the second set, so I am a bit intimidated by this task. Any help greatly appreciated, and please let me know if you need any more information
Assuming SQL Server, I use a stored procedure for the bulk of Dynamic PIVOTS. (Listed Below)
The source could be a table, #temp or even SQL
Exec [prc-Pivot] '#Temp','Category','max(''X'')[]','SponsorName,ProjectName',null
Returns
SponsorName ProjectName categoryA categoryB categoryD categoryX categoryZ
A ABC X X NULL NULL NULL
A DEF NULL NULL NULL X X
B EFG X NULL X NULL NULL
The Stored Procedure
CREATE PROCEDURE [dbo].[prc-Pivot] (
#Source varchar(1000), -- Any Table or Select Statement
#PvotCol varchar(250), -- Field name or expression ie. Month(Date)
#Summaries varchar(250), -- aggfunction(aggValue)[optionalTitle]
#GroupBy varchar(250), -- Optional additional Group By
#OtherCols varchar(500) ) -- Optional Group By or aggregates
AS
--Exec [prc-Pivot] 'Select Year=Year(TR_Date),* From [Chinrus-Series].[dbo].[DS_Treasury_Rates]','''Q''+DateName(QQ,TR_Date)','avg(TR_Y10)[-Avg]','Year','count(*)[Records],min(TR_Y10)[Min],max(TR_Y10)[Max],Avg(TR_Y10)[Avg]'
Set NoCount On
Set Ansi_Warnings Off
Declare #Vals varchar(max),#SQL varchar(max);
Set #Vals = ''
Set #OtherCols= IsNull(', ' + #OtherCols,'')
Set #Source = case when #Source Like 'Select%' then #Source else 'Select * From '+#Source end
Create Table #TempPvot (Pvot varchar(100))
Insert Into #TempPvot
Exec ('Select Distinct Convert(varchar(100),' + #PvotCol + ') as Pvot FROM (' + #Source + ') A')
Select #Vals = #Vals + ', isnull(' + Replace(Replace(#Summaries,'(','(CASE WHEN ' + #PvotCol + '=''' + Pvot + ''' THEN '),')[', ' END),NULL) As [' + Pvot ) From #TempPvot Order by Pvot
Drop Table #TempPvot
Set #SQL = Replace('Select ' + Isnull(#GroupBy,'') + #OtherCols + #Vals + ' From (' + #Source + ') PvtFinal ' + case when Isnull(#GroupBy,'')<>'' then 'Group By ' + #GroupBy + ' Order by ' + #GroupBy else '' end,'Select , ','Select ')
--Print #SQL
Exec (#SQL)
Set NoCount Off
Set Ansi_Warnings on

SQL Server stored procedure showing syntax error

I have created a stored procedure which creates a table at runtime. The table columns and their data types for this dynamic table comes from another table already in place in database.
I am calling this stored procedure from C# console application code. The stored procedure is throwing a syntax error and I am totally not able to figure out what is causing this syntax error.
This is the stored procedure code I've written:
CREATE procedure [dbo].[sproc_TableExists]
#TableName NVARCHAR(128)
,#Column1Name NVARCHAR(32)
,#Column1DataType NVARCHAR(32)
,#Column1Nullable NVARCHAR(32)
AS
DECLARE #SQLString NVARCHAR(MAX)
BEGIN
IF( EXISTS (select * from INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #TableName))
BEGIN
SET #SQLString = 'ALTER TABLE ' + #TableName + '( '+ #Column1Name + ' ' + #Column1DataType + ' '+ #Column1Nullable +')'
EXEC (#SQLString)
END
ELSE
BEGIN
SET #SQLString = 'CREATE TABLE ' + #TableName + '( '+ #Column1Name + ' ' + #Column1DataType + ' '+ #Column1Nullable +')'
EXEC (#SQLString)
END
END
GO
Error I am getting calling it from code :
Incorrect syntax near '('.
The problem lies in the following line of code, when altering an existing table:
SET #SQLString = 'ALTER TABLE ' + #TableName + '( '+ #Column1Name + ' ' + #Column1DataType + ' '+ #Column1Nullable +')'
This generates the following output (as an example using a fake table\column name):
ALTER TABLE TableEx( ColumnEx NVARCHAR(MAX) NULL)
However, this is not a valid SQL statement.
If you want the behaviour to be to add a new column when the table already exists, use this instead:
SET #SQLString = 'ALTER TABLE ' + #TableName + ' ADD '+ #Column1Name + ' ' + #Column1DataType + ' '+ #Column1Nullable
Which will produce the output:
ALTER TABLE TableEx ADD ColumnEx NVARCHAR(MAX) NULL
Note that if your intention is to change the name of a pre-existing column in a pre-existing table you will need to test for this separately - the above only adds a new column to an existing table.
Replace
EXEC (#SQLString)
with
EXEC #SQLString

What is the best way to loop through sql (SQL or Asp.net C#)?

I am creating a list to send an e-mail to. The individual who is logged in has a field in the database of who they report to (unless there is an error or they report to no one).
So for instance if I am logged in and clicked the submit button in SQL it would say I report to 'John Doe'
I then need to grab who 'John Doe' reports to and add that to the list. I need to keep climbing the list until we reach the top of the company (the GID will be blank or null).
Using me as an example, I report to 'John Doe' who reports to 'Tom Doe'. Tom reports to no-one his usrReportsTo field is like this '00000000-0000-0000-0000-000000000000'.
If the usrReportsTo field is "" or NULL or '00000000-0000-0000-0000-000000000000' the loop should terminate.
Here is the sql I used.
What is the cleanest/neatest/most effecient way to perform this loop and is it better to do it in SQL or ASP.net C#?
select usrReportsTo from dbo.CVT_Users_usr, usrEmailAddress
WHERE RTRIM(usrFirstName) + ' ' + RTRIM(usrLastName) = 'James Wilson'
-- Returns 'James Wilson' + email
SELECT RTRIM(usrFirstName) + ' ' + RTRIM(usrLastName) as Name, usrEmailAddress, usrReportsTo from dbo.CVT_Users_usr
WHERE usrGID = '38922F83-4B6E-499E-BF4F-EF0B094F47F7'
-- Returns 'John Doe' + email + reportsTo
SELECT RTRIM(usrFirstName) + ' ' + RTRIM(usrLastName) as Name, usrEmailAddress, usrReportsTo from dbo.CVT_Users_usr
WHERE usrGID = 'FB8F4939-3956-4834-9D89-D72AFB8BF3E0'
-- Returns 'Tom Doe' + email + reportsTo
Edit #3
Working copy of SQL just doesn't return 100% true data.
with cte
AS
(
select usrGID, RTRIM(usrFirstName) + ' ' + RTRIM(usrLastName) as Name, usrEmailAddress, usrReportsTo from dbo.CVT_Users_usr
union all
select u.usrGID, RTRIM(u.usrFirstName) + ' ' + RTRIM(u.usrLastName), cte.usrEmailAddress, cte.usrReportsTo from dbo.CVT_Users_usr u
inner join cte on u.usrReportsTo = cte.usrGID
)
select * from cte
where Name = 'James Wilson'
-- Returns
usrGID Name usrEmailAddress usrReportsTo
E1DAFC11-BE35-4730-9961-68EEF8D85DE4 James Wilson 38922F83-4B6E-499E-BF4F-EF0B094F47F7
E1DAFC11-BE35-4730-9961-68EEF8D85DE4 James Wilson john#1234.com FB8F4939-3956-4834-9D89-D72AFB8BF3E0
E1DAFC11-BE35-4730-9961-68EEF8D85DE4 James Wilson tom#1234.com 00000000-0000-0000-0000-000000000000
Shouldn't the usrGID and name match the same way usrEmailAddress and usrReportsTo does? I tried chaanging the sql to be cte.USRGID and cte.Name but it gave the max recursion error.
Any ideas?
Using a common table expression you can generate the complete result set in one SQL statement (via a recursive join), thus avoiding any looping at all.
A basic example with the key fields
create table #CVT_Users_usr (usrGid uniqueidentifier, usrEmailAddress varchar(50), usrFirstName varchar(20), usrLastName varchar(20), usrReportsTo uniqueidentifier)
insert #CVT_Users_usr values
('38922F83-4B6E-499E-BF4F-EF0B094F47F7' , 'james#wilson.com','james','wilson', 'E1DAFC11-BE35-4730-9961-68EEF8D85DE4'),
('E1DAFC11-BE35-4730-9961-68EEF8D85DE4', 'john#doe.com','john','doe', 'FB8F4939-3956-4834-9D89-D72AFB8BF3E0'),
('FB8F4939-3956-4834-9D89-D72AFB8BF3E0', 'tom#doe.com','tom','doe' ,'00000000-0000-0000-0000-000000000000'),
('FE6899A5-63BA-42B7-A70E-011711A5FAC6', 'ken#ken.com','ken', 'kenneths', 'FB8F4939-3956-4834-9D89-D72AFB8BF3E0')
declare #id uniqueidentifier
select #id = usrGid from #CVT_Users_usr where usrFirstName='james' and usrLastName='wilson'
;with cte as
(
select usrGid, usrEmailAddress, usrFirstName, usrLastName, usrReportsTo from #CVT_Users_usr
union all
select u.usrGid, cte.usrEmailAddress, cte.usrFirstName, cte.usrLastName, cte.usrReportsTo
from #CVT_Users_usr u
inner join cte on u.usrReportsTo= cte.usrGid
)
select usrFirstName + ' ' + usrLastName, usrEmailAddress from cte
where usrGid=#id
drop table #CVT_Users_usr

Categories

Resources