Sql Server - Variables inside select - sql

there's any way to declare and use variables INSIDE a select statement?
my idea is to use several times the same nested select for diferent math operations.
i'm using SQL Server 2008 R2 and here is some example:
--Example Table
declare #product_table table(id int, name varchar(20), price int, active bit)
insert into #product_table
values
(1,'productA',100,1),
(2,'productB',50,1),
(3,'productC',20,0),
(4,'productD',300,1),
(5,'productE',80,0)
select
name,
(select sum(price) from #product_table where active = 1), -- <-- Declare this as a Variable
(select sum(price) from #product_table where active = 1),
(select sum(price) from #product_table where active = 1/ price),
(select sum(price) from #product_table where active = 0)
from #product_table

You can use a CTE for this:
;WITH cte AS
(
SELECT (select sum(price) from #product_table where active = 1) AS Sum1
,(select sum(price) from #product_table where active = 0) AS Sum0
)
select
name,
Sum1,
Sum1 / price,
sum0
from #product_table
cross join cte;
The CTE will return one single row thus CROSS JOIN will just add it to the result set.
If you need computed values depending on row-values, you can reach something similar with CROSS APPLY to calculate values row-by-row and use them with a speaking name.
Just to show the principles. If your computation would include some row values, you can use cross apply to get variables:
select
name,
Sum1,
Sum1 / price,
sum0
from #product_table
cross apply
(
SELECT (select sum(price) from #product_table where active = 1) AS Sum1
,(select sum(price) from #product_table where active = 0) AS Sum0
) A

Simply your query using window functions:
select name,
sum(case when active = 1 then price end) over () as price_1,
sum(case when active = 1 then price end) over () / price as ratio,
sum(case when active = 0 then price end) over () as price_0
from #product_table;
You can do what you want using subqueries or CTEs. However, you should learn to use the best functions for what you want to do.

You cannot Declare a Variable Inside a SELECT Statement, but Instead, you can Declare it outside and use it in the SELECT. Like this :
declare #product_table table(id int, name varchar(20), price int, active bit)
DECLARE #SumPrice1 INT,#SumPrice0 INT
insert into #product_table
values
(1,'productA',100,1),
(2,'productB',50,1),
(3,'productC',20,0),
(4,'productD',300,1),
(5,'productE',80,0)
SELECT
#SumPrice1 = sum(price)
from #product_table where active = 1
SELECT
#SumPrice0 = sum(price)
from #product_table where active = 0
select
name,
#SumPrice1,
#SumPrice1,
(#SumPrice1/ price),
#SumPrice0
from #product_table
Or Like This
;WITH CTE
AS
(
SELECT
active,
SumAmt = SUM(price)
FROM #product_table
GROUP BY active
)
SELECT
PT.[Name],
[1] SumAmt,
[1] SumAmt,
[1] /price SumAmtPrice,
[0] ZeroSum
FROM #product_table PT
JOIN CTE
PIVOT
(
SUM(SumAmt)
FOR
Active IN
(
[0],[1]
)
)P
ON 1=1
Or More Simply Like This
SELECT
[name],
SUM(CASE Active WHEN 1 THEN Price ELSE 0 END) OVER(PARTITION BY Active) ActiveSum,
SUM(CASE Active WHEN 0 THEN Price ELSE 0 END) OVER(PARTITION BY Active) ZeroSum
FROM #product_table

Related

aggregation according to different conditions on same column

I have a table #tbl like below, i need to write a query like if there are more than 3 records availble
for particular cid then avg(val of particular cid ) for particular cid should be dispalyed against each id and if there are less than
3 records availble for particular cid then avg(val of all records availble).
Please suggest.
declare #tbl table(id int, cid int, val float )
insert into #tbl
values(1,100,20),(2,100,30),(3,100,25),(4,100,31),(5,100,50),
(6,200,30),(7,200,30),(8,300,90)
Your description is not clear, but I believe you need windowed functions:
WITH cte AS (
SELECT *, COUNT(*) OVER(PARTITION BY cid) AS cnt
FROM #tbl
)
SELECT id, (SELECT AVG(val) FROM cte) AS Av
FROM cte
WHERE cnt <=3
UNION ALL
SELECT id, AVG(val) OVER(PARTITION BY cid) AS Av
FROM cte
WHERE cnt > 3
ORDER BY id;
DBFiddle Demo
EDIT:
SELECT id,
CASE WHEN COUNT(*) OVER(PARTITION BY cid) <= 3 THEN AVG(val) OVER()
ELSE AVG(val) OVER(PARTITION BY cid)
END
FROM #tbl
ORDER BY id;
DBFiddle Demo2
You can try with the following. First calculate the average for each Cid depending in it's number of occurences, then join each Cid with the Id to display all table.
;WITH CidAverages AS
(
SELECT
T.cid,
Average = CASE
WHEN COUNT(1) >= 3 THEN AVG(T.val)
ELSE (SELECT AVG(Val) FROM #tbl) END
FROM
#tbl AS T
GROUP BY
T.cid
)
SELECT
T.*,
C.Average
FROM
#tbl AS T
INNER JOIN CidAverages AS C ON T.cid = C.cid
Given the clarifications in comments, I am thinking this is the intention
declare #tbl table(id int, cid int, val float )
insert into #tbl
values(1,100,20),(2,100,30),(3,100,25),(4,100,31),(5,100,50),
(6,200,30),(7,200,30),(8,300,90);
select distinct
cid
, case
when count(*) over (partition by cid) > 3 then avg(val) over (partition by cid)
else avg (val) over (partition by 1)
end as avg
from #tbl;
http://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=fdf4c4457220ec64132de7452a034976
cid avg
100 31.2
200 38.25
300 38.25
There are a number of aspects of a query like this that when run at scale though are going to be pretty bad on the query plan, I'd want to test this at a larger scale and tune before using.
The description was not clear on what happened if it was exactly 3, it mentions 'more than 3' and 'less than 3' - within this code the 'more than' was used to determine which category it was in, and less than interpreted to mean 'less than or equal to 3'

How to use CTE recursion to get a running total of a column

I have been researching CTE recursion, but I still seem to be confused.
I have table: utb(date, b_id, v_id, b_vol)
I need to get the running total of the column: b_vol.
The catch is that I need to divide if the row_number is even, and multiply if it is odd, so essentially:
P1= b_vol1, P2 = b_vol1/b_vol2, P3= b_vol1/b_vol2*b_vol3,
P4= b_vol1/b_vol2*b_vol3/b_vol4
So, it is basically P(n)= (P(n-1)(*OR/(b_vol(n))
I can't seem to figure out how to put that into a query.
Thanks for taking the time to read this. I hope you can help.
My sample table only has 1 column B_VOL. Try this
DECLARE #SampleData AS TABLE ( B_VOL int)
INSERT INTO #SampleData VALUES (50), (50), ( 50), (50) ,(155), (255)
;WITH temp AS
(
SELECT sd.*, row_number() OVER(ORDER BY (SELECT null)) - 1 AS RowIndex FROM #SampleData sd
)
,cte AS
(
SELECT TOP 1 t.RowIndex, CAST( t.B_VOL AS decimal(18,7)) AS sv FROM temp t ORDER BY t.RowIndex ASC
UNION ALL
SELECT t.RowIndex, CAST(cte.sv * (CASE WHEN t.RowIndex % 2 = 1 THEN CAST(1 AS decimal(18,7))/t.B_VOL ELSE t.B_VOL END) AS decimal(18,7)) AS sv
FROM cte
INNER JOIN temp t ON cte.RowIndex + 1 = t.RowIndex
)
SELECT t.*, c.sv
FROM temp t
INNER JOIN cte c ON t.RowIndex = c.RowIndex
OPTION(MAXRECURSION 0)
First, a CTE is not the best way to solve this problem. But . . .
with u as (
select u.*, row_number() over (order by date) - 1 as seqnum
from utb u
),
cte as (
select seqnum, b_vol as result
from u
union all
select cte.seqnum, cte.result * (case when seqnum % 2 = 1 then 1.0 / b_vol else b_vol end) as result
from cte join
u
on u.seqnum = cte.seqnum + 1
)
select *
from cte;
you should try this,
DECLARE #SampleData AS TABLE ( B_VOL int)
INSERT INTO #SampleData VALUES (50), (50), ( 50), (50) ,(155), (255)
;with CTE as
(
select S.B_VOL,ROW_NUMBER()over(order by ((select null)))rn
from #SampleData S
)
,CTE1 AS
(
select b_vol,cast(b_vol as float) sv,1 rn1
from cte where rn=1
union ALL
select c.b_vol,
case when (c1.rn1+1)%2=0
THEN c1.sv/cast(c.b_vol as float)
ELSE
c1.sv*cast(c.b_vol as float)
end
,c1.rn1+1
from CTE C
inner join CTE1 c1
on c.rn=c1.rn1+1
where c1.rn1<7
)
select * from cte1

Intersect Select Statements on Specific Columns

I've a table of SalesDetails, looking like this:
InvoiceID, LineID, Product
1,1,Apple
1,2,Banana
2,1,Apple
2,2,Mango
3,1,Apple
3,2,Banana
3,3,Mango
My requirement is to return rows where an Invoice contained sales of both: Apple AND Banana, but if there are other products on such an invoice, I don't want those.
So the result should be:
1,1,Apple
1,2,Banana
3,1,Apple
3,2,Banana
I tried the following:
Select * from SalesDetails where Product = 'Apple'
Intersect
Select * from SalesDetails where Product = 'Banana'
Didn't work, because it seems Intersect needs to match all the columns.
What I'm hoping to do is:
Select * from SalesDetails where Product = 'Apple'
Intersect ----On InvoiceID-----
Select * from SalesDetails where Product = 'Banana'
Is there a way to do this?
Or do I have to first Intersect on InvoiceIDs only using my criteria, then select the rows of those InvoiceIDs where the criteria is matched again, I.e.:
Select * From SalesDetails
Where Product In ('Apple', 'Banana') And InvoiceID In
(
Select InvoiceID from SalesDetails where Product = 'Apple'
Intersect
Select InvoiceID from SalesDetails where Product = 'Banana'
)
Which seems somewhat wasteful as it's examining the criteria twice.
Okay this time I've managed to get reuse of the Apple/Banana info by using a CTE.
with sd as (
Select * from SalesDetails
where (Product in ('Apple', 'Banana'))
)
Select * from sd where invoiceid in (Select invoiceid from
sd group by invoiceid having Count(distinct product) = 2)
SQL Fiddle
Do it with conditional aggregation:
select *
from SalesDetails
where product in ('apple', 'banana') and invoiceid in(
select invoiceid
from SalesDetails
group by invoiceid
having sum(case when product in('apple', 'banana') then 1 else 0 end) >= 2)
I think OP's suggestion is about the best one can do. The following might be faster, although I expect the difference to be slight and I have not done any benchmarking.
Select * From SalesDetails
Where Product ='Apple' And InvoiceID In
(
Select InvoiceID from SalesDetails where Product = 'Banana'
)
union all
select * from SalesDetails
Where Product ='Banana' And InvoiceID In
(
Select InvoiceID from SalesDetails where Product = 'Apple'
)
A self-join will solve the problem.
SELECT T1.*
FROM SalesDetails T1
INNER JOIN SalesDetails T2 ON T1.InvoiceId = T2.InvoiceId
AND (T1.Product = 'Apple' AND T2.Product = 'Banana'
OR T1.Product = 'Banana' AND t2.Product = 'Apple')
declare #t table (Id int,val int,name varchar(10))
insert into #t (id,val,name)values
(1,1,'Apple'),
(1,2,'Banana'),
(2,1,'Apple'),
(2,2,'Mango'),
(3,1,'Apple'),
(3,2,'Banana'),
(3,3,'Mango')
;with cte as (
select ID,val,name,ROW_NUMBER()OVER (PARTITION BY id ORDER BY val)RN from #t)
,cte2 AS(
select TOP 1 c.Id,c.val,c.name,C.RN from cte c
WHERE RN = 1
UNION ALL
select c.Id,c.val,c.name,C.RN from cte c
WHERE c.Id <> c.val)
select Id,val,name from (
select Id,val,name,COUNT(RN)OVER (PARTITION BY Id )R from cte2 )R
WHERE R = 2
Other was is to do PIVOT like this:
DECLARE #DataSource TABLE
(
[InvoiceID] TINYINT
,[LineID] TINYINT
,[Product] VARCHAR(12)
);
INSERT INTO #DataSource ([InvoiceID], [LineID], [Product])
VALUES (1,1,'Apple')
,(1,2,'Banana')
,(2,1,'Apple')
,(2,2,'Mango')
,(3,1,'Apple')
,(3,2,'Banana')
,(3,3,'Mango');
SELECT *
FROM #DataSource
PIVOT
(
MAX([LineID]) FOR [Product] IN ([Apple], [Banana])
) PVT
WHERE [Apple] IS NOT NULL
AND [Banana] IS NOT NULL;
It will give you the results in this format, but you are able to UNVPIVOT them if you want:
Or you can use window function like this:
;WITH DataSource AS
(
SELECT *
,SUM(1) OVER (PARTITION BY [InvoiceID]) AS [Match]
FROM #DataSource
WHERE [Product] = 'Apple' OR [Product] = 'Banana'
)
SELECT *
FROM DataSource
WHERE [Match] =2
First, you want to COUNT the number of rows per InvoiceID that matched the criteria Product = 'Apple' or 'Banana'. Then do a SELF-JOIN and filter the rows such that the COUNT must be >= 2, or the number of Products in your critera.
SQL Fiddle
SELECT sd.*
FROM (
SELECT InvoiceID, CC = COUNT(*)
FROM SalesDetails
WHERE Product IN('Apple', 'Banana')
GROUP BY InvoiceID
)t
INNER JOIN SalesDetails sd
ON sd.InvoiceID = t.InvoiceID
WHERE
t.CC >= 2
AND sd.Product IN('Apple', 'Banana')
WITH cte
AS
(
SELECT *
FROM [dbo].[SalesDetails]
WHERE [Product]='banana')
,cte1
AS
(SELECT *
FROM [dbo].[SalesDetails]
WHERE [Product]='apple')
SELECT *
FROM cte c INNER JOIN cte1 c1
ON c.[InvoiceID]=c1.[InvoiceID]
Here is a method using window functions:
select sd.*
from (select sd.*,
max(case when product = 'Apple' then 1 else 0 end) over (partition by invoiceid) as HasApple,
max(case when product = 'Banana' then 1 else 0 end) over (partition by invoiceid) as HasBanana
from salesdetails sd
) sd
where (product = 'Apple' and HasBanana > 0) or
(product = 'Banana' and HasApple > 0);
If you only want to write the condition once and are sure that each Product will only be once in any Order, you can use this:
SELECT * FROM (
SELECT InvoiceID, Product
,COUNT(*) OVER (PARTITION BY InvoiceID) matchcount
FROM SalesDetails
WHERE Product IN ('Apple','Banana') ) WHERE matchcount = 2;
This is what I ended up using, inspired by #Leon Bambrick:
(Expanded a little to support multiple products in the criteria)
WITH cteUnionBase AS
(SELECT * FROM SalesDetails
WHERE Product IN ('Apple Red','Apple Yellow','Apple Green','Banana Small','Banana Large')),
cteBanana AS
(SELECT * FROM cteUnionBase
WHERE Product IN ('Banana Small','Banana Large')),
cteApple AS
(SELECT * FROM cteUnionBase
WHERE Product IN ('Apple Red','Apple Yellow','Apple Green')),
cteIntersect AS
(
SELECT InvoiceID FROM cteApple
Intersect
SELECT InvoiceID FROM cteBanana
)
SELECT cteUnionBase.*
FROM cteUnionBase INNER JOIN cteIntersect
on cteUnionBase.InvoiceID = cteIntersect.InvoiceID

Adjust values of individual rows based upon order then roll up

OK - Let me explain this. I have a series of rows - the rows have a "weight" value and a "qty" and "rate" - others but am concerned with these for now...
What I need to do is to split the rows - which I have done in the below, based on the qty - so for a row that has a qty > 1 - a row is replicated for each qty value.
So, with them all split now - I need to determine which has the highest "weight" - then I need to keep that rate at 100%. All others will be rate/2.
Then - lastly - I need to roll the rows back up - so all like IDs are collapsed with aggregated rates - so back to single rows with sumed rates
/*
--create numbers table if don't already have one...
select top 1000000 row_number() over(order by t1.number) as N
into dbo.Numbers
from master..spt_values t1
cross join master..spt_values t2
*/
DECLARE #table TABLE
(
id int IDENTITY(1,1) ,
code varchar(10) ,
codeStatus varchar ,
qty int ,
rate money ,
codeWeight float ,
comment varchar(100)
)
INSERT INTO #table SELECT '12345' , 'T' , 3 , 375.86 , 5.6589 , NULL
INSERT INTO #table SELECT '45678' , 'T' , 2 , 2 , 4.0000 , NULL
INSERT INTO #table SELECT '11223' , 'T' , 1 , 2 , 3.0000 , NULL
SELECT t.id ,
t.code ,
t.qty ,
t.rate ,
t.codeWeight ,
n.N
FROM #table t
JOIN dbo.Numbers n on n.N <= t.qty
ORDER BY id ,
N
Here's a way to do the break down without needing the numbers table:
http://sqlfiddle.com/#!6/93b09/1
with cte as
(
SELECT id, code, qty, rate, codeWeight, qty N
FROM myTable t
union all
select id, code, qty, rate, codeWeight, N - 1
from cte
where N > 1
)
SELECT id, code, qty, rate, codeWeight, N
FROM cte
ORDER BY id, N
If I've understood the next steps correctly, then this should do what you're after...
http://sqlfiddle.com/#!6/93b09/4
declare #temp table(id int, code varchar(10), qty int, rate money, codeWeight float, N int)
;with cte as
(
SELECT id, code, qty, rate, codeWeight, qty N
FROM myTable t
union all
select id, code, qty, rate, codeWeight, N - 1
from cte
where N > 1
)
insert #temp
SELECT id, code, qty, rate, codeWeight, N
FROM cte
ORDER BY id, N
update t
set rate = t.rate / 2.0
from #temp t
inner join myTable m on m.id = t.id
and t.qty < m.qty
select id, code, max(qty) qty, sum(rate) rate, codeWeight
from #temp
group by id, code, codeWeight
Update
Based on discussion in comments, this SQL should do everything you need:
http://sqlfiddle.com/#!6/93b09/13
select id
, code
, codeStatus
, qty
, case (row_number() over (order by codeWeight desc))
when 1 then rate + case when qty > 0 then qty-1 else 0 end * rate / 2.0
else qty * rate / 2.0
end rate
, codeWeight
, comment
from myTable
i.e. the item with the greatest weight value returns a rate made up of one unit at the full rate, plus the remaining units at half price.
All other items are given rates which are their full quantity at half price.

SQL Server 2008 Reporting: Sum of Max of group

I have a table like this in the report designer:
Category: 1 2 3 4 Total
Max Amount: x y z c ?
I need to get the total of Max Amount, but expressions will not let me take Sum(Max(amount)), and the add total is disabled for this cell.
The max amount row is an expression that takes the max of each category. The source data has repeated values, so I just take the max. For example:
Category Amount
1 4.6
1 4.6
1 4.6
2 5
3 4
Other columns in the table are different, but amount will be the same so I cannot only select distinct values.
Maybe something like this:
SELECT
SUM(t1.maxAmout)
FROM
(
SELECT
MAX(t.Amout) AS maxAmout,
t.Category
FROM
yourTable AS t
GROUP BY
t.Category
) AS t1
You can also do it like this. If you are using sql server 2005+:
SELECT
pvt.[1],
pvt.[2],
pvt.[3],
(
pvt.[1]+
pvt.[2]+
pvt.[3]
) AS Total
FROM
(
SELECT
t.Category,
t.Amout
FROM
yourTable AS t
) AS SourceTable
PIVOT
(
MAX(Amout)
FOR Category IN([1],[2],[3])
) AS pvt
EDIT
If you have a 1000 categories. Then a dynamic pivot would be the best solution. So like this:
Test data
CREATE TABLE #T
(
Category INT,
Amout FLOAT
)
INSERT INTO #T
VALUES
(1,4.6),
(1,4.6),
(1,4.6),
(2,5),
(3,4)
Unique column names
DECLARE #cols VARCHAR(MAX)
DECLARE #colsTotal VARCHAR(MAX)
;WITH CTE
AS
(
SELECT
ROW_NUMBER() OVER(PARTITION BY t.Category ORDER BY t.Amout) AS RowNbr,
t.*
FROM
#T AS t
)
SELECT #cols = COALESCE(#cols + ','+QUOTENAME(Category),
QUOTENAME(Category)),
#colsTotal=COALESCE(#colsTotal + '+ISNULL('+QUOTENAME(Category)+',0)',
'ISNULL('+QUOTENAME(Category)+',0)')
FROM
CTE
WHERE
CTE.RowNbr=1
ORDER BY
Category
Dynamic pivot
DECLARE #query NVARCHAR(4000)=
N'SELECT
'+#cols+',
(
'+#colsTotal+'
) AS Total
FROM
(
SELECT
t.Category,
t.Amout
FROM
#T AS t
) AS SourceTable
PIVOT
(
MAX(Amout)
FOR Category IN('+#cols+')
) AS pvt'
EXECUTE(#query)
WITH
aggregate
AS
(
SELECT
category,
MAX(amount) AS max_amount
FROM
yourTable
GROUP BY
category
)
SELECT
MAX(CASE WHEN category = 1 THEN max_amount ELSE NULL END) AS [1],
MAX(CASE WHEN category = 2 THEN max_amount ELSE NULL END) AS [2],
MAX(CASE WHEN category = 3 THEN max_amount ELSE NULL END) AS [3],
MAX(CASE WHEN category = 4 THEN max_amount ELSE NULL END) AS [4],
SUM(max_amount) AS [total]
FROM
aggregate
The following returns a single value:
SELECT DISTINCT SUM(MAX(Amount)) OVER ()
FROM atable
GROUP BY Category
You could use this as a subquery calculating the value of the Total column.
Alternatively, TOP (1) could be used instead of DISTINCT (don't know why I couldn't think of it before):
SELECT TOP (1) SUM(MAX(Amount)) OVER ()
FROM atable
GROUP BY Category