1. مقدمه‌ای درباره تجزیه و تحلیل تجمعی

عملیات تجمعی یک مفهوم بسیار مهم در پرس و جوی پایگاه داده‌ها است. این عملیات معمولاً برای تجزیه و تحلیل آماری مانند جمع، تعداد‌گیری، میانگین، بیشینه و کمینه مقادیر استفاده می‌شود. این عملیات کمک می‌کنند تا کاربران اطلاعات معناداری را از مقدار بزرگی از داده استخراج کنند و از آن‌ها برای تجزیه و تحلیل داده‌ها و تصمیم‌گیری‌ها پشتیبانی کنند. توابع پیاده‌سازی شده در پایگاه داده برای تجمع عموماً به عنوان توابع تجمعی شناخته می‌شوند.

2. عملیات تجمعی پایه

2.1. مفهوم توابع تجمعی

توابع تجمعی، توابعی هستند که در زبان‌های پرس و جوی پایگاه داده برای انجام مجموعه‌ای محاسبات و بازگشت یک مقدار تکی استفاده می‌شوند. در SQL و زبان‌های پرس و جو مشابه، توابع تجمعی می‌توانند بر روی یک ستون داده عمل کرده و یک مقدار تکی مانند جمع (SUM)، میانگین (AVG)، تعداد (COUNT)، بیشینه (MAX) و کمینه (MIN) را بازگردانند. زمانی که نیاز به انجام تجزیه و تحلیل آماری داده داریم، توابع تجمعی ابزارهای مهمی برای پردازش مجموعه‌های داده و استخراج داده‌های آماری هستند.

2.2. تجمع تک‌فیلدی

در برنامه‌های عملی، تجزیه و تحلیل تک‌فیلدی تجمعی یک نیاز بسیار رایج است که اغلب برای به‌دست آوردن نتایج آماری مانند مجموع، میانگین، بیشینه و کمینه مقادیر یک ستون خاص استفاده می‌شود. فرض کنید یک جدول اطلاعات پرداخت داریم و می‌خواهیم مجموع مقدار پرداخت‌هایی که توسط کاربران انجام شده است را محاسبه کنیم. با استفاده از چارچوب ent، می‌توانیم یک پرس و جو را از موجودیت رشته‌ها بسازیم و توابع تجمعی را اعمال کنیم به‌این شکل:

func Do(ctx context.Context, client *ent.Client) {
    // محاسبه مجموع فیلد مقدار برای موجودیت پرداخت
    sum, err := client.Payment.Query().
        Aggregate(
            ent.Sum(payment.Amount),
        ).
        Int(ctx)
    if err != nil {
        log.Fatalf("خطا در دریافت مجموع: %v", err)
    }
    log.Printf("مجموع کل پرداخت‌ها: %d", sum)
}

در قطعه کد فوق، ما یک پرس و جو را برای موجودیت پرداخت با استفاده از client.Payment.Query() آغاز می‌کنیم، سپس از متد Aggregate() برای فراخوانی تابع ent.Sum با payment.Amount به‌عنوان پارامتر برای محاسبه مجموع مقادیر پرداخت استفاده می‌کنیم. ما از .Int(ctx) برای تبدیل نتیجه تجمعی به یک عدد صحیح و چاپ آن استفاده می‌کنیم.

2.3. تجمع چندفیلدی

در بسیاری از موارد، نیاز داریم تا عملیات تجمع را بر روی چندین فیلد به جای یک فیلد انجام دهیم. در این بخش، از طریق یک مثال نشان می‌دهیم که چگونه می‌توانیم تجمع چندفیلدی را دست‌یافته کنیم.

در این مثال، ما مجموع، مینیمم، بیشینه و تعداد فیلد Age را در جدول حیوان خانگی محاسبه می‌کنیم.

func Do(ctx context.Context, client *ent.Client) {
    var v []struct {
        Sum, Min, Max, Count int
    }
    err := client.Pet.Query().
        Aggregate(
            ent.Sum(pet.FieldAge), // مجموع Age
            ent.Min(pet.FieldAge), // حداقل Age
            ent.Max(pet.FieldAge), // بیشینه Age
            ent.Count(),           // تعداد
        ).
        Scan(ctx, &v)
    if err != nil {
        log.Fatalf("جستجو انجام نشد: %v", err)
    }
    // چاپ تمامی نتایج تجمعی
    for _, agg := range v {
        fmt.Printf("مجموع: %d حداقل: %d بیشینه: %d تعداد: %d\n", agg.Sum, agg.Min, agg.Max, agg.Count)
    }
}

در قطعه کد فوق، از تابع Aggregate برای انجام تجمع چندفیلدی استفاده کرده و از تابع Scan برای ذخیره نتایج تجمعی در آرایه v استفاده می‌کنیم. سپس، از v برای چاپ تمامی نتایج تجمعی استفاده می‌کنیم.

3. کاربرد تجمع با استفاده از گروه‌بندی

3.1. استفاده از گروه‌بندی برای گروه‌بندی فیلدها

در عملیات پایگاه‌داده‌ای، Group By یک روش معمول برای گروه‌بندی داده‌ها است. در این بخش، یاد می‌گیریم که چگونه از Group By برای گروه‌بندی داده در پایگاه داده استفاده کنیم.

مثال آموزشی، چگونگی گروه‌بندی یک یا چند فیلد با استفاده از Group By.

فرض کنید یک جدول کاربر داریم و نیاز داریم تا فیلد name کاربران را گروه‌بندی کرده و تعداد کاربران در هر گروه را محاسبه کنیم. زیرا کد یک مثال از چگونگی دستیابی به این نیاز را نشان می‌دهد:

func Do(ctx context.Context, client *ent.Client) {
    names, err := client.User.
        Query().
        GroupBy(user.FieldName).
        Strings(ctx)
    if err != nil {
        log.Fatalf("Failed to execute grouped query: %v", err)
    }
    // چاپ نام هر گروه
    for _, name := range names {
        fmt.Println("نام گروه:", name)
    }
}

در کد بالا، از متد GroupBy سازنده پرس و جو برای انتخاب این که کدام فیلد را می‌خواهیم بر اساس آن گروه‌بندی کنیم، استفاده می‌کنیم.

۳.۲. گروه‌بندی و تجمیع چندین فیلد

گاهی اوقات، ما می‌خواهیم داده‌ها را بر اساس چندین فیلد گروه‌بندی کرده و عملیات تجمیع را بر روی هر گروه انجام دهیم. در زیر یک مثال از چگونگی دستیابی به این نیاز آورده شده است:

کد زیر نشان می‌دهد چگونه می‌توان داده‌ها را در جدول کاربر براساس فیلدهای name و age گروه‌بندی کرده و مجموع سن و تعداد کاربران در هر گروه را محاسبه کرد.

func Do(ctx context.Context, client *ent.Client) {
    var v []struct {
        Name  string `json:"name"`
        Age   int    `json:"age"`
        Sum   int    `json:"sum"`
        Count int    `json:"count"`
    }
    err := client.User.Query().
        GroupBy(user.FieldName, user.FieldAge).
        Aggregate(ent.Count(), ent.Sum(user.FieldAge)).
        Scan(ctx, &v)
    if err != nil {
        log.Fatalf("امکان اجرای پرس و جو گروه‌بندی و تجمیع چندین فیلد وجود ندارد: %v", err)
    }
    // اطلاعات تفصیلی برای هر گروه را خروجی می‌دهد
    for _, group := range v {
        fmt.Printf("نام: %s سن: %d مجموع: %d تعداد: %d\n", group.Name, group.Age, group.Sum, group.Count)
    }
}

در این مثال، ما نه‌تنها داده‌ها را براساس فیلدهای name و age در جدول کاربر گروه‌بندی می‌کنیم، بلکه نیز از توابع تجمیع Count و Sum برای محاسبه تعداد کل رکوردها و مجموع سن در هر گروه استفاده می‌کنیم.

۴. ادغام Having با Group By

شرط Having نتایج تجمیع به‌دست‌آمده پس از عملیات Group By را فیلتر می‌کند. مثال زیر نشان می‌دهد چگونه تنها کاربران با بیشترین سن در هر نقش را انتخاب کنیم:

func Do(ctx context.Context, client *ent.Client) {
    var users []struct {
        Id      Int
        Age     Int
        Role    string
    }
    err := client.User.Query().
        Modify(func(s *sql.Selector) {
            s.GroupBy(user.FieldRole)
            s.Having(
                sql.EQ(
                    user.FieldAge,
                    sql.Raw(sql.Max(user.FieldAge)),
                ),
            )
        }).
        ScanX(ctx, &users)
    if err != nil {
        log.Fatalf("امکان اجرای پرس و جو ادغام شده `Having` با `Group By` وجود ندارد: %v", err)
    }
    // اطلاعات کاربری که شرط `Having` را ارضا می‌کنند را خروجی می‌دهد
    for _, user := range users {
        fmt.Printf("شناسه: %d سن: %d نقش: %s\n", user.Id, user.Age, user.Role)
    }
}

کد فوق یک پرس‌وجوی SQL معادل برای انتخاب کاربران با بیشترین سن در هر نقش تولید خواهد کرد.