251 Stimmen

LINQ - Vollständiger äußerer Join

Ich habe eine Liste mit den IDs der Personen und ihren Vornamen und eine Liste mit den IDs der Personen und ihren Nachnamen. Einige Personen haben keinen Vornamen und einige haben keinen Nachnamen. Ich möchte eine vollständige äußere Verknüpfung der beiden Listen durchführen.

Also die folgenden Listen:

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

Sollte produzieren:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

Ich bin neu in LINQ (so verzeihen Sie mir, wenn ich lahm bin) und haben einige Lösungen für "LINQ Outer Joins" gefunden, die alle ziemlich ähnlich aussehen, aber wirklich scheinen, um äußere Verbindungen zu verlassen.

Meine bisherigen Versuche gehen in etwa in diese Richtung:

private void OuterJoinTest()
{
    List<FirstName> firstNames = new List<FirstName>();
    firstNames.Add(new FirstName { ID = 1, Name = "John" });
    firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

    List<LastName> lastNames = new List<LastName>();
    lastNames.Add(new LastName { ID = 1, Name = "Doe" });
    lastNames.Add(new LastName { ID = 3, Name = "Smith" });

    var outerJoin = from first in firstNames
        join last in lastNames
        on first.ID equals last.ID
        into temp
        from last in temp.DefaultIfEmpty()
        select new
        {
            id = first != null ? first.ID : last.ID,
            firstname = first != null ? first.Name : string.Empty,
            surname = last != null ? last.Name : string.Empty
        };
    }
}

public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}

Aber dies kehrt zurück:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

Was mache ich falsch?

242voto

sehe Punkte 346808

Aktualisierung 1: Bereitstellung einer wirklich verallgemeinerten Erweiterungsmethode FullOuterJoin
Update 2: optionales Akzeptieren eines benutzerdefinierten IEqualityComparer für den Schlüsseltyp
Aktualisierung 3 : Diese Implementierung hat wurde kürzlich Teil von MoreLinq - Danke, Leute!

Editar Hinzugefügt FullOuterGroupJoin ( ideone ). Ich habe die GetOuter<> Implementierung, so dass diese ein Bruchteil weniger leistungsfähig als es sein könnte, aber ich bin für "High-Level"-Code, nicht blutenden Rand optimiert, gerade jetzt zielt.

Sehen Sie es live auf http://ideone.com/O36nWc

static void Main(string[] args)
{
    var ax = new[] { 
        new { id = 1, name = "John" },
        new { id = 2, name = "Sue" } };
    var bx = new[] { 
        new { id = 1, surname = "Doe" },
        new { id = 3, surname = "Smith" } };

    ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b})
        .ToList().ForEach(Console.WriteLine);
}

Druckt die Ausgabe:

{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b =  }
{ a = , b = { id = 3, surname = Smith } }

Sie können auch Standardwerte angeben: http://ideone.com/kG4kqO

    ax.FullOuterJoin(
            bx, a => a.id, b => b.id, 
            (a, b, id) => new { a.name, b.surname },
            new { id = -1, name    = "(no firstname)" },
            new { id = -2, surname = "(no surname)" }
        )

Drucken:

{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }

Erläuterung der verwendeten Begriffe:

Joining ist ein Begriff, der aus dem relationalen Datenbankdesign stammt:

  • A beitreten wiederholt Elemente aus a so oft, wie es Elemente gibt in b mit entsprechendem Schlüssel (d.h.: nichts, wenn b leer waren). Im Datenbankjargon heißt das inner (equi)join .
  • Eine äußere Verbindung enthält Elemente aus a bei denen keine entsprechende Element besteht in b . (d.h.: gleichmäßige Ergebnisse, wenn b leer waren). Dies wird in der Regel bezeichnet als left join .
  • A vollständige äußere Verknüpfung enthält Datensätze von a wie auch b wenn kein entsprechendes Element in der anderen existiert. (d.h. gleichmäßige Ergebnisse, wenn a leer waren)

Etwas nicht in der Regel in RDBMS ist eine Gruppenverbindung [1] :

  • A Gruppenverbindung macht das Gleiche wie oben beschrieben, sondern statt der Wiederholung von Elementen aus a für mehrere entsprechende b es Gruppen die Datensätze mit den entsprechenden Schlüsseln. Dies ist oft praktischer, wenn Sie durch "verbundene" Datensätze auf der Grundlage eines gemeinsamen Schlüssels aufzählen möchten.

Siehe auch GroupJoin die auch einige allgemeine Hintergrundinformationen enthält.


[1] (Ich glaube, Oracle und MSSQL haben proprietäre Erweiterungen für diese Aufgabe)

Vollständiger Code

Eine verallgemeinerte "Drop-in"-Erweiterungsklasse für diese

internal static class MyExtensions
{
    internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection,
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   let xa = alookup[key]
                   let xb = blookup[key]
                   select projection(xa, xb, key);

        return join;
    }

    internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<TA, TB, TKey, TResult> projection,
        TA defaultA = default(TA), 
        TB defaultB = default(TB),
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   from xa in alookup[key].DefaultIfEmpty(defaultA)
                   from xb in blookup[key].DefaultIfEmpty(defaultB)
                   select projection(xa, xb, key);

        return join;
    }
}

140voto

Jeff Mercado Punkte 120818

Ich weiß nicht, ob dies alle Fälle abdeckt, aber logisch gesehen scheint es richtig zu sein. Die Idee ist, eine linke äußere Verknüpfung und rechte äußere Verknüpfung zu nehmen und dann die Vereinigung der Ergebnisse zu nehmen.

var firstNames = new[]
{
    new { ID = 1, Name = "John" },
    new { ID = 2, Name = "Sue" },
};
var lastNames = new[]
{
    new { ID = 1, Name = "Doe" },
    new { ID = 3, Name = "Smith" },
};
var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last?.Name,
    };
var rightOuterJoin =
    from last in lastNames
    join first in firstNames on last.ID equals first.ID into temp
    from first in temp.DefaultIfEmpty()
    select new
    {
        last.ID,
        FirstName = first?.Name,
        LastName = last.Name,
    };
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

Dies funktioniert wie geschrieben, da es in LINQ to Objects ist. Wenn LINQ to SQL oder ein anderes Format verwendet wird, unterstützt der Abfrageprozessor möglicherweise keine sichere Navigation oder andere Operationen. Sie müssten den bedingten Operator verwenden, um die Werte bedingt abzurufen.

d.h.,

var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last != null ? last.Name : default,
    };

50voto

NetMage Punkte 23968

Ich denke, es gibt Probleme mit den meisten von ihnen, einschließlich der akzeptierten Antwort, weil sie nicht gut mit Linq über IQueryable entweder durch zu viele Server-Roundtrips und zu viele Daten zurückgibt, oder tun zu viel Client-Ausführung arbeiten.

Für IEnumerable mag ich nicht Sehe's Antwort oder ähnlich, weil es übermäßigen Speicherverbrauch hat (eine einfache 10000000 zwei Liste Test lief Linqpad aus dem Speicher auf meinem 32GB Maschine).

Außerdem implementieren die meisten anderen keinen richtigen Full Outer Join, da sie eine Union mit einem Right Join anstelle eines Concat mit einem Right Anti Semi Join verwenden, wodurch nicht nur die doppelten inneren Join-Zeilen aus dem Ergebnis entfernt werden, sondern auch alle echten Duplikate, die ursprünglich in den linken oder rechten Daten vorhanden waren.

Hier sind also meine Erweiterungen, die alle diese Probleme behandeln, SQL generieren sowie die Verknüpfung in LINQ to SQL direkt implementieren, auf dem Server ausgeführt werden und schneller und mit weniger Speicher als andere auf Enumerables sind:

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLgR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

Der Unterschied zwischen einem Right Anti-Semi-Join ist bei Linq to Objects oder in der Quelle meist irrelevant, macht aber auf der Server- (SQL-) Seite in der endgültigen Antwort einen Unterschied, indem eine unnötige JOIN .

Die manuelle Kodierung von Expression um das Zusammenführen einer Expression<Func<>> in ein Lambda zu verwandeln, könnte mit LinqKit verbessert werden, aber es wäre schön, wenn die Sprache/der Compiler dafür eine Hilfe eingebaut hätte. Die FullOuterJoinDistinct y RightOuterJoin Funktionen sind der Vollständigkeit halber enthalten, aber ich habe sie nicht neu implementiert FullOuterGroupJoin noch nicht.

Ich schrieb andere Version einer vollständigen äußeren Verknüpfung für IEnumerable für Fälle, in denen der Schlüssel geordnet werden kann, was zumindest bei kleinen Sammlungen etwa 50 % schneller ist als die Kombination der linken äußeren Verknüpfung mit der rechten Anti-Semi-Join. Dabei wird jede Sammlung nach der Sortierung nur einmal durchlaufen.

Ich habe auch hinzugefügt eine weitere Antwort für eine Version, die mit EF funktioniert, durch Ersetzen der Invoke mit einer benutzerdefinierten Erweiterung.

10voto

pwilcox Punkte 5224

Ich vermute, dass der Ansatz von @sehe stärker ist, aber bis ich ihn besser verstehe, überspringe ich die Erweiterung von @MichaelSander. Ich habe sie so geändert, dass sie mit der Syntax und dem Rückgabetyp der eingebauten Methode Enumerable.Join() übereinstimmt. aquí . Ich habe das Suffix "distinct" in Bezug auf den Kommentar von @cadrell0 unter der Lösung von @JeffMercado angefügt.

public static class MyExtensions {

    public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> (
        this IEnumerable<TLeft> leftItems, 
        IEnumerable<TRight> rightItems, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector
    ) {

        var leftJoin = 
            from left in leftItems
            join right in rightItems 
              on leftKeySelector(left) equals rightKeySelector(right) into temp
            from right in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        var rightJoin = 
            from right in rightItems
            join left in leftItems 
              on rightKeySelector(right) equals leftKeySelector(left) into temp
            from left in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        return leftJoin.Union(rightJoin);
    }

}

In diesem Beispiel würden Sie es so verwenden:

var test = 
    firstNames
    .FullJoinDistinct(
        lastNames,
        f=> f.ID,
        j=> j.ID,
        (f,j)=> new {
            ID = f == null ? j.ID : f.ID, 
            leftName = f == null ? null : f.Name,
            rightName = j == null ? null : j.Name
        }
    );

In Zukunft, wenn ich mehr lerne, werde ich wohl zu @sehe's Logik übergehen, da sie so beliebt ist. Aber selbst dann werde ich vorsichtig sein müssen, denn ich halte es für wichtig, zumindest eine Überladung zu haben, die der Syntax der bestehenden ".Join()"-Methode entspricht, wenn dies möglich ist, und zwar aus zwei Gründen:

  1. Einheitliche Methoden helfen, Zeit zu sparen, Fehler zu vermeiden und unbeabsichtigtes Verhalten zu verhindern.
  2. Wenn es in Zukunft eine sofort einsatzfähige ".FullJoin()"-Methode geben sollte, könnte ich mir vorstellen, dass sie versuchen wird, die Syntax der gegenwärtig existierenden ".Join()"-Methode beizubehalten, wenn sie es kann. Wenn dies der Fall ist, können Sie Ihre Funktionen einfach umbenennen, ohne die Parameter zu ändern oder sich Sorgen zu machen, dass unterschiedliche Rückgabetypen Ihren Code zerstören.

Ich bin immer noch neu mit Generika, Erweiterungen, Func-Anweisungen und anderen Funktionen, so dass Feedback ist sicherlich willkommen.

EDIT: Ich brauchte nicht lange, um zu erkennen, dass es ein Problem mit meinem Code gab. Ich führte ein .Dump() in LINQPad aus und sah mir den Rückgabetyp an. Es war einfach IEnumerable, so dass ich versuchte, es zu entsprechen. Aber als ich tatsächlich eine .Where() oder .Select() in meiner Erweiterung durchführte, erhielt ich einen Fehler: "'System Collections.IEnumerable' enthält keine Definition für 'Select' und ...". Am Ende konnte ich also die Eingabesyntax von .Join() anpassen, aber nicht das Rückgabeverhalten.

EDIT: Hinzufügen von "TResult" zum Rückgabetyp für die Funktion. Das habe ich beim Lesen des Microsoft-Artikels übersehen, und es macht natürlich Sinn. Mit dieser Korrektur scheint das Rückgabeverhalten nun doch mit meinen Zielen übereinzustimmen.

9voto

Michael Sander Punkte 2637

Hier ist eine Erweiterungsmethode, die das tut:

public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector)
{
    var leftOuterJoin = from left in leftItems
        join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp
        from right in temp.DefaultIfEmpty()
        select new { left, right };

    var rightOuterJoin = from right in rightItems
        join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp
        from left in temp.DefaultIfEmpty()
        select new { left, right };

    var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

    return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right));
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X