【PostgreSQL】【EntityFramework Core】SQL 디버깅 1
소개
내 응용 프로그램이 성능 문제를 일으키면 병목 지점을 찾기 위해 측정해야 합니다.
이번에는 EntityFramework Core를 사용하는 코드를 측정해 보려고 합니다.
환경
기본 프로젝트
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의 결과입니다.
예시
두 가지 예를 시도하고 실행 시간을 측정합니다.
샘플 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"
계획 시간
실행 시간
샘플 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"
계획 시간
실행 시간
이 경우 샘플 2를 선택해야 합니다.
이러한 차이는 작습니다. 그러나 샘플 1의 실행 시간은 샘플 2보다 2배 느립니다.
Reference
이 문제에 관하여(【PostgreSQL】【EntityFramework Core】SQL 디버깅 1), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/masanori_msl/postgresql-entityframework-core-debugging-sql-1-31d2텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)