44

I have a left outer join (below) returning results as expected. I need to limit the results from the 'right' table to the 'first' hit. Can I do that somehow? Currently, I get a result for every record in both tables, I only want to see one result from the table on the left (items) no matter how many results I have in the right table (photos).

        var query = from i in db.items
                join p in db.photos
                on i.id equals p.item_id into tempPhoto
                from tp in tempPhoto.DefaultIfEmpty()
                orderby i.date descending 
                select new
                {
                    itemName = i.name,
                    itemID = i.id,
                    id = i.id,
                    photoID = tp.PhotoID.ToString()
                };


    GridView1.DataSource = query;
    GridView1.DataBind();
ahmed
  • 14,316
  • 30
  • 94
  • 127

4 Answers4

80

This will do the job for you.

from i in db.items
let p = db.photos.Where(p2 => i.id == p2.item_id).FirstOrDefault()
orderby i.date descending
select new
{
  itemName = i.name,
  itemID = i.id,
  id = i.id,
  photoID = p == null ? null : p.PhotoID.ToString();
}

I got this sql when I generated it against my own model (and without the name and second id columns in the projection).

SELECT [t0].[Id] AS [Id], CONVERT(NVarChar,(
    SELECT [t2].[PhotoId]
    FROM (
        SELECT TOP (1) [t1].[PhotoId]
        FROM [dbo].[Photos] AS [t1]
        WHERE [t1].[Item_Id] = ([t0].[Id])
        ) AS [t2]
    )) AS [PhotoId]
FROM [dbo].[Items] AS [t0]
ORDER BY [t0].[Id] DESC

When I asked for the plan, it showed that the subquery is implemented by this join:

<RelOp LogicalOp="Left Outer Join" PhysicalOp="Nested Loops">
Amy B
  • 108,202
  • 21
  • 135
  • 185
  • I like the elegance of this solution, however I think this may create a query that is harder for SQL to optimize because of the sub select – Nick Berardi Jan 29 '09 at 18:04
  • I checked and was happy both with the generated SQL and the estimated execution plan. The subselect was planned to be a left outer join. – Amy B Jan 29 '09 at 18:15
  • 1
    cool any chance you can post the SQL, I am curious to see it. – Nick Berardi Jan 29 '09 at 18:17
  • because I have been doing things with sub table groups. and if you way works I will be much happier with the slim code. but like anything I want the best performing solution, not the prettiest. – Nick Berardi Jan 29 '09 at 18:18
  • not bad I have to give you props. would you mind running mine through, I don't have a model setup. You should have enough rep to edit my comment, to add the execution plan. – Nick Berardi Jan 29 '09 at 18:28
  • Yes, there was a syntax error that I corrected in a later edit. I forgot to specify a name for the parameter for that lambda. – Amy B Jan 29 '09 at 18:51
  • This is exactly what I was looking for! I needed to get the latest (by date) of something from a child row that may not exist but I still need to return the parent row. Since you can't have compound join clauses in LINQ and you can't test for null values on non-nullable columns, it was not working out well. let ast = Context.AccountStatementTotal .Where(st => st.AccountNoId == a.AccountNoId) .OrderByDescending(st => st.Statementdt) .FirstOrDefault() – JohnOpincar May 27 '09 at 23:52
  • unfortunately it generates subquery, not LEFT JOIN... it's bad for performance – vk_muse Apr 19 '13 at 13:49
  • @vk_muse "When I asked for the plan, it showed that the subquery is implemented by this join" – Amy B Apr 19 '13 at 13:56
  • This is what I was looking for, but I still need to get the subquery (in this case photos) order by one of the fields with a ordery by case statement. Could you help me with that? because when I do .Where(p2 => i.id == p2.item_id).OrderBy(p2 => p2.FILELD == VALUE ? 1 : 0).FirstOrDefault() I get an error... – John Mathison Jan 21 '16 at 15:40
6

What you want to do is group the table. The best way to do this is:

    var query = from i in db.items
                join p in (from p in db.photos
                           group p by p.item_id into gp
                           where gp.Count() > 0
                           select new { item_id = g.Key, Photo = g.First() })
            on i.id equals p.item_id into tempPhoto
            from tp in tempPhoto.DefaultIfEmpty()
            orderby i.date descending 
            select new
            {
                itemName = i.name,
                itemID = i.id,
                id = i.id,
                photoID = tp.Photo.PhotoID.ToString()
            };

Edit: This is Amy B speaking. I'm only doing this because Nick asked me to. Nick, please modify or remove this section as you feel is appropriate.

The SQL generated is quite large. The int 0 (to be compared with the count) is passed in via parameter.

SELECT [t0].X AS [id], CONVERT(NVarChar(MAX),(
    SELECT [t6].Y
    FROM (
        SELECT TOP (1) [t5].Y
        FROM [dbo].[Photos] AS [t5]
        WHERE (([t4].Y IS NULL) AND ([t5].Y IS NULL)) OR (([t4].Y IS NOT NULL) AND ([t5].Y IS NOT NULL) AND ([t4].Y = [t5].Y))
        ) AS [t6]
    )) AS [PhotoId]
FROM [dbo].[Items] AS [t0]
CROSS APPLY ((
        SELECT NULL AS [EMPTY]
        ) AS [t1]
    OUTER APPLY (
        SELECT [t3].Y
        FROM (
            SELECT COUNT(*) AS [value], [t2].Y
            FROM [dbo].[Photos] AS [t2]
            GROUP BY [t2].Y
            ) AS [t3]
        WHERE (([t0].X) = [t3].Y) AND ([t3].[value] > @p0)
        ) AS [t4])
ORDER BY [t0].Z DESC

The execution plan reveals three left joins. At least one is trivial and should not be counted (it brings in the zero). There is enough complexity here that I cannot clearly point to any problem for efficiency. It might run great.

Amy B
  • 108,202
  • 21
  • 135
  • 185
Nick Berardi
  • 54,393
  • 15
  • 113
  • 135
  • This may not of been the answer for Linq to SQL, but it did solve the problem for Entity Framework, with similar resultant sql. Looking for a better solution now. – Daniel Harvey Sep 28 '12 at 15:52
4

You could do something like:

var q = from c in
          (from s in args
           select s).First()
        select c;

Around the last part of the query. Not sure if it will work or what kind of wack SQL it will produce :)

leppie
  • 115,091
  • 17
  • 196
  • 297
0

Use an inner query. Include DefaultIfEmpty for the case of no photo and orderby for the case of more than one. The following example takes the photo with the greatest id.

var query = 
    from i in db.items
    let p = from p in db.photos where i.id == p.item_id orderby p.id select p).DefaultIfEmpty().Last()
    orderby i.date descending
    select new {
      itemName = i.name,
      itemID = i.id,
      id = i.id,
      photoID = p.PhotoID
    };

If you need to handle the case of no photo specially, you can omit DefaultIfEmpty and use FirstOrDefault/LastOrDefault instead.

Edward Brey
  • 40,302
  • 20
  • 199
  • 253