What is happening in this T-SQL code? (Concatenting the results of a SELECT statement) - sql

I'm just starting to learn T-SQL and could use some help in understanding what's going on in a particular block of code. I modified some code in an answer I received in a previous question, and here is the code in question:
DECLARE #column_list AS varchar(max)
SELECT #column_list = COALESCE(#column_list, ',') +
'SUM(Case When Sku2=' + CONVERT(varchar, Sku2) +
' Then Quantity Else 0 End) As [' +
CONVERT(varchar, Sku2) + ' - ' +
Convert(varchar,Description) +'],'
FROM OrderDetailDeliveryReview
Inner Join InvMast on SKU2 = SKU and LocationTypeID=4
GROUP BY Sku2 , Description
ORDER BY Sku2
Set #column_list = Left(#column_list,Len(#column_list)-1)
Select #column_list
----------------------------------------
1 row is returned:
,SUM(Case When Sku2=157 Then Quantity Else 0 End) As [157 -..., SUM(Case ...
The T-SQL code does exactly what I want, which is to make a single result based on the results of a query, which will then be used in another query.
However, I can't figure out how the SELECT #column_list =... statement is putting multiple values into a single string of characters by being inside a SELECT statement. Without the assignment to #column_list, the SELECT statement would simply return multiple rows. How is it that by having the variable within the SELECT statement that the results get "flattened" down into one value? How should I read this T-SQL to properly understand what's going on?

In SQL Server:
SELECT #var = #var + col
FROM TABLE
actually concatenates the values. It's a quirks mode (and I am unable at this time to find a reference to the documentation of feature - which has been used for years in the SQL Server community). If #var is NULL at the start (i.e. an uninitialized value), then you need a COALESCE or ISNULL (and you'll often use a separator):
SELECT #var = ISNULL(#var, '') + col + '|'
FROM TABLE
or this to make a comma-separated list, and then remove only the leading comma:
SELECT #var = ISNULL(#var, '') + ',' + col
FROM TABLE
SET #var = STUFF(#var, 1, 1, '')
or (courtesy of KM, relying on NULL + ',' yielding NULL to eliminate the need for STUFF for the first item in the list):
SELECT #var = ISNULL(#var + ',', '') + col
FROM TABLE
or this to make a list with a leading, separated and trailing comma:
SELECT #var = ISNULL(#var, ',') + col + ','
FROM TABLE

You will want to look into the COALESCE function. A good article describing what is happening can be seen here.

Related

SQL loop for each column in a table

Say I have a table called:
TableA
The following columns exist in the table are:
Column1, Column2, Column3
what I am trying to accomplish is to see how many records are not null.
to do this I have the following case statement:
sum(Case when Column1 is not null then 1 else 0 end)
What I want is the above case statement for every table that exists from a list provided and to be run for each columns that exists in the table.
So for the above example the case statment will run for Column1, Column2 and Column3 as there are 3 columns in that particular table etc
But I want to specfiy a list of tables to loop through executing the logic above
create procedure tab_cols (#tab nvarchar(255))
as
begin
declare #col_count nvarchar(max) = ''
,#col nvarchar(max) = ''
select #col_count += case ORDINAL_POSITION when 1 then '' else ',' end + 'count(' + QUOTENAME(COLUMN_NAME,']') + ') as ' + QUOTENAME(COLUMN_NAME,']')
,#col += case ORDINAL_POSITION when 1 then '' else ',' end + QUOTENAME(COLUMN_NAME,']')
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = #tab
order by ORDINAL_POSITION
declare #stmt nvarchar(max) = 'select * from (select ' + #col_count + ' from ' + #tab + ') t unpivot (val for col in (' + #col + ')) u'
exec sp_executesql #stmt
end
Wouldn't it be easy as this?
SELECT AccountID
,SUM(Total) AS SumTotal
,SUM(Profit) AS SumProfit
,SUM(Loss) AS SumLoss
FROM tblAccount
GROUP BY AccountID
If I understand this correctly you want to get the sums, but not for all rows in one go but for each accountID separately. This is what GROUP BY is for...
If ever possible try to avoid loops, cursors and other procedural approaches...
UPDATE: Generic approach for different tables
With different tables you will - probably - need exactly the statement I show above, but you'll have to generate it dynamically and use EXEC to execute it. You can go through INFORMATION_SCHEMA.COLUMNS to get the columns names...
But:
How should this script know generically, which columns should be summed up? You might head for data_type like 'decimal%' or similar...
What about the other columns and their usage in GROUP BY?
How would you want to place aliases
How do you want to continue with a table of unknown structure?
To be honest: I think, there is no real-generic-one-for-all approach for this...

Dynamic SQL: Grouping by one variable, counting another for column names

I am trying to do a dynamic sql query, similar to some that have appeared on this forum, but for the life of me, I cannot get it to work.
I am using SQL Server 2008. I have a table with a series of order_ref numbers. Each of these numbers has a varying number of advice_refs associated with it. advice_ref numbers are unique (they are a key from another table). There is at least one advice_ref for each order_ref. There are a bunch of columns that describe information for each advice_ref.
What I want to do is create a table with a row for each unique order_ref, with columns for each advice_ref, in ascending order. The columns would be Advice01, Advice02, ....Advice10, Advice11, etc. Not all the Advice# columns would be filled in for every order_ref and the number of advice# columns would depend on the order_ref with the greatest number of advice_refs.
The table would look like:
Order Advice01 Advice02 Advice03 Advice04.....
1 1 2 3
2 5 8 9 20
3 25
The code I've tried to use is:
DECLARE #SQL NVARCHAR(MAX)
DECLARE #PVT NVARCHAR(MAX)
SELECT #SQL = #SQL + ', COALESCE(' + QUOTENAME('Advice' + RowNum) + ', '''') AS ' + QUOTENAME('Advice' + RowNum),
#PVT = #PVT + ', ' + QUOTENAME('Advice' + RowNum)
FROM (SELECT case when RowNum2 < 10 then '0'+RowNum2 when RowNum2 >=10 then RowNum2 end [RowNum] From
( SELECT DISTINCT CONVERT(VARCHAR, ROW_NUMBER() OVER(PARTITION BY order_ref ORDER BY advice_ref)) [RowNum2]
FROM [ED_dups].[dbo].[NewEDDupsLongForm]
) rn2 ) rn
SET #SQL = 'SELECT order_ref' + #SQL + '
FROM ( SELECT order_ref,
advice_ref,
case when CONVERT(VARCHAR, ROW_NUMBER() OVER(PARTITION BY order_ref ORDER BY advice_ref)) < 10
then ''Advice0'' + CONVERT(VARCHAR, ROW_NUMBER() OVER(PARTITION BY order_ref ORDER BY advice_ref))
else ''Advice'' + CONVERT(VARCHAR, ROW_NUMBER() OVER(PARTITION BY order_ref ORDER BY advice_ref))
end [AdviceID]
FROM [ED_dups].[dbo].[NewEDDupsLongForm]
) data
PIVOT
( MAX(advice_ref)
FOR AdviceID IN (' + STUFF(#PVT, 1, 2, '') + ')
) pvt'
EXECUTE SP_EXECUTESQL #SQL
SQL server tells me that the query executed successfully, but there is no output. When I run snippets of the code, it seems that the problem either lies in the pivot statement, near
+ STUFF(#PVT, 1, 2, '') + ')
and/or in the select statement, near
''Advice0'' +
Thanks in advance for any help--I've been at this for days!
I think you have to initialize variables like
DECLARE #SQL NVARCHAR(MAX) = ''
DECLARE #PVT NVARCHAR(MAX) = ''
or
DECLARE #SQL NVARCHAR(MAX)
DECLARE #PVT NVARCHAR(MAX)
SELECT #SQL = '', #PVT = ''
Otherwise your #SQL would be null
fist thing that comes to my mind is - do you really need SQL to fetch you dataset with dynamic number of columns? If you are writting an application, then your user interface, being it a web page or desktop app form, would be much nicer place to transform your data into a desired structure.
If you really need to do so, you will make your life much easier when you will not try to do everything in one big and rather complicated query, but rather split it into smaller tasks done step by step. What I would do is to use temporary tables to store working results, then use cursors to process order by order and advice by advice while inserting my data into temporary table or tables, in the end return a content of this table. Wrap everything in a stored procedure.
This method will also allow you to debug it easier - you can check every single step if it has done what it was expected to do.
And final advice - share a definition of your NewEDDupsLongForm table - someone might write some code to help you out then.
cheers

How Can we use ISNULL to all Column Names in SQL Server 2008?

I have a question
I tried to google it but looks like they don't like *
I'm using SQL Server 2008.
I have the following database table:
P_Id ProductName UnitPrice UnitsInStock UnitsOnOrder
------------------------------------------------------------------------
1 Jarlsberg 10.45 16 15
2 Mascarpone Null 23 NULL
3 Gorgonzola 15.67 9 20
If I need to replace the null with a string I know I do :
SELECT ISNULL(UnitsOnOrder,'No Data') FROM tbl
Questions
How can I use ISNULL() with multi column names ?
is it possible to use it with *
Like
SELECT ISNULL(* , 'NO data') FROM tbl
I think this will be tricky because of the datatype, you can't pass string to INT datatype so how can I fix this too
Update
Okay if i use ISNULL() with a datatype of int it will return 0
which will be a value to me , how can i pass empty string instead ?
You can use ISNULL multiple times in the same SQL statement for different columns, but you must write it separately for each column:
SELECT
ISNULL(ProductName, 'No Data') AS ProductName,
ISNULL(CAST(UnitPrice AS NVARCHAR), 'No Data') AS UnitPrice,
ISNULL(CAST(UnitsInStock AS NVARCHAR), 'No Data') AS UnitsInStock,
ISNULL(CAST(UnitsOnOrder AS NVARCHAR), 'No Data') AS UnitsOnOrder
FROM tbl
If you are building a dynamic SQL query, you could theoretically gather a list of columns in the table and generate a query with ISNULL on each one. For example:
DECLARE #SQL nvarchar(max)
SET #SQL = 'SELECT '
SELECT #SQL = #SQL + 'ISNULL(CAST([' + sc.name + '] AS NVARCHAR), ''No Data'') AS [' + sc.name + '],'
FROM sys.objects so
INNER JOIN sys.columns sc ON sc.object_id = so.object_id
WHERE so.name = 'tbl'
-- Remove the trailing comma
SELECT #SQL = LEFT(#SQL, LEN(#SQL) - 1) + ' FROM tbl'
EXEC sp_sqlexec #SQL
This code has problems when converting some column types like timestamps to an nvarchar, but it illustrates the technique.
Note that if you had another column that should be returned if a value is null, you could use the COALESCE expression like this:
SELECT COALESCE(ProductName, P_Id) AS Product...
Try this...
ISNULL (COALESCE (column1, column2), 'No Data')
You would need to include all column names though, you can't use *
COALESCE returns the first non-null value in its argument list so if they are all null it will return null

SQL Server count number of distinct values in each column of a table

I have a table with ~150 columns. I'd like to find the count(distinct(colName)) for each column but am wondering if there's a way to do so without actually typing out each column name.
Ideally I would use count(distinct(*)) but this doesn't work.
Any other suggestions?
EDIT:
if this is my table:
id col1 col2 col3 ...
01 10001 west north
02 10001 west south
03 10002 east south
04 10002 west north
05 10001 east south
06 10003 west north
I'm looking for this output
count(distinct(id)) count(distinct(col1)) count(distinct(col2)) count(distinct(col3))
6 3 2 2
You can do this:
DECLARE #query varchar(max)
SELECT #query =
'SELECT ' + SUBSTRING((SELECT ',' +'COUNT(DISTINCT(' + column_name + '))
As ' + column_name + ' '
FROM information_schema.columns
WHERE
table_name = 'table_name'
for xml path('')),2,200000) + 'FROM table_name'
PRINT(#query)
Use the below script to build T-SQL query that will return a distinct count of each column in a table. Replace #Table value with your table name.
DECLARE #Table SYSNAME = 'TableName';
-- REVERSE and STUFF used to remove trailing UNION in string
SELECT REVERSE(STUFF(REVERSE((SELECT 'SELECT ''' + name
+ ''' AS [Column], COUNT(DISTINCT('
+ QUOTENAME(name) + ')) AS [Count] FROM '
+ QUOTENAME(#Table) + ' UNION '
-- get column name from sys.columns
FROM sys.columns
WHERE object_id = Object_id(#Table)
-- concatenate result strings with FOR XML PATH
FOR XML PATH (''))), 1, 7, ';'));
Expanded answer from Bryan. His great answer lists the fields alphabetically.
This is no problem if you have a dozen fields or so. If you have 150 fields, like OP stated, this keeps the fields in their table's order.
I modified his query in order to examine a 213 column (vendor's) table and wanted to post for future reference.
DECLARE #Table SYSNAME = 'Your table name; without schema; no square brackets';
-- REVERSE and STUFF used to remove trailing UNION in string
SELECT REVERSE(STUFF(REVERSE((SELECT 'SELECT '
+ CAST(column_id AS VarChar(4)) + ' AS [column_id],' -- extra column
+ '''' + name
+ ''' AS [Column], COUNT(DISTINCT('
+ QUOTENAME(name) + ')) AS [Count] FROM '
+ QUOTENAME(#Table) + ' UNION '
-- get column name from sys.columns
FROM sys.columns
WHERE system_type_id NOT IN (34,240) AND object_id = Object_id(#Table)
ORDER BY column_id -- keeps columns in table order
-- concatenate result strings with FOR XML PATH
FOR XML PATH (''))), 1, 7, ';'));
I decided to not edit Bryan's answer because often people won't need the extra column.
(The ORDER BY has no effect if you don't add the column_id column. I believe this is because only the outermost ORDER BY is guarenteed to order the final output; I'd love to have a msft reference that confirms this)
EDIT: Using function Count with field types Image and Geography throws error.
Added "system_type_id NOT IN (34,240)".
I don't believe this is possible with just MySQL.
I would think your going to have to use a server side language to get the results you want.
Use "DESC TABLE" as your first query and then for each "field" row, compile your query.
Ignore this, wrong system tag :)
This should do:
select count(*) from (select distinct * from myTable) as t
Here is SQL Fiddle test.
create table Data
(
Id int,
Data varchar(50)
)
insert into Data
select 1, 'ABC'
union all
select 1, 'ABC'
select count(*)
from (select distinct * from Data) as t

What is the best way to collapse the rows of a SELECT into a string?

In a SQL statement ( or procedure ) I want to collapse the rows of this table into a single comma delimited string.
simpleTable
id value
-- -----
1 "a"
2 "b"
3 "c"
Collapse to:
"a, b, c"
You can concatenate using an embedded 'set' statement in a query:
declare #combined varchar(2000)
select #combined = isnull(#combined + ', ','') + isnull(value,'')
from simpleTable
print #combined
(Note that the first isnull() initialises the string, and the second isnull() is especially important if there's any chance of nulls in the 'value' column, because otherwise a single null could wipe out the whole concatenation)
(edited code and explanation after comments)
Edit (ten years later):
SQL Server 2017 introduced the STRING_AGG() function which provides an official way of concatenating strings from different rows. Like other aggregation functions such as COUNT(), it can be used with GROUP BY.
So for the example above you could do:
select string_agg(value, ', ')
from simpleTable
If you had some other column and you wanted to concatenate for values of that column, you would add a 'group by' clause, e.g:
select someCategory, string_agg(value, ', ') as concatValues
from simpleTable
group by someCategory
Note string_agg will only work with SQL 2017 and above.
This will only work in MSSQL 2005+
select value + ',' from simpletable for xml path ('')
..one way to prevent the extra comma:
select case(row_number() over (order by id))
when 1 then value else ',' + value end
from simpletable
for xml path ('')
DECLARE #EmployeeList varchar(100)
SELECT #EmployeeList = COALESCE(#EmployeeList + ', ', '') +
CAST(Emp_UniqueID AS varchar(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1
SELECT #EmployeeList
Results:
1, 2, 4
This is based on #codeulike answer, but will prevent losing the portion of the string that gets concatenated before a null "value" is concatenated on.
declare #combined varchar(2000)
select #combined = isnull(#combined + ', ','') + ISNULL(value,'')
from simpleTable
print #combined