Should I use APPLY in this context - sql

Code included is a simplified version of our situation; the production table equivalent to #MyExample has 20 fields all of which need medians calculating therefore the second part of the script becomes very long - not a huge hard-ship but is there a more compact solution?
I've no experience with APPLY or custom FUNCTIONs but is this a situation where we should create a FUNCTION for the median and then use APPLY I'm guessing not as apply is applied to each row?
/*
DROP TABLE #MyExample
DROP TABLE #mediantable
*/
CREATE TABLE #MyExample
(
customer char(5),
amountPeriodA numeric(36,8),
amountPeriodB numeric(36,8),
amountPeriodC numeric(36,8)
)
INSERT INTO #MyExample
values
('a',10,20,30),
('b',5,10,15),
('c',500,100,150),
('d',5,1,1),
('e',5,1,15),
('f',5,10,150),
('g',5,100,1500)
SELECT
[Period] = 'amountPeriodA',
[Median] = AVG(x.amountPeriodA)
INTO #mediantable
FROM (
SELECT
r.customer,
r.amountPeriodA,
[RowASC] = ROW_NUMBER() OVER(ORDER BY r.amountPeriodA ASC, customer ASC),
[RowDESC] = ROW_NUMBER() OVER(ORDER BY r.amountPeriodA DESC, customer DESC)
FROM #MyExample r
) x
WHERE RowASC IN (RowDESC, ROWDESC-1, ROWDESC+1)
union
SELECT
[Period] = 'amountPeriodB',
[Median] = AVG(x.amountPeriodB)
FROM (
SELECT
r.customer,
r.amountPeriodB,
[RowASC] = ROW_NUMBER() OVER(ORDER BY r.amountPeriodB ASC, customer ASC),
[RowDESC] = ROW_NUMBER() OVER(ORDER BY r.amountPeriodB DESC, customer DESC)
FROM #MyExample r
) x
WHERE RowASC IN (RowDESC, ROWDESC-1, ROWDESC+1)
union
SELECT
[Period] = 'amountPeriodC',
[Median] = AVG(x.amountPeriodC)
FROM (
SELECT
r.customer,
r.amountPeriodC,
[RowASC] = ROW_NUMBER() OVER(ORDER BY r.amountPeriodC ASC, customer ASC),
[RowDESC] = ROW_NUMBER() OVER(ORDER BY r.amountPeriodC DESC, customer DESC)
FROM #MyExample r
) x
WHERE RowASC IN (RowDESC, ROWDESC-1, ROWDESC+1)
SELECT *
FROM #mediantable

Building on my previous reply I arrived on this which is a lot easier (and shorter) to expand for the number of columns and even runs a bit faster (probably a lot faster in case of 20+ columns!). However, it returns the results horizontally instead of vertically. This can be 'solved' again using UNPIVOT.
I've done the operation in 2 parts using an intermediate #result table; but you could easily do it in a single statement using a subquery or CTE.
DECLARE #rowcount int
DECLARE #first int
DECLARE #last int
DECLARE #divider numeric(36,8)
SELECT #rowcount = COUNT(*) FROM #MyExample
SELECT #first = (CASE WHEN #rowcount % 2 = 1 THEN (#rowcount + 1) / 2 ELSE (#rowcount / 2) END),
#last = (CASE WHEN #rowcount % 2 = 1 THEN (#rowcount + 1) / 2 ELSE (#rowcount / 2) + 1 END),
#divider = (CASE WHEN #rowcount % 2 = 1 THEN 1 ELSE 2 END)
SELECT amountPeriodA = SUM(amountPeriodA) / #divider,
amountPeriodB = SUM(amountPeriodB) / #divider,
amountPeriodC = SUM(amountPeriodC) / #divider
INTO #result
FROM
(
SELECT amountPeriodA = ((CASE WHEN ROW_NUMBER() OVER(ORDER BY amountPeriodA ASC, customer ASC) IN (#first, #last) THEN amountPeriodA ELSE 0.00 END)),
amountPeriodB = ((CASE WHEN ROW_NUMBER() OVER(ORDER BY amountPeriodB ASC, customer ASC) IN (#first, #last) THEN amountPeriodB ELSE 0.00 END)),
amountPeriodC = ((CASE WHEN ROW_NUMBER() OVER(ORDER BY amountPeriodC ASC, customer ASC) IN (#first, #last) THEN amountPeriodC ELSE 0.00 END))
FROM #MyExample
)t
and then
SELECT [Period], [Amount]
FROM #result as x
UNPIVOT ( [Amount] FOR Period IN (amountPeriodA, amountPeriodB, amountPeriodC)) As unpvt

I was thinking along the lines of :
DECLARE #rowcount int
DECLARE #first int
DECLARE #last int
SELECT #rowcount = COUNT(*) FROM #MyExample
SELECT #first = (CASE WHEN #rowcount % 2 = 1 THEN (#rowcount + 1) / 2 ELSE (#rowcount / 2) END),
#last = (CASE WHEN #rowcount % 2 = 1 THEN (#rowcount + 1) / 2 ELSE (#rowcount / 2) + 1 END)
SELECT [Period],
[Median] = AVG(Amount)
FROM (SELECT [Period] = 'amountPeriodA',
Amount = amountPeriodA,
rownbr = ROW_NUMBER() OVER(ORDER BY amountPeriodA ASC, customer ASC)
FROM #MyExample
UNION ALL
SELECT [Period] = 'amountPeriodB',
Amount = amountPeriodB,
rownbr = ROW_NUMBER() OVER(ORDER BY amountPeriodB ASC, customer ASC)
FROM #MyExample
UNION ALL
SELECT [Period] = 'amountPeriodC',
Amount = amountPeriodC,
rownbr = ROW_NUMBER() OVER(ORDER BY amountPeriodC ASC, customer ASC)
FROM #MyExample
) r
WHERE rownbr IN (#first, #last)
GROUP BY [Period]
Which seems to work well, is a bit less typing and turns out to be a bit faster too.... but it's still 'big'.
PS: Use UNION ALL rather than UNION as otherwise the server will try to make the end-result into 'distinct' records which in this case is not needed. (Period makes it unique anyway !)

Related

Multilevel CTE Expression slows the execution (Reposted with changes) [duplicate]

This question already exists:
Multilevel CTE Expression slows the execution [closed]
Closed 1 year ago.
The community reviewed whether to reopen this question 1 year ago and left it closed:
Duplicate This question has been answered, is not unique, and doesn’t differentiate itself from another question.
In SQL Server, I have a table-valued function which contains a multi-level CTE as shown in the code here:
CREATE FUNCTION [dbo].[GetDataset_Test]
(#XMLBlock XML, #Id INT)
RETURNS
#tempTable TABLE
(
methodID INT,
[Id] INT,
SavingMessage varchar(50)
)
AS
BEGIN
DECLARE #ObjectID INT = 2;
DECLARE #ExchangeRate INT = 2;
DECLARE #Cond INT = 1;
DECLARE #Param1 varchar(50) = 'name1';
DECLARE #Param2 varchar(50) = 'name2';
WITH CTE AS
(
SELECT
Col1,
con,
DiscountLine,
tempNumber,
ItemCostExVAT,
Quantity,
SomeValue,
methodID,
VATAmount,
SubTypeID,
Line,
VATMultiplier,
ROUND(dbo.GetAmountCustomFunction(COALESCE(Id, AnotherId), COALESCE(Price, PriceValue)), 2) As [Amount]
FROM
dbo.GetObject(#Id, #ObjectID, #ParameterBlock) AS BC
INNER JOIN
dbo.fnPrices(#ID, #CurrencyID) BPD ON BPD.Id = BC.productid
),
CTE1 AS
(
SELECT
*,
CASE WHEN con = 0 THEN Quantity ELSE 1 END AS Quantity
FROM
CTE
WHERE
1 = SomeValue
),
CTE2 AS
(
Select *,
MIN(CASE WHEN DiscountLine=1 THEN 1 ELSE 20 END) over (PARTITION by tempNumber) As StockControlled,
SUM(ItemCostExVAT * Quantity) OVER ( PARTITION BY tempNumber ) AS tempCost ,
ROUND(CASE
WHEN methodID = 8 THEN DiscValue
WHEN methodID = 2 THEN END ,
DicsValue,VATAmount),2) AS AmountExVAT
From CTE1
),
CTE3
(
SELECT
*,
ROUND(CASE
WHEN SubTypeID = 1 AND Line = 1 THEN -1 * AmountExVAT
ELSE 20
END, 2) PriceExVAT
FROM
CTE2
),
CTE4
(
SELECT
*,
ROUND(#ExchangeRate * CASE WHEN #Cond = 1 THEN AmountExVAT * VATMultiplier ELSE 20 END, 2) CashBack
FROM
CTE3
),
CTE5
(
SELECT
*,
dbo.FormatMessage(#Param1, #Param2) AS SavingMessage
FROM
CTE4
)
INSERT INTO #tempTable
SELECT
methodID, [Id], SavingMessage
FROM
CTE5
RETURN
END
In above query because of multi-level CTE and table value parameter I can think that its trying to query recursively and taking more execution time.
I know that we cannot use temporary table as function parameter, is there any alternative of this or can I use temporary table by any way in function?
Or can I make some changes in CTE to improve my T-SQL function query execution time?

Is there a way to separate query results in SQL Server Management Studio (SSMS)?

I got a simple query which return a results from an OrderLine table. Is there a way to visually separate the query results to make it easier to read, like in the image shown here?
SELECT [OrderNo], [LineNo]
FROM [OrderLine]
Results:
drop table if exists #OrderLine;
select object_id as OrderNo, abs(checksum(newid())) as [LineNo]
into #OrderLine
from sys.columns;
-- ... results to text (ctrl+T)?
select OrderNo, [LineNo],
case when lead(OrderNo, 1) over(partition by OrderNo order by OrderNo) = OrderNo then '' else replicate('-', 11) + char(10) end
from #OrderLine;
--inject NULL
select case when [LineNo] is null and flag=2 then null else TheOrderNo end as OrderNo, [LineNo]
from
(
select OrderNo AS TheOrderNo, [LineNo], 1 as flag
from #OrderLine
union all
select distinct OrderNo, NULL, 2
from #OrderLine
) as src
order by TheOrderNo, flag;
You could execute multiple queries like so:
DECLARE #i int = 1
DECLARE #OrderNo
DECLARE #OrderNos TABLE (
Idx smallint Primary Key IDENTITY(1,1)
, OrderNo int
)
INSERT #OrderNos
SELECT distinct [OrderNo] FROM [OrderLine]
WHILE (#i <= (SELECT MAX(idx) FROM #employee_table))
BEGIN
SET #OrderNo = (SELECT [OrderNo] FROM [OrderNos] WHERE [Idx] = #i)
SELECT [OrderNo], [LineNo]
FROM [OrderLine]
WHERE [OrderNo] = #OrderNo
SET #i = #i + 1
END
You can not directly do that. Perhaps add a new column that is either null or has a value in it so that it is alternating.
Something like this:
select ..., case when rank() over (order by OrderNO) % 2 then 'XXX' else null end
from....
The % 2 is a 'modulo' operator...

Selecting data from table where sum of values in a column equal to the value in another column

Sample data:
create table #temp (id int, qty int, checkvalue int)
insert into #temp values (1,1,3)
insert into #temp values (2,2,3)
insert into #temp values (3,1,3)
insert into #temp values (4,1,3)
According to data above, I would like to show exact number of lines from top to bottom where sum(qty) = checkvalue. Note that checkvalue is same for all the records all the time. Regarding the sample data above, the desired output is:
Id Qty checkValue
1 1 3
2 2 3
Because 1+2=3 and no more data is needed to show. If checkvalue was 4, we would show the third record: Id:3 Qty:1 checkValue:4 as well.
This is the code I am handling this problem. The code is working very well.
declare #checkValue int = (select top 1 checkvalue from #temp);
declare #counter int = 0, #sumValue int = 0;
while #sumValue < #checkValue
begin
set #counter = #counter + 1;
set #sumValue = #sumValue + (
select t.qty from
(
SELECT * FROM (
SELECT
ROW_NUMBER() OVER (ORDER BY id ASC) AS rownumber,
id,qty,checkvalue
FROM #temp
) AS foo
WHERE rownumber = #counter
) t
)
end
declare #sql nvarchar(255) = 'select top '+cast(#counter as varchar(5))+' * from #temp'
EXECUTE sp_executesql #sql, N'#counter int', #counter = #counter;
However, I am not sure if this is the best way to deal with it and wonder if there is a better approach. There are many professionals here and I'd like to hear from them about what they think about my approach and how we can improve it. Any advice would be appreciated!
Try this:
select id, qty, checkvalue from (
select t1.*,
sum(t1.qty) over (partition by t2.id) [sum]
from #temp [t1] join #temp [t2] on t1.id <= t2.id
) a where checkvalue = [sum]
Smart self-join is all you need :)
For SQL Server 2012, and onwards, you can easily achieve this using ROWS BETWEEN in your OVER clause and the use of a CTE:
WITH Running AS(
SELECT *,
SUM(qty) OVER (ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningQty
FROM #temp t)
SELECT id, qty, checkvalue
FROM Running
WHERE RunningQty <= checkvalue;
One basic improvement is to try & reduce the no. of iterations. You're incrementing by 1, but if you repurpose the logic behind binary searching, you'd get something close to this:
DECLARE #RoughAverage int = 1 -- Some arbitrary value. The closer it is to the real average, the faster things should be.
DECLARE #CheckValue int = (SELECT TOP 1 checkvalue FROM #temp)
DECLARE #Sum int = 0
WHILE 1 = 1 -- Refer to BREAK below.
BEGIN
SELECT TOP (#RoughAverage) #Sum = SUM(qty) OVER(ORDER BY id)
FROM #temp
ORDER BY id
IF #Sum = #CheckValue
BREAK -- Indicating you reached your objective.
ELSE
SET #RoughAverage = #CheckValue - #Sum -- Most likely incomplete like this.
END
For SQL 2008 you can use recursive cte. Top 1 with ties limits result with first combination. Remove it to see all combinations
with cte as (
select
*, rn = row_number() over (order by id)
from
#temp
)
, rcte as (
select
i = id, id, qty, sumV = qty, checkvalue, rn
from
cte
union all
select
a.id, b.id, b.qty, a.sumV + b.qty, a.checkvalue, b.rn
from
rcte a
join cte b on a.rn + 1 = b.rn
where
a.sumV < b.checkvalue
)
select
top 1 with ties id, qty, checkvalue
from (
select
*, needed = max(case when sumV = checkvalue then 1 else 0 end) over (partition by i)
from
rcte
) t
where
needed = 1
order by dense_rank() over (order by i)

sql select into select in function

When I try to alter the function below I get the following error message:
Only one expression can be specified in the select list when the
subquery is not introduced with EXISTS.
I guess it is probably because of select into select. But why does this select into select work separately ( not in function ) but not in function.
ALTER FUNCTION [dbo].[Getcurrentexchangerate] (#CurrencyFromId INT,
#CurrencyToId INT)
returns DECIMAL(13, 10)
AS
BEGIN
DECLARE #rate DECIMAL (13, 10)
DECLARE #dw INT
SET #dw = (SELECT Datepart(dw, Getdate()))
IF( #dw != 2 ) -- Monday
BEGIN
SET #rate = (SELECT TOP (1) [rate]
FROM currencyconversionrate
WHERE [currencyfromid] = #CurrencyFromId
AND [currencytoid] = #CurrencyToId
ORDER BY id DESC)
END
ELSE
BEGIN
SET #rate = (SELECT *
FROM (SELECT TOP(2) Row_number()
OVER (
ORDER BY id DESC) AS
rownumber,
rate
FROM currencyconversionrate
WHERE ( [currencyfromid] = 2
AND [currencytoid] = 5 )
ORDER BY id DESC) AS Rate
WHERE rownumber = 2)
END
IF( #rate IS NULL )
BEGIN
SET #rate = 1
END
RETURN #rate
END
See your "else" part
SET #rate = (SELECT *
FROM (SELECT TOP(2) Row_number()
OVER (
ORDER BY id DESC) AS
rownumber,
rate
FROM currencyconversionrate
WHERE ( [currencyfromid] = 2
AND [currencytoid] = 5 )
ORDER BY id DESC) AS Rate
WHERE rownumber = 2)
You're trying to select all fields from currencyconversionrate table, you can't do that, or do you want to select "RATE" column only?
Try changing to below:
SET #rate = (SELECT rate
FROM (SELECT TOP(1) Row_number()
OVER (
ORDER BY id DESC) AS
rownumber,
rate
FROM currencyconversionrate
WHERE ( [currencyfromid] = 2
AND [currencytoid] = 5 )
ORDER BY id DESC) AS Rate
WHERE rownumber = 2)

SQL Server: issue with adapting Rank statement in Select

I have a dynamic stored procedure that start as follows with the declaration of a temp table and then an insert statement.
Can someone here tell me how I need to adapt the following line so that it creates a rank based on the groupCount (desc) instead of by Count?
When I just say groupCount instead of Count then it returns:
Invalid column name 'groupCount'
The line in question:
RANK() OVER(ORDER BY COUNT(*) desc, <sel>) [Rank],
My procedure (first part):
SET #sql = N' DECLARE #temp AS TABLE
(
ranking int,
item nvarchar(100),
totalCount int,
matchCount int,
groupCount int,
groupName nvarchar(100)
)
INSERT INTO #temp
(
ranking,
item,
totalCount,
matchCount,
groupCount,
groupName
)
SELECT RANK() OVER(ORDER BY COUNT(*) desc, <sel>) [Rank],
<sel>,
COUNT(*) AS totalCount,
SUM(CASE WHEN suggestedAction = recommendation THEN 1 ELSE 0 END) AS matchCount,
ROUND(100 * AVG(CASE WHEN suggestedAction = recommendation THEN 1.0 ELSE 0.0 END), 0) AS groupCount,
''currentMonth'' AS groupName
FROM LogEsc
WHERE dateEsc LIKE ''' + #date0 + '%''
AND EID LIKE ''PE%''
GROUP BY <sel>
ORDER BY groupCount desc, <sel>
-- ...
Many thanks in advance for any help with this, Tim.
You can't use the alias.
use
ORDER BY ROUND(100 * AVG(CASE WHEN suggestedAction = recommendation THEN 1.0 ELSE 0.0 END), 0)