views:

3567

answers:

3

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();
+10  A: 

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">
David B
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
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.
David B
cool any chance you can post the SQL, I am curious to see it.
Nick Berardi
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
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
@David B - thanks, it gives me an error in this line db.photos.Where(i.id == p.item_id)the error is "the type arguments for method ‘where<TSource> (Collections.Generic,IEnumerable<TSource>,Func<TSource,int,bool>)’ cannot be inferred from the isage. Try specifying the type arguments explicitly"?
ahmed
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.
David B
@David B - Thank you very very much, I'm so happy! It finally works!, I wish I can learn more about LINQ, Thanks
ahmed
@David B - I like yours better the top part of mine is actually alsmost an exact replica of yours, except for all the NULL checking, not really sure what the point of the CROSS APPLY is, but I can safely say you probably found the better algorithm
Nick Berardi
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
A: 

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 David 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.

Nick Berardi
A: 

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