Stored Procedure Syntax with CTE - sql-server

This is probably trivial but I am just learning about CTE (thanks to help here).
I have a procedure that is used to determine totals.
The first part is the totals are the sum of a position at their level an below. So I needed a way to retrieve records that (1) determined the level of the record (hierarchy) and (2) returned all records at and below. That was asked and answered here.
Now want to take the CTE table from the answer above and use it in the second part of my procedure (get the totals)
CREATE PROCEDURE [dbo].[GetProgramTotals]
#programId nvarchar(10) = null,
#owner int = null,
#totalAmount money OUT,
#usedAmount money OUT,
#remainingAmount money OUT
AS
BEGIN
WITH rCTE AS
(
SELECT
*, 0 AS Level
FROM Forecasting.dbo.Addressbook
WHERE Addressbook = #owner
UNION ALL
SELECT
t.*, r.Level + 1 AS Level
FROM Addressbook t
INNER JOIN rCTE r ON t.ParentAddressbook = r.Addressbook
)
Select #totalAmount = (Select Sum(Amount) from dbo.Budget where
(#programId IS NULL or (ProgramId = #programId)) and (#owner IS NULL or (BudgetOwner in (SELECT Addressbook from rCTE))))
Select #usedAmount = (Select Sum(SubTotal) from dbo.OrderLine where
(#programId IS NULL or (ProgramId = #programId) and (#owner IS NULL) or (Budget in (SELECT Addressbook from rCTE))))
if (#totalAmount is null)
set #totalAmount = 0
if (#usedAmount is null)
set #usedAmount = 0
Set #remainingAmount = (#totalAmount - #usedAmount)
END
The idea of this procedure is the dynamically calculate an individual (or all) programs based an a users position in a hierarchy.
So a regional managers totals would be the sum of all districts and district reps.
UPDATE: I updated this based on squillman (thank you) comment below.
Now I have a different problem. When I execute the proc - I get 'Invalid object name rCTE'.

You can't use SET in the middle of a query like that. Change it to a SELECT and it should remedy your syntax error.
CREATE PROCEDURE [dbo].[GetProgramTotals]
#programId nvarchar(10) = null,
#owner int = null,
#totalAmount money OUT,
#usedAmount money OUT,
#remainingAmount money OUT
AS
BEGIN
WITH rCTE AS(
SELECT *, 0 AS Level FROM Forecasting.dbo.Addressbook WHERE Addressbook = #owner
UNION ALL
SELECT t.*, r.Level + 1 AS Level
FROM Addressbook t
INNER JOIN rCTE r ON t.ParentAddressbook = r.Addressbook)
SELECT #totalAmount = (Select Sum(Amount) from dbo.Budget where
(#programId IS NULL or (ProgramId = #programId)) and (#owner IS NULL or (BudgetOwner in (SELECT Addressbook from rCTE))))
, #usedAmount = (Select Sum(SubTotal) from dbo.OrderLine where
(#programId IS NULL or (ProgramId = #programId) and (#owner IS NULL) or (Budget in (SELECT Addressbook from rCTE))))
if (#totalAmount is null)
set #totalAmount = 0
if (#usedAmount is null)
set #usedAmount = 0
Set #remainingAmount = (#totalAmount - #usedAmount)
END
CTE's can be a bit confusing at first, but they are really quite simple once they make sense. For me it clicked when I began thinking of them as just another temp table syntax (pro-tip: they're not in reality, just conceptually). So basically:
Create one or more "temp tables". These are your CTE expressions, and there can be more than one.
Perform a standard operation using one or more of the CTE expressions in the statement immediately following your CTE(s).
As Martin mentioned in comments below, the CTE(s) are only scoped for the next immediate statement and fall out of scope after that.
So,
;WITH cte1 AS
(
SELECT Col1 FROM Table1
),
cte2 AS
(
SELECT Col1 FROM Table2
)
SELECT Col1 FROM cte1 //In scope here
UNION
SELECT Col1 FROM cte1; //Still in scope since we're still in the first statement
SELECT Col1 FROM cte1; //Fails. cte1 is now out of scope (as is cte2)
In your case you're using the recursive CTE to form a parent/child hierarchy and then setting variables based on the results. Your CTE syntax is pretty close after the edit, you just need the comma to bring things back together into one statement.
//Variable assignment example
;WITH cte1 AS
(
SELECT Col1 FROM Table1
),
cte2 AS
(
SELECT Col1 FROM Table2
)
SELECT #var1 = (SELECT TOP 1 Col1 FROM cte1)
,#var2 = (SELECT TOP 1 Col1 FROM cte2) //You're missing the comma at the start of this line
Change Select #usedAmount=... to , #usedAmount=...

Related

Is it always possible to transform multiple spatial selects with while loop and variables into a single query without using temp tables in sql?

This problem can be solved with temp table, however, I don't want to use Temp table or var table, this question is mostly for my personal educational purposes.
I inherited the following SQL:
DECLARE #i int = 993
while #i <=1000
begin
declare #lat nvarchar(20)
select top 1 #lat = SUBSTRING(Address,0,CHARINDEX(',',Address,0)) from dbo.rent
where id = #i;
declare #lon nvarchar(20)
select top 1 #lon = SUBSTRING(Address,CHARINDEX(',',Address)+1,LEN(Address)) from dbo.rent
where id = #i
declare #p GEOGRAPHY = GEOGRAPHY::STGeomFromText('POINT('+ #lat +' '+#lon+')', 4326)
select price/LivingArea sq_m, (price/LivingArea)/avg_sq_m, * from
(select (sum(price)/sum(LivingArea)) avg_sq_m, count(1) cnt, #i id from
(select *, GEOGRAPHY::STGeomFromText('POINT('+
convert(nvarchar(20), SUBSTRING(Address,0,CHARINDEX(',',Address,0)))+' '+
convert( nvarchar(20), SUBSTRING(Address,CHARINDEX(',',Address)+1,LEN(Address)))+')', 4326)
.STBuffer(500).STIntersects(#p) as [Intersects]
from dbo.rent
where Address is not null
) s
where [Intersects] = 1) prox
inner join dbo.rent r on prox.id = r.id
set #i = #i+1
end
it is used to analyze property prices per square meter that are in proximity and compare them to see which ones are cheaper...
Problem: a mechanism for calling has to be moved from C# to SQL and all queries have to be combined into a single result (now you get one row per one while run), i.e #i and #p has to go and become while id < x and id > y or somehow magically joined,
the procedure is a cut down version of actual thing but having a solution to the above I will have no problem making the whole thing work...
I am of the opinion that any SQL mechanism with variables and loops can be transformed to a single SQL statement, hence the question.
SqlFiddle
If I understand your question properly (Remove the need for loops and return one data set) then you can use CTE (Common Table Expressions) for the Lats, Lons and Geog variables.
You;re SQLFIddle was referencing a database called "webanalyser" so I removed that from the query below
However, the query will not return anything as the sample data has wrong data for address column.
;WITH cteLatsLongs
AS(
SELECT
lat = SUBSTRING(Address, 0, CHARINDEX(',', Address, 0))
,lon = SUBSTRING(Address, CHARINDEX(',', Address) + 1, LEN(Address))
FROM dbo.rent
)
,cteGeogs
AS(
SELECT
Geog = GEOGRAPHY ::STGeomFromText('POINT(' + LL.lat + ' ' + LL.lon + ')', 4326)
FROM cteLatsLongs LL
),cteIntersects
AS(
SELECT *,
GEOGRAPHY::STGeomFromText('POINT(' + CONVERT(NVARCHAR(20), SUBSTRING(Address, 0, CHARINDEX(',', Address, 0))) + ' ' + CONVERT(NVARCHAR(20), SUBSTRING(Address, CHARINDEX(',', Address) + 1, LEN(Address))) + ')', 4326).STBuffer(500).STIntersects(G.Geog) AS [Intersects]
FROM dbo.rent
CROSS APPLY cteGeogs G
)
SELECT avg_sq_m = (SUM(price) / SUM(LivingArea)), COUNT(1) cnt
FROM
cteIntersects I
WHERE I.[Intersects] = 1
It can be done, in this specific case 'discovery' that was necessary was the ability to perform JOINs on Point e.g ability to join tables on proximity (another a small cheat was to aggregate point-strings to actual points, but it's just an optimization). Once this is done, a query could be rewritten as follows:
SELECT adds.Url,
adds.Price/adds.LivingArea Sqm,
(adds.Price/adds.LivingArea)/k1.sale1Avg ratio,
*
FROM
(SELECT baseid,
count(k1Rent.rentid) rent1kCount,
sum(k1Rent.RperSqM)/(count(k1Rent.rentid)) AS rent1kAvgSqM,
count(around1k.SaleId) sale1kCount,
(sum(k1sale.price)/sum(k1Sale.LivingArea)) sale1Avg,
(sum(k1sale.price)/sum(k1Sale.LivingArea))/((sum(k1Rent.RperSqM)/(count(k1Rent.rentid)))*12) years --*
FROM
(SELECT sa.id baseid,
s.id saleid,
s.RoomCount,
POINT
FROM SpatialAnalysis sa
INNER JOIN Sale s ON s.Id = SaleId
WHERE sa.SalesIn1kRadiusCount IS NULL) AS base
JOIN SpatialAnalysis around1k ON base.Point.STBuffer(1000).STIntersects(around1k.Point) = 1
LEFT OUTER JOIN
(SELECT id rentid,
rc,
Price/avgRoomSize RperSqM
FROM
(SELECT *
FROM
(SELECT rc,
sum(avgArea*c)/sum(c) avgRoomSize
FROM
(SELECT roomcount rc,
avg(livingarea) avgArea,
count(1) c
FROM Rent
WHERE url LIKE '%systemname%'
AND LivingArea IS NOT NULL
GROUP BY RoomCount
UNION
(SELECT roomcount rc,
avg(livingarea) avgArea,
count(1) c
FROM sale
WHERE url LIKE '%systemname%'
AND LivingArea IS NOT NULL
GROUP BY RoomCount))uni
GROUP BY rc) avgRoom) avgrents
JOIN rent r ON r.RoomCount = avgrents.rc) k1Rent ON k1Rent.rentid =around1k.RentId
AND base.RoomCount = k1Rent.rc
LEFT OUTER JOIN Sale k1Sale ON k1Sale.Id = around1k.SaleId
AND base.RoomCount = k1Sale.RoomCount
GROUP BY baseid) k1
left outer join SpatialAnalysis sp on sp.Id = baseid
left outer join Sale adds on adds.Id = sp.SaleId
where adds.Price < 250000
order by years, ratio

T-SQL Merge from multiple source records

I've seen several similar questions but haven't found one that answers my question.
I have a source table with many individual notes for a company
CompanyName, Notes3
Company1, "spoke with someone"
Company2, "Email no longer works"
Company1, "Moved address"
I have a destination table (vchCompanyName is unique here)
vchCompanyName, vchNotes
Company1, "started business in 2005"
Company2, null
I want to end up with
vchCompanyName, vchNotes
Company1, "started business in 2005
spoke with someone
Moved address"
Company2, "Email no longer works"
I have tried this code
WITH CTE
AS (SELECT ROW_NUMBER() OVER (PARTITION BY CompanyName, Notes3 ORDER BY CompanyName) RowNum, *
FROM CompanyContact
)
merge dCompany as target
using CTE as source
on target.vchcompanyname = source.companyname
when matched and len(source.notes3)>0 and source.RowNum = 1
then
update set target.vchnote = vchnote + CHAR(13) + source.Notes3
But get the error
The MERGE statement attempted to UPDATE or DELETE the same row more than once. This happens when a target row matches more than one source row. A MERGE statement cannot UPDATE/DELETE the same row of the target table multiple times. Refine the ON clause to ensure a target row matches at most one source row, or use the GROUP BY clause to group the source rows.
Which is accurate.
I have also tried STRING_AGG but get an undefined UDF error.
How do I change my code to run iteratively?
--EDIT--
I had tried the following update code
WITH CTE
AS (SELECT ROW_NUMBER() OVER (PARTITION BY CompanyName, Notes3 ORDER BY CompanyName) RowNum, *
FROM CompanyContact
)
UPDATE dCompany SET vchNote = vchNote +
(select CHAR(13) + cc.Notes3 from CompanyContact cc
inner JOIN dCompany dc ON dc.vchCompanyName COLLATE database_default = LEFT(cc.CompanyName,50) COLLATE database_default
inner join CTE on dc.vchCompanyName COLLATE database_default = LEFT(CTE.CompanyName,50) COLLATE database_default
WHERE LEN(cc.Notes3)>0
and RowNum = 1
);
But get the error
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
#Chris Crawshaw, I will approach this by doing a 'union all' on the source and destination table to pick up all the notes for each company. Then using the STUFF function, it is easy to concatenate all the notes into one cell, while grouping by the induvidual company names. See the mockup below:
DECLARE #Source TABLE (CompanyName VARCHAR(20), Notes3 VARCHAR(50))
INSERT INTO #Source
SELECT 'Company1', 'spoke with someone' UNION ALL
SELECT 'Company2', 'Email no longer works' UNION ALL
SELECT 'Company1', 'Moved address'
DECLARE #Destination TABLE (vchCompanyName VARCHAR(20), vchNotes VARCHAR(500))
INSERT INTO #Destination
SELECT 'Company1', 'started business in 2005' UNION ALL
SELECT 'Company2', NULL
;WITH Temp AS (
SELECT *
FROM
(
SELECT *
FROM
#Destination D
WHERE D.vchNotes is not null
UNION ALL
SELECT *
FROM
#Source S
)h
)
update D
SET D.vchNotes=U.vchNotes
FROM #Destination D
LEFT JOIN(
SELECT t2.vchCompanyName, vchNotes=STUFF((
SELECT ',' + vchNotes
FROM Temp t1 where t1.vchCompanyName=t2.vchCompanyName
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM
#Destination t2
GROUP BY
t2.vchCompanyName
)U ON
U.vchCompanyName=D.vchCompanyName
--TEST--
SELECT *
FROM
#Destination

RETURN statements in scalar valued functions must include an argument

Didn't manage to find an aswer for the problem. Therefore, decided to share it here. Hope it will help anybody and professionals will share more efficient solution
I created a function in MSSQL that takes a parameter and try to find corresponding record in a mapping table. The extraction of mapping record was implemented using 'With clause':
CREATE FUNCTION [dbo].[GetMappedValue]
(
#field nvarchar(50) = null
)
RETURNS VARCHAR(50)
AS
BEGIN
;WITH active_mappings AS
(SELECT map_key, map_value, ROW_NUMBER() OVER (PARTITION BY map_key ORDER BY updated DESC) AS n
FROM MappingTable WHERE tag = 'Active')
RETURN SELECT TOP 1 map_value FROM active_mappings
WHERE n=1 AND map_key = #field
END
GO
But for a strange reason: 'RETURN statements in scalar valued functions must include an argument' error occurs
In order to solve the problem I had to declare a variable and assign result of select into the variable and only afterwards return it:
CREATE FUNCTION [dbo].[GetMappedValue]
(
#field nvarchar(50) = null
)
RETURNS VARCHAR(50)
AS
BEGIN
Declare #result nvarchar(50)
;WITH active_mappings AS
(SELECT map_key, map_value, ROW_NUMBER() OVER (PARTITION BY map_key ORDER BY updated DESC) AS n
FROM MappingTable WHERE tag = 'Active')
SELECT #Result = (SELECT TOP 1 map_value FROM active_mappings
WHERE n=1 AND map_key = #field)
RETURN #Result
END
GO
Here is how this same function would look as an inline table valued function.
CREATE FUNCTION [dbo].[GetMappedValue]
(
#field nvarchar(50) = null
)
RETURNS TABLE AS RETURN
WITH active_mappings AS
(
SELECT map_value
, ROW_NUMBER() OVER (PARTITION BY map_key ORDER BY updated DESC) AS n
FROM MappingTable
WHERE tag = 'Active'
AND map_key = #field
)
SELECT TOP 1 map_value
FROM active_mappings
WHERE n = 1
--ORDER BY ??? You have top 1 but no order by. As such you don't know which row will be returned
GO
---EDIT---
Since you said there can only be a single value returned from MappingTable you could greatly simplify this. The entire body of your function could be this simple.
select map_value
from MappingTable mt
where tag = 'Active'
and map_key = #field

SQL Server union temp tables

I have a piece of SQL that takes a parameter, assesses the contents of the parameter and depending on the contents applies different UDF to the input parameter.
the code goes something like this:
declare #order varchar(50) = 's12345..s12347'
if isnull( CHARINDEX('.',#order),0) >0
begin
select n as order_no into #temp
FROM F_GetAllNBetween(#order)
end
else if ( isnull( CHARINDEX(',',#order),0) >0 )
begin
select [value] as order_no into #temp2
FROM dbo.F_SplitList(#order,',')
end
if OBJECT_ID('tempdb..#temp') is not null
select * from #temp where order_no <>''
if OBJECT_ID('tempdb..#temp2') is not null
select * from #temp2 where order_no <>''
what I want to do is, get the output from the above union into another temp table and use it in the where clause of the rest of the code.
I cannot use union as it errors saying a select is expected.
I cannot use cte as I cannot use the check to see if the temp tables are empty
I cannot ignore checking for empty temp tables else it errors saying object does not exist ( if statement only creates one of the two temp tables)
I cannot figure out how to get the output to a different temp table. Any ideas suggestions or better way of doing the same please?
As mentioned in my comment, you can simplify your current code to achieve what you want with a UNION and WHERE clauses to replace the IF statements. e.g.
SELECT order_no
--INTO #someTempTable -- if needed
FROM (
SELECT order_no = n
FROM dbo.F_GetAllNBetween(#order)
WHERE CHARINDEX('.', #order) > 0
UNION ALL
SELECT value
FROM dbo.F_SplitList(#order,',')
WHERE CHARINDEX(',', #order) > 0
) AS T
WHERE order_no <> '';

Get more than 1 result set for recursive CTE?

I have a simple table which has leafs and sub leafs info. ( like a forum questions)
A main message is defined where childId and ParentID are the same
So here we see 2 main questions and their answers.
I've also managed to calc the depth of each element :
In short this is the main query :
WITH CTE AS
(
SELECT childID
,parentID,
0 AS depth,name
FROM #myTable
WHERE childID = parentID AND childID=1 -- problem line
UNION ALL
SELECT TBL.childID
,TBL.parentID,
CTE.depth + 1 , TBL.name
FROM #myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)
SELECT childID,parentID,REPLICATE('----', depth) + name
But the problem is Line #8 (commented).
I currently ask "give me all the cluster for question id #1"
So where is the problem ?
I want to have multiple result set , for each question !
so here i need to have 2 result sets :
one for childId=parentId=1
and one for
one for childId=parentId=6
full working sql online
(and I dont want to use cursor)
You can build your queries dynamically.
DECLARE #SQL NVARCHAR(MAX) =
(SELECT '
WITH CTE AS
(
SELECT childID
,parentID,
0 AS depth,name
FROM myTable
WHERE childID = parentID AND childID = '+CAST(childID AS NVARCHAR(10))+'
UNION ALL
SELECT TBL.childID
,TBL.parentID,
CTE.depth + 1 , TBL.name
FROM myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)
SELECT childID,parentID,REPLICATE(''----'', depth) + name
FROM CTE
ORDER BY
childID;'
FROM myTable
WHERE childID = parentID
FOR XML PATH(''), TYPE).value('text()[1]', 'NVARCHAR(MAX)');
EXEC sp_executesql #SQL;
Update:
As suggested by Bogdan Sahlean we can minimize compilations by making the actual query parameterized.
DECLARE #SQL1 NVARCHAR(MAX) =
'WITH CTE AS
(
SELECT childID
,parentID,
0 AS depth,name
FROM myTable
WHERE childID = parentID AND childID = #childID
UNION ALL
SELECT TBL.childID
,TBL.parentID,
CTE.depth + 1 , TBL.name
FROM myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)
SELECT childID,parentID,REPLICATE(''----'', depth) + name
FROM CTE
ORDER BY
childID;'
DECLARE #SQL2 NVARCHAR(MAX) =
(SELECT 'exec sp_executesql #SQL, N''#childID int'', '+CAST(childID AS NVARCHAR(10))+';'
FROM myTable
WHERE childID = parentID
FOR XML PATH(''), TYPE).value('text()[1]', 'NVARCHAR(MAX)');
EXEC sp_executesql #SQL2, N'#SQL NVARCHAR(MAX)', #SQL1;
To present multiple result sets to your client, you're going to have to use a cursor or a while loop to perform independent SELECT operations. You can't do that from a CTE, since a CTE can only be used by exactly one subsequent query.
Now, the source of the problem has nothing to do with cursors really, but the fact that you're using an HTML repeater. Why do you need to use an HTML repeater for this? A simple DataReader can loop through all of the results from the CTE's single set, and make conditional formatting decisions based on the loop and determining when the root ID changes. So I suggest you look into solving the presentation problem a different way, rather than trying to coerce SQL Server to accommodate your presentation implementation.
I am not sure what you mean by "returning two result sets". Could you just have one result set, with the root question being assigned into another column? The following tweak on your query does this:
WITH CTE AS (
SELECT ChildId as WhichQuestion, childID, parentID, 0 AS depth, name
FROM #myTable
WHERE childID = parentID
UNION ALL
SELECT cte.WhichQuestion, TBL.childID, TBL.parentID, CTE.depth + 1 , TBL.name
FROM #myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID <> TBL.parentID
)
SELECT WhichQuestion, childID, parentID, REPLICATE('----', depth) + name
FROM CTE
ORDER BY WhichQuestion, childID;
You can save in anchor part childId of root question and then access all the branch by needed Id:
WITH CTE AS (
SELECT childID as RootId
, childID
, parentID
, 0 AS depth,name
FROM #myTable
WHERE childID = parentID
UNION ALL
SELECT CTE.RootId
, TBL.childID
, TBL.parentID
, CTE.depth + 1 , TBL.name
FROM #myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)

Resources