مطالب
OpenCVSharp #11
خوشه بندی تصویر به کمک الگوریتم K-Means توسط OpenCV

الگوریتم k-Means clustering را می‌توان به کمک یک مثال بهتر بررسی کرد. فرض کنید شرکت منسوجاتی قرار است پیراهن‌های جدیدی را به بازار ارائه کند. بدیهی است برای فروش بیشتر، بهتر است پیراهن‌هایی را با اندازه‌های متفاوتی تولید کرد تا برای عموم مردم مفید باشد. اما ... برای این شرکت مقرون به صرفه نیست تا برای تمام اندازه‌های ممکن، پیراهن تولید کند. بنابراین اندازه‌های اشخاص را در سه گروه کوچک، متوسط و بزرگ تعریف می‌کند. این گروه بندی را می‌توان توسط الگوریتم k-means clustering نیز انجام داد و به کمک آن به سه اندازه‌ی بسیار مناسب رسید تا برای عموم اشخاص مناسب باشد. حتی اگر این سه گروه ناکافی باشند، این الگوریتم می‌تواند تعداد خوشه بندی‌های متغیری را دریافت کند تا بهینه‌ترین پاسخ حاصل شود. [برای مطالعه بیشتر]

ارتباط الگوریتم k-means clustering با مباحث پردازش تصویر، در پیش پردازش‌های لازمی است که جهت سرفصل‌هایی مانند تشخیص اشیاء، آنالیز صحنه، ردیابی و امثال آن ضروری هستند. از الگوریتم خوشه بندی k-means عموما جهت مفهومی به نام Color Quantization یا کاهش تعداد رنگ‌های تصویر استفاده می‌شود. یکی از مهم‌ترین مزایای این کار، کاهش فشار حافظه و همچنین بالا رفتن سرعت پردازش‌های بعدی بر روی تصویر است. همچنین گاهی از اوقات برای چاپ پوسترها نیاز است تعداد رنگ‌های تصویر را کاهش داد که در اینجا نیز می‌توان از این الگوریتم استفاده کرد.


پیاده سازی الگوریتم خوشه بندی K-means

در ادامه کدهای بکارگیری متد kmeans کتابخانه‌ی OpenCV را به کمک OpenCVSharp مشاهده می‌کنید:
var src = new Mat(@"..\..\Images\fruits.jpg", LoadMode.AnyDepth | LoadMode.AnyColor);
Cv2.ImShow("Source", src);
Cv2.WaitKey(1); // do events
 
Cv2.Blur(src, src, new Size(15, 15));
Cv2.ImShow("Blurred Image", src);
Cv2.WaitKey(1); // do events
 
// Converts the MxNx3 image into a Kx3 matrix where K=MxN and
// each row is now a vector in the 3-D space of RGB.
// change to a Mx3 column vector (M is number of pixels in image)
var columnVector = src.Reshape(cn: 3, rows: src.Rows * src.Cols);
 
// convert to floating point, it is a requirement of the k-means method of OpenCV.
var samples = new Mat();
columnVector.ConvertTo(samples, MatType.CV_32FC3);
 
for (var clustersCount = 2; clustersCount <= 8; clustersCount += 2)
{
    var bestLabels = new Mat();
    var centers = new Mat();
    Cv2.Kmeans(
        data: samples,
        k: clustersCount,
        bestLabels: bestLabels,
        criteria:
            new TermCriteria(type: CriteriaType.Epsilon | CriteriaType.Iteration, maxCount: 10, epsilon: 1.0),
        attempts: 3,
        flags: KMeansFlag.PpCenters,
        centers: centers);
 
 
    var clusteredImage = new Mat(src.Rows, src.Cols, src.Type());
    for (var size = 0; size < src.Cols * src.Rows; size++)
    {
        var clusterIndex = bestLabels.At<int>(0, size);
        var newPixel = new Vec3b
        {
            Item0 = (byte)(centers.At<float>(clusterIndex, 0)), // B
            Item1 = (byte)(centers.At<float>(clusterIndex, 1)), // G
            Item2 = (byte)(centers.At<float>(clusterIndex, 2)) // R
        };
        clusteredImage.Set(size / src.Cols, size % src.Cols, newPixel);
    }
 
    Cv2.ImShow(string.Format("Clustered Image [k:{0}]", clustersCount), clusteredImage);
    Cv2.WaitKey(1); // do events
}
 
Cv2.WaitKey();
Cv2.DestroyAllWindows();
با این خروجی


توضیحات

- ابتدا تصویر اصلی برنامه بارگذاری می‌شود و در یک پنجره نمایش داده خواهد شد. در اینجا متد Cv2.WaitKey را با پارامتر یک، مشاهده می‌کنید. این فراخوانی ویژه‌، شبیه به متد do events در برنامه‌های WinForms است. اگر فراخوانی نشود، تمام تصاویر پنجره‌های مختلف برنامه تا زمان پایان پردازش‌های مختلف برنامه، نمایش داده نخواهند شد و تا آن زمان صرفا یک یا چند پنجره‌ی خاکستری رنگ را مشاهده خواهید کرد.
- در ادامه متد Blur بر روی این تصویر فراخوانی شده‌است تا مقداری تصویر را مات کند. هدف از بکارگیری این متد در این مثال، برجسته کردن خوشه بندی گروه‌های رنگی مختلف در تصویر اصلی است.
- سپس متد Reshape بر روی ماتریس تصویر اصلی بارگذاری شده فراخوانی می‌شود.
هدف از بکارگیری الگوریتم k-means، انتساب برچسب‌هایی به هر نقطه‌ی RGB تصویر است. در اینجا هر نقطه به شکل یک بردار در فضای سه بعدی مشاهده می‌شود. سپس سعی خواهد شد تا این MxN بردار، به k قسمت تقسیم شوند.
متد Reshape تصویر اصلی MxNx3 را به یک ماتریس Kx3 تبدیل می‌کند که در آن K=MxN است و اکنون هر ردیف آن برداری است در فضای سه بعدی RGB.
- پس از آن توسط متد ConvertTo، نوع داده‌های این ماتریس جدید به float تبدیل می‌شوند تا در متد kmeans قابل استفاده شوند.
- در ادامه یک حلقه را مشاهده می‌کنید که عملیات کاهش رنگ‌های تصویر و خوشه بندی آن‌ها را 4 بار با مقادیر مختلف clustersCount انجام می‌دهد.
- در متد kmeans، پارامتر data یک ماتریس float است که هر نمونه‌ی آن در یک ردیف قرار گرفته‌است. K بیانگر تعداد خوشه‌ها، جهت تقسیم داده‌ها است.
در اینجا پارامترهای labels و centers خروجی‌های متد هستند. برچسب‌ها بیانگر اندیس‌های هر خوشه به ازای هر نمونه هستند. Centers ماتریس مراکز هر خوشه است و دارای یک ردیف به ازای هر خوشه است.
پارامتر criteria آن مشخص می‌کند که الگوریتم چگونه باید خاتمه یابد که در آن حداکثر تعداد بررسی‌ها و یا دقت مورد نظر مشخص می‌شوند.
پارامتر attempts مشخص می‌کند که این الگوریتم چندبار باید اجرا شود تا بهترین میزان فشردگی و کاهش رنگ حاصل شود.
- پس از پایان عملیات k-means نیاز است تا اطلاعات آن مجددا به شکل ماتریسی هم اندازه‌ی تصویر اصلی برگردانده شود تا بتوان آن‌را نمایش داد. در اینجا بهتر می‌توان نحوه‌ی عملکرد متد k-means را درک کرد. حلقه‌ی تشکیل شده به اندازه‌ی تمام نقاط طول و عرض تصویر اصلی است. به ازای هر نقطه، توسط الگوریتم k-means یک برچسب تشکیل شده (bestLabels) که مشخص می‌کند این نقطه متعلق به کدام خوشه و cluster رنگ‌های کاهش یافته است. سپس بر اساس این اندیس می‌توان رنگ این نقطه را از خروجی centers یافته و در یک تصویر جدید نمایش داد.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
OpenCVSharp #11
به طور مشخص با توجه به توضیحات بالا می‌توان گفت اگر مقدار k در الگوریتم k-means را برابر 2 در نظر بگیریم، تصویر ما به دو رنگ کوانتیزه  می‌شود
            using (var src = new Mat(@"test.jpg", LoadMode.Color))
            {
                image1.Source = KMeans(src, 2).ToWriteableBitmap();
                image2.Source = Binary(src).ToWriteableBitmap();
            }
که تقریبا با تصویر بدست آمده پس از باینری کردن آن معادل است 
        private static Mat Binary(Mat src)
        {
            // Convert the image to Gray
            src = src.CvtColor(ColorConversion.RgbToGray);
            // Convert the Gray to Binary with auto threshold
            Cv2.Threshold(src, src, 0, 255, ThresholdType.Binary | ThresholdType.Otsu);
            // Convert the Gray to Binary with manual threshold
            //Cv2.Threshold(src, src, 127, 255, ThresholdType.Binary);

            return src;
        }

        private static Mat KMeans(Mat src, int k)
        {
            var columnVector = src.Reshape(cn: 3, rows: src.Rows * src.Cols);
            var samples = new Mat();
            columnVector.ConvertTo(samples, MatType.CV_32FC3);

            var bestLabels = new Mat();
            var centers = new Mat();
            Cv2.Kmeans(
                data: samples,
                k: k,
                bestLabels: bestLabels,
                criteria: new TermCriteria(type: CriteriaType.Epsilon | CriteriaType.Iteration, maxCount: 10, epsilon: 1.0),
                attempts: 3,
                flags: KMeansFlag.PpCenters,
                centers: centers);


            var dst = new Mat(src.Rows, src.Cols, src.Type());
            for (var size = 0; size < src.Cols * src.Rows; size++)
            {
                var clusterIndex = bestLabels.At<int>(0, size);
                var newPixel = new Vec3b
                {
                    Item0 = (byte)(centers.At<float>(clusterIndex, 0)), // B
                    Item1 = (byte)(centers.At<float>(clusterIndex, 1)), // G
                    Item2 = (byte)(centers.At<float>(clusterIndex, 2)) // R
                };
                dst.Set(size / src.Cols, size % src.Cols, newPixel);
            }

            return dst;
        }

نتایج :

مطالب
آموزش MDX Query - قسمت دوازدهم – استفاده از توابع Head , Filter , TopCount , tail
در ادامه به بررسی توابع Head , Filter , TopCount و tail  می‌پردازیم

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Internet Tax Amount]
} on columns,
head(
[Customer].[Customer Geography].[Country],
2
)on rows
From [Adventure Works]

تابع Head، تعداد مشخص شده بر اساس پارامتر اول از آن محور را بر اساس نحوه‌ی نمایش تنظیم شده در SSAS، واکشی می‌کند. 

حال تصور کنید بخواهیم شرط زیر را بر روی کوئری بالا اجرا کنیم

( [Measures].[Internet Sales Amount] >= '2500000' )

به عبارت دیگر ما می‌خواهیم دو کشوری را انتخاب کنیم که میزان فروش اینترنتی آنها بالای 2500000 باشد.

کوئری مشابه زیر می‌باشد

 Select  {
[Measures].[Internet Sales Amount],
[Measures].[Internet Tax Amount]
} on columns,
head(
[Customer].[Customer Geography].[Country],
2
)on rows
From [Adventure Works]
Where
( [Measures].[Internet Sales Amount] >= '2500000' )

البته خطای زیر را خواهیم داشت. 

به یاد داشته باشیم در صورتیکه بخواهیم ایجاد محدودیت در نمایش داده‌ها را در یک محور داشته باشیم، باید از تابع Filter استفاده کنیم؛ به صورت زیر:

Select
Filter(
{
[Measures].[Internet Sales Amount],
[Measures].[Internet Tax Amount]
} ,
[Measures].[Internet Sales Amount] >= 2644017.71
  ) on columns,
head(
[Customer].[Customer Geography].[Country],
3
)on rows
From [Adventure Works]

تابع Filter دو پارامتر می گیرد. پارامتر اول نام ردیف یا ستونی می باشد که روی آن می خواهیم عمل فیلتر را انجام دهیم. پارامتر دوم شرط فیلترینگ می باشد که می بایست مانند T/SQL دارای یک خروجی Boolean باشد

همچنان نتیجه درست نمی‌باشد ! چرا؟

اگر بخواهیم شرط روی Axis  ردیف (کشور ها) اعمال گردد، باید عملیات فیلترینگ در این  Axis  انجام شود . بنابر این خروجی بدست  آمده صحیح نمی باشد زیرا ما عملیات فیلترینگ را روی ستون ها انجام داده ایم.

کوئری زیر را اجرا نمایید

Select {
[Measures].[Internet Sales Amount]
,[Measures].[Internet Tax Amount]
  }
on columns,
head(
Filter(
  [Customer].[Customer Geography].[Country] ,
  [Measures].[Internet Sales Amount] >= 2644017.71
),
3)
on rows
From [Adventure Works]

البته توجه کنید که این کوئری، سه کشور اول که در شرط زیر قرار دارند را بر می گرداند و الزاما این سه کشور از تمام  کشور های دیگر بیشتر نمی باشند.

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

اگر بخواهیم سه کشوری را که بالاترین میزان فروش را دارند پیدا کنیم و شرط هم همواره اعمال گردد، کوئری زیر درست می باشد:

Select {
[Measures].[Internet Sales Amount]
,[Measures].[Internet Tax Amount]
  }
on columns,
TopCount(
Filter(
  [Customer].[Customer Geography].[Country] ,
  [Measures].[Internet Sales Amount] >= 2644017.71
  ),
3, [Measures].[Internet Sales Amount])
on rows
From [Adventure Works]

در این حالت به جای تابع Head از تابع  TopCount استفاده گردیده است .این تابع سه کشوری را که بیشترین فروش اینترنتی را داشته اند و این فروش بالاتر از مقدار ذکر شده در شرط می‌ باشد را بر می‌ گرداند .البته در اینجا تابع  topcount  دارای سه پارامتر می‌ باشد و در پارامتر سوم اعلام میکند که تعداد بالای مجموعه  براساس چه شاخصی باید به دست بیاید.

حال اگر بخواهیم سه ردیف انتهایی جدول را واکشی کنیم داریم:

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Internet Tax Amount]
}on columns,
tail([Customer].[Customer Geography].[Country],
3)on rows
From [Adventure Works]

این تابع برعکس تابع Head  کار میکند و N ردیف آ اخر مجموعه را بدست می‌ آورد . البته در بالا فقط 3 ردیف انتهایی را در خروجی آورده ایم و هیچ شرطی اعمال نگردیده است.


اشتراک‌ها
NET Core 2.1.5. منتشر شد

Today, we are releasing the .NET Core September 2018 Update. This update includes .NET Core 2.1.5 and .NET Core SDK 2.1.403 and contains important reliability fixes. 

NET Core 2.1.5. منتشر شد
مطالب
آموزش MDX Query - قسمت هشتم – کار بر روی ساختار های سلسله مراتبی

اگر دایمنشنی بیشتر از یک ساختار سلسله مراتبی باشد ، باید حتما مشخص شود که اعضای کدام ساختار سلسله مراتبی مورد نظر ما می باشد . بنابراین کوئری‌های زیر دارای خطا می‌باشند.

Select
[Customer].members on columns
From [Adventure Works]
GO
Select
[Scenario].members on columns
From [Adventure Works]

دو کوئری زیر را در نظر بگیرید :

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].members on rows
From [Adventure Works]

و

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Country].members on rows
From [Adventure Works]

تفاوت این دو کوئری در واکشی اطلاعات از اعضای سطح ساختار سلسله مراتبی دایمنشن می‌باشد. برای توضیح بیشتر ابتدا به خروجی آن‌ها نگاهی بیندازید.

در کوئری اول تمامی سطوح موجود در ساختار سلسله مراتبی دایمنشن مورد نظر واکشی می‌شود. در کوئری دوم فقط سطح کشور از ساختار سلسله مراتبی واکشی می‌شود.

ساختار سلسله مراتبی دایمنشن [ Customer ] در شکل زیر نمایش داده شده است.


امکان واکشی عضوهای یک عضو از ساختار سلسله مراتبی وجود ندارد

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Country].[France].members on rows
From [Adventure Works]

دقت کنید که France خودش عضو سطح Country در ساختار سلسله مراتبی Customer Geography در دایمنشن Customer می‌باشد و دیگر امکان واکشی اعضای این عضو وجود ندارد

بچه‌های یک عضو را به شکل زیر می‌توان واکشی نمود.

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Country].[France].children on rows
From [Adventure Works]

برای بدست آوردن بچه های یک عضو در یک ساختار سلسله مراتبی، از  children استفاده می شود .


دقت داشته باشید، همانگونه که عضو، دارای Members نمی‌باشد، یک بچه هم دارای Children نمی‌باشد. بنابراین کوئری زیر با خطا مواجه می‌شود.

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Country].[France].children.children on rows
From [Adventure Works]

البته شاید تصور می‌شد که باید به عنوان یک ساختار سلسله مراتبی بعد از کشور، استان و سپس شهر‌ها واکشی شوند. اما برای واکشی ساختار سلسله مراتبی باید به صورت زیر عمل کرد.

Select
[Measures].[Internet Sales Amount] on columns,
descendants(
[Customer].[Customer Geography].[Country].[France],
[Customer].[Customer Geography].[City]
) on rows
From [Adventure Works]

عملا برای یافتن شهر‌های کشور فرانسه، باید دو سطح در ساختار سلسله مراتبی پایین برویم. برای این منظور از تابع descendants () استفاده کرده‌ایم و به خاطر داشته باشید که نوشتن [France].children.children مجاز نمی‌باشد. همچنین در تابع فوق دو پارامتر ورودی داریم؛ اولی مشخص کننده‌ی یک عضو از سطح مبدا می‌باشد و دومین پارامتر مشخص کننده‌ی سطحی است که باید واکشی در آن صورت بگیرد.


برای مشخص‌تر شدن موضوع به مثال زیر توجه کنید

Select
[Measures].[Internet Sales Amount] on columns,
descendants(
[Customer].[Customer Geography].[Country].[France],
[Customer].[Customer Geography].[Customer]
) on rows
From [Adventure Works]

در این مثال واکشی در چهار سطح پایین‌تر انجام شده است و به این معنی می‌باشد که ما مشتریان کشور فرانسه را در ردیف‌ها واکشی کرده‌ایم.

عدم وجود یک Member :

در صورت عدم وجود یک عضو در واکشی، خروجی خالی می‌باشد و خطا نمایش داده نمی‌شود.

Select
[Customer].[Customer Geography].[Customer].[Crystal Zhen] on columns
From [Adventure Works]

مشتری با نام Crystal Zhen وجود ندارد؛ بنابراین نتیجه خالی می‌باشد.

پیدا کردن یک عضو در مجموعه ایی از اعضا، البته اگر این عضو درآ  ن مجموعه وجود نداشته باشد خروجی خالی می باشد.

Select
exists(
[Customer].[Customer Geography].[Customer].[Crystal Zheng],
[Customer].[Customer Geography].members
  )on columns
From [Adventure Works]

در نوشتن کوئری‌ها دقت کنید که حتما بین دایمنشن‌ها و شاخص‌ها در SSAS ارتباط ایجاد شده باشد. در غیر این صورت خروجی نامعتبر خواهد بود. به عنوان مثال در کوئری زیر بین شاخص [Measures].[Reseller Sales Amount] و مشتری ارتباطی وجود ندارد (این ارتباط باید در هنگام ساخت Cube  در SSAS  ایجاد می‌گردید تا در هنگام Deploy  کردن محاسبات لازم صورت بگیرد) بنابر این در خروجی شاهد نمایش شاخص پیش فرض Cube می‌باشیم.

Select
[Measures].[Reseller Sales Amount] on columns,
[Customer].[Customer Geography].[Customer].members on rows
From [Adventure Works]

قبلا گفته شد که هر سطح دارای Members می‌باشد و میتوان برای هر عضو Children‌های آن عضو را نمایش داد، با این وجود دقت کنید که اعضای آخرین سطح ساختار سلسله مراتبی دارای Children نمی‌باشند.

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Customer].[Crystal Zheng].children on rows
From [Adventure Works]

خروجی خالی می‌باشد.

بدست آوردن پدر یک عضو در ساختار سلسله مراتبی :

کوئری زیر را اجرا کنید

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Customer].[Crystal Zheng].parent on rows
From [Adventure Works]

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

برای بدست آوردن پدربزرگ یک ساختار سلسله مراتبی داریم :

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Customer].[Crystal Zheng].parent.parent on rows
From [Adventure Works]

و برای بدست آوردن پدر در 4 سطح بالاتر داریم

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Customer].[Crystal Zheng].parent.parent.parent.parent
on rows
From [Adventure Works]

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

Select
[Measures].[Internet Sales Amount] on columns,
[Customer].[Customer Geography].[Customer].[Crystal Zheng].parent
.parent.parent.parent.parent.parent
on rows
From [Adventure Works]

برای بدست آوردن نیا می‌توانیم از تابع ancestor() استفاده کنیم.

بدست آوردن نیا برای یک مشتری در سطح شهر

Select
[Measures].[Internet Sales Amount] on columns,
ancestor(
[Customer].[Customer Geography].[Customer].[Crystal Zheng],
[Customer].[Customer Geography].[City]
)on rows
From [Adventure Works]

بدست آوردن نیا برای یک مشتری در سطح شهر

Select
[Measures].[Internet Sales Amount] on columns,
ancestor(
[Customer].[Customer Geography].[Customer].[Crystal Zheng],
[Customer].[Customer Geography].[City]
)on rows
From [Adventure Works]

پارامتر دوم در تابع ancestor می‌تواند هم مشخص کننده‌ی یک سطح باشد توسط نام آن سطح و هم می‌تواند یک عدد باشد که مشخص کننده‌ی تعداد سطحی است که باید در واکشی اطلاعات از ساختار سلسله مراتبی بالا برویم.

Select
[Measures].[Internet Sales Amount] on columns,
ancestor(
[Customer].[Customer Geography].[Customer].[Crystal Zheng],
2
) on rows
From [Adventure Works]

بدست آوردن نیا برای یک مشتری در دو سطح بالاتر

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

Select
[Measures].[Internet Sales Amount] on columns,
ascendants(
[Customer].[Customer Geography].[Customer].[Crystal Zheng]
  ) on rows
From [Adventure Works]

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

مرتب سازی اعضای یک ساختار سلسله مراتبی از بالا به پایین به صورت نزولی

select
[Measures].[Internet Sales Amount] on columns,
hierarchize(
ascendants(
[Customer].[Customer Geography].[Customer].[Crystal Zheng]
)
) on rows
From [Adventure Works]

برای مرتب سازی یک ساختار سلسله مراتبی از تابع hierarchize() استفاده می‌کنیم. دقت داشته باشید که کار برروی ساختار‌های سلسله مراتبی یکی از قسمت‌های اصلی در MDX Query ‌ها می‌باشد.

ادامه‌ی این مطالب و کار با ساختار‌های سلسله مراتبی را در مقالات بعدی دنبال خواهیم کرد.

اشتراک‌ها
بهبودهای EF Core 2.1 در زمینه پشتیبانی از DDD

Entity Framework half-heartedly supported Domain-Driven Design patterns. But the new-from-scratch EF Core has brought new hope for developers to map your well-designed domain classes to a database, reducing the cases where a separate data model is needed. EF Core 2.1 is very DDD friendly, even supporting things like fully encapsulated collections, backing fields and the return of support for value objects. In this session, we'll review some well-designed aggregates and explore how far EF Core 2.1 goes to act as the mapper between your domain classes and your data store. 

بهبودهای EF Core 2.1 در زمینه پشتیبانی از DDD
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت ششم - بررسی عملگرهای دسترسی به داده‌ها در یک Query Plan
پس از آشنایی مقدماتی با نحوه‌ی خواندن یک Query Plan، اکنون نوبت به بررسی عملگرهایی است که در آن مشاهده می‌شوند و همچنین تغییرات در کوئری‌ها چگونه بر روی آن‌ها تاثیر گذاشته و آن‌ها را تغییر می‌دهند و این تغییرات چه تاثیری را بر روی کارآیی خواهند داشت.


عملگرهای Scans و Seeks

در حالت کلی می‌توان دو نوع جدول بدون و با ایندکس را درنظر گرفت. در حالت جداول بدون ایندکس، برای جستجوی اطلاعات نیاز به Table Scan وجود دارد و برعکس آن شامل یک Clustered index scan خواهد بود. گاهی از اوقات Clustered index scanها بهترین روش دریافت اطلاعات هستند و گاهی از اوقات خیر و نیاز به بررسی بیشتری دارند. بنابراین قانون کلی، حذف آن‌ها به محض مشاهده، نیست.
نوع دیگر عملگرهای دسترسی به داده‌ها، Seeks هستند که شامل Clustered index seeks و Non-clustered index seeks می‌شوند. در بسیاری از موارد عنوان می‌شود که Seeks کارآیی بهتری را به همراه دارند. هرچند این مورد نیاز به بررسی بیشتری دارد که در ادامه با مثال‌هایی آن‌ها را مرور خواهیم کرد.


بررسی عملگر Table scan در یک Query Plan

در ادامه تعدادی از عملگرهای مرتبط با data access را از لحاظ نحوه‌ی انتخاب و تغییر آن‌ها توسط بهینه ساز کوئری‌های SQL Server بررسی می‌کنیم. برای این منظور ابتدا در management studio از منوی Query، گزینه‌ی Include actual execution plan را انتخاب می‌کنیم. سپس کوئری‌های زیر را اجرا می‌کنیم:
SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
GO

SELECT *
INTO [Sales].[Copy_Orders]
FROM [Sales].[Orders];
GO

SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Copy_Orders]
WHERE [CustomerID] > 550;
GO
در اینجا در ابتدا، تمام رکوردهای جدول [Sales].[Orders]، به جدول [Sales].[Copy_Orders] کپی می‌شوند. سپس یک کوئری را بر روی این جدول کپی، اجرا کرده‌ایم.


همانطور که مشاهده می‌کنید، برای برآورده کردن قسمت where این کوئری، یک Table Scan صورت گرفته‌است؛ چون این جدول کپی، به همراه هیچ ایندکسی نیست. به همین جهت برای یافتن رکوردهای مدنظر، راه دیگری بجز اسکن کل جدول بانک اطلاعاتی وجود ندارد که بسیار ناکارآمد است.
همچنین اگر به برگه‌ی messages دقت کنیم، با توجه به روشن بودن STATISTICS IO، میزان logical reads نیز قابل مشاهده‌است:
(33035 rows affected)
Table 'Copy_Orders'. Scan count 1, logical reads 689, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
به علاوه اجرای آن نیز کمی بیشتر از نیم ثانیه، طول کشیده‌است:
SQL Server Execution Times:
CPU time = 79 ms,  elapsed time = 762 ms.


بررسی عملگر Index Seek در یک Query Plan

اکنون سؤال اینجا است که آیا می‌توان این وضعیت را بهبود بخشید؟
بله. برای این منظور یک NONCLUSTERED INDEX را بر روی جدول کپی، ایجاد می‌کنیم؛ به نحوی که CustomerID لحاظ شده‌ی در قسمت where کوئری را پوشش دهد:
CREATE NONCLUSTERED INDEX [IX_Copy_Orders_CustomerID]
ON [Sales].[Copy_Orders] (
[CustomerID]
)
INCLUDE (
[OrderID], [OrderDate]
);
GO
چون مطابق کوئری، [OrderID] و [OrderDate] در قسمت where ذکر نشده‌اند، در اینجا INCLUDE شده‌اند.

در ادامه مجددا همان کوئری را اجرا می‌کنیم:
SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Copy_Orders]
WHERE [CustomerID] > 550;
GO
که سبب تولید کوئری پلن زیر می‌شود:


اینبار عملگر Table Scan قبلی به یک عملگر Index Seek بر روی NONCLUSTERED INDEX تعریف شده، تغییر کرده‌است و اگر به آمار I/O آن دقت کنیم، logical reads 106 قابل مشاهده‌است که بهبود قابل ملاحظه‌ای است نسبت به عدد 689 قبلی.


بررسی عملگر Clustered index scan در یک Query Plan

در ادامه همین کوئری را بر روی جدول [Sales].[Orders] اصلی اجرا می‌کنیم:
SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Orders]
WHERE [CustomerID] > 550;
GO
که به صورت پیش‌فرض شامل این ایندکس‌ها است:


اجرای کوئری فوق، چنین کوئری پلنی را تولید می‌کند:


جدول [Sales].[Orders]، یک CLUSTERED INDEX را بر روی [OrderID] دارد و یک NONCLUSTERED INDEX را بر روی [CustomerID].
در کوئری پلن تولید شده، یک Clustered index scan مشاهده می‌شود. علت اینجا است که هرچند در جدول [Sales].[Orders] یک NONCLUSTERED INDEX بر روی  [CustomerID] تعریف شده‌است:
CREATE NONCLUSTERED INDEX [FK_Sales_Orders_CustomerID] ON [Sales].[Orders]
(
[CustomerID] ASC
)
اما قسمت INCLUDE ایندکس قبلی را که تعریف کردیم، ندارد و به همراه [CustomerID] و [OrderDate] نیست. به همین جهت اینبار logical reads 692 است.

بنابراین وجود عملگر Clustered index scan در یک کوئری پلن، یعنی نیاز به خواندن و اسکن کل جدول وجود دارد. برای اثبات آن، همین کوئری قبلی را که بر روی [Sales].[Orders] انجام دادیم، اینبار بدون قسمت where آن اجرا کنید. یعنی کوئری بر روی کل جدول انجام شود:
SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Orders]
سپس به برگه‌ی messages مراجعه کرده و عدد logical reads آن‌را مشاهده کنید. این عدد دقیقا با عدد logical reads کوئری where دار، یکی است؛ که بیانگر اسکن کامل جدول در حالت Clustered index scan است.

سؤال: آیا Clustered index scan همواره کل یک جدول را اسکن می‌کند؟
پاسخ: خیر. اگر یک کوئری برای مثال دارای top/min/max باشد، کل جدول اسکن نخواهد شد:
SELECT TOP 10
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Orders]
WHERE [CustomerID] > 550;
تفاوت این کوئری با کوئری‌های قبلی، در داشتن یک top 10 است. اگر آن‌را اجرا کنیم، به کوئری پلن زیر خواهیم رسید:


هرچند در اینجا هم یک Clustered index scan صورت گرفته، اما اگر به برگه‌ی messages آن مراجعه کنیم، آمار I/O آن بیانگر تنها logical reads 5 است که معادل اسکن کل جدول نیست:
(10 rows affected)
Table 'Orders'. Scan count 1, logical reads 5, physical reads 0, read-ahead reads 510, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.


مقایسه‌ی عملگرهای Index Scan و Index Seek

ابتدا کوئری زیر را اجرا می‌کنیم:
SELECT
    [CustomerID],
    [OrderID]
FROM [Sales].[Orders]
WHERE [OrderID] > 30000;
این کوئری با کوئری قبلی از لحاظ قسمت select اندکی متفاوت بوده و در آن OrderDate حذف شده‌است. در قسمت where نیز کوئری بر روی OrderID صورت گرفته‌است.
در این جدول ایندکسی بر روی CustomerID وجود دارد و همچنین کلید اصلی جدول، OrderID است.

پس از اجرای این کوئری، به کوئری پلن زیر خواهیم رسید:


که بیانگر یک Index Scan است و نکته‌ی جالب آن، استفاده‌ی از ایندکس FK_Sales_Orders_CustomerID می‌باشد (نام این شیء، ذیل آیکن عملگر، مشخص است). یعنی SQL Server در اینجا از یک non-clustered index تعریف شده‌ی بر روی CustomerID استفاده کرده‌است.
اکنون اگر OrderID را تغییر دهیم چه اتفاقی رخ می‌دهد؟
SELECT
    [CustomerID],
    [OrderID]
FROM [Sales].[Orders]
WHERE [OrderID] > 60000;
اینبار به یک clustered index seek رسیدیم که بر روی کلید اصلی جدول یا همان PK_Sales_Orders که ذیل عملگر مشخص شده، رخ داده‌است:


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

اگر این دو کوئری را با هم اجرا کنیم (در طی یک batch)، به پلن مقایسه‌ای زیر خواهیم رسید که در آن هزینه‌ی Index Scan بیشتر است از clustered index seek:


به همراه آمار CPU و I/O ای به صورت زیر که اولی مرتبط است با index scan و دومی با clustered index seek:
(43595 rows affected)
Table 'Orders'. Scan count 1, logical reads 191, physical reads 1, read-ahead reads 182, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
CPU time = 31 ms,  elapsed time = 754 ms.


(13595 rows affected)
Table 'Orders'. Scan count 1, logical reads 131, physical reads 0, read-ahead reads 127, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
CPU time = 16 ms,  elapsed time = 276 ms.
به همین جهت است که عنوان می‌شود، scanها خوب نیستند و seekها بهترند.