TSQL Combine multiple sub queries on same table - sql-server

I've been trying to improve a SQL query which uses multiple sub queries over the same table but with different conditions and only retrieves the first result from each sub queries.
I will try to simplify the use-case :
I have a table Products like this:
Product_id
reference
field3
field 4
1
ref1
val1
val3
2
ref2
val2
val4
And another table History:
History_id
reference
utilcode
physicalcode
issue
media
datetime
1
ref1
'test'
'TST'
'0'
'&audio'
'a_date'
2
ref2
'phone'
'CALLER'
'1'
'&video'
'a_date'
3
ref2
'test'
'CALLER'
'2'
'&test'
'a_date'
History is a log table and therefore contains a lot of values.
Now I have a query like this
SELECT
p.reference,
p.field3, p.field4,
(SELECT TOP 1 a_date
FROM history h
WHERE h.reference = p.reference
AND physicalcode = 'TST'
AND issue = 0
ORDER BY a_date DESC) AS latest_date_issue_0,
(SELECT TOP 1 a_date
FROM history h
WHERE h.reference = p.reference
AND physicalcode = 'TST'
AND issue = 1
ORDER BY a_date DESC) AS latest_date_issue_1
(SELECT TOP 1 a_date
FROM history h
WHERE h.reference = p.reference
AND utilcode = 'phone'
ORDER BY a_date DESC) AS latest_date_phone,
(SELECT TOP 1 media
FROM history h
WHERE h.reference = p.reference
AND utilcode = 'phone'
ORDER BY a_date DESC) AS latest_media,
-- and so on with many possible combinations
-- Note that there are more than this few fields on the tables I work on.
WHERE
p.field3 = 'valX',
p.field4 = 'valY'
FROM
products p
How could I merge every sub selects ? Or even a few that are alike to improve the performance ?
History being a very big table, selecting over it multiple times drastically slows down the query.
The main problem being that I only need the first value every time.
Thank you for your time and I hope to find a better way to deal with this issue!
I tried to use ROW_NUMBER() but I could not find a suitable way to use it.
I also tried to create a tmp table using WITH to group every possibility from history but it was worse.
EDIT : Execution plan https://www.brentozar.com/pastetheplan/?id=Sy1AKIsUs

You can convert your correlated subqueries (you call them "subselects") to independent subqueries, then JOIN them. That way each subquery will only need to run once. I'll show you how to do this for your first subquery.
Here's a subquery replacing your first subquery.
SELECT reference, MAX(a_date) a_date
FROM history
WHERE physicalcode = 'TST'
AND issue = 0
GROUP BY reference
This gives a virtual table containing the latest date for each reference number from the history table matching the criteria in your question. A multicolumn index on history (physicalcode, issue, reference, a_date) makes this fast.
Then you can join it to the main table something like this:
SELECT
p.reference,
p.field3, p.field4,
a.a_date a_date_issue_0
FROM products p
LEFT JOIN ( /*the subquery */
SELECT reference, MAX(a_date) a_date
FROM history
WHERE physicalcode = 'TST'
AND issue = 0
GROUP BY reference
) a ON p.reference=a.reference
These subqueries can also be defined as VIEWs or Common Table Expressions (CTEs). If you have many of them you'll probably find it easier to read and reason about your query by doing them that way.
Your last subquery is a little trickier to handle this way. I suggest you work with this answer and then maybe ask another question about that.

Thanks to #O.Jones I've been able to find a way to improve this query.
What I did to merge a few requests was to use a CTE like so :
From
SELECT
(SELECT TOP 1 a_date
FROM history h
WHERE h.reference = p.reference
AND physicalcode = 'TST'
AND issue = 0
ORDER BY a_date DESC) AS latest_date_issue_0,
(SELECT TOP 1 a_date
FROM history h
WHERE h.reference = p.reference
AND physicalcode = 'TST'
AND issue = 1
ORDER BY a_date DESC) AS latest_date_issue_1
(SELECT top 1 a_date
FROM history h
WHERE h.reference = p.reference
AND h.physicalcode = 'TSTKO'
ORDER BY h.d_systeme DESC ) AS d_tst_ko,
(SELECT top 1 a_date
FROM history h
WHERE h.reference = p.reference
AND h.physicalcode = 'CALLERID'
ORDER BY h.d_systeme DESC ) AS d_wrong_number
FROM products p
To
WITH physicalcode_cte (reference, physicalcode, issue, a_date) as
(
SELECT reference, physicalcode, issue, max(a_date)
from historique
where codephysique in ('TST','TSTKO','CALLERID')
and a_date > dateadd(month, -4, getdate()) -- filter on date range to reduce number of rows
group by reference, physicalcode, issue
)
SELECT
date_issue_0.a_date,
date_issue_1.a_date,
tst_ko.a_date,
wrong_number.a_date
FROM products p
LEFT JOIN physicalcode_cte date_issue_0 on p.reference = date_issue_0.reference
AND date_issue_0.codephysique = 'TST'
AND date_issue_0.anomalie = 0
LEFT JOIN physicalcode_cte date_issue_1 on p.reference = date_issue_1.reference
AND date_issue_1.codephysique = 'TST'
AND date_issue_1.anomalie = 1
LEFT JOIN physicalcode_cte tst_ko on p.reference = tst_ko.reference
AND tst_ko.codephysique = 'TST'
LEFT JOIN physicalcode_cte wrong_number on p.reference = wrong_number.reference AND
AND wrong_number.codephysique = 'TST'
I've applied this idea for different scenarii and made 2 CTE.
I couldn't merge everything, sometime merging caused cost increase. But after several tests I've been able to go from 7100 total cost to 2100.
It is still a lot but 3 times less anyway. Takes 5 seconds instead of a timeout.
It's a query used for monthly reports so I don't need it to be super fast, I will keep it that way.
Thanks you!

Related

Optimise query with count and order by function

I have a problem with the optimization of this query, I have 3 tables (Products = Catalogo.GTIN, Sales Header = TEDEF.Factura and Sales Detail = TEDEF.Farmacia).
The query tries to find the Mode of the column VPRODEXENIGV_FAR. This query without the ORDER BY executes in less than 3 seconds (the table of details has about 30 million rows).
But when I add the ORDER BY clause, the query now takes more than 30 minutes to run.
I want to know how can I optimize this query or the indexes that I need to optimize this.
SELECT *
FROM Catalogo.GTIN G
CROSS APPLY
(SELECT TOP 1
COUNT(FAR.VPRODEXENIGV_FAR) [ROW],
YEAR(FAC2.VFECEMI_FAC) [AÑO],
MONTH(FAC2.VFECEMI_FAC) [MES],
FAR.VCODPROD_FAR_003,
CASE WHEN FAR.VPRODEXENIGV_FAR = 'A' THEN 1 ELSE 0 END AfectoIGV
FROM
TEDEF.Factura FAC2
INNER JOIN
TEDEF.Farmacia FAR ON FAC2.VTDOCPAGO_FAC = FAR.VTDOCPAGO_FAC
AND FAC2.VNDOCPAGO_FAC = FAR.VNDOCPAGO_FAC
WHERE
G.CODIGO = FAR.VCODPROD_FAR_003
GROUP BY
YEAR(FAC2.VFECEMI_FAC),
MONTH(FAC2.VFECEMI_FAC),
FAR.VCODPROD_FAR_003,
FAR.VPRODEXENIGV_FAR
ORDER BY
1 DESC --- <----- THE PROBLEM IS HERE
) GG
Ouch! You have a hugely expensive dependent subquery. It's expensive because SELECT TOP(n) ... ORDER BY col DESC does a whole lot of work to create a result set only to discard all but one row. And, it's a dependent subquery so it's run for every row of Catalogo.GTIN .
It looks like you want to count the resultset rows in the most recent month and year for each Catalogo.GTIN row. So, let's try to refactor your query to do that.
We'll start with a subquery to grab the month-start date of the latest Factura row for each catalog entry.
SELECT CODIGO,
DATEFROMPARTS(YEAR(maxd), MONTH(maxd),1) maxmes
FROM (
SELECT MAX(FAC2.VFECEMI_FAC) maxd,
G.CODIGO
FROM Catalogo.GTIN G
JOIN TDEF.Farmacia FAR
ON G.CODIGO = FAR.VCODPROD_FAR_003
JOIN TEDEF.Factura FAC2
ON FAC2.VTDOCPAGO_FAC = FAR.VTDOCPAGO_FAC
AND FAC2.VNDOCPAGO_FAC = FAR.VNDOCPAGO_FAC
GROUP BY G.CODIGO
) maxd
It's wise to test this and make sure it works correctly and performs tolerably well. If you test it in SSMS, you can use "Show Actual Execution Plan" and see if it recommends an extra index. This subquery need only be run once, rather than once per G.CODIGO row.
Then we'll use it in your larger query.
SELECT G.*,
COUNT(FAR.VPRODEXENIGV_FAR) [ROW],
YEAR(FAC2.VFECEMI_FAC) [AÑO],
MONTH(FAC2.VFECEMI_FAC) [MES],
FAR.VCODPROD_FAR_003,
CASE WHEN FAR.VPRODEXENIGV_FAR = 'A' THEN 1 ELSE 0 END AfectoIGV
FROM Catalogo.GTIN G
JOIN (
SELECT CODIGO,
DATEFROMPARTS(YEAR(maxd), MONTH(maxd),1) maxmes
FROM (
SELECT MAX(FAC2.VFECEMI_FAC) maxd,
G.CODIGO
FROM Catalogo.GTIN G
JOIN TDEF.Farmacia FAR
ON G.CODIGO = FAR.VCODPROD_FAR_003
JOIN TEDEF.Factura FAC2
ON FAC2.VTDOCPAGO_FAC = FAR.VTDOCPAGO_FAC
AND FAC2.VNDOCPAGO_FAC = FAR.VNDOCPAGO_FAC
GROUP BY G.CODIGO
) maxd
) maxmes ON G.CODIGO = maxmes.CODIGO
JOIN TEDEF.Farmacia FAR
ON G.CODIGO = FAR.VCODPROD_FAR_003
JOIN TEDEF.Factura FAC2
ON FAC2.VTDOCPAGO_FAC = FAR.VTDOCPAGO_FAC
AND FAC2.VNDOCPAGO_FAC = FAR.VNDOCPAGO_FAC
AND FAC2.VFECEMI_FAC >= maxmes.maxmes
GROUP BY maxmes.maxmes,
G.CODIGO,
FAR.VCODPROD_FAR_003,
FAR.VPRODEXENIGV_FAR
Here is the tricky bit:
DATEFROMPARTS(YEAR(maxd), MONTH(maxd),1) maxmes turns any date maxd into the first day of that month.
And, FAC2.VFECEMI_FAC >= maxmes.maxmes filters out rows before the first day of that month (for that CODIGO). It does so in a sargable way: a way that can exploit an index on FAC2.VFECEMI_FAC.
That is an alternative way to do TOP(1) ORDER BY d DESC. And faster.
It's all about sets of rows. Especially when using GROUP BY, it's performance-helpful to limit the number of rows in each set.
Obviously I cannot debug this.
Is me again, Finally i resolve the problem of the optimization, now the query delay is about 20 sec (with the sort instruction and with the count in a table over 30 million rows) i hope this way can help others or could be optimice more by the community.
I resolve the problem applying the sort but with the Row_Number instruction, in that way the server take my index for the sort instruction and make the magic:
WITH x
AS
(
SELECT *, ROW_NUMBER() OVER (PARTITION BY GG.COD, GG.[AÑO], GG.[MES] ORDER BY GG.[ROW] DESC) [ID]
FROM Catalogo.GTIN G
CROSS APPLY
(
SELECT COUNT(FAR.VPRODEXENIGV_FAR) [ROW]
, YEAR(FAC2.VFECEMI_FAC) [AÑO]
, MONTH(FAC2.VFECEMI_FAC) [MES]
, FAR.VCODPROD_FAR_003 [COD]
, CASE WHEN FAR.VPRODEXENIGV_FAR = 'A' THEN 1 ELSE 0 END AfectoIGV
FROM TEDEF.Factura FAC2
INNER JOIN TEDEF.Farmacia FAR
ON FAC2.VTDOCPAGO_FAC = FAR.VTDOCPAGO_FAC
AND FAC2.VNDOCPAGO_FAC = FAR.VNDOCPAGO_FAC
WHERE G.CODIGO = FAR.VCODPROD_FAR_003
GROUP BY YEAR(FAC2.VFECEMI_FAC)
, MONTH(FAC2.VFECEMI_FAC)
, FAR.VCODPROD_FAR_003
, FAR.VPRODEXENIGV_FAR
-- ORDER BY 1 DESC --- <---- this is the bad guy, please, don't do that xD
) GG
) SELECT *
FROM x WHERE ID = 1
In that way i can sort the Count instruction and calculate the Mode for the Column FAR.VPRODEXENIGV_FAR

Replacement for UPDATE statement with ORDER BY clause

I am having a hard time trying to execute an update query that should contain ORDER BY clause, but I'm unable to find a proper solution yet.
UPDATE I
SET RefItemID = AQ.ID,
I.MagParamNum = AQ.MagParamNum
FROM SRO_VT_SHARD.._Items I
JOIN SRO_VT_SHARD.._Inventory INV ON INV.ItemID = I.ID64
JOIN SRO_VT_SHARD.._RefObjCommon ROC ON ROC.ID = I.RefItemID
JOIN _AEQItems AQ ON AQ.TypeID3 = ROC.TypeID3
AND AQ.TypeID4 = ROC.TypeID4
WHERE
INV.Slot BETWEEN 0 AND 13
AND INV.Slot != 8
AND AQ.ReqLevel1 <= #Data2
AND INV.CharID = #CharID
ORDER BY AQ.ReqLevel1 DESC
Basically my query should work this way if ORDER BY clause is usable inside an update statement, but it doesn't. Is there something I can do which should solve this?
Thanks a lot in advance.
You need to determine the exact row to update for each TypeID3 / TypeID4 combination, and you can't do that in the outer query. You may need to add additional ORDER BY clauses here to break ties. You may also want to specify only a subset of columns if you have an index that covers the columns in _AEQItems used to search and the columns you're updating.
;WITH AQ AS
(
SELECT *, rn = ROW_NUMBER() OVER
(PARTITION BY TypeID3, TypeID4 ORDER BY ReqLevel1 DESC)
FROM _AEQItems
)
UPDATE I
SET RefItemID = AQ.ID,
MagParamNum = AQ.MagParamNum
FROM SRO_VT_SHARD.._Items I
JOIN SRO_VT_SHARD.._Inventory INV ON INV.ItemID = I.ID64
JOIN SRO_VT_SHARD.._RefObjCommon ROC ON ROC.ID = I.RefItemID
JOIN AQ ON AQ.TypeID3 = ROC.TypeID3 AND AQ.TypeID4 = ROC.TypeID4
WHERE AQ.rn = 1
AND INV.Slot BETWEEN 0 AND 13
AND INV.Slot!=8
AND AQ.ReqLevel1 <= #Data2
AND INV.CharID = #CharID;
Use a subquery:
UPDATE I
SET RefItemID=AQ.ID,I.MagParamNum=AQ.MagParamNum
FROM SRO_VT_SHARD.._Items I
JOIN SRO_VT_SHARD.._Inventory INV ON INV.ItemID=I.ID64
JOIN SRO_VT_SHARD.._RefObjCommon ROC ON ROC.ID=I.RefItemID
JOIN (SELECT TypeID3, TypeID4, MAX(ReqLevel1) AS ReqLevel1 FROM _AEQItems GROUP BY TypeID3, TypeID4) AQ
ON AQ.TypeID3=ROC.TypeID3
AND AQ.TypeID4=ROC.TypeID4
WHERE INV.Slot BETWEEN 0 AND 13 AND INV.Slot!=8 AND AQ.ReqLevel1<=#Data2 AND INV.CharID=#CharID

SQL combine two queries result into one dataset

I am trying to combine two SQL queries the first is
SELECT
EAC.Person.FirstName,
EAC.Person.Id,
EAC.Person.LastName,
EAC.Person.EmployeeId,
EAC.Person.IsDeleted,
Controller.Cards.SiteCode,
Controller.Cards.CardCode,
Controller.Cards.ActivationDate,
Controller.Cards.ExpirationDate,
Controller.Cards.Status,
EAC.[Group].Name
FROM
EAC.Person
INNER JOIN
Controller.Cards ON EAC.Person.Id = Controller.Cards.PersonId
INNER JOIN
EAC.GroupPersonMap ON EAC.Person.Id = EAC.GroupPersonMap.PersonId
INNER JOIN
EAC.[Group] ON EAC.GroupPersonMap.GroupId = EAC.[Group].Id
And the second one is
SELECT
IsActive, ActivationDateUTC, ExpirationDateUTC,
Sitecode + '-' + Cardcode AS Credential, 'Badge' AS Type,
CASE
WHEN isActive = 0
THEN 'InActive'
WHEN ActivationDateUTC > GetUTCDate()
THEN 'Pending'
WHEN ExpirationDAteUTC < GetUTCDate()
THEN 'Expired'
ELSE 'Active'
END AS Status
FROM
EAC.Credential
JOIN
EAC.WiegandCredential ON Credential.ID = WiegandCredential.CredentialId
WHERE
PersonID = '32'
Where I would like to run the second query for each user of the first query using EAC.Person.Id instead of the '32'.
I would like all the data to be returned in one Dataset so I can use it in Report Builder.
I have been fighting with this all day and am hoping one of you smart guys can give me a hand. Thanks in advance.
Based on your description in the comments, I understand that the connection between the two datasets is actually the PersonID field, which exists in both EAC.Credential and EAC.Person; however, in EAC.Credential, duplicate values exist for PersonID, and you want only the most recent one for each PersonID.
There are a few ways to do this, and it will depend on the number of rows returned, the indexes, etc., but I think maybe you're looking for something like this...?
SELECT
EAC.Person.FirstName
,EAC.Person.Id
,EAC.Person.LastName
,EAC.Person.EmployeeId
,EAC.Person.IsDeleted
,Controller.Cards.SiteCode
,Controller.Cards.CardCode
,Controller.Cards.ActivationDate
,Controller.Cards.ExpirationDate
,Controller.Cards.Status
,EAC.[Group].Name
,X.IsActive
,X.ActivationDateUTC
,X.ExpirationDateUTC
,X.Credential
,X.Type
,X.Status
FROM EAC.Person
INNER JOIN Controller.Cards
ON EAC.Person.Id = Controller.Cards.PersonId
INNER JOIN EAC.GroupPersonMap
ON EAC.Person.Id = EAC.GroupPersonMap.PersonId
INNER JOIN EAC.[Group]
ON EAC.GroupPersonMap.GroupId = EAC.[Group].Id
CROSS APPLY
(
SELECT TOP 1
IsActive
,ActivationDateUTC
,ExpirationDateUTC
,Sitecode + '-' + Cardcode AS Credential
,'Badge' AS Type
,'Status' =
CASE
WHEN isActive = 0
THEN 'InActive'
WHEN ActivationDateUTC > GETUTCDATE()
THEN 'Pending'
WHEN ExpirationDateUTC < GETUTCDATE()
THEN 'Expired'
ELSE 'Active'
END
FROM EAC.Credential
INNER JOIN EAC.WiegandCredential
ON EAC.Credential.ID = EAC.WiegandCredential.CredentialId
WHERE EAC.Credential.PersonID = EAC.Person.PersonID
ORDER BY EAC.Credential.ID DESC
) AS X
-- Optionally, you can also add conditions to return specific rows, i.e.:
-- WHERE EAC.Person.PersonID = 32
This option uses a CROSS APPLY, which means that every row of the first dataset will return additional values from the second dataset, based on the criteria that you described. In this CROSS APPLY, I'm joining the two datasets based on the fact that PersonID exists in both EAC.Person (in your first dataset) as well as in EAC.Credential. I then specify that I want only the TOP 1 row for each PersonID, with an ORDER BY specifying that we want the most recent (highest) value of ID for each PersonID.
The CROSS APPLY is aliased as "X", so in your original SELECT you now have several values prefixed with the X. alias, which just means that you're taking these fields from the second query and attaching them to your original results.
CROSS APPLY requires that a matching entry exists in both subsets of data, much like an INNER JOIN, so you'll want to check and make sure that the relevant values exist and are returned correctly.
I think this is pretty close to the direction you're trying to go. If not, let me know and I'll update the answer. Good luck!
Try like this;
select Query1.*, Query2.* from (
SELECT
EAC.Person.FirstName,
EAC.Person.Id as PersonId,
EAC.Person.LastName,
EAC.Person.EmployeeId,
EAC.Person.IsDeleted,
Controller.Cards.SiteCode,
Controller.Cards.CardCode,
Controller.Cards.ActivationDate,
Controller.Cards.ExpirationDate,
Controller.Cards.Status,
EAC.[Group].Name
FROM
EAC.Person
INNER JOIN
Controller.Cards ON EAC.Person.Id = Controller.Cards.PersonId
INNER JOIN
EAC.GroupPersonMap ON EAC.Person.Id = EAC.GroupPersonMap.PersonId
INNER JOIN
EAC.[Group] ON EAC.GroupPersonMap.GroupId = EAC.[Group].Id)
Query1 inner join (SELECT top 100
IsActive, ActivationDateUTC, ExpirationDateUTC,
Sitecode + '-' + Cardcode AS Credential, 'Badge' AS Type,
CASE
WHEN isActive = 0
THEN 'InActive'
WHEN ActivationDateUTC > GetUTCDate()
THEN 'Pending'
WHEN ExpirationDAteUTC < GetUTCDate()
THEN 'Expired'
ELSE 'Active'
END AS Status
FROM
EAC.Credential
JOIN
EAC.WiegandCredential ON Credential.ID = WiegandCredential.CredentialId
ORDER BY EAC.Credential.ID DESC) Query2 ON Query1.PersonId = Query2.PersonID
Just select two queries to join them like Query1 and Query2 by equaling PersonId data.

SQLITE Complex select statement. Help me speed it up

Some may or may not consider this a complex statement, but for me (since I've only been doing statement for about a month) this one is. The below statement returns me the exact results I am looking for, but my problem is that it take over 95 seconds to run on a iMac. I need this statement to run on an iPhone. Can anyone thing of a better (quicker) way to do this?
select categories.category
from categories join categories_listings
where categories_listings.category_id = categories.id
and categories.association_id = 1
and (select count(*)
from (select (
select categories.category
from categories left join categories_listings
where categories_listings.category_id = categories.id
and categories.association_id = 1
and listings.id = categories_listings.listing_id) as region
from listings left join chamber_specifics
on chamber_specifics.listing_id=listings.id
where region = categories.category
and listings.association_id=1
and listings.status = 1
and downtown='Y')) >0
group by categories.category;
Let me know if more info is needed.
Thanks!
I came up with a statement that gets me the same results as in my question, but much fast at about 0.062 seconds to complete. Below is my Statement: (Thanks Rudu for the direction!)
select (
select categories.category
from categories left join categories_listings
where categories_listings.category_id = categories.id
and categories.association_id = 1
and listings.id = categories_listings.listing_id) as region
from listings left join chamber_specifics
on chamber_specifics.listing_id=listings.id
where listings.association_id=1
and listings.status = 1
and downtown='Y'
group by region

Optimising this query. Relevant for DBA's working on a social network/community type website

I suppose this is quite a common SP present in socialnetworks and community type websites.
I have this SP that returns all of a user's friends on their 'friends' page order by those currently online, then alphabetically. It's taking quite a while to load and I am looking to speed it up.
I remember reading somewhere on SO that breaking up multiple joins into smaller result sets might speed it up. I haven't tried this yet but I am curious to see what other recommendations SO might have on this procedure.
DECLARE #userID INT -- This variable is parsed in
DECLARE #lastActivityMinutes INT
SET #lastActivitytMinutes = '15'
SELECT
Active = CASE WHEN DATEDIFF("n", b.LastActivityDate ,GETDATE()) < #lastActivityMinutes THEN 1 ELSE 0 END,
a.DisplayName, a.ImageFile, a.UserId, b.LastActivityDate
FROM
Profile AS a
INNER JOIN aspnet_Users as b on b.userId = a.UserId
LEFT JOIN Friend AS x ON x.UserID = a.UserID
LEFT JOIN Friend AS z ON z.FriendID = a.UserID
WHERE ((x.FriendId = #userID AND x.status = 1) -- Status = 1 means friendship accepted
OR (z.UserID = #userID AND z.Status = 1))
GROUP BY a.userID, a.DisplayName, a.ImageFile, a.UserId, b.LastActivityDate
ORDER BY Active DESC, DisplayName ASC
I am not sure how to clip in my execution plan but the main bottle neck seems to be occurring on a MERGE JOIN (Right Outer Join) that's costing me 29%. At various stages, Parallelism is also costing 9%, 6%, 5% and 9% for a total of 29% as well.
My initial thoughts are to first return the JOINED results from the Profile and aspnet tables with a CTE and then do LEFT JOINS to the Friends table.
You are joining Friend twice, using a LEFT JOIN, then you are removing the NULL's returned by the LEFT JOIN by WHERE condition, then using GROUP BY to get rid on distincts.
This is not the best query possible.
Why don't you just use this:
SELECT Active = CASE WHEN DATEDIFF("n", b.LastActivityDate ,GETDATE()) < #lastActivityMinutes THEN 1 ELSE 0 END,
a.DisplayName, a.ImageFile, a.UserId, b.LastActivityDate
FROM (
SELECT FriendID
FROM Friends
WHERE UserID = #UserId
AND status = 1
UNION
SELECT UserID
FROM Friends
WHERE FriendID = #UserId
AND status = 1
) x
INNER JOIN
Profile AS a
ON a.UserID = x.FriendID
INNER JOIN
aspnet_Users as b
ON b.userId = a.UserId
ORDER BY
Active DESC, DisplayName ASC

Resources