I have a football league database and I am trying to get the current score of the teams on a specific date. If I type in a date in the past I want the score at that specific date - the points of each teams on that date.
In my database I have a table that includes all the matches and I have a table with the teams and their point (this table is actually the same as the current score).
The two tables are:
create table teams
(
id char(3) primary key,
name varchar(40),
nomatches int,
owngoals int,
othergoals int,
points int
)
create table matches
(
id int identity(1,1),
homeid char(3) foreign key references teams(id),
outid char(3) foreign key references teams(id),
homegoal int,
outgoal int,
matchdate datetime
)
I am trying to use a stored procedure where I have a datetime as a parameter to show the current score (the team table) at that date defined by the parameter.
Right now I'm selecting all the matches that is bigger (newer) than the date I want til score table from and subtracting the result of that match from the teams point.
But it seems to me that it is a lot of work to do for something that simple.
Does anyone have a better idea?
Why do you subtract? Seems to me that the correct way to go would be to take the league start date and calculate the scores from that day up to selected date.
I'm not sure why you would want to put such a simple query into a procedure, but essentially what you're asking is as follows:
CREATE PROCEDURE sp_goals_to_date(#todate DATETIME) AS
SELECT id, SUM(homegoal) AS homegoal, SUM(outgoal) AS outgoal FROM matches WHERE matchdate <= #mydate
What Fedor says is correct - it is more efficient to do a single calculation from the beginning of time, than to have to do a calc from two different tables.
I would first transform the matches table like this:
SELECT
teamid = CASE t.calchometeam WHEN 1 THEN m.homeid ELSE m.outid END,
owngoal = CASE t.calchometeam WHEN 1 THEN m.homegoal ELSE m.outgoal END,
othergoal = CASE t.calchometeam WHEN 0 THEN m.homegoal ELSE m.outgoal END,
points = CASE m.homegoal
WHEN m.outgoal THEN #drawpoints
ELSE (SIGN(m.homegoal - m.outgoal) + 1) / 2 ^ ~m.playedhome) * #winpoints
+ (SIGN(m.homegoal - m.outgoal) + 1) / 2 ^ m.playedhome) * #losspoints
END
FROM matches m
CROSS JOIN (
SELECT CAST(0 AS bit) UNION ALL
SELECT CAST(1 AS bit)
) AS t (calchometeam)
WHERE m.matchdate <= #givendate
Now it's easier to calculate the necessary totals:
SELECT
teamid,
nomatches = COUNT(*),
owngoals = SUM(owngoal),
othergoals = SUM(othergoal),
points = SUM(points)
FROM transformed_matches
GROUP BY
teamid
Next step would be to join the last result set to the teams table to get the team's names. And if you actually need that final step, you could, of course, perform the calculations the way you intended from the beginning, i.e. calculate only the stats you need to subtract from the current values, rather than the actual standings. So, using this inverted logic, the entire query might look like this:
WITH
transformed_matches AS (
SELECT
matchid = m.id,
teamid = CASE t.calchometeam WHEN 1 THEN m.homeid ELSE m.outid END,
owngoal = CASE t.calchometeam WHEN 1 THEN m.homegoal ELSE m.outgoal END,
othergoal = CASE t.calchometeam WHEN 0 THEN m.homegoal ELSE m.outgoal END,
points = CASE m.homegoal
WHEN m.outgoal THEN #drawpoints
ELSE (SIGN(m.homegoal - m.outgoal) + 1) / 2 ^ ~m.playedhome) * #winpoints
+ (SIGN(m.homegoal - m.outgoal) + 1) / 2 ^ m.playedhome) * #losspoints
END
FROM matches m
CROSS JOIN (
SELECT CAST(0 AS bit) UNION ALL
SELECT CAST(1 AS bit)
) AS t (calchometeam)
WHERE m.matchdate > #givendate
),
aggregated AS (
SELECT
teamid,
nomatches = COUNT(*),
owngoals = SUM(owngoal),
othergoals = SUM(othergoal),
points = SUM(points)
FROM transformed_matches
GROUP BY
teamid
)
SELECT
t.id,
t.name,
nomatches = t.nomatches - ISNULL(a.nomatches , 0),
owngoals = t.owngoals - ISNULL(a.orngoals , 0),
othergoals = t.nomatches - ISNULL(a.othergoals, 0),
points = t.points - ISNULL(a.points , 0)
FROM teams t
LEFT JOIN aggregated a ON t.id = a.teamid
Note: You didn't specify which kind of football you meant, but living in Europe, it was easier for me to assume association football rather than any other kind. Yet, because I was not sure, I decided to parametrise my query. That is why you can see all those #winpoints, #drawpoints and #losspoints placeholders. You can replace the variables with the actual constants, if you like, or you could leave the query parametrised in case you wanted to satisfy your curiosity as to what the team's standings would have been if a different scoring system were in effect.
Related
I am trying to create a routine that can accept an SQL query as a string and the [table].[primaryKey] of the primary record in the returned dataset, then wrap that original query to implement pagination (return records 40-49 when requesting page 4 and 10 records per page).
The dataset returned by the original queries will frequently contain multiple instances of the primary record, one for each occurrence of supporting records. For the example provided, if a customer has three phone numbers on record the results for that customer in the original query would look like:
{5; John Smith; 205 W. Fort St; 17; Home; 123-123-4587}
{5; John Smith; 205 W. Fort St; 18; Work; 123-123-8547}
{5; John Smith; 205 W. Fort St; 19; Mobile; 123-123-1147}
I'm almost there, I think, with the following query:
DECLARE #PageNumber int = 4;
DECLARE #RecordsPerPage int = 10;
WITH OriginalQuery AS (
SELECT [Customer].[Id],
[Customer].[Name],
[Customer].[Address],
[Phone].[Id],
[Phone].[Type],
[Phone].[Number]
FROM [Customer] INNER JOIN [Phone] ON [Customer].[Id] = [Phone].[CustomerId]
)
SELECT [WrappedQuery].[RowNumber], [OriginalQuery].* FROM (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) [RowNumber], *
FROM (
SELECT DISTINCT [OriginalQuery].[{Customer.Id}] [PrimaryKey]
FROM [OriginalQuery]
) [RuwNumberQuery]
) [WrappedQuery]
INNER JOIN [OriginalQuery] ON [WrappedQuery].[PrimaryKey] = [OriginalQuery].[{Customer.Id}]
WHERE [WrappedQuery].[RowNumber] >= #PageNumber
AND [WrappedQuery].[RowNumber] < #PageNumber + #RecordsPerPage
This solution performs a SELECT DISTINCT on the primary key for the Primary (Customer) record and uses the SQL routine Row_Number() then joins the result with the results of the original query such that each unique primary (customer) record is numbered 1 - {end of file}, and I can pull only the RowNumber counts that I want.
But because OriginalQuery may have multiple fields named Id (from different tables), I can't figure out how to properly access [Customer].[Id] in my SELECT DISTINCT clause of [RowNumberQuery] or in the INNER JOIN.
Is there a better way to implement pagination at the SQL level, or a more direct method of accessing the field I need from within the subquery based on the table to which it belongs?
EDIT:
I've caused confusion in the pagination I am looking for. I am using Dapper in C# to compile the resulting dataset into individual complex objects, so the goal in the example would be to retrieve customers 31-40 in the list regardless of how many individual records exist for each customer. If Customer 31 had five phone records, Customer 32 had three phone records, Customer 33 had 1 phone record, and the remaining seven customers had two phone records each, I would expect the resulting dataset to contain 23 records total, but only 10 distinct customers.
SOLUTION
Thank you for all of the assistance, and I apologize for those areas I should have clarified sooner. I am creating a toolset that will allow C# Data Access Libraries to implement a set of standard parameters. If I have an option to implement the pagination in an internal function that can accept the SQL statement, I can defer to the toolset and not have to remember (or count on others to remember) to add the appropriate text each time. I'll set it up to return the finished objects, but if I were going to just modify the original query string it would look like:
public static string AddPagination(string sql, string primaryKey, Parameter requestParameters)
{
return $"WITH OriginalQuery AS ({sql.Replace("SELECT ", $"SELECT DENSE_RANK() OVER (ORDER BY {primaryKey}) AS PrimaryRecordCount, ",StringComparison.OrdinalIgnoreCase)}) " +
$"SELECT TOP ({requestParameters.MaxRecords}) * " +
$"FROM OriginalQuery " +
$"WHERE PrimaryRecordCount >= 1 + (({requestParameters.PageNumber - 1}) * {requestParameters.RecordsPerPage})" +
$" AND PrimaryRecordCount <= {requestParameters.Page} * {requestParameters.Limit}";
}
Just give your columns a different alias in your original query, e.g. [Customer].[Id] AS CustomerId, [Phone].[Id] AS PhoneId..., then you can reference OriginalQuery.CustomerId, or OriginalQuery.PhoneId
e.g.
DECLARE #PageNumber int = 4;
DECLARE #RecordsPerPage int = 10;
WITH OriginalQuery AS (
SELECT [Customer].[Id] AS CustomerId,
[Customer].[Name],
[Customer].[Address],
[Phone].[Id] AS PhoneId,
[Phone].[Type],
[Phone].[Number]
FROM [Customer] INNER JOIN [Phone] ON [Customer].[Id] = [Phone].[CustomerId]
)
SELECT [WrappedQuery].[RowNumber], [OriginalQuery].* FROM (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) [RowNumber], *
FROM (
SELECT DISTINCT [OriginalQuery].[{Customer.Id}] [PrimaryKey]
FROM [OriginalQuery]
) [RuwNumberQuery]
) [WrappedQuery]
INNER JOIN [OriginalQuery] ON [WrappedQuery].[PrimaryKey] = [OriginalQuery].[CustomerId]
WHERE [WrappedQuery].[RowNumber] >= #PageNumber
AND [WrappedQuery].[RowNumber] < #PageNumber + #RecordsPerPage
It's worth noting that your paging logic is wrong too. Currently you are adding page number to the number of pages so you are searching for:
Page 1: Customers 1 - 10
Page 2: Customers 2 - 11
Page 3: Customers 3 - 12
Your logic should be:
WHERE [WrappedQuery].[RowNumber] >= 1 + ((#PageNumber - 1) * #RecordsPerPage)
AND [WrappedQuery].[RowNumber] <= (#PageNumber * #RecordsPerPage)
Page 1: Customers 1 - 10
Page 2: Customers 11 - 20
Page 3: Customers 21 - 30
With that being said, you could just use DENSE_RANK() Rather than ROW_NUMBER which would simplify everything. I think this would give you the same result:
DECLARE #PageNumber int = 4;
DECLARE #RecordsPerPage int = 10;
WITH OriginalQuery AS (
SELECT c.Id AS CustomerId,
c.Name,
c.Address,
p.Id AS PhoneId,
p.Type,
p.Number,
DENSE_RANK() OVER(ORDER BY c.Id) AS RowNumber
FROM Customer AS c INNER JOIN Phone AS p ON c.Id = p.CustomerId
)
SELECT oq.CustomerId, oq.Name, oq.Address, oq.PhoneId, oq.Type, oq.Number
FROM OriginalQuery AS oq
WHERE oq.RowNumber >= 1 +((#PageNumber - 1) * #RecordsPerPage)
AND oq.RowNumber <= (#PageNumber * #RecordsPerPage);
I've added table aliases to try and make the code a bit cleaner, and also removed all the unnecessary square brackets. This is not necessary, but I personally find them quite hard on the eye, and only use them to escape key words.
Another difference is that in adding ORDER BY c.CustomerId you ensure consistent results for your paging. Using ORDER BY (SELECT NULL) implies that you don't care about the order, but you should if you using it for paging.
There are many concerns with what you are trying to do and you might be better off explaining why you are trying to make this process.
SQL query as a string
You are receiving a SQL query as a string, how are you parsing that string into the OriginalQuery CTE? This has both concerns about sql injection and concerns about global temp tables if you are using those.
Secondly, your example isn't doing pagination as it is commonly understood. If someone were to request page 1, 10 records per page, the calling application would expect to receive the first 10 records of the result set but your example will returns all records for the first 10 customers. Meaning the result could be 40+ if they each had 4 phone numbers as in your example data.
You should take a look at OFFSET and FETCH NEXT, as well as why this requirement to parse an arbitrary SQL string. There is probably a better way to do that.
Here is a rough example using OFFSET and FETCH NEXT from a static query, and returning only #RecordsPerPage number of records.
DECLARE #PageNumber int = 1;
DECLARE #RecordsPerPage int = 10;
SELECT [Customer].[Id],
[Customer].[Name],
[Customer].[Address],
[Phone].[Id],
[Phone].[Type],
[Phone].[Number]
FROM [Customer] INNER JOIN [Phone] ON [Customer].[Id] = [Phone].[CustomerId]
ORDER BY [Customer].[Id]
OFFSET (#PageNumber-1)*#RecordsPerPage rows
FETCH NEXT #RecordsPerPage ROWS ONLY
If you wanted to return all records for the the RecordsPerPage number of entries which have a corresponding phone number, then it would be something like...
DECLARE #PageNumber int = 1;
DECLARE #RecordsPerPage int = 10;
SELECT [Customer].[Id],
[Customer].[Name],
[Customer].[Address],
[Phone].[Id],
[Phone].[Type],
[Phone].[Number]
FROM [Customer] INNER JOIN [Phone] ON [Customer].[Id] = [Phone].[CustomerId]
WHERE Customer.ID IN (
SELECT DISTINCT Customer.ID FROM Customer INNER JOIN [Phone] ON [Customer].[Id] = [Phone].[CustomerId]
ORDER BY [Customer].[Id]
OFFSET (#PageNumber-1)*#RecordsPerPage rows
FETCH NEXT #RecordsPerPage ROWS ONLY
)
This does leave a question, what is the point of this query when the calling application can just use their own OFFSET and FETCH NEXT? They already have the SQL to generate the initial dataset, all they need to do is add OFFSET / FETCH NEXT to the end of it and they have their own pagination without trying to wrap it in a procedure of some sort.
To create a comparison, would you create a stored procedure that accepts a SQL string and then filters specific fields by specific values? Or would the people calling that stored procedure just add a Where clause to their own queries instead?
You can use alias name for the cuplicated column.
For example:
WITH OriginalQuery AS (
SELECT [Customer].[Id] as CustomerID,
[Customer].[Name],
[Customer].[Address],
[Phone].[Id] as PhoneID,
[Phone].[Type],
[Phone].[Number]
FROM [Customer] INNER JOIN [Phone] ON [Customer].[Id] = [Phone].[CustomerId]
)
now you can use the 2 ids whit the alias name for the next query.
I have a table that contains a breakdown of some data. I want to have that every 12 rows there is a row containing the total for the previous 12 rows. My understanding is that I cannot insert a row at a specific place in a table.
So i am attempting to create a second table that selects the data from the first table and on a case condition being met adds the total row.
This is my current attempt:
INSERT INTO #loanTempTable2 (payment,principal,interest,regular)
SELECT
(CASE
WHEN MonthNumber%12!=0
THEN CAST(MonthNumber AS varchar(50))
ELSE 'Year Total'
END) AS payment,
(CASE
WHEN MonthNumber%12=0
THEN (SELECT SUM(principal) FROM #loanTempTable WHERE payment BETWEEN (MonthNumber-11) AND MonthNumber)
WHEN MonthNumber=#LoanPeriod
THEN (SELECT SUM(principal) FROM #loanTempTable WHERE payment BETWEEN (#LoanPeriod-(#LoanPeriod%12)+1) AND #LoanPeriod)
ELSE (#RegularPayment - (#Rate*#LoanAmount))*(POWER((#Rate+1),(MonthNumber-1)))
END) AS principal,
(CASE
WHEN MonthNumber%12=0
THEN (SELECT SUM(interest) FROM #loanTempTable WHERE payment BETWEEN (MonthNumber-11) AND MonthNumber)
WHEN MonthNumber=#LoanPeriod
THEN (SELECT SUM(interest) FROM #loanTempTable WHERE payment BETWEEN (#LoanPeriod-(#LoanPeriod%12)+1) AND #LoanPeriod)
ELSE #RegularPayment - (#RegularPayment - (#Rate*#LoanAmount))*(POWER((#Rate+1),(AM.MonthNumber-1)))
END) AS interest,
(CASE
WHEN MonthNumber%12=0
THEN (SELECT SUM(regular) FROM #loanTempTable WHERE payment BETWEEN (MonthNumber-11) AND MonthNumber)
WHEN MonthNumber=#LoanPeriod
THEN (SELECT SUM(regular) FROM #loanTempTable WHERE payment BETWEEN (#LoanPeriod-(#LoanPeriod%12)+1) AND #LoanPeriod)
ELSE #RegularPayment
END) AS regular
FROM AllowedMonths AM
WHERE AM.MonthNumber >= 1 AND AM.MonthNumber <= #LoanPeriod
However, this is not working as I need it to, because it causes one of the payments to be skipped. The table results in having a total row where say the payment 12 row was.
My question is whether there is a way to have two rows added each time the case
WHEN MonthNumber%12=0
Occurs, hopefully meaing the payment 12 and the total row would appear in the table. Any ideas?
This is always tricky to handle in SQL - if possible, I'd simply handle this at the presentation level or somesuch.
If you persist in trying to do this in SQL, one way to do this would be by moveing the case to a join:
-- setup test data
declare #data table ([month] int)
declare #i int;
set #i = 1;
while #i < 100
begin
insert into #data values (#i);
set #i = #i + 1;
end;
with expander (idx) as
(
select 1
union all
select 2
)
select
case when E.idx = 1 then cast(D.[month] as varchar) else 'Year total' end as [month]
from #data D
left join expander E on E.idx = 1 or (D.[month] % 12 = 0)
The idea should be pretty obvious. You can do whatever aggregations you want with windowed functions:
case
when E.idx = 1 then D.[month]
else sum(D.[month]) over (partition by (D.[month] - 1) / 12) - D.[month]
end
A more reasonable (and easier to read and use) approach would be to take the two queries separately, then union them, and finally, order them by the month again. This makes much more sense, and is a lot easier to maintain - you don't have to do the silly "If I'm in a total row, emit this value, otherwise, emit this value".
Here's my data;
table A.pickup_date is a date column
table A.biz_days is the business days I want to add up to A.pickup_date
table B.date
table B.is_weekend (Y or N)
table B. is_holiday (Y or N)
Basically from table B, I know for each date, if any date is a business day or not. Now I want to have a third column in table A for the exact date after I add A.business_days to A.pickup_date.
Can anyone provide me with either a case when statement or procedure statement for this? Unfortunately we are not allowed to write our own functions in Teradata.
This is pretty darned ugly, but I think it should get you started.
First I created a volatile table to represent your table a:
CREATE VOLATILE TABLE vt_pickup AS
(SELECT CURRENT_DATE AS pickup_date,
8 AS Biz_Days) WITH DATA PRIMARY INDEX(pickup_date)
ON COMMIT PRESERVE ROWS;
INSERT INTO vt_pickup VALUES ('2015-02-24',5);
Then I joined that with sys_calendar.calendar to get the days of the week:
CREATE VOLATILE TABLE VT_Days AS
(
SELECT
p.pickup_date,
day_of_week
FROM
vt_pickup p
INNER JOIN sys_calendar.CALENDAR c
ON c.calendar_date >= p.pickup_date
AND c.calendar_date < (p.pickup_date + Biz_Days)
) WITH DATA
PRIMARY INDEX(pickup_date)
ON COMMIT PRESERVE ROWS
Then I can use all that to generate the actual delivery date:
SELECT
p.pickup_date,
p.biz_days,
biz_days + COUNT(sundays.day_of_week) + COUNT (saturdays.day_of_week) AS TotalDays,
COUNT (sundays.day_of_week) AS Suns,
COUNT (saturdays.day_of_week) AS Sats,
p.pickup_date + totaldays AS Delivery_Date,
FROM
vt_pickup p
LEFT JOIN vt_days AS Sundays ON
p.pickup_date = sundays.pickup_date
AND sundays.day_of_week = 1
LEFT JOIN vt_days AS saturdays ON
p.pickup_date = saturdays.pickup_date
AND saturdays.day_of_week = 7
GROUP BY 1,2
You should be able to use the logic with another alias for your holidays.
The easiest way to do this is calculating a sequential number of business days (add it as a new column to your calendar table if it's a recurring operation, otherwise using WITH):
SUM(CASE WHEN is_weekend = 'Y' OR is_holiday = 'Y' THEN 0 ELSE 1 END)
OVER (ORDER BY calendar_date
ROWS UNBOUNDED PRECEDING) AS biz_day#
Then you need two joins:
SELECT ..., c2.calendar_date
FROM tableA AS a
JOIN tableB AS c1
ON a.pickup_date = c1.calendar_date
JOIN tableB AS c2
ON c2.biz_day# = c1.biz_day# + a.biz_days
AND is_weekend = 'N'
AND is_holiday = 'N'
What I'm looking for is a way in MSSQL to create a complex IN or LIKE clause that contains a SET of values, some of which will be ranges.
Sort of like this, there are some single numbers, but also some ranges of numbers.
EX: SELECT * FROM table WHERE field LIKE/IN '1-10, 13, 24, 51-60'
I need to find a way to do this WITHOUT having to specify every number in the ranges separately AND without having to say "field LIKE blah OR field BETWEEN blah AND blah OR field LIKE blah.
This is just a simple example but the real query will have many groups and large ranges in it so all the OR's will not work.
One fairly easy way to do this would be to load a temp table with your values/ranges:
CREATE TABLE #Ranges (ValA int, ValB int)
INSERT INTO #Ranges
VALUES
(1, 10)
,(13, NULL)
,(24, NULL)
,(51,60)
SELECT *
FROM Table t
JOIN #Ranges R
ON (t.Field = R.ValA AND R.ValB IS NULL)
OR (t.Field BETWEEN R.ValA and R.ValB AND R.ValB IS NOT NULL)
The BETWEEN won't scale that well, though, so you may want to consider expanding this to include all values and eliminating ranges.
You can do this with CTEs.
First, create a numbers/tally table if you don't already have one (it might be better to make it permanent instead of temporary if you are going to use it a lot):
;WITH Numbers AS
(
SELECT
1 as Value
UNION ALL
SELECT
Numbers.Value + 1
FROM
Numbers
)
SELECT TOP 1000
Value
INTO ##Numbers
FROM
Numbers
OPTION (MAXRECURSION 1000)
Then you can use a CTE to parse the comma delimited string and join the ranges with the numbers table to get the "NewValue" column which contains the whole list of numbers you are looking for:
DECLARE #TestData varchar(50) = '1-10,13,24,51-60'
;WITH CTE AS
(
SELECT
1 AS RowCounter,
1 AS StartPosition,
CHARINDEX(',',#TestData) AS EndPosition
UNION ALL
SELECT
CTE.RowCounter + 1,
EndPosition + 1,
CHARINDEX(',',#TestData, CTE.EndPosition+1)
FROM CTE
WHERE
CTE.EndPosition > 0
)
SELECT
u.Value,
u.StartValue,
u.EndValue,
n.Value as NewValue
FROM
(
SELECT
Value,
SUBSTRING(Value,1,CASE WHEN CHARINDEX('-',Value) > 0 THEN CHARINDEX('-',Value)-1 ELSE LEN(Value) END) AS StartValue,
SUBSTRING(Value,CASE WHEN CHARINDEX('-',Value) > 0 THEN CHARINDEX('-',Value)+1 ELSE 1 END,LEN(Value)- CHARINDEX('-',Value)) AS EndValue
FROM
(
SELECT
SUBSTRING(#TestData, StartPosition, CASE WHEN EndPosition > 0 THEN EndPosition-StartPosition ELSE LEN(#TestData)-StartPosition+1 END) AS Value
FROM
CTE
)t
)u INNER JOIN ##Numbers n ON n.Value BETWEEN u.StartValue AND u.EndValue
All you would need to do once you have that is query the results using an IN statement, so something like
SELECT * FROM MyTable WHERE Value IN (SELECT NewValue FROM (/*subquery from above*/)t)
I have a table INDICATORS that stores details and current scores of performance indicators. I have another table IND_HISTORIES that stores historical values of the indicator scores. Data are stored from INDICATORS to IND_HISTORIES at set periods (ie quarterly), to establish score / rating trends.
IND_HISTORIES has a column structure similar to this-
pk_IndHistId fk_IndId Score DateSaved
Rating levels are also defined, meaning a score value of 1 to 3 is Low, 4 to 6 is Avg, and 7 to 9 is High.
I am trying to build an alert feature, whereby a record will be returned if it's most recent rating level (based on most recent score in IND_HISTORIES) is greater than it's second-most recent rating level (based on second-most recent score in IND_HISTORIES).
I am using code like below to build a temp table that translates score values to rating level thresholds...
-- opt_IND_ScoreValues = 1;2;3;4;5;6;7;8;9
DECLARE #tblScores TABLE (idx int identity, val int not null)
INSERT INTO #tblScores (val) SELECT IntValue FROM dbo.fn_getSettingList('opt_IND_ScoreValues')
-- opt_IND_RatingLevels = Low;Low;Low;Avg;Avg;Avg;High;High;High
DECLARE #tblRatings TABLE (idx int identity, txt nvarchar(128))
INSERT INTO #tblRatings (txt) SELECT TxtValue FROM dbo.fn_getSettingList('opt_IND_RatingLevels')
-- combine two tables above using a common index
DECLARE #tblRatingScores TABLE (val int, txt nvarchar(128))
INSERT INTO #tblRatingScores SELECT s.val, r.txt FROM #tblScores s JOIN #tblRatings r ON s.idx = r.idx
-- reduce table rows above to find score thresholds for each rating level
DECLARE #tblRatingBands TABLE (idx int identity, score int not null, rating nvarchar(128))
INSERT INTO #tblRatingBands
SELECT rs.val, rs.txt FROM #tblRatingScores rs
INNER JOIN (SELECT MIN(val) as val FROM #tblRatingScores GROUP BY txt) AS x ON rs.txt = x.txt AND rs.val = x.val
ORDER BY val
QUESTION: Is there an elegant query I can run against the IND_HISTORIES table that will return records where the most recent rating level for an INDICATOR is above the second-most recent rating level?
UPDATE: To clarify, INDICATORS is not used in the calculation - it's a parent table that holds general information of the performance measure and current 'volatile' scores. Scores are saved to IND_HISTORY periodically - this provides point-in-time 'snapshots' of data, helping to establish score trends.
I'm looking to query the IND_HISTORY table, to find where the most recent 'snapshot' value of an indicator is higher than its second-most recent 'snapshot' value. (It would be ideal to also join the Rating Levels table, as described above, in the determination, so that rows are only returned if the score increase results in a Rating Level increase.)
Any solution should be compatible with SQL Server 2005.
I've implemented the below, which seems to work. But I'd be interested to hear any recommendations to optimize or consolidate.
First, I realize that I do not need the last temp table #tblRatingBands constructed above. Instead, I simply select matching text ratings from #tblRatingScores in my first query set below.
Then in the final query, I check if the score value has increased and if the rating text has changed -- this indicates the trend score has increased and resulted in a change to the rating level.
DECLARE #tblTrendScores TABLE (indId int not null, ih_date datetime, rowNo int, ih_score int, rating nvarchar(128));
WITH LastTwoScores AS (
SELECT fk_IndId,
DateSaved,
ROW_NUMBER() OVER (PARTITION BY fk_IndId ORDER BY DateSaved DESC) AS RowNo,
Score
FROM Ind_History
)
INSERT INTO #tblTrendScores
SELECT *,
(SELECT txt FROM #tblRatingScores WHERE val = Score)
FROM LastTwoScores
WHERE RowNo BETWEEN 1 AND 2
ORDER BY fk_IndId, RowNo
SELECT a.indId,
a.ih_date,
CASE WHEN ((a.ih_score > IsNull(b.ih_score, 0)) AND (a.rating <> IsNull(b.rating, 'none'))) THEN 'Up'
WHEN ((a.ih_score < IsNull(b.ih_score, 0)) AND (a.rating <> IsNull(b.rating, 'none'))) THEN 'Down'
ELSE 'no-change'
END AS TrendRatingChange
FROM #tblTrendScores a
JOIN #tblTrendScores b ON a.indId = b.indId AND b.rowNo = 2
WHERE a.rowNo = 1