‫افزایش Performance در NHibernate با استفاده از اپراتورهای Projection

یکی از راه‌های توصیه شده برای افزایش Performance در Queryهای دیتابیس، پرهیز از استفاده‌های بی‌مورد از select * from some_table می‌باشد. راه حل توصیه شده استفاده از projection و برگرداندن صرفاً ستون‌های مورد نیاز از دیتابیس است. مثلاً باید از select col1, col2 from some_table استفاده شود.

در همین راستا LINQ-to-NHibernate هم پشتیبانی خوبی از اپراتورهای Projection در LINQ دارد. به نمونه زیر توجه کنید:

var query = from
entity in ActiveRecordLinq.AsQueryable()
where
entity.Prop1 == "123"
select
new SomeEntityDTO(Prop1, Prop2);

List list = query.ToList();
اسکریپت SQL تولید شده از query بالا فقط شامل Prop1 و Prop2 خواهد بود. توجه شود query بالا با استفاده Castle ActiveRecord نوشته شده ولی با LINQ-to-NHibernate اصلی هیچ فرقی ندارد.

‫اندازه گیری سرعت صفحات ASP.NET

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

۱- یکی از دقیق‌ترین ابزارها برای اندازه‌گیری سرعت صفحات ASP.NET استفاده از Performance Monitor در ویندوز ۲۰۰۸ است.

با استفاده از این ابزار می‌توانید بفمهید اجرای صفحه مورد نظر شما چقدر طول می‌کشد و در کدام یک از مراحل اجرای کد دات‌نت قرار دارد. سه تا از counterهای مفید عبارتند از

  • ‭% Time in Jit (.Net CLR Jit)
  • ‭Request Execution Time (ASP.NET Apps)
  • ‭Requests/Sec (ASP.NET Apps)

۲- ابزار دیگری که بدون دسترسی به سرور هم قابل استفاده است و بیشتر روی مراحل مختلف ساخت شی Page متمرکز است، عبارت است از ابزار Trace.

برای فعال کردن این امکان در ASP.NET باید تنظیمات زیر را در مدخل system.web در web.config قرار دهید:


۳- حواستان باشد زمان مورد نیاز برای JIT را به حساب کندی برنامه نگذارید. dllهای دات‌نت به زبان IL هستند و برای اجرا نیاز دارند به کد ماشین ترجمه شوند. انجام این کار به عهده JIT هست. وقتی که یک برنامه ASP.NET را در IIS قرار می‌دهید، با اولین درخواست برای هر صفحه، کد آن توسط JIT تبدیل به کد ماشین می‌شود. این تبدیل فقط یک بار انجام خواهد شد و برای دفعات بعد cache خواهد شد.

‫رکورد اضافی در NHibernate

من مشکلی با NHibernate دارم که نمی‌دانم آیا بقیه هم این مشکل را با NHibernate یا دیگر ORMها یا حتی ADO دارند یا نه. البته اصل این مشکل در صفحات ASP.NET Webform وجود دارد. مشکل این است که وقتی می‌خواهم یک آیتم را در دیتابیس ذخیره کنم یا حتی وقتی می‌خواهم یک رکورد را روی صفحه نمایش دهم مجبور می‌شوم یک یا چند رکورد اضافی را هم از دیتابیس بخوانم.

به عنوان مثال یک صفحه ASP.NET را به شکل زیر تصور کنید:





protected void btnSave_Click(object sender, EventArgs e)
{
Company company = new Company()
{
City = City.Find(drpCity.SelectedValue),
Address = txtAddress.Text
};

company.Save();
}


protected void Search()
{
IList result = MyCustomSearch.SearchCompanies(
City.Find(drpCity.SelectedValue), txtAddress.Text);

//display results in GridView
}

در صفحه بالا، همه Cityها به طور خودکار به drpCity بایند می‌شوند. سپس با کلیک روی دکمه btnSave یک Company با کمک اطلاعات txtAddress و drpCity ذخیره می‌شود. البته کد بالا با Castle ActiveRecord نوشته شده ولی اصل مشکل با دیگر ORMها همان است. همان طور در متود btnSave_Click مشاهده می‌کنید من مجبور هستم برای ذخیره آبجکت Company یک بار به دیتابیس مراجعه کرده و City مورد نظر را از آن فراخوانی نمایم. در متود Search هم همین اتفاق می‌افتد چون متود SearchCompanies پارامتری از نوع City نیاز دارد.

مدتی است که با خودم فکر می‌کنم اگر بتوان این مراجعات بی‌مورد به دیتابیس را حذف کرد، به performance بسیار بهتری می‌رسیم. به همین دلیل به دنبال راه‌هایی هستم که نیاز به load مجدد object را از بین ببرد. در اولین قدم می‌توان همه‌ی متودهایی را که به عنوان ورودی به کل object نیاز دارند را پیدا کرده و آنها را طوری تغییر دهیم که صرفاً بتوانند با ID آبجکت مورد نظر کار کنند. مثلاً متود زیر را در نظر بگیرید:

IList SearchCompanies(City city, string address)
{
var query = from company in ActiveRecordLinq.AsQueryable()
where City == city
select company;

//...
}

این متود را به راحتی می‌توان طوری تغییر داد که به جای City از CityID استفاده کند:

IList SearchCompanies(long cityID, string address)
{
var query = from company in ActiveRecordLinq.AsQueryable()
where City.ID == cityID
select company;

//...
}

‫تاثیر Index گذاری بر سرعت جداول MS SQL

si2 چند روزی را در حال بررسی سرعت Query بر روی جداول حجیم بودم. Query مورد نظر من یک sum ساده بر روی جدولی به اسم amort بود. این Query به صورت یک function پیاده سازی شده بود. البته آن Query نهایی که من زمان آن را بررسی کرده و نتایجش را در نظر داشتم این function را همراه با یک select ساده صدا می زد. خود این select آخر روی یک جدول با ۷۰۰۰ رکورد اجرا می‌شد. در نتیجه اگر فرض کنیم جدول amort دارای ۳۰۰ رکورد است پس ما عملا با انجام عملیات روی ۳۰۰ ضرب در ۷۰۰۰ یعنی ۲۱۰۰۰۰۰ رکورد طرف هستیم. تنها عاملی که آن را در آزمایشات مختلف تغییر می‌دادم تعداد رکوردهای جدول amort (که function مورد نظر من روی آن اجرا می‌شد) و ایندکس داشتن و نداشتن همان جدول amort بود. تمام آزمایشات بر روی یک کامپیوتر Pentium 4, 2.26 Ghz, 1GB RAM, Win Xp SP2 و Microsoft Sql Server 2008 انجام شده است.

نتایج بدون ایندکس واقعا فاجعه بار بودند. برای ۷۲۰۰ رکورد ۲۱۳ ثانیه طول کشید! این در حالی بود که هر سال همین تعداد رکورد به جدول مورد نظر من یعنی amort اضافه می‌شد. به عبارتی دیگر اجرای Query مورد نظر برای سال هشتم حدود نیم ساعت کشنده طول می‌کشد! با این که روی همه جداول دیتابیس بر روی ستون id ایندکس وجود داشت به علت کندی بیش از حد مجبور شدم باز هم به ایندکس‌ها، انواع آن و تاثیرشان بر کارایی فکر کنم. در MS SQL چهار نوع ایندکس وجود دارد که من فقط دو تای پر استفاده‌تر آن را مطالعه و بررسی کردم: Clustered Index و Non-Clustered Index. هر دوی آنها اطلاعات را به صورت sort نگهداری می‌کنند. بر اساس توضیحات اینجا، نوع Clustered اطلاعات را به صورت فیزیکی به حالت sort نگه می‌دارد و در نتیجه در هر جدولی فقط یک ایندکس از این نوع مجاز است در حالی که نوع Non-Clustered از یک ساختار اضافه برای نگهداری اطلاعات sort جدول استفاده می‌کند و می‌توان حدود اقلا ۲۵۰ ایندکس از این نوع در یک جدول داشت. در بعضی منابع خوانده بودم که performance ایجاد شده در هر دو نوع تقریبا یکسان است در حالی که در آزمایشات خودم فهمیدم سرعت Clustered حدود ۱۰ برابر Non-Clustered است. دقت کنید تعریف یک یا چند ستون به عنوان Primary Key باعث می‌شود آن ستون یا ستون‌ها خود به خود به یک Clustered Index تعریف شوند. در همه انواع ایندکس‌ها دو مفهوم ReOrganize و ReBuild وجود دارد که مفهوم آنها را خیلی نفهمیدم ولی متوجه شدم که برای دستیابی به حداکثر کارایی هر از چندگاهی باید این دو عملیات را بر روی جدول مورد نظر اجرا کرد.

نتایج آزمایشم را با هر دو نوع ایندکس تکرار کردم. پرس و جو آن قدر سریع اجرا می‌شد که خیلی سریع تعداد رکوردها را از ۷۲۰۰ به ۷۲۰ هزار، یک و نیم میلیون رکورد و نهایتا ۳ میلیون رکورد رساندم. نتایج کار اگر من مرتکب اشتباهی نشده باشم واقعا شگفت آور بود: زمان مورد نیاز برای اجرا در حالت استفاده از Clustered Index کمتر از یک ثانیه و در حالت استفاده از None Clustered Index حدود ۱۰ ثانیه بود! دقت کنید که ۳ میلیون رکورد در جدول amort و ۷ هزار رکورد در select استفاده کننده function من یعنی ۲۱ میلیارد حالت!!

 

پ. ن.: برای تولید ۳ میلیون رکورد از مقادیر اتفاقی int و تاریخ در یک حلقه while استفاده کردم. نحوه تولید «تاریخ» به صورت random در اینجا و اینجا بحث شده است.