How Coalesce works in sql server? - tsql

Create table test(Names varchar(100) primary key )
insert into test values('Hugeman')
insert into test values('Jack')
insert into test values('William')
insert into test values('Kevin')
insert into test values('Peter')
Query 1:
declare #sql varchar(100)
select #sql = coalesce(#sql+'+','')+Names from test order by names-- where object_id =object_id('temp')
print #sql
This will result as
Hugeman+Jack+Kevin+Peter+William
Query 2
declare #sql varchar(100)
select #sql = coalesce(Names+'+','') from test order by names-- where object_id =object_id('temp')
print #sql
This will results William+
As per the documentation of coalesce, will return the first not null value. So it has to result Hugeman+. But it returns the entire rows.
Why query2 haven't done the same ?

This is not stricly connected to COALESCE.
Try these SELECTs:
DECLARE #sql1 AS VARCHAR(1000)
SELECT #sql1 = ISNULL(#sql1, '') + Names FROM test ORDER BY Names
PRINT #sql1
DECLARE #sql2 AS VARCHAR(1000)
SELECT #sql2 = Names FROM test ORDER BY Names
PRINT #sql2
So what happened? For EACH record selected:
in query 1 you keep adding values to #sql
in query 2 you reset #sql as the last name extracted
I don't understand exactly what you want to obtain from your SELECT, but a better example of COALESCE could be:
CREATE TABLE TEST2(Name VARCHAR(100) PRIMARY KEY, Telephone VARCHAR(10), Mobile VARCHAR(10))
INSERT INTO TEST2 VALUES('Hugeman', 1, 2)
INSERT INTO TEST2 VALUES('Jack', NULL, 3)
INSERT INTO TEST2 VALUES('William', 4, NULL)
INSERT INTO TEST2 VALUES('Kevin', 5, 6)
INSERT INTO TEST2 VALUES('Peter', NULL, NULL)
SELECT Name,
COALESCE(Telephone, Mobile) AS Tel
FROM TEST2

#sql is on the right hand side on the first to get appended
On the last #sql is all alone on the left side and has the value of the last row

Query-1: Appending the values of all rows to #sql
Query-2: Re-setting values to #sql
The below is the rows with order by names
Hugeman
Jack
Kevin
Peter
William
Query-1 Appends all values to #sql.
Query-2 Re-writing values to #sql, so the last row in the list is William so when you print the variable the last re-assigned value is printed.

The COALESCE() function returns the first non null value from the from the columns as parameters in COALESCE() function.
e.g.
SELECTCOALESCE(null,null,1,2) ///return --1
SELECT Id, COALESCE(FirstName, MiddleName, LastName) AS Name
FROM tblEmployee //return 1st non null value from FirstName/MiddleName/LastName

Related

Run a stored procedure using select columns as input parameters?

I have a select query that returns a dataset with "n" records in one column. I would like to use this column as the parameter in a stored procedure. Below a reduced example of my case.
The query:
SELECT code FROM rawproducts
The dataset:
CODE
1
2
3
The stored procedure:
ALTER PROCEDURE [dbo].[MyInsertSP]
(#code INT)
AS
BEGIN
INSERT INTO PRODUCTS description, price, stock
SELECT description, price, stock
FROM INVENTORY I
WHERE I.icode = #code
END
I already have the actual query and stored procedure done; I just am not sure how to put them both together.
I would appreciate any assistance here! Thank you!
PS: of course the stored procedure is not as simple as above. I just choose to use a very silly example to keep things small here. :)
Here's two methods for you, one using a loop without a cursor:
DECLARE #code_list TABLE (code INT);
INSERT INTO #code_list SELECT code, ROW_NUMBER() OVER (ORDER BY code) AS row_id FROM rawproducts;
DECLARE #count INT;
SELECT #count = COUNT(*) FROM #code_list;
WHILE #count > 0
BEGIN
DECLARE #code INT;
SELECT #code = code FROM #code_list WHERE row_id = #count;
EXEC MyInsertSP #code;
DELETE FROM #code_list WHERE row_id = #count;
SELECT #count = COUNT(*) FROM #code_list;
END;
This works by putting the codes into a table variable, and assigning a number from 1..n to each row. Then we loop through them, one at a time, deleting them as they are processed, until there is nothing left in the table variable.
But here's what I would consider a better method:
CREATE TYPE dbo.code_list AS TABLE (code INT);
GO
CREATE PROCEDURE MyInsertSP (
#code_list dbo.code_list)
AS
BEGIN
INSERT INTO PRODUCTS (
[description],
price,
stock)
SELECT
i.[description],
i.price,
i.stock
FROM
INVENTORY i
INNER JOIN #code_list cl ON cl.code = i.code;
END;
GO
DECLARE #code_list dbo.code_list;
INSERT INTO #code_list SELECT code FROM rawproducts;
EXEC MyInsertSP #code_list = #code_list;
To get this to work I create a user-defined table type, then use this to pass a list of codes into the stored procedure. It means slightly rewriting your stored procedure, but the actual code to do the work is much smaller.
(how to) Run a stored procedure using select columns as input
parameters?
What you are looking for is APPLY; APPLY is how you use columns as input parameters. The only thing unclear is how/where the input column is populated. Let's start with sample data:
IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL DROP TABLE dbo.Products;
IF OBJECT_ID('dbo.Inventory','U') IS NOT NULL DROP TABLE dbo.Inventory;
IF OBJECT_ID('dbo.Code','U') IS NOT NULL DROP TABLE dbo.Code;
CREATE TABLE dbo.Products
(
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Inventory
(
icode INT NOT NULL,
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Code(icode INT NOT NULL);
INSERT dbo.Inventory
VALUES (10,'',20.10,3),(11,'',40.10,3),(11,'',25.23,3),(11,'',55.23,3),(12,'',50.23,3),
(15,'',33.10,3),(15,'',19.16,5),(18,'',75.00,3),(21,'',88.00,3),(21,'',100.99,3);
CREATE CLUSTERED INDEX uq_inventory ON dbo.Inventory(icode);
The function:
CREATE FUNCTION dbo.fnInventory(#code INT)
RETURNS TABLE AS RETURN
SELECT i.[description], i.price, i.stock
FROM dbo.Inventory I
WHERE I.icode = #code;
USE:
DECLARE #code TABLE (icode INT);
INSERT #code VALUES (10),(11);
SELECT f.[description], f.price, f.stock
FROM #code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
Results:
description price stock
-------------- -------- -----------
20.10 3
40.10 3
Updated Proc (note my comments):
ALTER PROC dbo.MyInsertSP -- (1) Lose the input param
AS
-- (2) Code that populates the "code" table
INSERT dbo.Code VALUES (10),(11);
-- (3) Use CROSS APPLY to pass the values from dbo.code to your function
INSERT dbo.Products ([description], price, stock)
SELECT f.[description], f.price, f.stock
FROM dbo.code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
This ^^^ is how it's done.

Taking result from SQL/T-SQL Subselect into the parent select statement

I want to extend ListA with Company coming from #MyList.CompanyNo, plese refer to the code listing
Data&Init:
begin /*Just the init data*/
DECLARE #MyList TABLE (Mail nvarchar(max), CompanyNo int)
INSERT INTO #MyList VALUES ('...com',20)
INSERT INTO #MyList VALUES ('...com',230)
INSERT INTO #MyList VALUES ('...com',120)
INSERT INTO #MyList VALUES ('...com',223)
end
--DECLARE
DECLARE #ListA TABLE (Id nvarchar(max), Mail nvarchar(max))
DECLARE #ListB TABLE (Id nvarchar(max), Mail nvarchar(max),Company int)
Starting point(this works):
INSERT INTO #ListA(Id,Mail) select someId,name from [somedb].[dbo].aers where name IN (SELECT Mail FROM #MyList)
I was trying to do it the following way:
INSERT INTO #ListB(Id,Mail,Company) select someId,name,#MyList.CompanyNo from [somedb].[dbo].aers where name IN (SELECT Mail FROM #MyList)
So actually I want to extend ListB with the corrosponding #MyList.CompanyNo.
Thanks, what can I do ?
You could use JOIN based on condition from WHERE:
INSERT INTO #ListB(Id,Mail,Company)
select a.someId,a.name,m.CompanyNo
from [somedb].[dbo].aers a
join #MyList m
ON a.name = m.Mail;

How to conditionally exclude or add columns when calculating row total

I have a stored procedure with a parameter defined like below
#CategoryNames NVARCHAR(MAX)
The above parameter gets a comma separated string from the application, for example the value can be like this #StaffCategoryIds=N'Category1,Category2,Category3,Category4,Category5'
Then i have the sample below sql query
SET #sql = #sql + 'TypeId,
TypeName,
Condition,
Region,
(Category1 + Category2 + Category3 + Category4 + Category5) AS Total INTO ##QueryResults
FROM Table1;'
When computing my Total column in the query above, how can i first check whether a category column (say Category2 or Category5) exists in my #StaffCategoryIds comma separated string before including it in the computation of my Total column?
Eg. if 'Category2' exists in #StaffCategoryIds, then i include it in computation of Total else i exclude it.
Try:
DECLARE #CategoryNames NVARCHAR(MAX) = N'Category1,Category2,Category3,Category4,Category5',
#sql NVARCHAR(MAX) = ''
SET #sql = 'SELECT TypeId,
TypeName,
Condition,
Region, ('+ REPLACE(#CategoryNames,',','+')+')
AS Total INTO ##QueryResults
FROM Table1;'
SELECT #SQL
It returns:
SELECT TypeId,
TypeName,
Condition,
Region, (Category1+Category2+Category3+Category4+Category5)
AS Total INTO ##QueryResults
FROM Table1;
Then you can Execute that statement:
EXEC #Sql
You can validate column names again system table "sys.columns" and whatever rows are returned , concatenate them as shown above to tun your query
SELECT [name]
FROM
sys.syscolumns
WHERE [name] IN ('Col1,Col2...ColN')
AND OBJECT_NAME(id) = 'TableName'

If numeric then Insert numeric else Insert non-numeric

I have table source SRC such as:
*Column1*
First
Second
45
Fouth
Now I want to insert these data into table DEST (ID, NAME) with this logic:
If row is numeric, insert into (ID, NAME) VAUES (45, 'TBD').
If the row is not numeric, generate ID and insert into (ID, NAME) VALUES (*GENERATED ID*, NAME).
I tried something like:
DECLARE #i INT;
SELECT #i = MAX ( ID ) + 1
FROM DEST;
IF ( SELECT ISNUMERIC ( SELECT Column1 FROM SRC ) AS help ) = 1
BEGIN
INSERT INTO DEST (ID, NAME) VALUES (45, 'TBD')
END;
ELSE
BEGIN
INSERT INTO DEST (ID, NAME) SELECT ROW_NUMBER() OVER(ORDER BY NAME) +#i, 'First';
INSERT INTO DEST (ID, NAME) SELECT ROW_NUMBER() OVER(ORDER BY NAME) +#i, 'Second';
INSERT INTO DEST (ID, NAME) SELECT ROW_NUMBER() OVER(ORDER BY NAME) +#i, 'Fourth';
END;
(simplified solution to demonstrate the purpose, it should be dynamic, not hardcoded)
.., but that obviously does not work. How to do that?
One approach you can take is the following, which uses a CASE statement to allow you to differentiate between numeric and non-numeric values of Column1:
-- Some temporary tables to make the example work
CREATE TABLE #SRC (Column1 VARCHAR(50))
INSERT INTO #SRC (Column1) VALUES ('First'), ('Second'), ('45'), ('Fourth')
CREATE TABLE #DEST (ID INT)
DECLARE #i INT
-- If #DEST is empty we need to have an initial value of 1
SELECT #i = ISNULL(MAX(ID),0) + 1 FROM #DEST
PRINT #i
INSERT INTO #DEST (ID)
SELECT CASE ISNUMERIC(Column1)
WHEN 1 THEN Column1
ELSE ROW_NUMBER() OVER (ORDER BY Column1) + #i
END
FROM #SRC
SELECT *
FROM #DEST
DROP TABLE #SRC
DROP TABLE #DEST
The example will not port directly into your code, but should give you a good basis to begin working from in order to achieve your desired result.
Note my comment to your original post that you may get clashes on the ID column if inserting multiple rows with this. You will need to consider what to do in that situation.
You could try something like this.
First insert numeric values by checking ISNUMERIC.
INSERT INTO DEST (ID,Name)
SELECT TRY_CONVERT(INT,Column1),'TBD'
FROM SRC
WHERE ISNUMERIC(Column1) = 1
Now Insert other values
DECLARE #maxid INT
SELECT #maxid = MAX(ID) FROM DEST;
INSERT INTO DEST (ID,Name)
SELECT #maxid + ROW_NUMBER()OVER(ORDER BY Column1 ASC),Column1
FROM SRC
WHERE ISNUMERIC(Column1) = 0
Note : ISNUMERIC doesn't guarantee that the value will be converted successfully to a numeric value. Also it looks like you want to check if the value is integer and not numeric unless you are ok with truncation of decimal point. You can use Column1 LIKE '%[0-9]%' to check if a value contains only numbers and not decimal value if that is the case
For Example, the below value '.' returns ISNUMERIC as 1 however cannot be converted to a numeric or an int :
DECLARE #value varchar(10) = '.'
SELECT ISNUMERIC(#value),TRY_CONVERT(INT,#value),TRY_CONVERT(NUMERIC(18,2),#value)

Most succinct way to transform a CSV string to a table in T-SQL?

-- Given a CSV string like this:
declare #roles varchar(800)
select #roles = 'Pub,RegUser,ServiceAdmin'
-- Question: How to get roles into a table view like this:
select 'Pub'
union
select 'RegUser'
union
select 'ServiceAdmin'
After posting this, I started playing with some dynamic SQL. This seems to work, but seems like there might be some security risks by using dynamic SQL - thoughts on this?
declare #rolesSql varchar(800)
select #rolesSql = 'select ''' + replace(#roles, ',', ''' union select ''') + ''''
exec(#rolesSql)
If you're working with SQL Server compatibility level 130 then the STRING_SPLIT function is now the most succinct method available.
Reference link: https://msdn.microsoft.com/en-gb/library/mt684588.aspx
Usage:
SELECT * FROM string_split('Pub,RegUser,ServiceAdmin',',')
RESULT:
value
-----------
Pub
RegUser
ServiceAdmin
See my answer from here
But basically you would:
Create this function in your DB:
CREATE FUNCTION dbo.Split(#origString varchar(max), #Delimiter char(1))
returns #temptable TABLE (items varchar(max))
as
begin
declare #idx int
declare #split varchar(max)
select #idx = 1
if len(#origString )<1 or #origString is null return
while #idx!= 0
begin
set #idx = charindex(#Delimiter,#origString)
if #idx!=0
set #split= left(#origString,#idx - 1)
else
set #split= #origString
if(len(#split)>0)
insert into #temptable(Items) values(#split)
set #origString= right(#origString,len(#origString) - #idx)
if len(#origString) = 0 break
end
return
end
and then call the function and pass in the string you want to split.
Select * From dbo.Split(#roles, ',')
Here's a thorough discussion of your options:
Arrays and Lists in SQL Server
What i do in this case is just using some string replace to convert it to json and open the json like a table. May not be suitable for every use case but it is very simple to get running and works with strings and files. With files you just need to watch your line break character, mostly i find it to be "Char(13)+Char(10)"
declare #myCSV nvarchar(MAX)= N'"Id";"Duration";"PosX";"PosY"
"•P001";223;-30;35
"•P002";248;-28;35
"•P003";235;-26;35'
--CSV to JSON
--convert to json by replacing some stuff
declare #myJson nvarchar(MAX)= '[['+ replace(#myCSV, Char(13)+Char(10), '],[' ) +']]'
set #myJson = replace(#myJson, ';',',') -- Optional: ensure coma delimiters for json if the current delimiter differs
-- set #myJson = replace(#myJson, ',,',',null,') -- Optional: empty in between
-- set #myJson = replace(#myJson, ',]',',null]') -- Optional: empty before linebreak
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT 0))-1 AS LineNumber, *
FROM OPENJSON( #myJson )
with (
col0 varchar(255) '$[0]'
,col1 varchar(255) '$[1]'
,col2 varchar(255) '$[2]'
,col3 varchar(255) '$[3]'
,col4 varchar(255) '$[4]'
,col5 varchar(255) '$[5]'
,col6 varchar(255) '$[6]'
,col7 varchar(255) '$[7]'
,col8 varchar(255) '$[8]'
,col9 varchar(255) '$[9]'
--any name column count is possible
) csv
order by (SELECT 0) OFFSET 1 ROWS --hide header row
Using SQL Server's built in XML parsing is also an option. Of course, this glosses over all the nuances of an RFC-4180 compliant CSV.
-- Given a CSV string like this:
declare #roles varchar(800)
select #roles = 'Pub,RegUser,ServiceAdmin'
-- Here's the XML way
select split.csv.value('.', 'varchar(100)') as value
from (
select cast('<x>' + replace(#roles, ',', '</x><x>') + '</x>' as xml) as data
) as csv
cross apply data.nodes('/x') as split(csv)
If you are using SQL 2016+, using string_split is better, but this is a common way to do this prior to SQL 2016.
Using BULK INSERT you can import a csv file into your sql table -
http://blog.sqlauthority.com/2008/02/06/sql-server-import-csv-file-into-sql-server-using-bulk-insert-load-comma-delimited-file-into-sql-server/
Even the accepted answer is working fine. but I got this function much faster even for thousands of record. create below function and use.
IF EXISTS (
SELECT 1
FROM Information_schema.Routines
WHERE Specific_schema = 'dbo'
AND specific_name = 'FN_CSVToStringListTable'
AND Routine_Type = 'FUNCTION'
)
BEGIN
DROP FUNCTION [dbo].[FN_CSVToStringListTable]
END
GO
CREATE FUNCTION [dbo].[FN_CSVToStringListTable] (#InStr VARCHAR(MAX))
RETURNS #TempTab TABLE (Id NVARCHAR(max) NOT NULL)
AS
BEGIN
;-- Ensure input ends with comma
SET #InStr = REPLACE(#InStr + ',', ',,', ',')
DECLARE #SP INT
DECLARE #VALUE VARCHAR(1000)
WHILE PATINDEX('%,%', #INSTR) <> 0
BEGIN
SELECT #SP = PATINDEX('%,%', #INSTR)
SELECT #VALUE = LEFT(#INSTR, #SP - 1)
SELECT #INSTR = STUFF(#INSTR, 1, #SP, '')
INSERT INTO #TempTab (Id)
VALUES (#VALUE)
END
RETURN
END
GO
---Test like this.
declare #v as NVARCHAR(max) = N'asdf,,as34df,234df,fs,,34v,5fghwer,56gfg,';
SELECT Id FROM dbo.FN_CSVToStringListTable(#v)
I was about you use the solution mentioned in the accepted answer, but doing more research led me to use Table Value Types:
These are far more efficient and you don't need a TVF (Table valued function) just to create a table from csv. You can use it directly in your scripts or pass that to a stored procedure as a Table Value Parameter. The Type can be created as :
CREATE TYPE [UniqueIdentifiers] AS TABLE(
[Id] [varchar](20) NOT NULL
)