【PostgreSQL】【EntityFramework Core】SQL 디버깅 1

소개



내 응용 프로그램이 성능 문제를 일으키면 병목 지점을 찾기 위해 측정해야 합니다.
이번에는 EntityFramework Core를 사용하는 코드를 측정해 보려고 합니다.

환경


  • .NET 버전 5.0.100-rc.1.20452.10
  • Microsoft.EntityFrameworkCore 버전 5.0.0-rc.1.20451.13
  • Npgsql.EntityFrameworkCore.PostgreSQL 버전 5.0.0-rc1
  • NLog.Web.AspNetCore 버전 4.9.3
  • Microsoft.AspNetCore.Mvc.NewtonsoftJson 버전 5.0.0-rc.1.20451.17

  • 기본 프로젝트


    Company.cs



    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace BookStoreSample.Models
    {
        public class Company
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; set; }
            [Required]
            public string Name { get; set; }
    
            public List<Book> Books { get; set; } = new List<Book>();
        }
    }
    

    장르.cs



    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace BookStoreSample.Models
    {
        public class Genre
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; set; }
            [Required]
            public string Name { get; set; }
        }
    }
    

    Book.cs



    using System;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace BookStoreSample.Models
    {
        public class Book
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; set; }
            [Required]
            public string Name { get; set; }
            [Column(TypeName = "timestamp with time zone")]
            public DateTime? PublishDate { get; set; }
            [ForeignKey(nameof(Company))]
            public int CompanyId { get; set; }
            [ForeignKey(nameof(Genre))]
            public int GenreId { get; set; }        
            public Company Company { get; set; }
            public Genre Genre { get; set; }
        }
    }
    

    BookStoreContext.cs



    using Microsoft.EntityFrameworkCore;
    
    namespace BookStoreSample.Models
    {
        public class BookStoreContext: DbContext
        {
            public BookStoreContext(DbContextOptions<BookStoreContext> options)
                : base(options)
            {
            }
            public DbSet<Company> Companies => Set<Company>();
            public DbSet<Genre> Genres => Set<Genre>();
            public DbSet<Book> Books => Set<Book>();
        }
    }
    

    샘플 데이터 생성



    SQL 성능을 측정하기 위해 샘플 데이터를 생성합니다.

    ISampleCreator.cs



    using System.Threading.Tasks;
    
    namespace BookStoreSample.Samples
    {
        public interface ISampleCreator
        {
            Task CreateAsync();
        }
    }
    

    SampleCreator.cs



    using System;
    using System.Threading.Tasks;
    using BookStoreSample.Models;
    
    namespace BookStoreSample.Samples
    {
        public class SampleCreator: ISampleCreator
        {
            private readonly BookStoreContext _context;
            public SampleCreator(BookStoreContext context)
            {
                _context = context;
            }
            public async Task CreateAsync()
            {
                using(var transaction = _context.Database.BeginTransaction())
                {
                    try
                    {
                        for(var i = 0; i < 1000; i++)
                        {
                            _context.Companies.Add(new Company
                            {
                                Name = $"Company: {i}",
                            });
                        }
                        for(var i = 0; i < 1000; i++)
                        {
                            _context.Genres.Add(new Genre
                            {
                                Name = $"Genre: {i}",
                            });
                        }
                        await _context.SaveChangesAsync();
                        var random = new Random();
                        for(var i = 0; i < 1000000; i++)
                        {
                            _context.Books.Add(new Book
                            {
                                Name = $"Book: {i}",
                                PublishDate = DateTime.Now,
                                CompanyId = random.Next(999) + 1,
                                GenreId = random.Next(999) + 1,
                                Price = 600,
                            });
                        }
                        await _context.SaveChangesAsync();
                        transaction.Commit();
                    }
                    catch(Exception ex)
                    {
                        transaction.Rollback();
                        throw ex;
                    }
                }
            }
        }
    }
    

    HomeController.cs



    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using BookStoreSample.Samples;
    
    namespace BookStoreSample.Controllers
    {
        public class HomeController: Controller
        {
            private readonly ILogger<HomeController> _logger;
            private readonly ISampleCreator _sample;
            public HomeController(ILogger<HomeController> logger,
                ISampleCreator sample)
            {
                _logger = logger;
                _sample = sample;
            }
            [Route("Sample")]
            public async Task CreateSamples()
            {
                await _sample.CreateAsync();
            }
        }
    
    }
    

    출력 생성 SQL



    EntityFramework Core는 내 C# 코드에서 SQL을 생성합니다.
    SQL을 측정하기 위해 생성된 SQL을 얻고 싶습니다.

    "EnableSensitiveDataLogging"으로 출력할 수 있습니다.

    Startup.cs



    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Newtonsoft.Json;
    ...
    
    namespace BookStoreSample
    {
        public class Startup
        {
            private readonly IConfiguration configuration;
            public Startup(IConfiguration config)
            {
                configuration = config;
            }
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllers()
                    .AddNewtonsoftJson(options =>
                        options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
                services.AddDbContext<BookStoreContext>(options =>
                {
                    options.EnableSensitiveDataLogging();
                    options.UseNpgsql(configuration["ConnectionStrings"]);
                });
    ...
            }
    ...
    

    출력되는 SQL의 로그 레벨은 Information이므로 Microsoft 로그 레벨을 낮게 설정해야 합니다.

    appsettings.개발.json



    {
      "Logging": {
        "LogLevel": {
          "Default": "Debug",
          "Microsoft": "Information",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      }
    }
    

    예를 들어, 이 코드를 실행할 때,

    public async Task<List<SearchedCompany>> SearchCompaniesAsync()
    {
        return await _context.Companies
            .ToListAsync();
    }
    

    아래와 같이 로그를 얻을 수 있습니다.

    ...
    2020-10-06 18:20:17.1528|20101|INFO|Microsoft.EntityFrameworkCore.Database.Command|Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
    SELECT c."Id", c."Name"
    FROM "Companies" AS c |url: http://localhost/Company/Search|action: SearchCompany
    ...
    

    설명, 분석



    SQL 성능을 측정하기 위해 EXPLAIN 및 ANALYZE를 사용할 수 있습니다.

    EXPLAIN ANALYZE SELECT c."Id", c."Name" FROM "Companies" AS c
    

    SQL 쿼리 앞에 추가하고 실행(예: PgAdmin4)하면 분석 결과를 얻을 수 있습니다.
    결과에는 실행 시간, 검색에 사용된 키 등이 포함됩니다.

    이것은 PgAdmin4의 결과입니다.

  • PostgreSQL: Documentation: 13: 14.1. Using EXPLAIN

  • 예시



    두 가지 예를 시도하고 실행 시간을 측정합니다.

    샘플 1



    SearchedCompany.cs



    using BookStoreSample.Models;
    
    namespace BookStoreSample.Books
    {
        public class SearchedCompany
        {
            public Company? Company { get; set; }
            public Book? Book { get; set; }
        }
    }
    

    BookSearchSample.cs



    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using BookStoreSample.Models;
    using Microsoft.EntityFrameworkCore;
    
    namespace BookStoreSample.Books
    {
        public class BookSearchSample: IBookSearchSample
        {
            private readonly BookStoreContext _context;
            public BookSearchSample(BookStoreContext context)
            {
                _context = context;
            }
            public async Task<List<SearchedCompany>> SearchCompaniesAsync()
            {
                return await _context.Companies
                    .Include(c => c.Books)
                    .Select(c => new SearchedCompany
                    {
                        Company = c,
                        Book = c.Books
                            .OrderByDescending(b => b.Id).First(),
                    })
                    .ToListAsync();
            }
        }
    }
    

    생성된 SQL



    SELECT c."Id", c."Name", t0."Id", t0."CompanyId", t0."GenreId", t0."Name", t0."Price", t0."PublishDate", b0."Id", b0."CompanyId", b0."GenreId", b0."Name", b0."Price", b0."PublishDate"
    FROM "Companies" AS c
    LEFT JOIN (
        SELECT t."Id", t."CompanyId", t."GenreId", t."Name", t."Price", t."PublishDate"
        FROM (
            SELECT b."Id", b."CompanyId", b."GenreId", b."Name", b."Price", b."PublishDate", ROW_NUMBER() OVER(PARTITION BY b."CompanyId" ORDER BY b."Id" DESC) AS row
            FROM "Books" AS b
        ) AS t
        WHERE t.row <= 1
    ) AS t0 ON c."Id" = t0."CompanyId"
    LEFT JOIN "Books" AS b0 ON c."Id" = b0."CompanyId"
    ORDER BY c."Id", t0."Id", b0."Id"
    

    계획 시간


  • 0.942ms

  • 실행 시간


  • 4941.233ms

  • 샘플 2



    ...
        public class SearchedCompany
        {
            public int CompanyId { get; set; }
            public string CompanyName { get; set; } = "";
            public Book? Book { get; set; }
        }
    ...
    

    BookSearchSample.cs



    ...
            public async Task<List<SearchedCompany>> SearchCompaniesAsync()
            {
                return await _context.Companies
                    .Include(c => c.Books)
                    .Select(c => new SearchedCompany
                    {
                        CompanyId = c.Id,
                        CompanyName = c.Name,
                        Book = c.Books
                            .OrderByDescending(b => b.Id).First(),
                    })
                    .ToListAsync();
            }
    ...
    

    생성된 SQL



    SELECT c."Id", c."Name", t0."Id", t0."CompanyId", t0."GenreId", t0."Name", t0."Price", t0."PublishDate"
    FROM "Companies" AS c
    LEFT JOIN (
        SELECT t."Id", t."CompanyId", t."GenreId", t."Name", t."Price", t."PublishDate"
        FROM (
            SELECT b."Id", b."CompanyId", b."GenreId", b."Name", b."Price", b."PublishDate", ROW_NUMBER() OVER(PARTITION BY b."CompanyId" ORDER BY b."Id" DESC) AS row
            FROM "Books" AS b
        ) AS t
        WHERE t.row <= 1
    ) AS t0 ON c."Id" = t0."CompanyId"
    

    계획 시간


  • 0.341ms

  • 실행 시간


  • 2209.166ms

  • 이 경우 샘플 2를 선택해야 합니다.
    이러한 차이는 작습니다. 그러나 샘플 1의 실행 시간은 샘플 2보다 2배 느립니다.

    좋은 웹페이지 즐겨찾기