مطالب
امنیت در LINQ to SQL

سؤال: LINQ to SQL تا چه میزان در برابر حملات تزریق SQL امن است؟
جواب کوتاه: بسیار زیاد!

توضیحات:
string query = @"SELECT * FROM USER_PROFILE
WHERE LOGIN_ID = '"+loginId+@"' AND PASSWORD = '"+password+@"'";
گاهی از اوقات هر چقدر هم در مورد خطرات کوئری‌هایی از نوع فوق مقاله نوشته شود کافی نیست و باز هم شاهد این نوع جمع زدن‌ها و نوشتن کوئری‌هایی به شدت آسیب پذیر در حالت استفاده از ADO.Net کلاسیک هستیم. مثال فوق یک نمونه کلاسیک از نمایش آسیب پذیری در مورد تزریق اس کیوال است. یا نمونه‌ی بسیار متداول دیگری از این دست که با ورودی خطرناک می‌تواند تا نمایش کلیه اطلاعات تمامی جداول موجود هم پیش برود:
protected void btnSearch_Click(object sender, EventArgs e)
{
String cmd = @"SELECT [CustomerID], [CompanyName], [ContactName]
FROM [Customers] WHERE CompanyName ='" + txtCompanyName.Text
+ @"'";

SqlDataSource1.SelectCommand = cmd;

GridView1.Visible = true;
}
در اینجا فقط کافی است مهاجم با تزریق عبارت SQL مورد نظر خود، کوئری اولیه را کاملا غیرمعتبر کرده و از یک جدول دیگر در سیستم کوئری تهیه کند!
راه حلی که برای مقابله با آن در دات نت ارائه شده نوشتن کوئری‌های پارامتری است و در این حالت کار encoding اطلاعات ورودی به صورت خودکار توسط فریم ورک مورد استفاده انجام خواهد شد؛ همچنین برای مثال اس کیوال سرور، execution plan این نوع کوئری‌های پارامتری را همانند رویه‌های ذخیره شده، کش کرده و در دفعات آتی فراخوانی آن‌ها به شدت سریعتر عمل خواهد کرد. برای مثال:
SqlCommand cmd = new SqlCommand("SELECT UserID FROM Users WHERE UserName=@UserName AND Password=@Password");
cmd.Parameters.Add(new SqlParameter("@UserName", System.Data.SqlDbType.NVarChar, 255, UserName));
cmd.Parameters.Add(new SqlParameter("@Password", System.Data.SqlDbType.NVarChar, 255, Password));
dr = cmd.ExecuteReader();
if (dr.Read()) userId = dr.GetInt32(dr.GetOrdinal("UserID"));
زمانیکه از کوئری پارامتری استفاده شود، مقدار پارامتر، هیچگاه فرصت و قدرت اجرا پیدا نمی‌کند. در این حالت صرفا به آن به عنوان یک مقدار معمولی نگاه خواهد شد و نه جزء قابل تغییر بدنه کوئری وارد شده که در حالت جمع زدن رشته‌ها همانند اولین کوئری معرفی شده، تا حد انحراف کوئری به یک کوئری دلخواه مهاجم قابل تغییر است.

اما در مورد LINQ to SQL چطور؟
این سیستم به صورت پیش فرض طوری طراحی شده است که تمام کوئری‌های SQL نهایی حاصل از کوئری‌های LINQ نوشته شده توسط آن، پارامتری هستند. به عبارت دیگر این سیستم به صورت پیش فرض برای افرادی که دارای حداقل اطلاعات امنیتی هستند به شدت امنیت بالایی را به همراه خواهد آورد.
برای مثال کوئری LINQ زیر را در نظر بگیرید:
var products = from p in db.products
where p.description.StartsWith(_txtSearch.Text)
select new
{
p.description,
p.price,
p.stock

};
اکنون فرض کنید کاربر به دنبال کلمه sony باشد، آنچه که بر روی اس کیوال سرور اجرا خواهد شد، دستور زیر است (ترجمه نهایی کوئری فوق به زبان T-SQL) :
exec sp_executesql N'SELECT [t0].[description], [t0].[price], [t0].[stock]
FROM [dbo].[products] AS [t0]
WHERE [t0].[description] LIKE @p0',N'@p0 varchar(5)',@p0='sony%'
برای لاگ کردن این عبارات SQL یا می‌توان از SQL profiler استفاده نمود و یا خاصیت log زمینه مورد استفاده را باید مقدار دهی کرد:
 db.Log = Console.Out;
و یا می‌توان بر روی کوئری مورد نظر در VS.Net یک break point قرار داد و سپس از debug visualizer مخصوص آن استفاده نمود.

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

و یا همان مثال کلاسیک اعتبار سنجی کاربر را در نظر بگیرید:
public bool Validate(string loginId, string password)
{
DataClassesDataContext db = new DataClassesDataContext();

var validUsers = from user in db.USER_PROFILEs
where user.LOGIN_ID == loginId
&& user.PASSWORD == password
select user;

if (validUsers.Count() > 0) return true;
else return false;
}
کوئری نهایی T-SQL تولید شده توسط این ORM از کوئری LINQ فوق به شکل زیر است:
SELECT [t0].[LOGIN_ID], [t0].[PASSWORD]
FROM [dbo].[USER_PROFILE] AS [t0]
WHERE ([t0].[LOGIN_ID] = @p0) AND ([t0].[PASSWORD] = @p1)
و این کوئری پارامتری نیز در برابر حملات تزریق اس کیوال امن است.

تذکر مهم هنگام استفاده از سیستم LINQ to SQL :

اگر با استفاده از LINQ to SQL مجددا به روش قدیمی اجرای مستقیم کوئری‌های SQL خود همانند مثال زیر روی بیاورید (این امکان نیز وجود دارد)، نتیجه این نوع کوئری‌های حاصل از جمع زدن رشته‌ها، پارامتری "نبوده" و مستعد به تزریق اس کیوال هستند:
string sql = "select * from Trade where DealMember='" + this.txtParams.Text + "'";
var trades = driveHax.ExecuteQuery<Trade>(sql);
در اینجا باید در نظر داشت که اگر شخصی مجددا بخواهد از این نوع روش‌های کلاسیک استفاده کند شاید همان ADO.Net کلاسیک برای او کافی باشد و نیازی به تحمیل سربار یک ORM را به سیستم نداشته باشد. در این حالت برنامه از type safety کوئری‌های LINQ نیز محروم شده و یک لایه بررسی مقادیر و پارامترها را توسط کامپایلر نیز از دست خواهد داد.

اما روش صحیحی نیز در مورد بکارگیری متد ExecuteQuery وجود دارد. استفاده از این متد به شکل زیر مشکل را حل خواهد کرد:
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
"SELECT contactname FROM customers WHERE city = {0}", "Tehran");
در این حالت، پارامترهای بکارگرفته شده (همان {0} ذکر شده در کوئری) به صورت خودکار به پارامترهای T-SQL ترجمه خواهند شد و مشکل تزریق اس کیوال برطرف خواهد شد (به عبارت دیگر استفاده از +، علامت مستعد بودن به تزریق اس کیوال است و بر عکس).

Vote on iDevCenter
مطالب
امکان انجام محاسبات سمت کلاینت در EF Core
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی.  در حالت اول با <IEnumerable<T‌ها کار می‌کنیم که تمام عملیات در حافظه انجام می‌شود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبان‌های متفاوتی مانند SQL ترجمه می‌شوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع داده‌ی آن‌ها دقت کنیم، می‌توانیم نوع آن‌ها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که می‌تواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
var blogs = from blog in Blogs
   where blog.Name.Contains("Development")
   select blog;
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه می‌تواند بر اساس بانک‌های اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام می‌شود.
اما اکنون چطور؟
var blogs = from blog in Blogs
   where blog.Name.ComputeHash() == 0
   select blog;
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کرده‌ایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمه‌ی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئری‌هایی بهبود یافته‌است که به آن، محاسبات سمت کلاینت گفته می‌شود.


یک مثال: بررسی تاثیر ارزیابی‌های سمت کلاینت در EF Core

فرض کنید ساختار جدول بلاگ‌ها به صورت زیر است:
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }  
}
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کرده‌ایم:
    public static class StringExtensions
    {
        public static int ComputeHash(this string str)
        {
            var hash = 0;
            foreach (var ch in str)
            {
                hash += (int)ch;
            }
            return hash;
        }
    }
اکنون می‌خواهیم بلاگ‌هایی را پیدا کنیم که Hash مربوط به Url آن‌ها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
using (var context = new BloggingContext())
{
   var blogs = context.Blogs
     .Where(blog => blog.Url.ComputeHash() >= 10)
     .ToList();
   Console.WriteLine(blogs.First().Url);
}
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمی‌کند:
SELECT [blog].[BlogId], [blog].[Url]
   FROM [Blogs] AS [blog]
به این معنا که در ارزیابی‌های سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده می‌کند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آن‌را از کوئری حذف می‌کند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده می‌شود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کرده‌است، متد ComputeHash را در سمت کلاینت بر روی این نتیجه‌ی دریافتی اعمال می‌کند. یعنی مرحله‌ی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.


چگونه متوجه شویم که ارزیابی سمت کلاینت رخ داده‌است؟

EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابی‌های سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs
   .Select(b => new
   {
      IdUrlString = string.Join(", ", b.BlogId, b.Url),
   }).ToList();
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا می‌شود، اما بهتر است از وقوع یک چنین رخ‌دادهایی مطلع شویم:
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;");
                optionsBuilder.ConfigureWarnings(warnings =>
                {
                    warnings.Log(CoreEventId.IncludeIgnoredWarning);
                    warnings.Log(RelationalEventId.QueryClientEvaluationWarning);
                });
            }
        }
    }
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ می‌شود:
 warn: Microsoft.EntityFrameworkCore.Query[200500]
The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
عنوان کرده‌است که قابلیت ترجمه‌ی ComputeHash را به SQL نداشته و آن‌را در نهایت به صورت محلی و در سمت کلاینت محاسبه می‌کند.

اگر می‌خواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابی‌ها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).


تاثیر ارزیابی‌های سمت کلاینت بر روی کارآیی برنامه

هرچند قابلیت ارزیابی‌های سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبه‌ی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال می‌شود.
ب) این قابلیت ممکن است برنامه نویس‌ها را از تفکر در مورد یافتن روش‌های محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا می‌شود، اما اگر آن‌را به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs
   .Select(b => new
   {
     IdUrlString = b.BlogId + "," + b.Url
   }).ToList();
اینبار به یک خروجی SQL قابل محاسبه‌ی در سمت سرور، خواهیم رسید:
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString]
FROM [Blogs] AS [b]
به همین جهت حداقل لاگ کردن ارزیابی‌های سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: ClientSideEvaluation.zip
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت چهارم - شاخص‌های مهم اطلاعات آماری کوئری‌ها
تا اینجا با روش‌های مختلف جمع آوری اطلاعات آماری مرتبط با کوئری‌های اجرا شده‌ی در SQL Server آشنا شدیم. در این قسمت قصد داریم بررسی کنیم این اطلاعات جمع آوری شده، چه مفاهیمی را در بر دارند و مهم‌ترین‌های آن‌ها کدامند؟


شاخص‌های مهم بررسی کارآیی کوئری‌ها

در ابتدای بررسی هر کوئری، باید 4 شاخص بسیار مهم، مدنظر باشند:
- مدت زمان اجرای کوئری: هرچند بررسی مدت زمان اجرای کوئری، شاخص مهمی‌است، اما الزاما حاوی اطلاعات مفیدی در مورد آن کوئری نیست. برای مثال اگر یک کوئری زیاد طول می‌کشد، حتما به معنای وجود مشکلی با آن نیست؛ ممکن است اطلاعات زیادی را واکشی می‌کند یا ممکن است توسط عاملی سد شده‌است. در این موارد هرچند مشکلاتی وجود دارند، اما مستقیما مرتبط با آن کوئری نیستند.
- میزان مصرف CPU: میزان کاری که باید توسط CPU انجام شود تا کوئری به نتیجه برسد.
- I/O: در SQL Server می‌توان هم physical I/O و هم logical I/O را بررسی کرد. برای مثال اگر اطلاعات مورد درخواست توسط کوئری هم اکنون در حافظه موجود باشند، نیازی به physical I/O پرهزینه نخواهد بود و در مقابل آن logical I/O کم هزینه‌تر است.
- میزان مصرف حافظه

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


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

Management studio
درون Management studio می‌توان اطلاعات مرتبط با یک کوئری را به صورت زنده مشاهده کرد. البته این اطلاعات صرفا مرتبط با یک کوئری و یا تعدادی مشخص هستند؛ چون باید کوئری را به صورت دستی درون این برنامه اجرا کرد و سپس اطلاعات اجرای کوئری‌ها را دریافت نمود. اطلاعات آماری که توسط آن نیز ارائه می‌شود محدودیت‌هایی دارد. برای مثال مدت زمان اجرای کوئری و یا تعداد رکوردهای تحت تاثیر قرار گرفته شده را می‌توان مشاهده کرد. اما به اندازه‌ی اطلاعات ارائه شده‌ی در یک execution plan کامل نیست. به علاوه بازگشت اطلاعات حاصل از اجرای کوئری‌ها درون این برنامه، سربار خودش را داشته و سبب کند شدن برنامه می‌شود. در آخر اطلاعات ارائه شده‌ی توسط آن‌را نیز باید از قسمت‌های مختلفی جمع آوری و به صورت دستی ذخیره کرد.

Extended Events
توسط Extended Events نیز می‌توان همانند Management studio، اطلاعات آماری یک تک کوئری و یا یک batch را جمع آوری کرد؛ اما پس از ایجاد و تنظیم آن، به صورت خودکار اجرا می‌شود. در حین تعریف یک سشن Extended Events می‌توان شاخص‌های خاصی را انتخاب کرد و یا شرط‌های دقیقی را اعمال کرد. خروجی آن نیز به صورت خودکار در یک فایل ذخیره می‌شود.

Dynamic management objects
با استفاده از DMO's از نتایج آماری مرتبط با تک کوئری‌ها، به نتایج تجمعی حاصل از اجرای آن‌ها می‌رسیم. این نتایج نیز در plan cache ذخیره می‌شوند. به این معنا که اگر کش، تخلیه (با اجرای دستور DBCC FREEPROCCACHE) و یا سرور ری‌استارت شود، این اطلاعات از دست خواهند رفت. هدف آن بیشتر رفع اشکال کوئری‌هایی است که هم اکنون در حال اجرا هستند. اگر نیاز به اطلاعات دوره‌ای را داشته باشید، نیاز خواهید داشت تا با تهیه‌ی snapshotهایی از بانک اطلاعاتی، این تاریخچه را تکمیل کنید. به همین جهت Query Store ارائه شده‌است تا نیازی به اینکار نباشد.

Query Store
Query Store کار ذخیره سازی متن plan و آمار تجمعی مرتبط با آن‌را به صورت خودکار انجام می‌دهد و آن‌را درون بانک اطلاعاتی کاربر ذخیره می‌کند. به همین جهت با خالی شدن کش، برخلاف DMO's، اطلاعات آن حذف نمی‌شود.


مثالی از روش‌های مختلف جمع آوری اطلاعات آماری حاصل از اجرای کوئری‌ها در SQL Server

در ادامه قصد داریم با مثالی، خلاصه‌ای را از سه قسمتی که تاکنون بررسی کردیم، ارائه دهیم. برای این منظور ابتدا رویه‌ی ذخیره شده‌ی زیر را ایجاد می‌کنیم:
USE [WideWorldImporters];
GO

DROP PROCEDURE IF EXISTS [Application].[usp_GetPersonInfo];
GO

CREATE PROCEDURE [Application].[usp_GetPersonInfo]
    (@PersonID INT)
AS

SELECT
    [p].[FullName],
    [p].[EmailAddress],
    [c].[FormalName]
FROM [Application].[People] [p]
    LEFT OUTER JOIN [Application].[Countries] [c]
    ON [p].[PersonID] = [c].[LastEditedBy]
WHERE [p].[PersonID] = @PersonID;
GO
کار آن دریافت اطلاعات یک کاربر بر اساس ID او می‌باشد.

سپس یک سشن Extended event را با نام QueryPerf ایجاد می‌کنیم:
IF EXISTS (
SELECT *
FROM sys.server_event_sessions
WHERE [name] = 'QueryPerf')
BEGIN
    DROP EVENT SESSION [QueryPerf] ON SERVER;
END
GO

CREATE EVENT SESSION [QueryPerf]
ON SERVER
ADD EVENT sqlserver.sp_statement_completed(
WHERE ([duration]>(1000))),
ADD EVENT sqlserver.sql_statement_completed(
WHERE ([duration]>(1000))),
ADD EVENT sqlserver.query_post_execution_showplan
ADD TARGET package0.event_file(
SET filename=N'C:\Temp\QueryPerf\test.xel',max_file_size=(256))
WITH (
MAX_MEMORY=16384 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,
MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF);
GO
این سشن به رخ‌دادهای sql_statement_completed، sp_statement_completed و query_post_execution_showplan، اگر طول مدت آن کوئری بیش از 1 میلی ثانیه باشد، واکنش نشان می‌دهد. نتیجه‌ی نهایی را نیز در پوشه‌ی C:\Temp\QueryPerf ذخیره می‌کند (این پوشه را باید به صورت دستی ایجاد کنید).

در ادامه Query Store را نیز بر روی بانک اطلاعاتی WideWorldImporters فعال کرده و همچنین اگر اطلاعاتی از پیش در آن وجود دارند، پاک می‌شود.
USE [master];
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE = ON;
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
DATA_FLUSH_INTERVAL_SECONDS = 60,
INTERVAL_LENGTH_MINUTES = 5,
MAX_STORAGE_SIZE_MB = 100,
QUERY_CAPTURE_MODE = ALL,
SIZE_BASED_CLEANUP_MODE = AUTO,
MAX_PLANS_PER_QUERY = 200);
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE CLEAR;
GO

سپس هر آنچه را که در plan cache نیز وجود دارد، حذف می‌کنیم:
DBCC FREEPROCCACHE;
GO

اکنون سشن QueryPerf را که پیشتر ایجاد کردیم، آغاز می‌کنیم:
ALTER EVENT SESSION [QueryPerf]
ON SERVER
STATE = START;
GO
نتیجه‌ی آن‌را در قسمت management->extended events، با سبز شدن آیکن QueryPerf می‌توانید مشاهده کنید.


در ادامه چون می‌خواهیم نتایج آماری را در management studio نیز مشاهده کنیم، ابتدا جمع آوری شاخص‌های آماری را در یک پنجره‌ی جدید new query، فعال می‌کنیم:
SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
GO
SET STATISTICS XML ON;
GO

همچنین در منوی Query، گزینه‌ی Include client statistics را نیز انتخاب می‌کنیم تا مشخص شود که آیا عملیات insert/update/delete انجام شده‌است. چه تعداد ردیف تحت تاثیر اجرای این کوئری قرار گرفته‌اند. چه تعداد تراکنش انجام شده‌است. همچنین اطلاعات آماری شبکه و زمان نیز ارائه شوند.

پس از این تنظیمات، اکنون نوبت به اجرای کوئری‌های زیر رسیده‌است که یکی پارامتری است و دیگری AdHoc:
USE [WideWorldImporters];
GO

EXECUTE [Application].[usp_GetPersonInfo] 1234;
GO

SELECT
    [s].[StateProvinceName],
    [s].[SalesTerritory],
    [s].[LatestRecordedPopulation],
    [s].[StateProvinceCode]
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = 'United States';
GO
با اجرای آن، در management studio، برگه‌های messages و client statistics ظاهر می‌شوند که هر کدام اینبار اطلاعات آماری اجرای این کوئری را به همراه دارند. همچنین در قسمت results، امکان مشاهده‌ی query plan، به علت فعال بودن اطلاعات آماری XML، وجود دارد.




سپس سشن QueryPerf را متوقف و حذف می‌کنیم:
ALTER EVENT SESSION [QueryPerf]
ON SERVER
STATE = STOP;
GO

DROP EVENT SESSION [QueryPerf] ON SERVER;
GO
فایل خروجی با پسوند xel آن را که در پوشه‌ی C:\Temp\QueryPerf ذخیره شده‌است، می‌توان در management studio مشاهده کرد. البته در ابتدای نمایش آن، صرفا دو ستون name و timestamp را نمایش می‌دهد که می‌توان با انتخاب هر ردیف آن و سپس انتخاب و کلیک راست بر روی ردیف‌های details آن، گزینه‌ی Show Column in table را انتخاب کرد تا شاخص مدنظر، در ستون‌های گزارش نیز ظاهر شود.


اگر بخواهیم از عملیات صورت گرفته توسط DMO's کوئری بگیریم:
SELECT
    [qs].[last_execution_time],
    [qs].[execution_count],
    [qs].[total_elapsed_time],
    [qs].[total_elapsed_time]/[qs].[execution_count] [AvgDuration],
    [qs].[total_logical_reads],
    [qs].[total_logical_reads]/[qs].[execution_count] [AvgLogicalReads],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_query_stats [qs]
CROSS APPLY sys.dm_exec_sql_text([qs].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([qs].[plan_handle]) [p]
WHERE [t].[text] LIKE '%Countries%';
GO
که در آن تنها ردیف‌هایی که متن کوئری آن‌ها حاوی Countries است، فیلتر شده، به یک چنین خروجی خواهیم رسید:


همانطور که مشاهده می‌کنید، شاخص‌های چهارگانه‌ای که در ابتدای بحث معرفی شدند، در مورد کوئری پارامتری نوشته شده، وضعیت بسیار بهتری نسبت به کوئری AdHoc دوم دارند.

از Query Store هم می‌توان به صورت زیر کوئری گرفت (علاوه بر قسمت رابط کاربری Query Store که ذیل اشیاء مرتبط با بانک اطلاعاتی WideWorldImporters در management studio قابل مشاهده‌است):
USE [WideWorldImporters];
GO

SELECT
    [qsq].[query_id],
    [qst].[query_sql_text],
    CASE
WHEN [qsq].[object_id] = 0 THEN N'Ad-hoc'
ELSE OBJECT_NAME([qsq].[object_id])
END AS [ObjectName],
    [qsp].[plan_id],
    [rs].[count_executions],
    [rs].[avg_logical_io_reads],
    [rs].[avg_duration],
    TRY_CONVERT(XML, [qsp].[query_plan]),
    [rs].[last_execution_time],
    (DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())),
[rs].[last_execution_time])) AS [LocalLastExecutionTime]
FROM [sys].[query_store_query] [qsq]
    JOIN [sys].[query_store_query_text] [qst]
    ON [qsq].[query_text_id] = [qst].[query_text_id]
    JOIN [sys].[query_store_plan] [qsp]
    ON [qsq].[query_id] = [qsp].[query_id]
    JOIN [sys].[query_store_runtime_stats] [rs]
    ON [qsp].[plan_id] = [rs].[plan_id]
WHERE [qst].[query_sql_text] LIKE '%Countries%';
GO

اشتراک‌ها
لیست جدید کتاب‌های رایگان Microsoft

Largest FREE Microsoft eBook Giveaway! I’m Giving Away MILLIONS of FREE Microsoft eBooks again, including: Windows 10, Office 365, Office 2016, Power BI, Azure, Windows 8.1, Office 2013, SharePoint 2016, SharePoint 2013, Dynamics CRM, PowerShell, Exchange Server, System Center, Cloud, SQL Server and more! 

لیست جدید کتاب‌های رایگان Microsoft
مطالب
نحوه ذخیره کلمات عبور در SQL Server
در این مقاله قصد داریم با نحوه ذخیره کلمات عبور در SQL Server و نحوه کار با آن‌ها آشنا شویم.
به عنوان توسعه دهنده / مدیر، احتمالا از طریق لاگین ویندوزی به SQL Server دسترسی پیدا می‌کنید. با این حال طریق گزینه‌ی دیگری به صورت تغییر در Instance SQL به حالت مخلوط (Mixed mode)، و از طریق SQL Login به SQL نیز می‌توان به آن دسترسی پیدا کرد. این SQL Logins در دیتابیس Master ساخته می‌شوند و از طریق sys.server_principals می توان لیست آنها را مشاهده کرد.
همچنین اطلاعات اضافی در sys.sql_logins وجود دارند که از sys.server_principals ارث‌بری شده‌اند. این ۳ ستون اضافه شده، is_policy_checked ،is_expiration_checked و password_hash هستند.
اگر شما با استفاده از تابع HASHBYTES یک متن مشخص را هش کنید و با کلمه عبوری که در زمان ساخت SQL Login وارد کرده‌اید مقایسه کنید، خواهید دید که این دو با هم برابر نمی‌باشند (ستون password_hash در sys.sql_logins). این اختلاف به علت اطلاعات اضافی (salt) است که به کلمه عبور افزوده شده‌است.
(x = Hash(PlainText + Salt به جای استفاده از (x = Hash(PlainText
SELECT HASHBYTES('SHA2_512',CAST(N'VMT' AS VARBINARY(MAX)))
--0x53DB8AFD20AA76B93DB7BBE855950E679288C843A83899ED4E713B829873306495B6025B7B4FBCDC1BA3EE19C7005BE843A30AC51050C9652E5D1E978DBC3A11
SELECT HASHBYTES('SHA2_512',CAST(N'VMT' AS VARBINARY(MAX))+0xBFE14699)
--0xEF934A8668BD52BEC3295C5DBD2E99555CC074AECBDF32A496C39851A35847DDA7270486B3EC3C77B99334693ECE598617F232C5DC3FCD67EC7D734196913A05
این کار به امنیت بیشتر کمک می‌کند؛ اگرچه SQL Server اطلاعات Salt را به عنوان بخشی از اطلاعات هش نگهداری می‌کند. اگر به ستون password_hash دقت کنید، Salt را مشاهده خواهید کرد.
0x0200  BFE14699  EF934A8668BD52BEC3295C5DBD2E99555CC074AECBDF32A496C39851A35847DDA7270486B3EC3C77B99334693ECE598617F232C5DC3FCD67EC7D734196913A05
همانطور که مشاهده می‌کنید 4 بایت (به صورت جدا شده) برابر با Salt استفاده شده در مثال قبل است. قسمت سمت راست Salt، هش کلمه عبور ماست.

حدس کلمه عبور مربوط به SQL Logins

با استفاده از قطعه کد زیر سعی در حدس کلمه عبور SQL Loginهای سیستم خودم را دارم. از نسخه SQL Server 2012 به بعد، الگوریتم هش کلمه عبور از SHA1 به SHA2-512 تغییر کرده‌است.
-- جدول کلمات مورد استفاده برای حدس کلمه عبور
DECLARE @WordList TABLE
    (
        [Plain] NVARCHAR (MAX)
    );

-- درج کلمه عبور با تغییر در نام کاربری
INSERT INTO @WordList ( [Plain] )
            SELECT [name] FROM [sys].[sql_logins]
            UNION
            SELECT REPLACE (REPLACE (REPLACE ([name], 'o', '0'), 'i', '1'), 'e', '3')
            FROM   [sys].[sql_logins]
            UNION
            SELECT REPLACE (REPLACE (REPLACE ([name], 'o', '0'), 'i', '1'), 'e', '3') + '.'
            FROM   [sys].[sql_logins]
            UNION
            SELECT REPLACE (REPLACE (REPLACE ([name], 'o', '0'), 'i', '1'), 'e', '3') + '!'
            FROM   [sys].[sql_logins];
-- درج کلمات معمول برای کلمه عبور
INSERT INTO @WordList ( [Plain] )
            SELECT N''
            UNION ALL
            SELECT N'password'
            UNION ALL
            SELECT N'sa'
            UNION ALL
            SELECT N'dev'
            UNION ALL
            SELECT N'test'
            UNION ALL
            SELECT N'server'
            UNION ALL
            SELECT N'123456'
            UNION ALL
            SELECT N'654321'
            UNION ALL
            SELECT N'asd!@#'
            UNION ALL
            SELECT N'VMT';

-- تشخیص نوع الگوریتم مورد استفاده در هش کردن کلمه عبور
DECLARE @Algorithm VARCHAR (10);
SET @Algorithm = CASE WHEN @@MICROSOFTVERSION / 0x01000000 >= 11
                          THEN 'SHA2_512'
                      ELSE 'SHA1'
                 END;

-- در صورت یافتن کلمه عبور اطلاعات مربوط به آن در این قسمت بدست می‌آید
SELECT
     [name] ,
     [password_hash] ,
     SUBSTRING ([password_hash], 3, 4)                                                                [Salt] ,
     SUBSTRING ([password_hash], 7, ( LEN ([password_hash]) - 6 ))                                    [Hash] ,
     HASHBYTES (@Algorithm, CAST([w].[Plain] AS VARBINARY (MAX)) + SUBSTRING ([password_hash], 3, 4)) [ComputedHash] ,
     [w].[Plain]
FROM [sys].[sql_logins]
     INNER JOIN @WordList [w]
                ON SUBSTRING ([password_hash], 7, ( LEN ([password_hash]) - 6 )) = HASHBYTES ( @Algorithm , CAST([w].[Plain] AS VARBINARY (MAX)) + SUBSTRING ([password_hash], 3, 4));

-- همه نام‌های کاربری و اطلاعات مربوط به آنها را در اینجا بدست می‌آید
SELECT
     [name] ,
     [password_hash] ,
     SUBSTRING ([password_hash], 3, 4)                             [Salt] ,
     SUBSTRING ([password_hash], 7, ( LEN ([password_hash]) - 6 )) [Hash]
FROM [sys].[sql_logins];
همانطور که مشاهده می‌کنید کلمه عبور دو تا از SQL Login‌ها را بدست آوردیم:

با تغییر در دیکشنری کلمات مربوط به کلمه عبور، کارهای بیشتری را می‌شود انجام داد.
یکی دیگر از روش‌های بررسی کلمه عبور استفاده از تابع  PWDCOMPARE است.
SELECT [name],[password_hash]
FROM sys.sql_logins
WHERE PWDCOMPARE(N'VMT',[password_hash]) = 1
نظرات مطالب
استفاده از خواص راهبری در Entity framework بجای Join نویسی
از Any استفاده کنید:
var citiesContainPerson = context.Cities.Where(city => city.People.Any(person => person.Name == "user-1")).ToList();
با این خروجی SQL:
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name]
FROM [dbo].[Cities] AS [Extent1]
WHERE  EXISTS (SELECT 
1 AS [C1]
FROM [dbo].[People] AS [Extent2]
WHERE ([Extent1].[Id] = [Extent2].[BornInCityId]) AND (N'user-1' = [Extent2].[Name])
)
مطالب دوره‌ها
استفاده از XQuery - قسمت اول
XQuery زبانی است که در ترکیب با T-SQL، جهت کار با نوع داده‌ای XML در SQL Server مورد استفاده قرار می‌گیرد. XQuery یک زبان declarative است. عموما زبان‌های برنامه نویسی یا declarative هست و یا imperative. در زبان‌های imperative مانند سی‌شارپ، در هر بار، یک سطر به پردازشگر برای توضیح اعمالی که باید انجام شوند، معرفی خواهد شد. در زبان‌های declarative، توسط زبانی سطح بالا، به پردازشگر عنوان می‌کنیم که قرار است جواب چه چیزی باشد. در این حالت پردازشگر سعی می‌کند تا بهینه‌ترین روش را برای یافتن پاسخ بیابد. SQL و XQuery، هر دو جزو زبان‌های declarative هستند.
XQuery پیاده سازی شده در SQL Server با استانداردهای XQuery 1.0 و XPath 2.0 سازگار است. XQuery برای کار با نودهای مختلف یک سند XML، از XPath استفاده می‌کند. همچنین باید دقت داشت که این زبان به بزرگی و کوچکی حروف حساس است. در آن تمام واژه‌های کلیدی lowercase هستند و تمام متغیرها با علامت $ شروع می‌شوند.


ورودی و خروجی در XQuery

استاندارد XQuery از یک سری توابع ورودی مانند doc برای کار با یک سند و collection برای پردازش چندین سند کمک می‌گیرد. SQL Server از هیچکدام از این توابع پشتیبانی نمی‌کند. در اینجا از XQuery، به کمک متدهای نوع داده‌ای XML استفاده خواهد شد. این متدها شامل موارد ذیل هستند:
- query : یک xml را به عنوان ورودی گرفته و نهایتا یک خروجی XML دیگر را بر می‌گرداند.
- exist : خروجی bit دارد؛ true یا false.
- value : یک خروجی SQL Type را ارائه می‌دهد.
- nodes : خروجی جدولی دارد.
- modify : برای تغییر اطلاعات بکار می‌رود.

این موارد را در طی مثال‌هایی بررسی خواهیم کرد. بنابراین در ادامه نیاز است یک سند XML را که در طی مثال‌های این قسمت مورد استفاده قرار خواهد گرفت، به شرح ذیل مدنظر داشته باشیم:
DECLARE @data XML 

SET @data = 
'<people>
 <person>
  <name>
<givenName>name1</givenName>
<familyName>lname1</familyName>
  </name>
  <age>33</age>
  <height>short</height>
 </person>
 <person>
  <name>
<givenName>name2</givenName>
<familyName>lname2</familyName>
  </name>
  <age>40</age>
  <height>short</height>
 </person>
 <person>
  <name>
<givenName>name3</givenName>
<familyName>lname3</familyName>
  </name>
  <age>30</age>
  <height>medium</height>
 </person>
</people>'
در اینجا people در ریشه سند قرار گرفته و سپس سه شخص به مجموعه نودهای آن اضافه شده‌اند.
همانطور که در قسمت قبل نیز ذکر شد، اگر اطلاعات شما در یک فایل XML قرار دارند، نحوه‌ی خواندن آن به شکل یک فیلد XML با کمک openrowset مطابق دستورات زیر خواهد بود:
 declare @data xml
set @data = (select * from openrowset(bulk 'c:\path\data.xml', single_blob) as x)


بررسی متد query

متد query یک XQuery متنی را دریافت کرده، آن‌را بر روی XML ورودی اجرا نموده و سپس یک خروجی XML دیگر را ارائه خواهد داد.
اگر به کتاب‌های استاندارد XQuery مراجعه کنید، به یک چنین کوئری‌هایی خواهید رسید:
  for $p in doc("data.xml")/people/person
 where $p/age > 30
 return $p/name/givenName/text()
همانطور که عنوان شد، متد doc در SQL Server پیاده سازی نشده‌است. بجای آن حداقل از دو روشی که برای مقدار دهی متغیر data عنوان شد، می‌توان استفاده کرد. پس از آن معادل کوئری فوق در SQL Server به نحو ذیل توسط متد query نوشته می‌شود:
 SELECT @data.query('
 for $p in /people/person
 where $p/age > 30
 return $p/name/givenName/text()
 ')
این کوئری givenName تمام اشخاص بالای 30 سال را از سند XML مطرح شده در ابتدای بحث، استخراج می‌کند. خروجی آن نیز یک XML  است و اگر آن‌را در SQL Server managment studio اجرا کنید، یک خط آبی زیر نتیجه‌ی آن کشیده می‌شود که بیانگر لینکی است، به محتوای XML حاصل.



بررسی متد value

در ادامه متد value را بررسی خواهیم کرد. در اینجا قصد داریم مقدار سن اولین شخص را نمایش دهیم:
 SELECT @data.value('/people/person/age', 'int')
پارامتر اول متد value یک XQuery است و پارامتر دوم آن، نوع داده‌ای که قرار است بازگشت داده شود. در اینجا اگر اطلاعاتی یافت نشود، نال بازگشت داده خواهد شد.
اگر کوئری فوق را اجرا کنیم با خطای ذیل مواجه خواهیم شد:
 XQuery [value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'
در اینجا چون از XML Schema استفاده نشده، به untyped Atomic اشاره شده‌است و * پس از آن به zero to many اشاره دارد که برخلاف خروجی zero to one متد value است. این متد، صفر یا حداکثر یک مقدار را باید بازگشت دهد.
برای رفع این مشکل و اشاره به اولین شخص، می‌توان از روش ذیل استفاده کرد:
 SELECT @data.value('(/people/person/age)[1]', 'int')



تولید schema برای سند XML بحث جاری

با استفاده از برنامه Infer.exe مایکروسافت به سادگی می‌توان برای یک سند XML، فایل Schema ایجاد کرد. این برنامه را از اینجا می‌توانید دریافت کنید. پس از آن، اگر فرض کنیم اطلاعات سند XML مثال فوق در فایلی به نام people.xml ذخیره شده‌است، می‌توان schema آن‌را توسط دستور ذیل تولید کرد:
 Infer.exe people.xml -o schema.xsd
people.xml و people.xsd

که نهایتا چنین شکلی را خواهد داشت:
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="people">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" name="person">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="name">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="givenName" type="xs:string" />
                    <xs:element name="familyName" type="xs:string" />
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
              <xs:element name="age" type="xs:unsignedByte" />
              <xs:element name="height" type="xs:string" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
البته این فایل تولید شده به صورت خودکار، نوع age را unsignedByte تشخیص داده است که در صورت نیاز می‌توان آن‌را به int تبدیل کرد. ولی در کل خروجی آن بسیار با کیفیت و نزدیک به واقعیت است.
این خروجی را که اکنون به صورت یک فایل xsd، در کنار فایل xml معرفی شده به آن می‌توان یافت، با استفاده از openrowset قابل بارگذاری است:
 declare @schema xml
set @schema = (select * from openrowset(bulk 'c:\path\schema_1.xsd', single_blob) as x)
و یا حتی می‌توان یک متغیر از نوع XML را تعریف و سپس محتوای آن را به صورت رشته‌ای در همانجا مقدار دهی کرد.
سپس از این متغیر برای تعریف یک اسکیما کالکشن جدید استفاده خواهیم کرد:
 CREATE XML SCHEMA COLLECTION poeple_xsd AS @schema
در ادامه می‌توان متغیر data را که جهت مقدار دهی سند XML در ابتدای بحث تعریف کردیم، به صورت strongly typed تعریف کنیم:
 DECLARE @data XML(poeple_xsd)
SET @data = 'مانند قبل با همان محتوایی که در ابتدای بحث عنوان شد'
اینبار اگر کوئری ذیل را برای یافتن سن اولین شخص اجرا کنیم:
 SELECT @data.value('/people/person[1]/age', 'int')
خطای واضح‌تری را دریافت خواهیم کرد:
 XQuery [value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xs:unsignedByte *'
در اینجا xs:unsignedByte بجای xdt:untypedAtomic پیشین گزارش شده‌است.
مشکل کوئری نوشته در اینجا این است که زمانیکه نوع XML تعریف می‌شود، پیش فرض آن content است. یعنی در این حالت چندین root elemnt مجاز هستند. بنابراین person 1 درخواستی، می‌تواند چندین خروجی داشته باشد که در متد value مجاز نیست. این متد، پیش از اجرای کوئری، توسط parser تعیین اعتبار می‌شود و الزاما نیازی نیست تا حتما اجرا شده و سپس مشخص شود که چندین خروجی حاصل آن است.
 اینبار تنها کاری که باید برای رفع مشکل گزارش شده انجام شود، تغییر content پیش فرض به document است:
 DECLARE @data XML(document poeple_xsd)
تغییر دیگری نیاز نیست. حتی نیاز نیست از پرانتزها برای مشخص کردن اولین age استفاده کنیم. چون به کمک schema دقیقا مشخص شده‌است که این سند، چه ساختاری دارد و همانند مثال ابتدای بحث، دیگر یک untyped xml نیست.



sequences در XQuery

Sequences بسیار شبیه به آرایه‌ای از آیتم‌ها هستند و منظور مجموعه‌ای از نودها یا مقادیر آن‌ها است. برای مثال به ورودی کوئری‌های XQuery به شکل توالی از یک سند و به خروجی آن‌ها همانند توالی صفر تا چند نود نگاه کنید.
 DECLARE @x XML
SET @x=''
SELECT @x.query(
'
1,2
(: 1,2 :)
')
در مثال فوق یک توالی اصطلاحا دو atomic value را ایجاد کرده‌ایم. این آیتم‌ها با کاما از یکدیگر جدا می‌شوند. همچنین x، پیش از بکارگیری مقدار دهی شده‌است تا null نباشد. عبارتی که بین (: :) قرار می‌گیرد، یک کامنت تفسیر خواهد شد.

همچنین باید دقت داشت که این توالی خطی تفسیر می‌شود.
 DECLARE @x XML
SET @x=''
SELECT @x.query(
'
for $x in (1,2,3)
for $y in (4,5)
return ($x,$y)
')
در اینجا یک جوین کارتزین نوشته شده است، که در آن یک x با یک y جوین خواهد شد. شاید تصور کنید که خروجی آن مجموعه‌ای است با سه عضو که هر عضو آن با دو عضو دیگر جوین می‌شود. اما اگر کوئری فوق را اجرا کنید، یک خروجی خطی را مشاهده خواهید کرد.

به علاوه در SQL Server امکان تعریف Heterogeneous sequences وجود ندارد؛ به عبارتی توالی بین مقادیر و نودها مجاز نیست. برای مثال اگر کوئری زیر را اجرا کنید:
 DECLARE @x XML
SET @x=''
SELECT @x.query(
'
1, <node/>
')
با خطای ذیل مواجه خواهید شد:
 XQuery [query()]: Heterogeneous sequences are not allowed: found 'xs:integer' and 'element(node,xdt:untyped)'
 
نظرات مطالب
PersianDateTime جایگزینی برای System.DateTime
using (var db = new DBTestEntities())
            {
                dataGridView1.DataSource =(from t in db.test
                                              select  new
                                              { 
                                                  Id= t.Id,
                                                  time = new PersianDateTime( DateTime.Parse(t.ResponseDate.ToString())).ToString(PersianDateTimeFormat.DateShortTime)
                                              }).ToList();                
            }

برای ذخیره اطلاعات در بانک این کلاس خوب کار میکند و مشکلی نیست . ولی برای نمایش در برنامه ویندوزی کد بالا عمل نمیکند و هیچ چیزی نشان نمی‌دهد . یک جدول ساده دارم که شامل دو فیلد  Id  و datetime  است .
نظرات مطالب
استفاده از توابع Scalar بجای case
در SQL server 2012 تابعی اضافه شده به اسم  IIF    که بجای 
SELECT CASE @GEN WHEN 0 THEN 'Male' ELSE 'Woman' AS Gender
 از این می‌توان استفاده کرد
SELECT IIF(Gen=0,'Male','Woman')