Get closest value to AVG TSQL function - sql-server

I have a select like this:
SELECT
FORMAT(AVG([DC].[ContractedAmount]) , '$###,###,###,###.##') AS [AverageContractedAmount]
FROM
[DesignCustomer] AS [DC]
INNER JOIN
[Design] AS [D] ON [DC].[DesignKey] = [D].[DesignKey]
INNER JOIN
[Task] AS [T] ON [D].[DesignKey] = [t].[DesignKey]
INNER JOIN
[ProjectDesign] AS [PD] ON [D].[DesignKey] = [PD].[DesignKey]
INNER JOIN
[Project] AS [P] ON [PD].[ProjectKey] = [P].[ProjectKey]
INNER JOIN
[Address] AS [A] ON [A].[AddressGuid] = [P].[ProjectGuid]
As you can see I get the Average of Contracted Amount. I get something like this:
+---------------------------+
| [AverageContractedAmount] |
+---------------------------+
| $1,000.00 |
+---------------------------+
Now I want to get project who is more closest to that value
For example if I have 5 projects in project table like:
+----------------+
| ProjectName |
+----------------+
| First Project |
| Second Project |
| Third Project |
| Four Project |
| Five Project |
+----------------+
Relation of [DC] with project is something like this:
+----------------+------------------+
| ProjectName | ContractedAmount |
+----------------+------------------+
| First Project | 500 |
| Second Project | 700 |
| Third Project | 300 |
| Four Project | 950 |
| Five Project | 800 |
+----------------+------------------+
I want the query to return the Four Project Name because its ContractedAmount value is the closest to the AVG value. How can I achieve this? Regards

Without putting much thought into this, you can just dump that into a subquery and subtract, sort by the difference, and keep the top result:
SELECT TOP 1
project_name,
FROM Project
ORDER BY Abs(ContractedAmount -
(
SELECT
AVG([DC].[ContractedAmount]) AS [AverageContractedAmount]
FROM [DesignCustomer] AS [DC]
INNER JOIN [Design] AS [D] ON [DC].[DesignKey] = [D].[DesignKey]
INNER JOIN [Task] AS [T] ON [D].[DesignKey] = [t].[DesignKey]
INNER JOIN [ProjectDesign] AS [PD] ON [D].[DesignKey] = [PD].[DesignKey]
INNER JOIN [Project] AS [P] ON [PD].[ProjectKey] = [P].[ProjectKey]
INNER JOIN [Address] AS [A] ON [A].[AddressGuid] = [P].[ProjectGuid]
)) ASC

It seems that you've already figured out getting your average, so taking your sample tables, but fudged to get an average of $1000, I wrote a single select statement using the average to get your answer. For this answer, I've just calculated the average from the table rather than your code in order to get the variable #AVERAGE.
CREATE TABLE #Amts (Project VARCHAR(20), Amount INT);
INSERT INTO #Amts
VALUES
('One Project', 500),
('Two Project', 1500),
('Three Project', 300),
('Four Project', 1700),
('Five Project', 1100),
('Six Project', 900) ;
DECLARE #AVERAGE INT = (SELECT AVG(Amount) FROM #Amts) -- $1000
SELECT TOP 1 Project -- Since you said whichever project is suitable, this should be fine.
FROM #Amts AS A
WHERE ABS(A.Amount - #AVERAGE) = (SELECT MIN(ABS(Amin.Amount - #AVERAGE)) FROM #Amts AS Amin)
ORDER BY Project
DROP TABLE #Amts
This gives you the answer of "Five Project".

using DENSE_RANK:
WITH getavg AS (
SELECT AVG([DC].[ContractedAmount]) OVER() AS [AverageContractedAmount]
, p.projectname, p.contractedamount
FROM [DesignCustomer] AS [DC]
INNER JOIN [Design] AS [D] ON [DC].[DesignKey] = [D].[DesignKey]
INNER JOIN [Task] AS [T] ON [D].[DesignKey] = [t].[DesignKey]
INNER JOIN [ProjectDesign] AS [PD] ON [D].[DesignKey] = [PD].[DesignKey]
INNER JOIN [Project] AS [P] ON [PD].[ProjectKey] = [P].[ProjectKey]
INNER JOIN [Address] AS [A] ON [A].[AddressGuid] = [P].[ProjectGuid]
),
ranked as (
SELECT projectname, contractedamount
DENSE_RANK() OVER(ORDER BY ABS([AverageContractedAmount]-contractedamount)) AS dr
FROM getavg)
SELECT * FROM ranked
WHERE dr <= 5

Strictly closest would be:
; WITH AvgAmt AS (
SELECT AVG([DC].[ContractedAmount]) AS [AverageContractedAmount]
FROM [DesignCustomer] AS [DC]
INNER JOIN [Design] AS [D] ON [DC].[DesignKey] = [D].[DesignKey]
INNER JOIN [Task] AS [T] ON [D].[DesignKey] = [t].[DesignKey]
INNER JOIN [ProjectDesign] AS [PD] ON [D].[DesignKey] = [PD].[DesignKey]
INNER JOIN [Project] AS [P] ON [PD].[ProjectKey] = [P].[ProjectKey]
INNER JOIN [Address] AS [A] ON [A].[AddressGuid] = [P].[ProjectGuid]
)
SELECT TOP(1) P.ProjectName, DC.ContractedAmount
FROM [DesignCustomer] AS [DC]
INNER JOIN [Design] AS [D] ON [DC].[DesignKey] = [D].[DesignKey]
INNER JOIN [ProjectDesign] AS [PD] ON [D].[DesignKey] = [PD].[DesignKey]
INNER JOIN [Project] AS [P] ON [PD].[ProjectKey] = [P].[ProjectKey]
ORDER BY ABS(AvgAmt.[AverageContractedAmount] - ContractedAmount);
Depending on your data, you might want to exclude a few INNER JOIN's from the CTE itself simply to improve performance if the data doesn't require it.
The solution can also be easily parametrized to select closest #n contracted amounts instead of the closest one (converting SELECT TOP(1) to SELECT TOP(#n)).
As an addition, if there are multiple projects with the same difference to the average contracted amount, you might want to add some other columns into ORDER BY to break the tie. This is something you can decide on as the one with the knowledge of what data represents and what you expect from it.

Related

SQL sum calc returning multiple of same rows in stead of single sum

I have the following sql query,i only want the total sum of a column but its returing multiple of the same records as follows
SELECT
b.[Name]
sum(td.detailNumber) over () as Totaldetail,
sum(t.Original) over () as TotalOriginal
FROM Detail td WITH(NOLOCK)
inner JOIN Ticket t WITH(NOLOCK) ON t.ID = td.ID
inner JOIN [system].Branch b WITH(NOLOCK) ON t.BranchID = b.BranchID
inner JOIN Option mo WITH(NOLOCK) ON OptionID = td.MID
WHERE (td.ResultDateTime >= '2018/03/10 00:00:00:000' AND td.ResultDateTime <= '2018/03/10 23:59:59:000')
AND t.BranchID IN (SELECT Data FROM [system].Split('37', ','))
AND t.Code IN ('RS','AC')
results given
Totaldetail | Totaloriginal
54868 | 78569
54868 | 78569
54868 | 78569
54868 | 78569
54868 | 78569
54868 | 78569
54868 | 78569
as you can see above its showing me multiple rows for the date i provided.i only want one result at a time example
Totaldetail | Totaloriginal
54868 | 78569 //only one record to be shown
First of all, delete you OVER clauses. The OVER() clause makes your SUM a windowed function, so it doesn't group anything. This is why you were seeing the rows without grouping.
If you want to see aggregates by each name then add GROUP BY.
SELECT
b.[Name],
sum(td.detailNumber) as Totaldetail,
sum(t.Original) as TotalOriginal
FROM Detail td WITH(NOLOCK)
inner JOIN Ticket t WITH(NOLOCK) ON t.ID = td.ID
inner JOIN [system].Branch b WITH(NOLOCK) ON t.BranchID = b.BranchID
inner JOIN Option mo WITH(NOLOCK) ON OptionID = td.MID
WHERE (td.ResultDateTime >= '2018/03/10 00:00:00:000' AND td.ResultDateTime <= '2018/03/10 23:59:59:000')
AND t.BranchID IN (SELECT Data FROM [system].Split('37', ','))
AND t.Code IN ('RS','AC')
GROUP BY
b.[Name]
If you just want to see totals, remove the b.[name] column.
SELECT
sum(td.detailNumber) as Totaldetail,
sum(t.Original) as TotalOriginal
FROM Detail td WITH(NOLOCK)
inner JOIN Ticket t WITH(NOLOCK) ON t.ID = td.ID
inner JOIN [system].Branch b WITH(NOLOCK) ON t.BranchID = b.BranchID
inner JOIN Option mo WITH(NOLOCK) ON OptionID = td.MID
WHERE (td.ResultDateTime >= '2018/03/10 00:00:00:000' AND td.ResultDateTime <= '2018/03/10 23:59:59:000')
AND t.BranchID IN (SELECT Data FROM [system].Split('37', ','))
AND t.Code IN ('RS','AC')

How to Join to "Other" row

I have two tables. One holds Objects and the other holds Settings about each object. Not all of the rows in the Objects table have a corresponding row in the Settings table. There is a special row in the Settings table that is supposed to be used for the "Other" objects.
How can I create a join between Objects and Settings such that I get the given setting if there is one or the "Other" setting if there isn't?
For example consider the following script:
CREATE TABLE #Objects (Code nvarchar(20) not null);
CREATE TABLE #Settings (Code nvarchar(20) not null, Value int not null);
INSERT INTO #Objects
VALUES
('A'),
('B'),
('D')
INSERT INTO #Settings
VALUES
('A', 1),
('B', 2),
('C', 3),
('Other', 4)
SELECT
#Objects.Code,
#Settings.Value
FROM
#Objects
JOIN #Settings
ON #Objects.Code = #Settings.Code
OR #Settings.Code = 'Other'
DROP TABLE #Settings, #Objects
I'm wanting to get this:
Code | Value
---- | -----
A | 1
B | 2
D | 4
What I'm actually getting is:
Code | Value
----- | -----
A | 1
A | 4
B | 2
B | 4
D | 4
You can do this with an APPLY:
SELECT o.Code, s.Value
FROM #Objects o
CROSS APPLY (
SELECT TOP 1 *
FROM #Settings s
WHERE s.Code = o.Code or s.Code = 'Other'
ORDER BY case when s.Code = o.Code then 0 else 1 end
) s
For fun: a hybrid from answers by Gurv, jyao and SqlZim, which are all variations on the same basic theme:
SELECT o.Code, s2.Value
FROM #Objects o
LEFT JOIN #Settings s1 on s1.Code = o.Code
INNER JOIN #Settings s2 on s2.Code = coalesce(s1.Code, 'Other')
So far, this approach (LEFT JOIN + the INNER JOIN ON COALESCE() ) is my favorite option.
Note that this only works if there can be only one Settings record per Object record. If that ever changes, the APPLY answer still works, but other answers here might not work.
Another way is to use CTE to add an additional column [Alternative_code] for [#Object] table that has value "Other" for [Code] not existing in [#Settings]
and then using this CTE to join with #Settings table as shown below
; with c as (
select alternative_Code = isnull(s.code, 'Other'), o.Code
from #Objects o
left join #Settings s
on o.Code = s.Code)
select c.Code, s.value
from c
inner join #Settings s
on c.alternative_Code = s.Code
Using a left join to get null where o.Code has no match in #Settings
, and using coalesce() to return the designated replacement value
from #Settings when s.Value is null.
You could use isnull() instead of coalesce, the result would be the same in this instance.
I am not sure if this acceptable, but it returns the correct results:
select
o.Code
, coalesce(s.Value,x.Value) as Value
from #Objects o
left join #Settings s
on o.Code = s.Code
cross join (
select top 1 value
from #Settings
where Code = 'Other'
) x
rextester demo: http://rextester.com/EBUG86037
returns:
+------+-------+
| Code | Value |
+------+-------+
| A | 1 |
| B | 2 |
| D | 4 |
+------+-------+
In the form #RBarryYoung prefers:
select
o.Code
, coalesce(s.Value,x.Value) as Value
from #Objects o
left join #Settings s
on o.Code = s.Code
inner join #Settings x
on x.Code = 'Other'
This is more concise (saves you many keystrokes) and generates the same execution plan as my initial answer. Whether it is more or less clear about what it is doing is up to you, I like both.
If there is going to be one "Other" value then you can just do the join twice - a left join and another one which is effectively a cross join:
select o.Code,
coalesce(s.Value, s2.value) as value
from #Objects o
left join #Settings s on o.Code = s.Code
join #Settings s2 on s2.Code = 'Other'

Combining rows in T-SQL using STUFF or CONCAT command

I am looking at modifying the following query in a program (this was written before my time here as Software Engineer so please bear with me...):
SELECT
Participant.ParticipantID AS "ParticipantId",
Stream.StreamName AS "StreamName",
ParticipantStatistics.ConnectTime AS "ConnectTime",
ParticipantStatistics.DisconnectTime AS "DisconnectTime",
FormField.FieldLabel AS "FieldLabel",
RegistrantAnswer.Answer AS "Answer"
FROM ParticipantStatistics
INNER JOIN Participant ON ParticipantStatistics.ParticipantId = Participant.ParticipantID
INNER JOIN Registrant ON Registrant.RegistrantId = Participant.RegistrantID
LEFT OUTER JOIN RegistrantAnswer ON RegistrantAnswer.RegistrantID = Registrant.RegistrantID
INNER JOIN Event ON Event.EventId = Participant.EventID
INNER JOIN Stream ON Stream.MediaEventId = Event.EventId
LEFT OUTER JOIN FormField ON RegistrantAnswer.FormFieldId = FormField.FormFieldId
LEFT OUTER JOIN (SELECT DISTINCT ParticipantID, SurveyID FROM ParticipantSurvey)
AS SurveyCompleted ON SurveyCompleted.ParticipantID = Participant.ParticipantID
WHERE Stream.StreamId = '2235'
AND Participant.Visible = 1
ORDER BY Participant.ParticipantID, OrderNumber, ParticipantStatistics.ConnectTime ASC
This query gives me the following information:
What I would like to do is modify the above query to return the results as one row as follows:
| 315314 | ffbc110729 | 2011-10-27 03:13:06.240 | 2011-10-27 03:17:12.473 | **First Name, Last Name, Email, Company** | **ads, asd, asd#asd.com, asdf** |
Where the last two columns are combined, separated by commas in a single row.
Is this possible using either STUFF or CONCAT? I am new to T-SQL so please let me know if you need further clarification.
Best Regards,
EDIT: When I try to edit with STUFF FOR XML PATH I set it up the following way:
SELECT
Participant.ParticipantID AS "ParticipantId",
Stream.StreamName AS "StreamName",
ParticipantStatistics.ConnectTime AS "ConnectTime",
ParticipantStatistics.DisconnectTime AS "DisconnectTime",
STUFF ((SELECT ','+ FormField.FieldLabel FROM FormField WHERE FormField.FormFieldID = RegistrantAnswer.FormFieldID FOR XML PATH ('')),1,1,'')
FROM ParticipantStatistics
INNER JOIN Participant ON ParticipantStatistics.ParticipantId = Participant.ParticipantID
INNER JOIN Registrant ON Registrant.RegistrantId = Participant.RegistrantID
LEFT OUTER JOIN RegistrantAnswer ON RegistrantAnswer.RegistrantID = Registrant.RegistrantID
INNER JOIN Event ON Event.EventId = Participant.EventID
INNER JOIN Stream ON Stream.MediaEventId = Event.EventId
LEFT OUTER JOIN FormField ON RegistrantAnswer.FormFieldId = FormField.FormFieldId
LEFT OUTER JOIN (SELECT DISTINCT ParticipantID, SurveyID FROM ParticipantSurvey)
AS SurveyCompleted ON SurveyCompleted.ParticipantID = Participant.ParticipantID
WHERE Stream.StreamId = '2235'
AND Participant.Visible = 1
ORDER BY Participant.ParticipantID, OrderNumber, ParticipantStatistics.ConnectTime ASC
I then receive the following:
Again, I am fairly new to T-SQL so maybe I am setting it up wrong? Any help is greatly appreciated.
Try something like this:
SELECT
Participant.ParticipantID AS "ParticipantId",
Stream.StreamName AS "StreamName",
ParticipantStatistics.ConnectTime AS "ConnectTime",
ParticipantStatistics.DisconnectTime AS "DisconnectTime",
STUFF(
(SELECT ', ' + FormField.FieldLabel as'text()'
FROM Registrant
LEFT OUTER JOIN RegistrantAnswer ON RegistrantAnswer.RegistrantID = Registrant.RegistrantID
LEFT OUTER JOIN FormField ON RegistrantAnswer.FormFieldId = FormField.FormFieldId
WHERE Registrant.RegistrantId = Participant.RegistrantID
FOR XML PATH('')
), 1, 2, '') AS "FieldLabel",
STUFF(
(SELECT ', ' + RegistrantAnswer.Answer as'text()'
FROM Registrant
LEFT OUTER JOIN RegistrantAnswer ON RegistrantAnswer.RegistrantID = Registrant.RegistrantID
WHERE Registrant.RegistrantId = Participant.RegistrantID
FOR XML PATH('')
), 1, 2, '') AS "Answer"
FROM ParticipantStatistics
INNER JOIN Participant ON ParticipantStatistics.ParticipantId = Participant.ParticipantID
INNER JOIN Registrant ON Registrant.RegistrantId = Participant.RegistrantID
INNER JOIN Event ON Event.EventId = Participant.EventID
INNER JOIN Stream ON Stream.MediaEventId = Event.EventId
WHERE Stream.StreamId = '2235'
AND Participant.Visible = 1
ORDER BY Participant.ParticipantID, OrderNumber, ParticipantStatistics.ConnectTime ASC
Using STUFF + FOR XML PATH('') is usually the most practical in SQL Server to concatenate strings. This sample first populates the resultset in an intermediary temporary table for the sake of keeping things readable and manageable.
SELECT *
INTO #fiddle_table
FROM (
VALUES
(315314,'ffbc110729',{ts '2011-10-27 03:13:06.240'},{ts '2011-10-27 03:17:12.473'},'First Name','ads'),
(315314,'ffbc110729',{ts '2011-10-27 03:13:06.240'},{ts '2011-10-27 03:17:12.473'},'Last Name','asd'),
(315314,'ffbc110729',{ts '2011-10-27 03:13:06.240'},{ts '2011-10-27 03:17:12.473'},'Email','asd#asd.com'),
(315314,'ffbc110729',{ts '2011-10-27 03:13:06.240'},{ts '2011-10-27 03:17:12.473'},'Company','asdf')
) AS v(participantid,streamname,connecttime,disconnecttime,fieldlabel,answer);
;WITH cte AS (
SELECT DISTINCT
participantid,streamname,connecttime,disconnecttime
FROM
#fiddle_table
)
SELECT
participantid,streamname,connecttime,disconnecttime,
fieldlabels=STUFF((
SELECT ', '+fieldlabel
FROM #fiddle_table AS i
WHERE i.participantid=o.participantid
FOR XML PATH('')
),1,2,''
),
answers=STUFF((
SELECT ', '+answer
FROM #fiddle_table AS i
WHERE i.participantid=o.participantid
FOR XML PATH('')
),1,2,''
)
FROM cte AS o;
DROP TABLE #fiddle_table;
Result:
+---------------+------------+-------------------------+-------------------------+---------------------------------------+-----------------------------+
| participantid | streamname | connecttime | disconnecttime | fieldlabels | answers |
+---------------+------------+-------------------------+-------------------------+---------------------------------------+-----------------------------+
| 315314 | ffbc110729 | 2011-10-27 03:13:06.240 | 2011-10-27 03:17:12.473 | First Name, Last Name, Email, Company | ads, asd, asd#asd.com, asdf |
+---------------+------------+-------------------------+-------------------------+---------------------------------------+-----------------------------+

How can I only include one match when using INNER JOIN while doing SELECT statement in SQL Server?

SELECT distinct
sr.ServiceDescription,
os.ServiceID
FROM
OrderedServices as os
INNER JOIN
Orders as o ON o.OrderID = os.OrderID
INNER JOIN
ServiceRelation as sr ON os.ServiceID = sr.ServiceID
WHERE
o.OrderID = 2802882;
Let's say this returns something like this:
Service Description | ServiceID
--------------------------------
Basic Cable | 2
Extended Cable | 5
ExtendedPlus Cable | 5
Everything Cable | 10
However, I only want to return a single value for Service Description if two ServiceIDs match. See that ServiceID of 5 has two Service Descriptions? I only want to grab one of them. How can I modify the select statement above to accomplish something like this?
you can use MAX(),MIN() or Row_Number()
SELECT distinct
max(sr.ServiceDescription),
os.ServiceID
FROM
OrderedServices as os
INNER JOIN
Orders as o ON o.OrderID = os.OrderID
INNER JOIN
ServiceRelation as sr ON os.ServiceID = sr.ServiceID
WHERE
o.OrderID = 2802882 group by os.ServiceID;
Or
SELECT distinct
MIN(sr.ServiceDescription),
os.ServiceID
FROM
OrderedServices as os
INNER JOIN
Orders as o ON o.OrderID = os.OrderID
INNER JOIN
ServiceRelation as sr ON os.ServiceID = sr.ServiceID
WHERE
o.OrderID = 2802882 group by os.ServiceID;
Or
;with cte as
(
SELECT distinct
sr.ServiceDescription,
os.ServiceID,
Row_Number() Over(partition by os.ServiceID order by os.ServiceID)
as rn
FROM
OrderedServices as os
INNER JOIN
Orders as o ON o.OrderID = os.OrderID
INNER JOIN
ServiceRelation as sr ON os.ServiceID = sr.ServiceID
WHERE
o.OrderID = 2802882
)
select * from cte where rn=1

Error when using WHERE in Correlated Subquery

I have the following relationship: Bank -> Financing -> Contracts -> Supplier
I have to select all the Banks that is related to a Supplier, using the query below:
SELECT DISTINCT A.Name AS BankName, A2.Name as SupplierName
FROM Companies A
INNER JOIN Financing F ON F.IdFinancialCompany = A.Id
INNER JOIN Contracts C ON F.IdContract = C.Id
INNER JOIN Companies A2 ON C.IdSupplier = A2.Id
GROUP BY A.Name, A2.Name
So far, so good.
Now I have to list the number of contracts that are active and the number of total contracts, so I thought about including a subquery:
SELECT DISTINCT A.Name AS BankName, A2.Name as SupplierName,
(SELECT COUNT(C.Id)),
(SELECT COUNT(C.Id) WHERE C.ContractStatus = 1)
FROM Companies A
INNER JOIN Financing F ON F.IdFinancialCompany = A.Id
INNER JOIN Contracts C ON F.IdContract = C.Id
INNER JOIN Companies A2 ON C.IdSupplier = A2.Id
GROUP BY A.Name, A2.Name
Line 2 Works well, but I got na error in line 3:
Column 'Contracts.ContractStatus' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
I expect the following result:
BANK NAME | SUPPLIER NAME | TOTAL CONTRACTS | ACTIVE CONTRACTS
Bank 1 | Supplier 1| 5 | 2
How can I achieve this? I'm using SQL Server 2014
Thanks in advance!
Remove the distinct since your group by and use CASE
SELECT A.Name AS BankName, A2.Name as SupplierName,
COUNT(C.Id),
SUM(CASE WHEN C.ContractStatus = 1 THEN 1 ELSE 0 END)
FROM Companies A
INNER JOIN Financing F ON F.IdFinancialCompany = A.Id
INNER JOIN Contracts C ON F.IdContract = C.Id
INNER JOIN Companies A2 ON C.IdSupplier = A2.Id
GROUP BY A.Name, A2.Name

Resources