LINQ: unión externa completa
Tengo una lista de identificación de personas y su nombre, y una lista de identificación de personas y su apellido. Algunas personas no tienen nombre y otras no tienen apellido; Me gustaría hacer una unión externa completa en las dos listas.
Entonces las siguientes listas:
ID FirstName
-- ---------
1 John
2 Sue
ID LastName
-- --------
1 Doe
3 Smith
Debe producir:
ID FirstName LastName
-- --------- --------
1 John Doe
2 Sue
3 Smith
He encontrado bastantes soluciones para 'Uniones externas de LINQ' que parecen bastante similares, pero en realidad parecen ser uniones externas izquierdas.
Mis intentos hasta ahora son algo como esto:
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;
}
Pero esto regresa:
ID FirstName LastName
-- --------- --------
1 John Doe
2 Sue
¿Qué estoy haciendo mal?
Actualización 1: proporcionar un método de extensión verdaderamente generalizado FullOuterJoin
Actualización 2: aceptar opcionalmente una personalización IEqualityComparer
para el tipo de clave
Actualización 3 : esta implementación se ha convertido recientemente en parte deMoreLinq
- ¡Gracias chicos!
Editar agregado FullOuterGroupJoin
( ideone ). Reutilicé la GetOuter<>
implementación, lo que hizo que esto tuviera un rendimiento una fracción menor de lo que podría ser, pero ahora mi objetivo es un código de 'alto nivel', no optimizado de última generación.
Véalo en vivo en 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);
}
Imprime la salida:
{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b = }
{ a = , b = { id = 3, surname = Smith } }
También puede proporcionar los valores predeterminados: 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)" }
)
Impresión:
{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }
Explicación de términos utilizados:
Unirse es un término tomado del diseño de bases de datos relacionales:
- Una combinación repetirá elementos
a
tantas veces como elementos tengab
la clave correspondiente (es decir, nada sib
estuviera vacío). La jerga de las bases de datos lo llamainner (equi)join
. - Una unión externa incluye elementos
a
para los cuales no existe ningún elemento correspondiente enb
. (es decir: resultados pares sib
estuvieran vacíos). A esto se le suele denominarleft join
. - Una combinación externa completa incluye registros de
a
yb
si no existe ningún elemento correspondiente en el otro. (es decir, incluso resultados sia
estuvieran vacíos)
Algo que no suele verse en RDBMS es unirse a un grupo [1] :
- Una unión de grupo hace lo mismo que se describe anteriormente, pero en lugar de repetir elementos
a
para múltiples correspondientesb
, agrupa los registros con las claves correspondientes. Esto suele ser más conveniente cuando desea enumerar registros 'unidos', basándose en una clave común.
Consulte también GroupJoin , que también contiene algunas explicaciones generales.
[1] (Creo que Oracle y MSSQL tienen extensiones propietarias para esto)
código completo
Una clase de extensión generalizada para esto
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;
}
}
No sé si esto abarca todos los casos, lógicamente parece correcto. La idea es tomar una combinación externa izquierda y una combinación externa derecha y luego tomar la unión de los resultados.
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);
Esto funciona tal como está escrito ya que está en LINQ to Objects. Si es LINQ to SQL u otro, es posible que el procesador de consultas no admita una navegación segura u otras operaciones. Tendrías que usar el operador condicional para obtener los valores de forma condicional.
es decir,
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,
};
Creo que hay problemas con la mayoría de estos, incluida la respuesta aceptada, porque no funcionan bien con Linq sobre IQueryable, ya sea porque realizan demasiados viajes de ida y vuelta al servidor y demasiadas devoluciones de datos, o porque realizan demasiadas ejecuciones del cliente.
Para IEnumerable no me gusta la respuesta de Sehe o similar porque tiene un uso excesivo de memoria (una simple prueba de dos listas de 10000000 hizo que Linqpad se quedara sin memoria en mi máquina de 32 GB).
Además, la mayoría de los demás en realidad no implementan una unión externa completa adecuada porque están usando una unión con una unión derecha en lugar de Concat con una semiunión derecha, lo que no solo elimina las filas de unión interna duplicadas del resultado, sino que cualquier duplicado adecuado que existiera originalmente en los datos izquierdo o derecho.
Entonces, aquí están mis extensiones que manejan todos estos problemas, generan SQL e implementan la unión en LINQ to SQL directamente, se ejecutan en el servidor y son más rápidas y con menos memoria que otras en Enumerables:
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));
}
}
La diferencia entre un Right Anti-Semi-Join es mayormente discutible con Linq to Objects o en el código fuente, pero hace una diferencia en el lado del servidor (SQL) en la respuesta final, eliminando un archivo JOIN
.
La codificación manual Expression
para manejar la fusión de un Expression<Func<>>
en una lambda podría mejorarse con LinqKit, pero sería bueno si el lenguaje/compilador hubiera agregado algo de ayuda para eso. Las funciones FullOuterJoinDistinct
y RightOuterJoin
se incluyen para que estén completas, pero aún no las he vuelto a implementar FullOuterGroupJoin
.
Escribí otra versión de una unión externa completa para IEnumerable
los casos en los que la clave se puede ordenar, lo cual es aproximadamente un 50 % más rápido que combinar la unión externa izquierda con la anti semi unión derecha, al menos en colecciones pequeñas. Pasa por cada colección después de clasificarla solo una vez.
También agregué otra respuesta para una versión que funciona con EF reemplazando Invoke
con una expansión personalizada.
Supongo que el enfoque de @sehe es más fuerte, pero hasta que lo entienda mejor, me encuentro saltando de la extensión de @MichaelSander. Lo modifiqué para que coincida con la sintaxis y el tipo de retorno del método incorporado Enumerable.Join() que se describe aquí . Agregué el sufijo "distinto" con respecto al comentario de @cadrell0 en la solución de @JeffMercado.
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);
}
}
En el ejemplo, lo usarías así:
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
}
);
En el futuro, a medida que aprenda más, tengo la sensación de que migraré a la lógica de @sehe dada su popularidad. Pero incluso entonces tendré que tener cuidado, porque creo que es importante tener al menos una sobrecarga que coincida con la sintaxis del método ".Join()" existente, si es posible, por dos razones:
- La coherencia en los métodos ayuda a ahorrar tiempo, evitar errores y evitar comportamientos no deseados.
- Si alguna vez hay un método ".FullJoin()" listo para usar en el futuro, me imagino que, si puede, intentará mantener la sintaxis del método ".Join()" actualmente existente. Si es así, si desea migrar a él, simplemente puede cambiar el nombre de sus funciones sin cambiar los parámetros ni preocuparse de que los diferentes tipos de retorno rompan su código.
Todavía soy nuevo en el mundo de los genéricos, las extensiones, las declaraciones Func y otras características, por lo que los comentarios son bienvenidos.
EDITAR: No me tomó mucho tiempo darme cuenta de que había un problema con mi código. Estaba haciendo un .Dump() en LINQPad y mirando el tipo de retorno. Era simplemente IEnumerable, así que intenté hacer coincidirlo. Pero cuando hice .Where() o .Select() en mi extensión, recibí un error: "'System Collections.IEnumerable' no contiene una definición para 'Select' y ...". Entonces, al final pude hacer coincidir la sintaxis de entrada de .Join(), pero no el comportamiento de devolución.
EDITAR: Se agregó "TResult" al tipo de retorno de la función. Se me pasó por alto eso al leer el artículo de Microsoft y, por supuesto, tiene sentido. Con esta solución, ahora parece que, después de todo, el comportamiento de devolución está en consonancia con mis objetivos.
Aquí hay un método de extensión que hace eso:
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));
}