다음을 통해 공유


Join LINQ의 작업

한 데이터 소스의 개체를 공통된 특성을 공유하고 있는 다른 데이터 소스의 개체와 연결하는 것을 두 데이터 소스의 join(이)라고 합니다.

Important

이 샘플은 System.Collections.Generic.IEnumerable<T> 데이터 원본을 사용합니다. System.Linq.IQueryProvider 기반 데이터 원본은 System.Linq.IQueryable<T> 데이터 원본과 식 트리를 사용합니다. 식 트리에는 허용되는 C# 구문에 대한 제한 사항이 있습니다. 또한 EF Core와 같은 각 IQueryProvider 데이터 원본에는 더 많은 제한이 적용될 수 있습니다. 데이터 원본에 대한 설명서를 확인합니다.

조인은 서로 간의 관계를 직접 적용할 수 없는 데이터 소스를 대상으로 한 쿼리에 중요한 작업입니다. 개체 지향 프로그래밍에서 조인한다는 것은 모델링되지 않은 개체 간에 상관 관계가 있음을 의미할 수 있습니다(예: 단방향 관계에서 반대 방향을 사용). 단방향 관계의 예로는 Student 클래스가 주를 나타내는 Department 형식 속성을 포함하는데 Department 클래스는 Student 개체의 컬렉션인 속성을 포함하지 않는 경우를 들 수 있습니다. Department 개체 목록이 있는 경우에는 join 작업을 사용하여 각 학과의 모든 학생을 찾을 수 있습니다.

JoinGroupJoin은(는) LINQ 프레임워크에 제공되는 join 메서드입니다. 이러한 메서드는 키가 같은지 여부에 따라 두 데이터 소스의 일치 여부를 확인하는 조인인 동등 조인을 수행합니다. (비교해 보자면, Transact-SQL은 equals 연산자 이외의 join 연산자를 지원하며, 예를 들어 less than 연산자가 있습니다.) 관계형 데이터베이스 용어로, Join은(는) 다른 데이터 세트에서 일치하는 객체만 반환되는 join 유형인 내부 join을(를) 구현합니다. GroupJoin 메서드에는 관계형 데이터베이스 측면에 직접 상응하는 기능이 없지만 내부 조인 및 왼쪽 우선 외부 조인의 상위 집합을 구현합니다. 왼쪽 우선 외부 join은(는) 다른 데이터 소스에 서로 관련된 요소가 없어도 첫 번째(왼쪽) 데이터 소스의 각 요소를 반환하는 join입니다.

내부 join 또는 왼쪽 우선 외부 join에 포함된 두 집합 및 해당 집합에 포함되어 있는 요소를 다음과 같은 그림을 통해 개념적으로 보여 줍니다.

내부/외부를 보여주는 두 개의 겹치는 원

메서드

메서드 이름 설명 C# 쿼리 식 구문 추가 정보
Join 키 선택기 함수를 기준으로 두 시퀀스를 조인한 다음 값 쌍을 추출합니다. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin 키 선택기 함수를 기준으로 두 시퀀스를 조인한 다음 결과로 생성된 일치 항목을 요소마다 그룹화합니다. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

참고 항목

이 문서의 다음 예제에서는 이 영역에 대한 공통 데이터 원본을 사용합니다.
Student에는 학년 수준, 기본 부서 및 일련의 점수가 있습니다. Teacher에는 교사가 수업을 진행하는 캠퍼스를 식별하는 City 속성도 있습니다. Department에는 이름이 있고 부서장 역할을 하는 Teacher에 대한 참조가 있습니다.
원본 리포지토리에서 예제 데이터 집합을 찾을 수 있습니다.

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}

public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

다음의 예시는 특정 값을 기준으로 join … in … on … equals … 절을 사용하여 두 시퀀스를 join합니다.

var query = from student in students
            join department in departments on student.DepartmentID equals department.ID
            select new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name };

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

위의 쿼리는 다음 코드와 같이 메서드 구문을 사용하여 표현할 수 있습니다.

var query = students.Join(departments,
    student => student.DepartmentID, department => department.ID,
    (student, department) => new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name });

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

다음의 예시는 특정 값을 기준으로 join … in … on … equals … into … 절을 사용하여 두 시퀀스를 join하고, 각 요소에 관해 결과 일치 항목을 그룹화합니다.

IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
                    join student in students on department.ID equals student.DepartmentID into studentGroup
                    select studentGroup;

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

위의 쿼리는 다음 예제와 같이 메서드 구문을 사용하여 표현할 수 있습니다.

// Join department and student based on DepartmentId and grouping result
IEnumerable<IEnumerable<Student>> studentGroups = departments.GroupJoin(students,
    department => department.ID, student => student.DepartmentID,
    (department, studentGroup) => studentGroup);

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

내부 조인 수행

관계형 데이터베이스 용어에서 내부 join은(는) 첫 번째 컬렉션의 각 요소가 두 번째 컬렉션에서 일치하는 모든 요소에 관해 한 번만 표시되는 결과 집합을 생성합니다. 첫 번째 컬렉션의 요소에 일치하는 요소가 없는 경우에는 결과 집합에 표시되지 않습니다. C#의 join 절을 통해 호출하는 Join 메서드가 내부 join을(를) 구현합니다. 내부 join의 네 가지 변형에 관한 수행 방법을 다음의 예시를 통해 보여 줍니다.

  • 두 데이터 소스의 요소를 단순 키에 따라 상호 연결하는 간단한 내부 join입니다.
  • 두 데이터 소스의 요소를 복합 키에 따라 상호 연결하는 내부 join입니다. 둘 이상의 값으로 구성된 키인 복합 키를 사용하면 둘 이상의 속성에 따라 요소를 상호 연결할 수 있습니다.
  • 연속 join 작업이 서로 추가되는 여러 join입니다.
  • 그룹 join을(를) 통해 구현하는 내부 join입니다.

단일 키 join

다음 예제에서는 해당 TeacherIdTeacher가 일치하는 DepartmentTeacher와 일치시킵니다. C#의 select 절은 결과 개체의 모양을 정의합니다. 다음 예제에서 결과 개체는 부서 이름과 부서를 이끄는 교사의 이름으로 구성된 익명 형식입니다.

var query = from department in departments
            join teacher in teachers on department.TeacherID equals teacher.ID
            select new
            {
                DepartmentName = department.Name,
                TeacherName = $"{teacher.First} {teacher.Last}"
            };

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

Join 메서드 구문을 사용하여 동일한 결과를 얻을 수 있습니다.

var query = teachers
    .Join(departments, teacher => teacher.ID, department => department.TeacherID,
        (teacher, department) =>
        new { DepartmentName = department.Name, TeacherName = $"{teacher.First} {teacher.Last}" });

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

부서장이 아닌 교사는 최종 결과에 나타나지 않습니다.

복합 키 join

속성 하나만 기준으로 요소를 상호 연결하는 대신 복합 키를 사용하여 여러 속성을 기준으로 요소를 비교할 수 있습니다. 비교하려는 속성으로 구성된 무명 형식을 반환할 각 컬렉션에 대한 키 선택기 함수를 지정합니다. 속성에 레이블을 지정하는 경우 각 키의 무명 형식에 동일한 레이블이 있어야 합니다. 또한 속성은 동일한 순서로 나타나야 합니다.

다음 예제에서는 Teacher 개체 목록과 Student 개체 목록을 사용하여 학생이기도 한 교사를 확인합니다. 이러한 두 형식에는 각 사람의 이름과 성을 나타내는 속성이 있습니다. 속성으로 구성된 무명 형식을 각 목록의 요소에서 join 키를 만드는 함수가 반환합니다. join 작업은 각 목록에서 이러한 복합 키가 같은지를 비교하며 이름과 성이 모두 일치하는 개체 쌍을 반환합니다.

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query =
    from teacher in teachers
    join student in students on new
    {
        FirstName = teacher.First,
        LastName = teacher.Last
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select teacher.First + " " + teacher.Last;

string result = "The following people are both teachers and students:\r\n";
foreach (string name in query)
{
    result += $"{name}\r\n";
}
Console.Write(result);

다음 예제와 같이 Join 메서드를 사용할 수 있습니다.

IEnumerable<string> query = teachers
    .Join(students,
        teacher => new { FirstName = teacher.First, LastName = teacher.Last },
        student => new { student.FirstName, student.LastName },
        (teacher, student) => $"{teacher.First} {teacher.Last}"
 );

Console.WriteLine("The following people are both teachers and students:");
foreach (string name in query)
{
    Console.WriteLine(name);
}

여러 join

여러 join을(를) 수행하기 위해 개수에 제한없이 join 작업을 서로 추가할 수 있습니다. C#의 각 join 절은 이전 join의 결과와 지정된 데이터 소스를 상호 연결합니다.

첫 번째 join 절은 개체와 Department 개체의 ID와 일치하는 Student 개체의 DepartmentID에 따라 학생과 부서를 일치시킵니다. Student 개체 및 Department 개체를 포함하는 무명 형식의 시퀀스를 반환합니다.

부서장 ID와 일치하는 해당 교사의 ID를 기반으로 하는 Teacher 개체와 첫 번째 join 절에서 반환된 익명 형식을 두 번째 join 절이 상호 연결합니다. 학생 이름, 부서 이름 및 부서장 이름을 포함하는 익명 형식의 시퀀스를 반환합니다. 이러한 작업이 내부 join이므로, 두 번째 데이터 소스에 일치 항목이 있는 첫 번째 데이터 소스의 개체만이 반환됩니다.

// The first join matches Department.ID and Student.DepartmentID from the list of students and
// departments, based on a common ID. The second join matches teachers who lead departments
// with the students studying in that department.
var query = from student in students
    join department in departments on student.DepartmentID equals department.ID
    join teacher in teachers on department.TeacherID equals teacher.ID
    select new {
        StudentName = $"{student.FirstName} {student.LastName}",
        DepartmentName = department.Name,
        TeacherName = $"{teacher.First} {teacher.Last}"
    };

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

여러 Join 메서드를 사용하는 동등한 방법은 익명 형식과 동일한 방법을 사용합니다.

var query = students
    .Join(departments, student => student.DepartmentID, department => department.ID,
        (student, department) => new { student, department })
    .Join(teachers, commonDepartment => commonDepartment.department.TeacherID, teacher => teacher.ID,
        (commonDepartment, teacher) => new
        {
            StudentName = $"{commonDepartment.student.FirstName} {commonDepartment.student.LastName}",
            DepartmentName = commonDepartment.department.Name,
            TeacherName = $"{teacher.First} {teacher.Last}"
        });

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

그룹화된 join을(를) 사용한 내부 join

내부 join을(를) 구현하기 위해 그룹 join을(를) 사용하는 방법을 다음의 예시에서 보여 줍니다. Department 개체 목록은 Student.DepartmentID 속성과 일치하는 Department.ID를 기준으로 Student 개체 목록에 그룹 조인됩니다. 각 그룹이 Department 개체 및 일치하는 Student 개체 시퀀스로 구성되는 중간 그룹 컬렉션을 그룹 join이(가) 생성합니다. 두 번째 from 절은 이 시퀀스를 하나의 긴 시퀀스로 결합하거나 평면화합니다. select 절은 최종 시퀀스의 요소 형식을 지정합니다. 이 형식은 학생 이름과 일치하는 부서 이름으로 구성된 익명 형식입니다.

var query1 =
    from department in departments
    join student in students on department.ID equals student.DepartmentID into gj
    from subStudent in gj
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
    };
Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in query1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

다음과 같이 GroupJoin 메서드를 사용하여 동일한 결과를 달성할 수 있습니다.

var queryMethod1 = departments
    .GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, gj) => new { department, gj })
    .SelectMany(departmentAndStudent => departmentAndStudent.gj,
        (departmentAndStudent, subStudent) => new
        {
            DepartmentName = departmentAndStudent.department.Name,
            StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
        });

Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in queryMethod1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

해당 결과는 into 절 없이 join 절을 사용해서 내부 join을(를) 수행하여 얻은 결과 집합과 일치합니다. 다음 코드는 이와 동등한 쿼리를 보여 줍니다.

var query2 = from department in departments
    join student in students on department.ID equals student.DepartmentID
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    };

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in query2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

체인을 방지하기 위해 다음과 같이 단일 Join 메서드를 사용할 수 있습니다.

var queryMethod2 = departments.Join(students, departments => departments.ID, student => student.DepartmentID,
    (department, student) => new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    });

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in queryMethod2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

그룹화 조인 수행

계층적 데이터 구조를 생성하는 데 그룹 join이(가) 유용합니다. 첫 번째 컬렉션의 각 요소와 두 번째 컬렉션에서 상관 관계가 지정된 요소 집합을 쌍으로 구성합니다.

참고 항목

상관 관계가 지정된 요소가 두 번째 컬렉션에 있는지에 관계없이, 그룹 join의 결과 집합에는 첫 번째 컬렉션의 각 요소가 표시됩니다. 상관 관계가 지정된 요소가 없는 경우 해당 요소에 대해 상관 관계가 지정된 요소의 시퀀스가 비어 있습니다. 따라서 결과 선택기에서 첫 번째 컬렉션의 모든 요소에 액세스할 수 있습니다. 이러한 사항은 두 번째 컬렉션에 일치 항목이 없는 첫 번째 컬렉션의 요소에 액세스할 수 없는 비그룹 join의 결과 선택기와 일치하지 않습니다.

Warning

Enumerable.GroupJoin에는 기존 관계형 데이터베이스 용어에 직접적으로 해당하는 항목이 없습니다. 그러나 이 메서드는 내부 조인 및 왼쪽 우선 외부 조인의 상위 집합을 구현합니다. 이러한 작업은 모두 그룹화된 join와(과) 관련하여 작성될 수 있습니다. 자세한 내용은 Entity Framework Core, GroupJoin을 참조하세요.

그룹 join을(를) 수행하는 방법을 이 문서의 첫 번째 예시에서 보여 줍니다. 그룹 join을(를) 사용하여 XML 요소를 만드는 방법을 두 번째 예시에서 보여 줍니다.

그룹 join

다음의 예시는 Student.DepartmentID 속성과 일치하는 Department.ID에 따라 DepartmentStudent 형식 개체의 그룹 join을(를) 수행합니다. 각 일치 항목에 관한 요소 쌍을 생성하는 비그룹 join와(과) 달리, 그룹 join은(는) 첫 번째 컬렉션의 각 요소에 관해 하나의 결과 개체를 생성하며, 이 예시에서는 Department 개체입니다. 두 번째 컬렉션의 해당 요소(이 예제에서는 Student 개체)는 컬렉션으로 그룹화됩니다. 마지막으로, 결과 선택기 함수는 Department.NameStudent 개체 컬렉션으로 구성된 각 일치 항목에 대해 무명 형식을 만듭니다.

var query = from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new
    {
        DepartmentName = department.Name,
        Students = studentGroup
    };

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

위의 예제에서 query 변수에는 각 요소가 부서의 이름과 해당 부서에서 학습하는 학생의 컬렉션을 포함하는 익명 형식인 목록을 만드는 쿼리가 포함되어 있습니다.

메서드 구문을 사용하는 동일한 쿼리는 다음 코드에 나와 있습니다.

var query = departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
    (department, Students) => new { DepartmentName = department.Name, Students });

foreach (var v in query)
{
    // Output the department's name.
    Console.WriteLine($"{v.DepartmentName}:");

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

XML을 생성하기 위한 그룹 join

그룹 조인은 LINQ to XML을 사용하여 XML을 만드는 데 적합합니다. 다음 예제는 무명 형식을 만드는 대신 결과 선택기 함수가 조인된 개체를 나타내는 XML 요소를 만든다는 점을 제외하고 앞의 예제와 비슷합니다.

XElement departmentsAndStudents = new("DepartmentEnrollment",
    from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new XElement("Department",
        new XAttribute("Name", department.Name),
        from student in studentGroup
        select new XElement("Student",
            new XAttribute("FirstName", student.FirstName),
            new XAttribute("LastName", student.LastName)
        )
    )
);

Console.WriteLine(departmentsAndStudents);

메서드 구문을 사용하는 동일한 쿼리는 다음 코드에 나와 있습니다.

XElement departmentsAndStudents = new("DepartmentEnrollment",
    departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, Students) => new XElement("Department",
            new XAttribute("Name", department.Name),
            from student in Students
            select new XElement("Student",
                new XAttribute("FirstName", student.FirstName),
                new XAttribute("LastName", student.LastName)
            )
        )
    )
);

Console.WriteLine(departmentsAndStudents);

왼쪽 우선 외부 조인 수행

왼쪽 우선 외부 join은(는) 두 번째 컬렉션에 상호 연결된 요소가 있는지에 관계없이 첫 번째 컬렉션의 각 요소가 반환되는 join입니다. 왼쪽 우선 외부 join을(를) 수행하기 위해, LINQ를 통해 그룹 join의 결과에서 DefaultIfEmpty 메서드를 호출할 수 있습니다.

그룹 join의 결과에서 DefaultIfEmpty 메서드를 사용하여 왼쪽 우선 외부 join을(를) 수행하는 방법을 다음의 예시에서 보여 줍니다.

그룹 join을(를) 사용하여 내부 join을(를) 수행하는 것이 두 컬렉션의 왼쪽 우선 외부 join을(를) 생성하는 첫 번째 단계입니다. 이 프로세스에 대한 설명은 내부 조인 수행을 참조하세요. 이 예제에서 Department 개체 목록은 학생의 DepartmentID와 일치하는 Department 개체의 ID를 기준으로 Student 개체 목록에 내부 조인됩니다.

두 번째 단계는 오른쪽 컬렉션에 일치하는 항목이 없는 경우에도 첫 번째(왼쪽) 컬렉션의 각 요소를 결과 집합에 포함하는 것입니다. 그룹 join에서 일치하는 요소의 각 시퀀스에 관해 DefaultIfEmpty을(를) 호출하여 이를 수행합니다. 이 예제에서는 일치하는 Student 개체의 각 시퀀스에서 DefaultIfEmpty를 호출합니다. 메서드는 Department 개체에 대해 일치하는 Student 개체의 시퀀스가 비어 있는 경우 단일 기본값을 포함하는 컬렉션을 반환하여 각 Department 개체가 결과 컬렉션에 반환되도록 합니다.

참고 항목

참조 형식의 기본값은 null이므로 예제에서는 각 Student 컬렉션의 각 요소에 액세스하기 전에 null 참조를 확인합니다.

var query =
    from student in students
    join department in departments on student.DepartmentID equals department.ID into gj
    from subgroup in gj.DefaultIfEmpty()
    select new
    {
        student.FirstName,
        student.LastName,
        Department = subgroup?.Name ?? string.Empty
    };

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

메서드 구문을 사용하는 동일한 쿼리는 다음 코드에 나와 있습니다.

var query = students
    .GroupJoin(
        departments,
        student => student.DepartmentID,
        department => department.ID,
        (student, departmentList) => new { student, subgroup = departmentList })
    .SelectMany(
        joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
        (student, department) => new
        {
            student.student.FirstName,
            student.student.LastName,
            Department = department.Name
        });

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

참고 항목