بررسی تاثیر تبدیل ضمنی نوعهای دادهی رشتهای بر روی کارآیی کوئریها
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۲۶ ۰۹:۰۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
WHERE ساده روی یک ستون که اتفاقاً ایندکسگذاری (Indexed) هم شدهاست.const string sql = "SELECT * FROM Products WHERE ProductCode = @productCode";
var result = await connection.QueryFirstOrDefaultAsync<Product>(sql, new { productCode });ProductCode در پایگاه داده شما از نوع VARCHAR باشد، همین دو خط کد کارایی دیتابیس را به شدت کاهش میدهد.NVARCHAR(4000) نگاشت (Map) میکند. این رفتار، نگاشت پیشفرض برای System.String در ADO.NET است و منطقاً به عنوان یک انتخاب امن (Safe Default) برای پشتیبانی از کاراکترهای یونیکد (Unicode) در نظر گرفته شده است.NVARCHAR مواجه میشود، در حالیکه ستون مورد نظر در جدول از نوع VARCHAR (غیر یونیکد) است. در قوانین اولویت انواع داده در SQL Server، نوع داده NVARCHAR اولویت بالاتری نسبت به VARCHAR دارد. بنابراین، موتور دیتابیس برای مقایسه دو مقدار، نمیتواند پارامتر شما را به VARCHAR تبدیل کند؛ بلکه مجبور است تکتک مقادیر موجود در آن ستون از جدول را به NVARCHAR تبدیل کند.CONVERT_IMPLICIT یا تبدیل ضمنی میگویند. وقتی این اتفاق میافتد، SQL Server دیگر نمیتواند از عملیات بهینه Index Seek (یافتن مستقیم ردیف از طریق ایندکس) استفاده کند و مجبور به انجام عملیات سنگین Index Scan یا حتی Table Scan (پیمایش کامل ایندکس یا جدول) میشود. یعنی دیتابیس برای پیدا کردن یک ردیف، باید تمام ردیفها را بخواند و تبدیل کند!CONVERT_IMPLICIT(nvarchar(255), [Sales].[ProductCode], 0)
ProductCode دارید:نکتهای در مورد Collation دیتابیس:
میزان آسیب این مشکل تا حدی به Collation پایگاه داده شما بستگی دارد. در رایجترین حالت یعنیSQL_Latin1_General_CP1_CI_AS، شما دقیقاً با بدترین سناریو یعنی Full Index Scan مواجه میشوید. در برخی از Collationهای ویندوزی (مانندLatin1_General_CI_AS) ممکن است موتور دیتابیس همچنان بتواند Index Seek انجام دهد، اما بار پردازشی (Overhead) ناشی از تبدیل ضمنی همچنان پابرجا خواهد بود. در هر صورت، همخوانی داشتن نوع پارامتر با نوع ستون، تنها راهکار اصولی است.
| سناریو | حالت پیشفرض (NVARCHAR) | حالت اصلاح شده (VARCHAR) | میزان کندتر بودن حالت پیشفرض |
| جدول محصولات (۱ میلیون ردیف) | ۲۳ ثانیه | ۰.۱۳ ثانیه | ۱۷۶ برابر کندتر |
| جدول سفارشات (۵۰۰ هزار ردیف) | ۳۵ ثانیه | ۰.۱۳ ثانیه | ۲۶۸ برابر کندتر |
| شاخص (Metric) | حالت پیشفرض (NVARCHAR) | حالت اصلاح شده (VARCHAR) |
| نوع پیمایش (Scan Type) | Index SCAN | Index SEEK |
| تعداد Logical Reads | دهها هزار | تکرقمی |
| زمان CPU به ازای هر اجرا | میلیثانیه | میکروثانیه |
| هزینه تخمینی پرسوجو (Query Cost) | 7.78 | 0.007 |
VARCHAR (یا همان ANSI String) است و نه NVARCHAR. برای این کار دو روش استاندارد وجود دارد:DynamicParameters و DbType.AnsiStringconst string sql = "SELECT * FROM Products WHERE ProductCode = @productCode";
var parameters = new DynamicParameters();
parameters.Add("productCode", productCode, DbType.AnsiString, size: 100);
var result = await connection.QueryFirstOrDefaultAsync<Product>(sql, parameters);DbType.AnsiString به ADO.NET اعلام میکند که یک پارامتر از نوع VARCHAR ارسال کند. مقدار size نیز باید دقیقاً با اندازه ستون در پایگاه داده (مثلاً VARCHAR(100)) مطابقت داشته باشد. این انطباق اندازه به SQL Server کمک میکند تا طرحهای پرسوجوی کششده (Cached Query Plans) را بهینهتر بازاستفاده (Reuse) کند.DbStringDbString را برای این منظور ارائه داده است:var result = await connection.QueryFirstOrDefaultAsync<Product>(sql,
new { productCode = new DbString { Value = productCode, IsAnsi = true, Length = 100 } });VARCHAR ارسال میکنند که بلافاصله مشکل تبدیل ضمنی را برطرف میسازد.DynamicParameters را ببینند و با خود بگویند: "چرا این کد اینقدر طولانی است؟ بگذار آن را به یک شیء ناشناس ساده تبدیل کنم". آنها کد شما را ساده میکنند و بدون اینکه متوجه شوند، این باگ مهلک کارایی را دوباره به سیستم بازمیگردانند!var parameters = new DynamicParameters();
// استفاده از DbType.AnsiString الزامی است:
// ستون Products.ProductCode از نوع varchar(100) است.
// در صورت عدم تعیین، داپرت به صورت پیشفرض nvarchar(4000) ارسال میکند
// که باعث رخ دادن CONVERT_IMPLICIT روی تمام ردیفها شده و ایندکس را غیرفعال میکند.
parameters.Add("productCode", productCode, DbType.AnsiString, size: 100);SELECT TOP 20
qsqt.query_sql_text,
qsrs.avg_cpu_time,
qsrs.count_executions
FROM sys.query_store_runtime_stats qsrs
JOIN sys.query_store_plan qsp ON qsrs.plan_id = qsp.plan_id
JOIN sys.query_store_query qsq ON qsp.query_id = qsq.query_id
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
WHERE qsqt.query_sql_text LIKE '%@%nvarchar(4000)%'
ORDER BY qsrs.avg_cpu_time * qsrs.count_executions DESC;VARCHAR پاس داده میشوند:// این الگو مشکوک به ایجاد افت کارایی شدید است
await connection.QueryAsync<T>(sql, new { someVarcharColumn });VARCHAR است، در کدهای #C الزاماً از DbType.AnsiString یا DbString(IsAnsi = true) استفاده کنید و طول آن را مشخص نمایید.NVARCHAR است، رفتار پیشفرض Dapper (یعنی همان DbType.String) کاملاً درست و بهینه است.VARCHAR میفرستد، پتانسیل این را دارد که یک پیمایش کامل جدول (Full Table Scan) را به سیستم شما تحمیل کند. اصلاح این اشتباهات کوچک، تفاوتی شگرف در پایداری و کارایی سرورهای شما ایجاد خواهد کرد.CONVERT_IMPLICIT)، بررسی رفتار Entity Framework Core (EF Core) بسیار حائز اهمیت است؛ چراکه این فریمورک به عنوان محبوبترین ORM در دنیای داتنت، رفتار پیشفرض مشابهی با Dapper دارد، اما روشهای مدیریت و حل آن به دلیل ساختار شیءگراتر و وجود کنترلر Context متفاوت است. در ادامه، رفتار پیشفرض EF Core در مواجهه با رشتهها، فاجعه کارایی ناشی از آن و راهکارهای پیشگیری جامع را بررسی میکنیم.string در کلاسها، نوع داده یونیکد یعنی NVARCHAR(MAX) یا NVARCHAR(4000) در SQL Server است.VARCHAR است و پروپرتی متناظر آن در کدهای #C را بدون پیکربندی خاصی رها کنید، با همان مشکل کشنده مواجه خواهید شد. برای مثال، فرض کنید چنین کوئری LINQ سادهای را بنویسید:var product = await _context.Products
.FirstOrDefaultAsync(p => p.ProductCode == "ABC12345");ProductCode در دیتابیس VARCHAR باشد، EF Core در پشت صحنه پارامتری با نوع NVARCHAR میسازد و کوئری زیر را به SQL Server ارسال میکند:exec sp_executesql N'SELECT TOP(1) [p].[Id], [p].[ProductCode], [p].[Name] FROM [Products] AS [p] WHERE [p].[ProductCode] = @__productCode_0',N'@__productCode_0 nvarchar(4000)',@__productCode_0=N'ABC12345'
nvarchar(4000) ارسال شده است که بلافاصله موتور دیتابیس را مجبور به انجام فرآیند CONVERT_IMPLICIT روی تکتک ردیفهای ستون VARCHAR کرده و ایندکس شما را کاملاً بیاثر میکند.DbContext و متد OnModelCreating است. با این کار به EF Core میگویید این رشته از نوع کدهای استاندارد ANSI (یعنی VARCHAR) است:protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.Property(p => p.ProductCode)
.IsUnicode(false) // تبدیل رشته به VARCHAR
.HasMaxLength(100); // تعیین دقیق طول ستون
});
}IsUnicode(false) دقیقاً معادل همان کاری است که DbType.AnsiString در Dapper انجام میداد.[Column] بهره ببرید:using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
public int Id { get; set; }
[Column(TypeName = "varchar")]
[StringLength(100)]
public string ProductCode { get; set; } = string.Empty;
}VARCHAR هستند، بررسی تکتک پروپرتیها کار سختی است. در EF Core میتوانید یک قانون کلی (Convention) وضع کنید تا تمام رشتهها به صورت پیشفرض به VARCHAR تبدیل شوند، مگر اینکه خلاف آن ثابت شود!ConfigureConventions بسیار ساده شده است:protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// تمام پروپرتیهای رشتهای در کل پروژه به صورت پیشفرض VARCHAR(255) میشوند
configurationBuilder.Properties<string>()
.HaveIsUnicode(false)
.HaveMaxLength(255);
}.IsUnicode(true) یا نوع داده NVARCHAR را تنظیم کنید.EF.Functions.LikeLike استفاده میکنند:var results = await _context.Products
.Where(p => EF.Functions.Like(p.ProductCode, $"%{searchTerm}%"))
.ToListAsync();ProductCode را با IsUnicode(false) به عنوان VARCHAR معرفی کرده باشید، اگر متغیر searchTerm یک رشتهی سادهی #C باشد، EF Core ممکن است باز هم پارامتر ورودی لایک را به صورت NVARCHAR به دیتابیس بفرستد!LIKE، حتماً مطمئن شوید که نوع داده ورودی با استفاده از روشهای بالا کاملاً با ساختار مدل همخوانی دارد، یا در صورت نیاز از قابلیت تعریف صریح نوع پارامتر در زمان استفاده از کوئریهای خام (FromSqlRaw) بهره ببرید.VARCHAR تعریف کرده و در EF Core با متد IsUnicode(false) تنظیم کنید.VARCHAR(MAX) یا NVARCHAR(MAX) از بازاستفاده بهینه کدهای نقشه پرسوجو (Query Plan) در SQL Server جلوگیری میکند. همیشه طول منطقی (HasMaxLength) برای ستونها در نظر بگیرید.unicode: false ساخته شده باشند.