Turn quarterly data into monthly by repeating the quarterly rows by 3 - sql-server

I'm wondering how to repeat each of these rows 3 times to get them from Quarters into months.
I need to repeat the same values in the first 2 columns but depending on the quarter in the third column I would need the other months in that quarter, i.e for the first row '31/01/2021' and '28/02/2021'
So desired output would look like:

Another option is via a CROSS APPLY
Select A.Code
,A.Value
,B.Date
From YourTable A
Cross Apply ( values (EOMonth(dateadd(MONTH,-2,A.Date)))
,(EOMonth(dateadd(MONTH,-1,A.Date)))
,(EOMonth(dateadd(MONTH,-0,A.Date)))
) B(Date)
Results

WITH TABLE_DATA(CODE,VAL,DATED)AS
(
SELECT 'R01',777,'2021-03-31' UNION ALL
SELECT 'R01',833,'2021-06-30' UNION ALL
SELECT 'R01',882,'2021-09-30'
)
SELECT D.CODE,D.VAL,CAST(DATEADD(MONTH,-X.PLACEHOLDER,D.DATED)AS DATE)AS DATED,X.PLACEHOLDER
FROM TABLE_DATA AS D
CROSS JOIN
(
SELECT 0 AS PLACEHOLDER
UNION ALL
SELECT 1
UNION ALL
SELECT 2
)X
ORDER BY D.CODE,DATED;
Could you please check if this query is suitable for you. TABLE_DATA is an example of data you have provided

Related

Calling a function repeatedly and joining its output

I have 1 function given to me that takes parameters of #month and #year.
The function returns a table like this: Assuming #month=9 and #year=2020
select * FROM dbo.TotalClicksForMonth(9,2020)
Partner,Percentage
Partner1,0.25
Partner2,0.5
Partner3,0.25
I have a scalar function that returns a float
select dbo.TotalSpendForMonth(9,2020)
100
If I run a select statement
select dbo.TotalSpendForMonth(9,2020)*cp.PercentageClicks as percentsplit from dbo.TotalClicksForMonth(9,2020) as cp
This works, and I get an output like
Partner PercentageClicks percentsplit
Partner1 0.25 25
Partner2 0.50 50
Partner3 0.25 25
Taking the total amount and allocating it across all the rows in the right ratios.
Now, thats ok for Sept 2020 (9,2020). But I need the output in for all the months and years specified in a 3rd table
select datepart(MONTH,[date]) as mh,datepart(year,[date]) as yr
from sales
I cant work out how to do this.
I tried
select datepart(MONTH,sales.date) as mh,datepart(year,sales.date) as yr
join (select dbo.TotalspendForMonth(sales.mh,sales.yr)*cp.PercentageClicks as percentsplit from dbo.TotalClicksForMonth(sales.mh,sales.yr) as cp) as xx on 1=1
from sales
group by datepart(MONTH,sales.date),datepart(year,sales.date)
But that doesnt work.
I think my options are to create a temporary table and then repeatedly in a cursor call the select statement to add the rows, but there must be a better way,
Can you help please?
I appears what you need to do is APPLY to the function:
SELECT DATEPART(MONTH,s.[date]) AS mh,
DATEPART(year,s.[date]) AS yr,
dbo.TotalSpendForMonth(DATEPART(MONTH,s.[date]),DATEPART(year,s.[date])) * cp.PercentageClicks AS percentsplit
FROM dbo.sales s
CROSS APPLY dbo.TotalClicksForMonth(DATEPART(MONTH,s.[date]),DATEPART(year,s.[date])) cp;
If you prefer, you can derive the DATEPARTs in a VALUES table construct, so as not the repeat the expressions:
SELECT V.mh,
V.yr,
dbo.TotalSpendForMonth(V.mh,V.yr) * cp.PercentageClicks AS percentsplit
FROM dbo.sales s
CROSS APPLY (VALUES(DATEPART(MONTH,s.[date]),DATEPART(year,s.[date])))V(mh,yr)
CROSS APPLY dbo.TotalClicksForMonth(V.mh,V.yr) cp;
To calculate the click percentages you could CROSS APPLY the table valued function. To summarize across MONTH and YEAR you could use the SUM aggregate function and GROUP BY. Something like this.
select dt.mo, dt.yr,
sum(dbo.TotalSpendForMonth(dt.mo, dt.yr)*cp.PercentageClicks) total_amount
from dbo.sales s
cross apply (select datepart(month,s.[date]) as mo,
datepart(year,s.[date]) as yr) dt
cross apply dbo.TotalClicksForMonth(dt.mo, dt.yr) cp
group by dt.mo, dt.yr
order by dt.yr, dt.mo;

Trying to get tie results on column not included in order by

Given the following result sets, sorted in descending order on column Date.
I want to use Top clause like:
select top 4 *
from donation d
order by d.Date desc;
Which give me the following result:
Even though I limit my result with top 4, I want to be able to include the last row, because it is tie with the last record (id : 5) based on the Name.
This query first selects the names of the people in the top 4 dates (in a Common Table Expression), then uses these names to shown all their data. I guess that is what you want.
;WITH Top4Names AS
(
SELECT TOP 4 [Name]
FROM donation
ORDER BY [Date] DESC
)
SELECT d.id, d.[Name], d.Amount, d.[Date]
FROM donation d
INNER JOIN Top4Names t
ON t.[Name] = d.[Name]
ORDER BY d.[Date] DESC;

select statement with "Group by" on specific columns but displaying other columns along with group by columns

I want to get all data based on group by of only encounter,medicationname
column data..
select encounter,medicationname,count(*) as freq,labdate,result
from Medications where (labdate between #admitdate and DATEDIFF(dd,24,#admitdate))
group by encounter,medicationname having count(*)>2
I have records like
encounter medicationname freq
8604261 ACC 3
Now based on this data ,I want to get
This is my desired output
encounter medicationname labtime result
8604261 ACC 2015-05-22 18
8604261 ACC 2015-07-23 23
8604261 ACC 2015-09-09 27
You can use COUNT() as a window function, something like this:
;With Counted as (
SELECT encounter,medicationname,labdate,result,
COUNT(*) OVER (PARTITION BY encounter,medicationname) as cnt
from Medications
where (labdate between #admitdate
and DATEDIFF(dd,24,#admitdate))
)
select encounter,medicationname,labdate,result
from Counted
where cnt > 2
I would note that I think DATEDIFF1 is probably wrong also but since I don't have your data, inputs and an actual spec, I've left it as is for now.
1DATEDIFF returns an int, but you're using it in a comparison against a column which is apparently a date. DATEADD would be the more probably desired function here, but as I say, I don't have full information to go on.
If I understand you question correctly what you need is this
;WITH CTE AS
(
select encounter,medicationname,count(*) as freq,labdate,result
from Medications where (labdate between #admitdate and DATEDIFF(dd,24,#admitdate))
group by encounter,medicationname having count(*) > 2
)
select encounter,medicationname,labdate,result
from Medications M
INNER JOIN CTE C
ON M.encounter = C.encounter
AND M.medicationname = C.medicationname
where (labdate between #admitdate and DATEDIFF(dd,24,#admitdate))
or better yet using COUNT()OVER()
;WITH CTE AS
(
SELECT encounter,medicationname,COUNT(*) OVER(PARTITION BY encounter,medicationname)as freq,labdate,result
FROM Medications
WHERE (labdate between #admitdate and DATEDIFF(dd,24,#admitdate))
)
SELECT * FROM CTE
WHERE freq > 2
select encounter,medicationname,count(*) as freq,labdate,result
from Medications
where (labdate between #admitdate and DATEDIFF(dd,24,#admitdate))
group by encounter,medicationname having count(*) > 2

SQL Server: Joining in rows via. comma separated field

I'm trying to extract some data from a third party system which uses an SQL Server database. The DB structure looks something like this:
Order
OrderID OrderNumber
1 OX101
2 OX102
OrderItem
OrderItemID OrderID OptionCodes
1 1 12,14,15
2 1 14
3 2 15
Option
OptionID Description
12 Batteries
14 Gift wrap
15 Case
[etc.]
What I want is one row per order item that includes a concatenated field with each option description. So something like this:
OrderItemID OrderNumber Options
1 OX101 Batteries\nGift Wrap\nCase
2 OX101 Gift Wrap
3 OX102 Case
Of course this is complicated by the fact that the options are a comma separated string field instead of a proper lookup table. So I need to split this up by comma in order to join in the options table, and then concat the result back into one field.
At first I tried creating a function which splits out the option data by comma and returns this as a table. Although I was able to join the result of this function with the options table, I wasn't able to pass the OptionCodes column to the function in the join, as it only seemed to work with declared variables or hard-coded values.
Can someone point me in the right direction?
I would use a splitting function (here's an example) to get individual values and keep them in a CTE. Then you can join the CTE to your table called "Option".
SELECT * INTO #Order
FROM (
SELECT 1 OrderID, 'OX101' OrderNumber UNION SELECT 2, 'OX102'
) X;
SELECT * INTO #OrderItem
FROM (
SELECT 1 OrderItemID, 1 OrderID, '12,14,15' OptionCodes
UNION
SELECT 2, 1, '14'
UNION
SELECT 3, 2, '15'
) X;
SELECT * INTO #Option
FROM (
SELECT 12 OptionID, 'Batteries' Description
UNION
SELECT 14, 'Gift Wrap'
UNION
SELECT 15, 'Case'
) X;
WITH N AS (
SELECT I.OrderID, I.OrderItemID, X.items OptionCode
FROM #OrderItem I CROSS APPLY dbo.Split(OptionCodes, ',') X
)
SELECT Q.OrderItemID, Q.OrderNumber,
CONVERT(NVarChar(1000), (
SELECT T.Description + ','
FROM N INNER JOIN #Option T ON N.OptionCode = T.OptionID
WHERE N.OrderItemID = Q.OrderItemID
FOR XML PATH(''))
) Options
FROM (
SELECT N.OrderItemID, O.OrderNumber
FROM #Order O INNER JOIN N ON O.OrderID = N.OrderID
GROUP BY N.OrderItemID, O.OrderNumber) Q
DROP TABLE #Order;
DROP TABLE #OrderItem;
DROP TABLE #Option;

SQL Select Statement For Calculating A Running Average Column

I am trying to have a running average column in the SELECT statement based on a column from the n previous rows in the same SELECT statement. The average I need is based on the n previous rows in the resultset.
Let me explain
Id Number Average
1 1 NULL
2 3 NULL
3 2 NULL
4 4 2 <----- Average of (1, 3, 2),Numbers from previous 3 rows
5 6 3 <----- Average of (3, 2, 4),Numbers from previous 3 rows
. . .
. . .
The first 3 rows of the Average column are null because there are no previous rows. The row 4 in the Average column shows the average of the Number column from the previous 3 rows.
I need some help trying to construct a SQL Select statement that will do this.
This should do it:
--Test Data
CREATE TABLE RowsToAverage
(
ID int NOT NULL,
Number int NOT NULL
)
INSERT RowsToAverage(ID, Number)
SELECT 1, 1
UNION ALL
SELECT 2, 3
UNION ALL
SELECT 3, 2
UNION ALL
SELECT 4, 4
UNION ALL
SELECT 5, 6
UNION ALL
SELECT 6, 8
UNION ALL
SELECT 7, 10
--The query
;WITH NumberedRows
AS
(
SELECT rta.*, row_number() OVER (ORDER BY rta.ID ASC) AS RowNumber
FROM RowsToAverage rta
)
SELECT nr.ID, nr.Number,
CASE
WHEN nr.RowNumber <=3 THEN NULL
ELSE ( SELECT avg(Number)
FROM NumberedRows
WHERE RowNumber < nr.RowNumber
AND RowNumber >= nr.RowNumber - 3
)
END AS MovingAverage
FROM NumberedRows nr
Assuming that the Id column is sequential, here's a simplified query for a table named "MyTable":
SELECT
b.Id,
b.Number,
(
SELECT
AVG(a.Number)
FROM
MyTable a
WHERE
a.id >= (b.Id - 3)
AND a.id < b.Id
AND b.Id > 3
) as Average
FROM
MyTable b;
Edit: I missed the point that it should average the three previous records...
For a general running average, I think something like this would work:
SELECT
id, number,
SUM(number) OVER (ORDER BY ID) /
ROW_NUMBER() OVER (ORDER BY ID) AS [RunningAverage]
FROM myTable
ORDER BY ID
A simple self join would seem to perform much better than a row referencing subquery
Generate 10k rows of test data:
drop table test10k
create table test10k (Id int, Number int, constraint test10k_cpk primary key clustered (id))
;WITH digits AS (
SELECT 0 as Number
UNION SELECT 1
UNION SELECT 2
UNION SELECT 3
UNION SELECT 4
UNION SELECT 5
UNION SELECT 6
UNION SELECT 7
UNION SELECT 8
UNION SELECT 9
)
,numbers as (
SELECT
(thousands.Number * 1000)
+ (hundreds.Number * 100)
+ (tens.Number * 10)
+ ones.Number AS Number
FROM digits AS ones
CROSS JOIN digits AS tens
CROSS JOIN digits AS hundreds
CROSS JOIN digits AS thousands
)
insert test10k (Id, Number)
select Number, Number
from numbers
I would pull the special case of the first 3 rows out of the main query, you can UNION ALL those back in if you really want it in the row set. Self join query:
;WITH NumberedRows
AS
(
SELECT rta.*, row_number() OVER (ORDER BY rta.ID ASC) AS RowNumber
FROM test10k rta
)
SELECT nr.ID, nr.Number,
avg(trailing.Number) as MovingAverage
FROM NumberedRows nr
join NumberedRows as trailing on trailing.RowNumber between nr.RowNumber-3 and nr.RowNumber-1
where nr.Number > 3
group by nr.id, nr.Number
On my machine this takes about 10 seconds, the subquery approach that Aaron Alton demonstrated takes about 45 seconds (after I changed it to reflect my test source table) :
;WITH NumberedRows
AS
(
SELECT rta.*, row_number() OVER (ORDER BY rta.ID ASC) AS RowNumber
FROM test10k rta
)
SELECT nr.ID, nr.Number,
CASE
WHEN nr.RowNumber <=3 THEN NULL
ELSE ( SELECT avg(Number)
FROM NumberedRows
WHERE RowNumber < nr.RowNumber
AND RowNumber >= nr.RowNumber - 3
)
END AS MovingAverage
FROM NumberedRows nr
If you do a SET STATISTICS PROFILE ON, you can see the self join has 10k executes on the table spool. The subquery has 10k executes on the filter, aggregate, and other steps.
Want to improve this post? Provide detailed answers to this question, including citations and an explanation of why your answer is correct. Answers without enough detail may be edited or deleted.
Check out some solutions here. I'm sure that you could adapt one of them easily enough.
If you want this to be truly performant, and arn't afraid to dig into a seldom-used area of SQL Server, you should look into writing a custom aggregate function. SQL Server 2005 and 2008 brought CLR integration to the table, including the ability to write user aggregate functions. A custom running total aggregate would be the most efficient way to calculate a running average like this, by far.
Alternatively you can denormalize and store precalculated running values. Described here:
http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/denormalizing-to-enforce-business-rules-running-totals.aspx
Performance of selects is as fast as it goes. Of course, modifications are slower.

Resources