This is my query in SQL Server 2008 R2:
INSERT VMP_Staging_Products_Temporary
(FRM_ID, ITEM_ID, ItemDesc, NUMBER_ITEM)
SELECT upvt.FARM_ID,
p.REC_ITEM_CODE,
p.REC_ITEM_NAME_A,
upvt.NUMBER_ITEM
FROM VMP_Staging AS f
UNPIVOT
(
NUMBER_ITEM
FOR ItemDesc IN ([KHALAS], [FARDH], [OTHER_LULU], [KHENAIZI], [BOUMAAN], [BARHI], [JESH_KHARMA], [REZIZ]
, [JABRI], [ANBARET_AL_MADINA], [SHISHI], [DABBAS], [NABTET_SAIF], [KHEDRAWI], [HILALI], [MAKTOUMY]
, [NAMISHI], [SULTANAH], [BAQLAT_AL_TAWAA], [BAQLAT_AL_DAHLA], [BAQLAT_AL_RARENJA], [SUKARY], [SAQEI], [ABU_ZEBED]
, [MAJDOUL], [SHABIBI], [YOUWANI], [YARDI], [KHADI], [HATIMI], [NEGHAL], [OTHER_SAYER])
) AS upvt
INNER JOIN REC_ITEM AS P
on p.REC_ITEM_NAME_A = upvt.ItemDesc
As you see, I am doing a join between REC_ITEM table and the result of the unpivot.
The problem happens in the column names that has _ like ABU_ZEBED where the value in the REC_ITEM is abu zebed
How can I solve it please?
Update 1
If there is a way to do that using c# code, it is good too. because this is a stored procedure called from c# so if there is solution to deal with this in c#, I would like that too.
Depends on whether it's a regular pattern or not, but at least:
INNER JOIN REC_ITEM AS P
on replace(p.REC_ITEM_NAME_A, ' ', '_') = upvt.ItemDesc
Performance will be terrible, though.
You can try joining in multiple ways:
on p.REC_ITEM_NAME_A = upvt.ItemDesc or
p.REC_ITEM_NAME_A = replace(upvt.ItemDesc, '_', ' ')
Alternatively, remove both spaces and underscores from both sides:
on replace(replace(p.REC_ITEM_NAME_A, '_', ''), ' ', '') =
replace(replace(upvt.ItemDesc, '_', ''), ' ', '')
Related
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
I need to create a report of all our items with a URL link to the webpage that is generated for each item. The website uses the .ToUrlString extension (http://www.extensionmethod.net/1935/csharp/string/tourlstring) to generate the link address. Is there a way to get the same result in SQL? The database is SqlServer 2008R2.
This is the code for the extension:
public static string ToUrlString(this string str) {
if (String.IsNullOrEmpty(str)) return "";
// Unicode Character Handling: http://blogs.msdn.com/b/michkap/archive/2007/05/14/2629747.aspx
string stFormD = str.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
var sb = new StringBuilder();
foreach (char t in
from t in stFormD
let uc = CharUnicodeInfo.GetUnicodeCategory(t)
where uc != UnicodeCategory.NonSpacingMark
select t) {
sb.Append(t);
}
return Regex.Replace(sb.ToString().Normalize(NormalizationForm.FormC), "[\\W\\s]{1,}", "-").Trim('-');
}
This function turns "Cups & Saucers" into "cups-saucers" and
"Spoon-Style Long Sticks-Set of 4" into "spoonstyle-long-sticksset-of-4"
EDIT:
Because it was 2am, I was tired, and was pretty sure I wasn't going to successfully teach my self anything new at that time, I created a long-azz REPLACE() statement to filter out what I needed. That's below in a separate answer for anyone that decides they want to do it the hard way...
The correct way is to create a CLR function, as suggested by #DaleBurrell in the comments. This article explains it, step by step: SQL CLR Functions:
UPDATED ON 20190424 BASED ON OP'S COMMENTS BELOW
This is easy as pie using T-SQL. Using PatReplace8K and a function I just wrote to remove repeated instances of a specific character (RemoveDupes8K) you could do this:
DECLARE #string VARCHAR(1000) = 'Spoon-Style Long Sticks-Set of 4'
SELECT newstring = LOWER(r.NewString)
FROM dbo.patreplace8k(REPLACE(#string,'-',''),'[^a-zA-Z0-9]','-') AS f
CROSS APPLY dbo.RemoveDupes8K(f.NewString,'-') AS r;
Returns: spoonstyle-long-sticksset-of-4
Here's what's going on. PatReplace8k is taking the input string (#string) and replacing all non-alphanumeric characters with a hyphen. RemoveDupes8K takes that new string and replaces duplicate hyphens with one.
Here's the functions:
CREATE FUNCTION dbo.RemoveDupes8K
(
#string VARCHAR(8000),
#char CHAR(1)
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT NewString =
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(#string)),
REPLICATE(#char,33),#char),
REPLICATE(#char,17),#char),
REPLICATE(#char,9), #char),
REPLICATE(#char,5), #char),
REPLICATE(#char,3), #char),
REPLICATE(#char,2), #char),
REPLICATE(#char,2), #char);
GO
CREATE FUNCTION dbo.PatReplace8K
(
#string VARCHAR(8000),
#pattern VARCHAR(50),
#replace VARCHAR(1)
)
/*****************************************************************************************
Purpose:
Given a string (#String), a pattern (#Pattern), and a replacement character (#Replace)
PatReplace8K will replace any character in #String that matches the #Pattern parameter
with the character, #Replace.
Usage:
--===== Basic Syntax Example
SELECT pr.NewString
FROM dbo.PatReplace8K(#String,#Pattern,#Replace);
--===== Replace numeric characters with a "*"
SELECT pr.NewString
FROM dbo.PatReplace8K('My phone number is 555-2211','[0-9]','*') pr;
--==== Using againsts a table
DECLARE #table TABLE(OldString varchar(40));
INSERT #table VALUES
('Call me at 555-222-6666'),
('phone number: (312)555-2323'),
('He can be reached at 444.665.4466');
SELECT t.OldString, pr.NewString
FROM #table t
CROSS APPLY dbo.PatReplace8K(t.oldstring,'[0-9]','*') pr;
Programmer Notes:
1. Required SQL Server 2008+
2. #Pattern IS case sensitive but can be easily modified to make it case insensitive
3. There is no need to include the "%" before and/or after your pattern since since we
are evaluating each character individually
4. Certain special characters, such as "$" and "%" need to be escaped with a "/"
like so: [/$/%]
Revision History:
Rev 00 - 10/27/2014 Initial Development - Alan Burstein
Rev 01 - 10/29/2014 Mar 2007 - Alan Burstein
- Redesigned based on the dbo.STRIP_NUM_EE by Eirikur Eiriksson
(see: http://www.sqlservercentral.com/Forums/Topic1585850-391-2.aspx)
- change how the cte tally table is created
- put the include/exclude logic in a CASE statement instead of a WHERE clause
- Added Latin1_General_BIN Colation
- Add code to use the pattern as a parameter.
Rev 02 - 20141106
- Added final performane enhancement (more cudo's to Eirikur Eiriksson)
- Put 0 = PATINDEX filter logic into the WHERE clause
Rev 03 - 20150516
- Updated to deal with special XML characters
Rev 04 - 20170320
- changed #replace from char(1) to varchar(1) to address how spaces are handled
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN
WITH
E1(N) AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS E1(N)),
iTally(N) AS
(
SELECT TOP (LEN(#String)) CHECKSUM(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))
FROM E1 a,E1 b,E1 c,E1 d
)
SELECT NewString =
((
SELECT
CASE
WHEN PATINDEX(#Pattern,SUBSTRING(#String COLLATE Latin1_General_BIN,N,1)) = 0
THEN SUBSTRING(#String,N,1)
ELSE #replace
END
FROM iTally
FOR XML PATH(''), TYPE
).value('.[1]','varchar(8000)'));
GO
As promised, for anyone that wanted to see the REPLACE() method...
SELECT 'Product' Source
,prd.[id]
,prd.[name] as Title
,CAST(prd.[description] as varchar(max)) as [Description]
,'/' + lower(replace(replace(RTrim(par.name), ' &', ''), ' ' ,'-') + '/' + REPLACE(replace(replace(RTrim(child.name),'\',''), ' &', ''), ' ', '-') + '/' + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LTRIM(RTrim(prd.name)),'.',''),'$',''),'/',''),')',''),'(',''),'-',''),',',''), '''',''), '"', ''), ' &', ''),' ',' '), ' ' , '-') ) as Link
,prd.[upc] as GTIN
,prd.[active]
FROM [dbo].[Products] prd
INNER join [dbo].[CategoryProducts] catprd ON catprd.productId = prd.id
INNER JOIN [dbo].[Categories] child on child.Id = catprd.categoryId
INNER join [dbo].[Categories] par on par.id = child.parentCategoryId
I want to retrieve the results from SQL Server 2012.But for checking condition I rewrote the same subquery.Is there any idea to use the subquery just once and retrieve the result?
My query:
sql = "SELECT customer_id,ISNULL(first_name, '') + ' ' + ISNULL(middle_name, ' ') + ' ' + ISNULL(last_name, ' ') AS 'Customer_name', (ISNULL(city, '') + ',' + ISNULL(district, ' ') + ',' + ISNULL(zone, ' ')) as 'Location' FROM customer_detail WHERE 1=1";
if(location != "")
{
sql += " AND (ISNULL(city, '') + ',' + ISNULL(district, ' ') + ',' + ISNULL(zone, ' ')) LIKE '%" + location + "%'"";
}
Query after AND is same as above subquery. Thanks in advance.
It seems you are not looking so much for sub-query than a user defined function (UDF) to merge 3 text column in a prettier way.
If you do not want to use UDF then you could use Common Table Expressions (CTE) to write the expression just once.
Using CTE has also the benefit of encapsulating your reused block right inside query, whereas the UDF would need to be added to your DB before executing your query. This may or may not be a desirable thing depending on reusability needs.
CTE solution would be along the lines of this:
WITH CTE (Id, [Name], [Location]) as
(
SELECT customer_id,
ISNULL(first_name, '') + ' ' + ISNULL(middle_name, ' ') + ' ' + ISNULL(last_name, ' '),
ISNULL(city, '') + ',' + ISNULL(district, ' ') + ',' +ISNULL(zone, ' ')
FROM customer_detail
)
select *
from CTE
where 1=1
AND [Location] LIKE '%' + #location + '%'
Also, generally you can expect CTE to perform generally better than UDFs as the query optimizer can modify the plan to match specific query needs.
I would go with a reusable view and stored procedure for the actual search.
create view vCustomerDetail
as
select [customer_id]
, isnull([first_name] + ' ', '') + isnull([middle_name] + ' ', '') + isnull([last_name], '') as [Customer_name]
, isnull([city] + ', ', '') + isnull([district] + ', ', '') + isnull([zone], '') as [Location]
from [customer_detail]
go
create proc pCustomerDetailByLocation
(
#location nvarchar(200) = ''
)
as
begin
set nocount on;
select *
from [vCustomerDetail]
where [Location] = ''
union all
select *
from [vCustomerDetail]
where [Location] like '%' + #location + '%';
end
go
You would call the stored procedure from your code with and pass along the location in the parameter. That way you can prevent SQL injection and also make use of better execution plan caching than with an ad-hoc query.
You can either use a union like I've done in my example, or combine it with an OR statement, like so:
where [Location] = '' or [Location] like '%' + #location + '%';
I found the union to be beneficial in some cases over because you're helping the optimizer split up his lookup logic.
I'm trying to migrate a MySQL-based app over to Microsoft SQL Server 2005 (not by choice, but that's life).
In the original app, we used almost entirely ANSI-SQL compliant statements, with one significant exception -- we used MySQL's group_concat function fairly frequently.
group_concat, by the way, does this: given a table of, say, employee names and projects...
SELECT empName, projID FROM project_members;
returns:
ANDY | A100
ANDY | B391
ANDY | X010
TOM | A100
TOM | A510
... and here's what you get with group_concat:
SELECT
empName, group_concat(projID SEPARATOR ' / ')
FROM
project_members
GROUP BY
empName;
returns:
ANDY | A100 / B391 / X010
TOM | A100 / A510
So what I'd like to know is: Is it possible to write, say, a user-defined function in SQL Server which emulates the functionality of group_concat?
I have almost no experience using UDFs, stored procedures, or anything like that, just straight-up SQL, so please err on the side of too much explanation :)
No REAL easy way to do this. Lots of ideas out there, though.
Best one I've found:
SELECT table_name, LEFT(column_names , LEN(column_names )-1) AS column_names
FROM information_schema.columns AS extern
CROSS APPLY
(
SELECT column_name + ','
FROM information_schema.columns AS intern
WHERE extern.table_name = intern.table_name
FOR XML PATH('')
) pre_trimmed (column_names)
GROUP BY table_name, column_names;
Or a version that works correctly if the data might contain characters such as <
WITH extern
AS (SELECT DISTINCT table_name
FROM INFORMATION_SCHEMA.COLUMNS)
SELECT table_name,
LEFT(y.column_names, LEN(y.column_names) - 1) AS column_names
FROM extern
CROSS APPLY (SELECT column_name + ','
FROM INFORMATION_SCHEMA.COLUMNS AS intern
WHERE extern.table_name = intern.table_name
FOR XML PATH(''), TYPE) x (column_names)
CROSS APPLY (SELECT x.column_names.value('.', 'NVARCHAR(MAX)')) y(column_names)
I may be a bit late to the party but this method works for me and is easier than the COALESCE method.
SELECT STUFF(
(SELECT ',' + Column_Name
FROM Table_Name
FOR XML PATH (''))
, 1, 1, '')
SQL Server 2017 does introduce a new aggregate function
STRING_AGG ( expression, separator).
Concatenates the values of string expressions and places separator
values between them. The separator is not added at the end of string.
The concatenated elements can be ordered by appending WITHIN GROUP (ORDER BY some_expression)
For versions 2005-2016 I typically use the XML method in the accepted answer.
This can fail in some circumstances however. e.g. if the data to be concatenated contains CHAR(29) you see
FOR XML could not serialize the data ... because it
contains a character (0x001D) which is not allowed in XML.
A more robust method that can deal with all characters would be to use a CLR aggregate. However applying an ordering to the concatenated elements is more difficult with this approach.
The method of assigning to a variable is not guaranteed and should be avoided in production code.
Possibly too late to be of benefit now, but is this not the easiest way to do things?
SELECT empName, projIDs = replace
((SELECT Surname AS [data()]
FROM project_members
WHERE empName = a.empName
ORDER BY empName FOR xml path('')), ' ', REQUIRED SEPERATOR)
FROM project_members a
WHERE empName IS NOT NULL
GROUP BY empName
Have a look at the GROUP_CONCAT project on Github, I think I does exactly what you are searching for:
This project contains a set of SQLCLR User-defined Aggregate functions (SQLCLR UDAs) that collectively offer similar functionality to the MySQL GROUP_CONCAT function. There are multiple functions to ensure the best performance based on the functionality required...
To concatenate all the project manager names from projects that have multiple project managers write:
SELECT a.project_id,a.project_name,Stuff((SELECT N'/ ' + first_name + ', '+last_name FROM projects_v
where a.project_id=project_id
FOR
XML PATH(''),TYPE).value('text()[1]','nvarchar(max)'),1,2,N''
) mgr_names
from projects_v a
group by a.project_id,a.project_name
With the below code you have to set PermissionLevel=External on your project properties before you deploy, and change the database to trust external code (be sure to read elsewhere about security risks and alternatives [like certificates]) by running ALTER DATABASE database_name SET TRUSTWORTHY ON.
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.SqlServer.Server;
[Serializable]
[SqlUserDefinedAggregate(Format.UserDefined,
MaxByteSize=8000,
IsInvariantToDuplicates=true,
IsInvariantToNulls=true,
IsInvariantToOrder=true,
IsNullIfEmpty=true)]
public struct CommaDelimit : IBinarySerialize
{
[Serializable]
private class StringList : List<string>
{ }
private StringList List;
public void Init()
{
this.List = new StringList();
}
public void Accumulate(SqlString value)
{
if (!value.IsNull)
this.Add(value.Value);
}
private void Add(string value)
{
if (!this.List.Contains(value))
this.List.Add(value);
}
public void Merge(CommaDelimit group)
{
foreach (string s in group.List)
{
this.Add(s);
}
}
void IBinarySerialize.Read(BinaryReader reader)
{
IFormatter formatter = new BinaryFormatter();
this.List = (StringList)formatter.Deserialize(reader.BaseStream);
}
public SqlString Terminate()
{
if (this.List.Count == 0)
return SqlString.Null;
const string Separator = ", ";
this.List.Sort();
return new SqlString(String.Join(Separator, this.List.ToArray()));
}
void IBinarySerialize.Write(BinaryWriter writer)
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(writer.BaseStream, this.List);
}
}
I've tested this using a query that looks like:
SELECT
dbo.CommaDelimit(X.value) [delimited]
FROM
(
SELECT 'D' [value]
UNION ALL SELECT 'B' [value]
UNION ALL SELECT 'B' [value] -- intentional duplicate
UNION ALL SELECT 'A' [value]
UNION ALL SELECT 'C' [value]
) X
And yields: A, B, C, D
Tried these but for my purposes in MS SQL Server 2005 the following was most useful, which I found at xaprb
declare #result varchar(8000);
set #result = '';
select #result = #result + name + ' '
from master.dbo.systypes;
select rtrim(#result);
#Mark as you mentioned it was the space character that caused issues for me.
About J Hardiman's answer, how about:
SELECT empName, projIDs=
REPLACE(
REPLACE(
(SELECT REPLACE(projID, ' ', '-somebody-puts-microsoft-out-of-his-misery-please-') AS [data()] FROM project_members WHERE empName=a.empName FOR XML PATH('')),
' ',
' / '),
'-somebody-puts-microsoft-out-of-his-misery-please-',
' ')
FROM project_members a WHERE empName IS NOT NULL GROUP BY empName
By the way, is the use of "Surname" a typo or am i not understanding a concept here?
Anyway, thanks a lot guys cuz it saved me quite some time :)
2021
#AbdusSalamAzad's answer is the correct one.
SELECT STRING_AGG(my_col, ',') AS my_result FROM my_tbl;
If the result is too big, you may get error "STRING_AGG aggregation result exceeded the limit of 8000 bytes. Use LOB types to avoid result truncation." , which can be fixed by changing the query to this:
SELECT STRING_AGG(convert(varchar(max), my_col), ',') AS my_result FROM my_tbl;
For my fellow Googlers out there, here's a very simple plug-and-play solution that worked for me after struggling with the more complex solutions for a while:
SELECT
distinct empName,
NewColumnName=STUFF((SELECT ','+ CONVERT(VARCHAR(10), projID )
FROM returns
WHERE empName=t.empName FOR XML PATH('')) , 1 , 1 , '' )
FROM
returns t
Notice that I had to convert the ID into a VARCHAR in order to concatenate it as a string. If you don't have to do that, here's an even simpler version:
SELECT
distinct empName,
NewColumnName=STUFF((SELECT ','+ projID
FROM returns
WHERE empName=t.empName FOR XML PATH('')) , 1 , 1 , '' )
FROM
returns t
All credit for this goes to here:
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/9508abc2-46e7-4186-b57f-7f368374e084/replicating-groupconcat-function-of-mysql-in-sql-server?forum=transactsql
For SQL Server 2017+, use STRING_AGG() function
SELECT STRING_AGG(Genre, ',') AS Result
FROM Genres;
Sample result:
Result
Rock,Jazz,Country,Pop,Blues,Hip Hop,Rap,Punk
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