مقدمه
نوع داده با دقت - وابسته به طول
نوع داده - داده وابسته به طول
مثالی از کاربرد Sparse columns
مقدار null به معنی پوچ و هیچ میباشد اما زمانی که در مقدار دهی جداول از آن استفاده مینمایم با توجه به نوع آن ستون فضای متفاوتی اشتغال مینماید. شاید در پایگاه دادههای کوچک زیاد مطرح نباشد اما زمانی که حداقل چند گیگ حجم آن باشد و فرضا 20 تا 30 درصد آن از مقادیر null پر شده باشد فضای زیای از پوچ گرفته شده است این در حالی است که خیلی از توسعه دهندگان اصلا به اهمیت استفاده از null توجهی نمیکنند و از مقادیری غیر معتبری مثل 0 یا 1- در آن ستون به جای null استفاده میکنند.
SQL Server Sparce Columns
sparse column یا ستونهای تنک قابلیتی از که از SQL Server 2008 اضافه شده و به ستونهای عادی امکان استفاده بهینه از فضای ذخیره شده برای مقادیر null را میدهد. در واقع sparse column فضای مورد نیاز برای مقادیر null نسبت به مقادیر غیر null را کاهش میدهد. با استفاده از sparse column فضای ذخیره شده حداقل 20 تا 40 درصد کمتر خواهد شد.
ویژگیهای Sparse Columns
- SQL Server Database Engine از کلمه کلیدی SPARSE برای تعریف یک ستون که مقادیر آن میبایست بهینه شود استفاده مینماید.
- نمای Catalog جداول با ستون sparse شبیه جداول معمولی میباشد.
- مقدار برگشتی از تابع COLUMNS_UPDATED با ستون sparce متفاوت از ستون معمولی است.
geography | text |
geometry | timestamp |
image | user-defined data types |
ntext |
sparse column فضای بیشتری برای ذخیره دادههای غیر null نسبت به دادههای نشانه گذاری نشده با SPARSE لازم دارد و این فضا4 بایت بیشتر از ستون معمولی است. برآورد فضای ذخیره شده براساس نوع داده با طول ثابت در جدول زیر آورده شده است:
نوع داده | بایت بدون sparse | بایت sparse | درصد null |
bit | 0.125 | 5 | 98% |
tinyint | 1 | 5 | 86% |
smallint | 2 | 6 | 76% |
int | 4 | 8 | 64% |
bigint | 8 | 12 | 52% |
real | 4 | 8 | 64% |
float | 8 | 12 | 52% |
smallmoney | 4 | 8 | 64% |
money | 8 | 12 | 52% |
smalldatetime | 4 | 8 | 64% |
datetime | 8 | 12 | 52% |
uniqueidentifier | 16 | 20 | 43% |
date | 3 | 7 | 69% |
نوع داده | بایت بدون sparse | یابت sparse | درصد null |
(datetime(2 | 6 | 10 | 57% |
(datetime(2 | 8 | 12 | 52% |
(time(0 | 3 | 7 | 69% |
(time(7 | 5 | 9 | 60% |
(datetimetoffset(0 | 8 | 12 | 52% |
(datetimetoffset (7 | 10 | 14 | 49% |
(decimal/numeric(1,s | 5 | 9 | 60% |
(decimal/numeric(38,s | 17 | 21 | 42% |
(vardecimal(p,s |
نوع داده |
بایت بدون sparse | یابت sparse | درصد null |
sql_variant | 2* | 2* | 60% |
varchar or char | 2* | 4*+ | 60% |
nvarchar or nchar | 2* | 4* | 60% |
varbinary or binary | 2* | 4* | 60% |
xml | 2* | 4* | 60% |
hierarchyid | 2* | 4* | 60% |
محدویتهای استفاده از Sparse columns
- sparse column می بایست nullable باشد و نمیتواند ROWGUIDCOL یا IDENTITY باشد.
- sparse column مقدار پیش فرض نمیتواند داشته باشد
- ستون محاسبه ای نمیتواند sparse باشد
- sparse column نمیتواند بخشی از clustered index یا unique primary key index باشد
- sparse column نمی تواند بخشی از user-defined table باشد
CREATE TABLE Employees_sparse ( EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, SSN CHAR(9) NOT NULL, TITLE CHAR(10) SPARSE NULL, FIRSTNAME VARCHAR(50) NOT NULL, MIDDLEINIT CHAR(1) SPARSE NULL, LASTNAME VARCHAR(50) NOT NULL, EMAIL CHAR(50) SPARSE NULL) GO
CREATE TABLE Employees ( EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, SSN CHAR(9) NOT NULL, TITLE CHAR(10) NULL, FIRSTNAME VARCHAR(50) NOT NULL, MIDDLEINIT CHAR(1) NULL, LASTNAME VARCHAR(50) NOT NULL, EMAIL CHAR(50) NULL) GO
در این دو جدول یکی با سه ستون Sparse و دیگری بدون Sparse ایجاد شده و با 50000 ردیف داده پر شده است حال با رویه ذخیره شده sp_spaceused میتوان فضای ذخیره شده دو جدول را باهم مقایسه نمود.
sp_spaceused 'Employees' GO sp_spaceused 'Employees_sparse'
البته ذکر این نکته گفتی است که بهتر است از این تکنیک برای جداولی که تعداد زیادی ستون null دارند استفاده شود.
درحال حاضر، باتوجه به خرده نداشتن مقادیر پولی در ایران، عموما از نوعهای int و bigint برای ذخیره سازی این مقادیر استفاده میشود؛ اما در آینده با احتمال حذف تعدادی از صفرها، نیاز به ثبت خردهها هم ضروری خواهد بود و در اینجا این سؤال مهم مطرح میشود که نوع دادهای مناسب برای انجام اینکار چیست؟ برای نمونه در SQL Server، نوعهای دادهای decimal، money، smallmoney و امثال آن وجود دارند که در این مطلب، تفاوتهای مهم آنها و روش صحیح انتخاب نوع دادهای مناسب مخصوص اینکار را بررسی خواهیم کرد.
مشکل مهم نوع دادهای int جهت ذخیره سازی مقادیر پولی
فرض کنید جدول سادهای را با دو فیلد Id و Price دارید که نوع مبلغ آنرا با توجه به عدم داشتن خرده در واحد پولی، int انتخاب کردهاید:
اگر در این جدول فقط 7 رکورد زیر را ثبت کنیم:
به نظر شما خروجی کوئری سادهی زیر که جهت نمایش جمع مبالغ وارد شده تهیه شده، چیست؟
خروجی آن فقط استثنای زیر است!
عنوان میکند که جمع آن از بازهی اعداد صحیح خارج شدهاست و در سیستمی که نوع مبالغ آنرا int انتخاب کردهاید، دیر یا زود به این مشکل خواهید رسید. فقط کافی است کاربران، یکسالی با آن برنامه کار کنند!
برای حل این مشکل میتوان به صورت موقت، نوع دادهای را به bigint تبدیل کرد و مجددا جمع رکوردها را محاسبه کرد:
یک روش دیگر مواجه شدن با این مساله، عدم انتخاب نوع int برای فیلد Price، از ابتدای کار است.
از نوع دادهای float برای ذخیره سازی مقادیر پولی استفاده نکنید!
هیچگاه نباید از نوع دادهی float برای ذخیره سازی مقادیر پولی استفاده کرد؛ از این جهت که این نوع اعداد، به صورت تقریبی از یک مقدار decimal و به صورت باینری در SQL Server ذخیره میشوند. به همین جهت به محض ذخیره شدن، با عددی غیر دقیق مواجه خواهیم بود. همچنین مقایسهی دقیق این نوع اعداد هم مشکلات خاصی را به همراه دارد.
SQL Server چگونه مقادیر پولی money و small money را ذخیره میکند؟
SQL Server برای کار با مقادیر پولی، دو نوع MONEY و SMALLMONEY را ارائه میدهد که شبیه به نوعهای BIGINT و INT، نیاز به 8 و 4 بایت برای ذخیره سازی دارند. در عمل نوع MONEY شبیه به نوع DECIMAL(19,4) و نوع SMALLMONEY همانند DECIMAL(10,4) رفتار میکند. یعنی نوع MONEY میتواند تا 15 رقم دسیمال پیش از ممیز و 4 رقم اعشار را ذخیره کند و نوع SMALLMONEY تنها میتواند 6 رقم دسیمال و 4 رقم اعشاری را ذخیره کند.
اما ... هرچند نوع دادهی MONEY و DECIMAL(19,4) به ظاهر یکی هستند، اما به نحو متفاوتی بر روی دیسک سخت ذخیره میشوند. برای نمونه فرض کنید که قصد داریم عدد 4,513.19 را یکبار به صورت MONEY و بار دیگر به صورت SMALLMONEY ذخیره کنیم که در نهایت به جدول زیر میرسیم:
همانطور که مشاهده میکنید، نوعهای MONEY و SMALLMONEY، دقیقا همانند BIGINT هشت بایتی و INT، چهار بایتی ذخیره میشوند و عملا در پشت صحنهی SQL Server، اعداد صحیح هستند. اما نوع DECIMAL(19,4) که هرچند شبیه به MONEY عمل میکند، 9 بایتی است.
الگوریتم انتخاب نوع دادهی مناسب ذخیره سازی مقادیر پولی
در فلوچارت زیر که از کتاب «Donald Knuth’s "The Art of Computer Programming – Volume 1".» انتخاب شده، روش مواجه شدن با انواع و اقسام نوعهای دادهای عددی را به خوبی مشخص میکند که آیا عدد در حال ذخیره شدن، خرده دارد یا خیر؟ آیا از 922,337,203,685,477.5807 کوچکتر است یا خیر و امثال آن که در تصمیمگیری نهایی مؤثر هستند:
اعدادی را که در این نمودار مشاهده میکنید، در جدول زیر بهتر توضیح داده شدهاند. به عبارتی چه تفاوتی بین نوع Money و Decimal(19,4) مشابه وجود دارد:
تفاوت مهم نوع Money و Decimal(19,4)، در دقت آنها است
تا اینجا به نظر آنچنان تفاوتی بین نوع Money و Decimal(19,4) وجود ندارد و نوع money اتفاقا یک بایت را کمتر اشغال میکند و کوچکتر است. اما تفاوت اصلی را با مثال زیر بهتر میتوان توضیح داد:
در اینجا جدولی تهیه شده که دو ستون اصلی Mon1 و Dec1 را دارد و مابقی ستونهای آن، محاسباتی هستند:
همانطور که مشاهده میکنید، با ضرب دو عدد دسیمال، مقادیر پیش و پس از ممیز، یعنی precision و scale تغییر کردهاند، اما در مورد money چنین چیزی رخ نداده و ثابت است. برای مثال زمانیکه با یک عدد DECIMAL(4,2) کار میکنیم، اگر آنرا ضربدر همین عدد کنیم، به یک عدد DECIMAL(8,4) خواهیم رسید که البته حداکثر precision ممکن آن در SQL Server عدد 38 است، اما یک چنین تغییری در حین ضرب اعداد از نوع money رخ نمیدهد.
موضوع دقت را با مثال زیر بهتر میتوان بررسی کرد:
فرض کنید جدولی را داریم با دو فیلد از نوع Money و مشابه آن یعنی decimal(19,4) به صورت فوق. اگر رکوردهای زیر را به آن اضافه کنیم:
و سپس سعی کنیم که جمع اعداد وارد شده را محاسبه کنیم:
به نتیجهی زیر میرسیم:
همانطور که مشخص است در حین محاسباتی مانند جمع و منها و محاسبهی sum، تفاوتی بین این نوعها نیست. اما اگر سعی در تقسیم آنها کنیم:
به خروجی زیر میرسیم:
نتیجهی واقعی 0,00009 است که پس از گرد شدن، به 0.0001 مقدار دسیمال میرسیم، اما این دقت در نوع money از دست رفتهاست.
نکتهی مهمی که در اینجا قابل مشاهدهاست، محدود نبودن نتیجهی حاصل، به دقت اعشارها در عدد decimal تعریف شده و scale تعریف شدهی اولیهی آن است. نمونهی دیگر آنرا در مثال زیر میتوانید مشاهده کنید که هرچند عدد دسیمال تعریف شده، فقط 2 رقم اعشاری دارد، اما در حین تقسیم، از این مساله صرفنظر شده و خروجی آن محدود به 2 رقم اعشار نیست؛ برخلاف نوع money که حداکثر 4 رقم ثابت اعشاری را بیشتر نمیتواند داشته باشد:
نتیجهگیری
برای ذخیره سازی مقادیر پولی در SQL Server، اگر سیستم شما OLTP-like است و با اعدادی مانند 1000.24 کار میکنید و حداکثر میخواهید جمع و منهای آنها را محاسبه کنید، انتخاب نوع MONEY و یا SMALLMONEY بسیار مناسب است؛ اما اگر سیستم شما OLAP-like است و در آن اعمال ضرب و تقسیم زیاد رخ میدهد، فقط از نوع Decimal استفاده کنید.
مشکل مهم نوع دادهای int جهت ذخیره سازی مقادیر پولی
فرض کنید جدول سادهای را با دو فیلد Id و Price دارید که نوع مبلغ آنرا با توجه به عدم داشتن خرده در واحد پولی، int انتخاب کردهاید:
CREATE TABLE [Test1]( [Id] [int] IDENTITY(1,1) NOT NULL, [Price] [int] NOT NULL, CONSTRAINT [PK_Test1] PRIMARY KEY CLUSTERED ( [Id] ASC ));
Insert into Test1 values (1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000)
select sum(price) from Test1
Arithmetic overflow error converting expression to data type int.
برای حل این مشکل میتوان به صورت موقت، نوع دادهای را به bigint تبدیل کرد و مجددا جمع رکوردها را محاسبه کرد:
select sum(cast(price as bigint)) from Test1
از نوع دادهای float برای ذخیره سازی مقادیر پولی استفاده نکنید!
هیچگاه نباید از نوع دادهی float برای ذخیره سازی مقادیر پولی استفاده کرد؛ از این جهت که این نوع اعداد، به صورت تقریبی از یک مقدار decimal و به صورت باینری در SQL Server ذخیره میشوند. به همین جهت به محض ذخیره شدن، با عددی غیر دقیق مواجه خواهیم بود. همچنین مقایسهی دقیق این نوع اعداد هم مشکلات خاصی را به همراه دارد.
DECLARE @f AS FLOAT = '29545428.0211111'; SELECT CAST(@f AS NUMERIC(28, 14)) AS value;
SQL Server چگونه مقادیر پولی money و small money را ذخیره میکند؟
SQL Server برای کار با مقادیر پولی، دو نوع MONEY و SMALLMONEY را ارائه میدهد که شبیه به نوعهای BIGINT و INT، نیاز به 8 و 4 بایت برای ذخیره سازی دارند. در عمل نوع MONEY شبیه به نوع DECIMAL(19,4) و نوع SMALLMONEY همانند DECIMAL(10,4) رفتار میکند. یعنی نوع MONEY میتواند تا 15 رقم دسیمال پیش از ممیز و 4 رقم اعشار را ذخیره کند و نوع SMALLMONEY تنها میتواند 6 رقم دسیمال و 4 رقم اعشاری را ذخیره کند.
اما ... هرچند نوع دادهی MONEY و DECIMAL(19,4) به ظاهر یکی هستند، اما به نحو متفاوتی بر روی دیسک سخت ذخیره میشوند. برای نمونه فرض کنید که قصد داریم عدد 4,513.19 را یکبار به صورت MONEY و بار دیگر به صورت SMALLMONEY ذخیره کنیم که در نهایت به جدول زیر میرسیم:
همانطور که مشاهده میکنید، نوعهای MONEY و SMALLMONEY، دقیقا همانند BIGINT هشت بایتی و INT، چهار بایتی ذخیره میشوند و عملا در پشت صحنهی SQL Server، اعداد صحیح هستند. اما نوع DECIMAL(19,4) که هرچند شبیه به MONEY عمل میکند، 9 بایتی است.
الگوریتم انتخاب نوع دادهی مناسب ذخیره سازی مقادیر پولی
در فلوچارت زیر که از کتاب «Donald Knuth’s "The Art of Computer Programming – Volume 1".» انتخاب شده، روش مواجه شدن با انواع و اقسام نوعهای دادهای عددی را به خوبی مشخص میکند که آیا عدد در حال ذخیره شدن، خرده دارد یا خیر؟ آیا از 922,337,203,685,477.5807 کوچکتر است یا خیر و امثال آن که در تصمیمگیری نهایی مؤثر هستند:
اعدادی را که در این نمودار مشاهده میکنید، در جدول زیر بهتر توضیح داده شدهاند. به عبارتی چه تفاوتی بین نوع Money و Decimal(19,4) مشابه وجود دارد:
تفاوت مهم نوع Money و Decimal(19,4)، در دقت آنها است
تا اینجا به نظر آنچنان تفاوتی بین نوع Money و Decimal(19,4) وجود ندارد و نوع money اتفاقا یک بایت را کمتر اشغال میکند و کوچکتر است. اما تفاوت اصلی را با مثال زیر بهتر میتوان توضیح داد:
CREATE TABLE MoneyTest ( Mon1 money, Mon2 AS Mon1*Mon1, Mon3 AS Mon1*Mon1*Mon1, Dec1 decimal(19,4), Dec2 AS Dec1*Dec1, Dec3 AS Dec1*Dec1*Dec1, MonDec AS Mon1*Dec1, DecMon AS Dec1*Mon1);
همانطور که مشاهده میکنید، با ضرب دو عدد دسیمال، مقادیر پیش و پس از ممیز، یعنی precision و scale تغییر کردهاند، اما در مورد money چنین چیزی رخ نداده و ثابت است. برای مثال زمانیکه با یک عدد DECIMAL(4,2) کار میکنیم، اگر آنرا ضربدر همین عدد کنیم، به یک عدد DECIMAL(8,4) خواهیم رسید که البته حداکثر precision ممکن آن در SQL Server عدد 38 است، اما یک چنین تغییری در حین ضرب اعداد از نوع money رخ نمیدهد.
موضوع دقت را با مثال زیر بهتر میتوان بررسی کرد:
CREATE TABLE [MoneyTest]( [Id] [int] IDENTITY(1,1) NOT NULL, decimalMoney decimal(19,4), moneyMoney money CONSTRAINT [PK_MoneyTest] PRIMARY KEY CLUSTERED ( [Id] ASC ));
INSERT INTO MoneyTest VALUES (12321423442.3456,12321423442.3456), (1111111.1919,1111111.1919)
SELECT * FROM MoneyTest SELECT SUM(decimalMoney) AS [sumDecimal], SUM(moneyMoney) AS [sumMoney] FROM MoneyTest
همانطور که مشخص است در حین محاسباتی مانند جمع و منها و محاسبهی sum، تفاوتی بین این نوعها نیست. اما اگر سعی در تقسیم آنها کنیم:
DECLARE @moneyPer money, @decimalPer decimal(19,4) SET @moneyPer = (SELECT moneyMoney FROM MoneyTest WHERE id = 2)/((SELECT moneyMoney FROM MoneyTest WHERE id = 1)) SET @decimalPer = (SELECT decimalMoney FROM MoneyTest WHERE id = 2)/((SELECT decimalMoney FROM MoneyTest WHERE id = 1)) SELECT @moneyPer AS[moneyPer], @decimalPer AS [decimalPer];
نتیجهی واقعی 0,00009 است که پس از گرد شدن، به 0.0001 مقدار دسیمال میرسیم، اما این دقت در نوع money از دست رفتهاست.
نکتهی مهمی که در اینجا قابل مشاهدهاست، محدود نبودن نتیجهی حاصل، به دقت اعشارها در عدد decimal تعریف شده و scale تعریف شدهی اولیهی آن است. نمونهی دیگر آنرا در مثال زیر میتوانید مشاهده کنید که هرچند عدد دسیمال تعریف شده، فقط 2 رقم اعشاری دارد، اما در حین تقسیم، از این مساله صرفنظر شده و خروجی آن محدود به 2 رقم اعشار نیست؛ برخلاف نوع money که حداکثر 4 رقم ثابت اعشاری را بیشتر نمیتواند داشته باشد:
DECLARE @M MONEY = 1234, @D DECIMAL(6,2) = 1234 SELECT @M/$1000000 AS [MONEY] , @D/$1000000 AS [DECIMAL]
نتیجهگیری
برای ذخیره سازی مقادیر پولی در SQL Server، اگر سیستم شما OLTP-like است و با اعدادی مانند 1000.24 کار میکنید و حداکثر میخواهید جمع و منهای آنها را محاسبه کنید، انتخاب نوع MONEY و یا SMALLMONEY بسیار مناسب است؛ اما اگر سیستم شما OLAP-like است و در آن اعمال ضرب و تقسیم زیاد رخ میدهد، فقط از نوع Decimal استفاده کنید.
DECLARE @dOne DECIMAL(19,4) = 1, @dThree DECIMAL(19,4) = 3, @mOne MONEY = 1, @mThree MONEY = 3 SELECT (@dOne/@dThree) * @dThree AS DecimalResult, (@mOne/@mThree) * @mThree AS MoneyResult
قبل از مطالعه این بخش لطفا آشنایی با Window Functionها در SQL Server بخش اول را مطالعه نمایید.
دربخش اول،در مورد Syntax مربوط به Over Clause صحبت کردیم، و برای درک استفاده از Over Clause، مثالهایی را بررسی نمودیم، در این بخش نیز،به تفاوت Row Clause و Range Clause می پردازیم.
مثال: با ایجاد یک Script،عملیات جمع روی یک فیلد خاص، بوسیله Row Clause و Range Clause انجام میدهیم. تا تفاوت آنها را درک نماییم.
در ادامه Script زیر را اجرا نمایید:
DECLARE @Test TABLE ( RowID INT IDENTITY, FName VARCHAR(20), Salary SMALLINT ); INSERT INTO @Test (FName, Salary) VALUES ('George', 800), ('Sam', 950), ('Diane', 1100), ('Nicholas', 1250), ('Samuel', 1250), ('Patricia', 1300), ('Brian', 3000), ('Thomas', 1600), ('Fran', 2450), ('Debbie', 2850), ('Mark', 2975), ('James', 3000), ('Cynthia', 3000), ('Christopher', 5000); SELECT RowID,FName,Salary, SumByRows = SUM(Salary) OVER (ORDER BY Salary ROWS UNBOUNDED PRECEDING), SumByRange = SUM(Salary) OVER (ORDER BY Salary RANGE UNBOUNDED PRECEDING) FROM @Test ORDER BY RowID;
خروجی بصورت زیر خواهد بود:
با مشاهده شکل بالا، به وضوح میتوان تفاوت Row و Range را تشخیص داد. در Script بالا از UNBOUNDED PRECEDING استفاده کردیم ، و مفهوم قالب آن به شرح ذیل میباشد:
مقدار فیلد Salary سطر جاری = جمع مقادیر فیلد Salary همه سطرهای ماقبل،سطر جاری + مقدار فیلد Salary سطر جاری
Row Clause بصورت فیزیکی به سطرها مینگرد و قالب بیان شده در Script را،روی تمامی سطرها،نسبت به جایگاه آنها در جدول، به ترتیب اعمال مینماید.و در شکل نیز قابل مشاهده می باشد، یعنی به چیدمان سطرها در خروجی که بصورت فیزیکی نمایش داده شده است، توجه می کند، و حاصل جمع هر سطر برابر است با حاصل جمع سطرهای ماقبل + سطر جاری
اما Range Clause:به چیدمان فیزیکی سطرها توجه نمیکند، بلکه بصورت منطقی به مقدار فیلد Salary سطرها توجه مینماید، یعنی مقادیری که در یک محدوده(Range) قرار دارند، حاصل جمع آنها،یکی است.
مقدار فیلد Salary سطر چهار و پنج برابر است با 1250 بنابراین حاصل جمع آنها برابر هم میباشد. و بصورت زیر محاسبه میشود:
800 + 950 + 1100 + 1250 + 1250 =5350
روش بیان شده، در مورد سطرهای 12 و 13 نیز صادق است.
امیدوارم با مثالهایی که در بخش اول و بخش دوم بررسی نمودیم، روش استفاده از Over Clause را درک کرده باشیم.
Window Functionها را به چهار بخش تقسیم بندی شده اند، که به شرح ذیل میباشد:
1- Ranking functions (توابع رتبه بندی)، که بررسی نمودیم.
2- NEXT VALUE FOR ، که در بحث ایجاد Sequence آن را بررسی نمودیم.
3- Aggregate Functions (توابع جمعی)، اکثرا با اینگونه توابع آشنا هستیم.
4- Analytic Functions (توابع تحلیلی) که در بخش بعدی آن را بررسی مینماییم.
یکی از منابع بسیار مفید در مورد Window Function ها کتاب Microsoft SQL Server 2012 High-Performance T-SQL Using Window Functions ، می باشد،که بطور کامل به Window Functionها اختصاص دارد و تکنیکهای بسیار مفیدی را بیان میکند. مطالعه آن به علاقمندان، پیشنهاد میگردد.
موفق باشید.
اشتراکها
ابزار مدیریت تحت وب ASP.NET Identity
اطلاعات بیشتر در مورد روش هش کردن پسورد ASP.NET Identity
Window Functionها برای اولین بار در نسخه SQL Server 2005 ارائه گردیدند، و در ورژنهای جدیدتر SQL Server، به تعداد این فانکشنها افزوده شده است.
تعریف Window Function :
معمولا از این نوع فانکشنها روی مجموعه ای از ROWهای یک جدول، در جهت اعمال عملیاتهای محاسباتی ،ارزیابی داده ها، رتبه بندی و غیرو... استفاده میگردد، به بیان سادهتر بوسیله Window Functionها میتوان، ROWهای یک جدول را گروه بندی نمود. و روی گروهها از توابع جمعی (Aggregate Functions ) استفاده کرد. این نوع فانکشنها از قابلیت و انعطاف پذیری زیادی برخوردار میباشند، و بوسیله آنها میتوان نتایج (خروجی) بسیار مفیدی از Query ها، بدست آورد، معمولا از این نوع فانکشنها در Data Mining (داده کاوی) و گزارشگیریها استفاده میگردد. و آگاهی و روش استفاده از Window Functionها برای برنامه نویسان و DBA ها، میتواند بسیار مفید باشد.
مفهوم Window Function مطابق استاندارد ISO و ANSI میباشد، و دیتابیس هایی همچون Oracle،DB2،Sybase از آن پشتیبانی مینمایند.برای اطلاعات بیشتر میتوانید به سایتهای زیر مراجعه کنید:
کلمه "Window" در Window Function، به مجموعه ROW هایی اشاره میکند، که محاسبات و ارزیابی و غیرو... روی آنها اعمال میگردد.
Window Functionها برای ارائه قابلیتهای خود، از Over Clause استفاده میکنند. اگر مقاله آشنایی با Row_Number،Rank،Dense_Rank،NTILE را مطالعه کرده باشید، میتوان هریک از آنها را یک Window Function دانست.
برای شروع، به بررسی Over Clause میپردازیم، و Syntax آن به شرح ذیل میباشد:
OVER ( [ <PARTITION BY clause> ] [ <ORDER BY clause> ] [ <ROW or RANGE clause> ] ) <PARTITION BY clause> ::= PARTITION BY value_expression , ... [ n ] <ORDER BY clause> ::= ORDER BY order_by_expression [ COLLATE collation_name ] [ ASC | DESC ] [ ,...n ] <ROW or RANGE clause> ::= { ROWS | RANGE } <window frame extent> <window frame extent> ::= { <window frame preceding> | <window frame between> } <window frame between> ::= BETWEEN <window frame bound> AND <window frame bound> <window frame bound> ::= { <window frame preceding> | <window frame following> } <window frame preceding> ::= { UNBOUNDED PRECEDING | <unsigned_value_specification> PRECEDING | CURRENT ROW } <window frame following> ::= { UNBOUNDED FOLLOWING | <unsigned_value_specification> FOLLOWING | CURRENT ROW } <unsigned value specification> ::= { <unsigned integer literal> }
OVER دارای سه آرگومان اختیاری است که هر کدام را به تفصیل بررسی میکنیم:
1- PARTITION BY clause : بوسیله این پارامتر میتوانیم Rowهای یک جدول را گروه بندی نماییم. این پارامتر یک value_expression می پذیرد. یک Value_expression میتواند نام یک ستون ، یک Scalar Subquery ، Scalar Function و غیرو باشد.
2- ORDER BY clause : از نامش مشخص است و برای Sort استفاده میشود، و ویژگیهای Order By در آن اعمال میگردد. به جز Offset.
3- ROW or RANGE clause :این پارامتر بیشتر برای محدود نمودن Row در یک Partition (گروه) مورد استفاده قرار میگیرد، به عنوان مثال نقطه شروع و پایان را میتوان بوسیله پارامتر فوق تعیین نمود.
Row و Range نسبت به هم یک تفاوت عمده دارند،و آن این است که، اگر از ROW Clause استفاده نمایید، ارتباط ROWهای قبلی یا بعدی، نسبت به Row جاری،بصورت فیزیکی (physical association ) سنجیده میشود، بطوریکه با استفاده از Range Clause ارتباط سطرهای قبلی و بعدی، نسبت به سطر جاری بصورت منطقی (logical association ) در نظر گرفته میشود. ممکن است درک این مطلب کمی سخت باشد، در ادامه با مثالهایی که بررسی مینماییم، براحتی تفاوت این دو را متوجه میشوید.
Row یا Range در قالبهای متفاوتی مقدار میپذیرند، که هر کدام را بررسی میکنیم:
UNBOUNDED PRECEDING : بیانگر اولین سطر Partition میباشد. UNBOUNDED PRECEDING فقط نقطه شروع را مشخص مینماید.
UNBOUNDED FOLLOWING : بیانگر آخرین سطر Partition میباشد. UNBOUNDED FOLLOWING فقط نقطه پایانی را مشخص مینماید.
CURRENT ROW : اولین سطر جاری یا آخرین سطر جاری را مشخص مینماید.
n PRECEDING یا unsigned value specification> PRECEDING> : تعداد سطرهای قبل از سطر جاری را تعیین میکند، n یا <unsigned value specification>تعداد سطرهای قبل از سطر جاری را تعیین مینماید. از n PRECEDING نمی توان برای Range استفاده نمود.
n FOLLOWING یا unsigned value specification> FOLLOWING> : تعداد سطرهای بعد از سطر جاری را تعیین میکند، n یا<unsigned value specification> تعداد سطر های بعد از سطر جاری را تعیین مینماید. از n FOLLOWING نمی توان برای Range استفاده نمود.
<BETWEEN <window frame bound > AND <window frame bound : از چارچوب فوق برای Range و Row میتوان استفاده نمود، و نقطه آغازین و نقطه پایانی توسط قالب فوق تعیین میگردد. نکته قابل توجه آن است که نقطه پایانی نمیتواند، کوچکتر از نقطه آغازین گردد.
در ادامه برای درک هرچه بیشتر تعاریف بیان شده، چندین مثال میزنیم و هر کدام را بررسی مینماییم:
در ابتدا Script زیر را اجرا نمایید، که شامل جدولی به نام Revenue (سود،درآمد) و درج چند درکورد در آن:
CREATE TABLE REVENUE ( [DepartmentID] int, [Revenue] int, [Year] int ); insert into REVENUE values (1,10030,1998),(2,20000,1998),(3,40000,1998), (1,20000,1999),(2,60000,1999),(3,50000,1999), (1,40000,2000),(2,40000,2000),(3,60000,2000), (1,30000,2001),(2,30000,2001),(3,70000,2001)
مثال اول : میخواهیم براساس فیلد DepartmentID جدول Revenue را Partition بندی نماییم و از توابع جمعی AVG و SUM روی فیلد درآمد(Revenue) استفاده کنیم.
ابتدا Script زیر را اجرا میکنیم:
select *, avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue, sum(Revenue) OVER (PARTITION by DepartmentID) as TotalRevenue from REVENUE order by departmentID, year;
خروجی بصورت زیر خواهد بود:
مطابق شکل، جدول براساس فیلد DepartmentID به سه Partition تقسیم شده است، و عملیات میانگین و جمع روی فیلد Revenue انجام شده است و عملیات Sort روی هرگروه بطور مستقل انجام گرفته است. چنین کاری را نمیتوانستیم بوسیله Group By انجام دهیم.
مثال دوم : نحوه استفاده از ROWS PRECEDING،در این مثال قصد داریم عملیات جمع را روی فیلدRevenue انجام دهیم. بطوریکه جمع هر مقدار برابر است با سه مقدار قبلی + مقدار جاری:
لطفا رکوردهای زیر را به جدول فوق درج نمایید:
insert into REVENUE values(1,90000,2002),(2,20000,2002),(3,80000,2002), (1,10300,2003),(2,1000,2003), (3,90000,2003), (1,10000,2004),(2,10000,2004),(3,10000,2004), (1,20000,2005),(2,20000,2005),(3,20000,2005), (1,40000,2006),(2,30000,2006),(3,30000,2006), (1,70000,2007),(2,40000,2007),(3,40000,2007), (1,50000,2008),(2,50000,2008),(3,50000,2008), (1,20000,2009),(2,60000,2009),(3,60000,2009), (1,30000,2010),(2,70000,2010),(3,70000,2010), (1,80000,2011),(2,80000,2011),(3,80000,2011), (1,10000,2012),(2,90000,2012),(3,90000,2012)
سپس Script زیر را اجرا مینماییم:
select Year, DepartmentID, Revenue, sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3 From REVENUE order by departmentID, year;
خروجی :
در Script بالا، جدول را براساس فیلد DepartmentID گروه بندی میکنیم، که سه گروه ایجاد میشود، هر گروه را بطور مستقل، روی فیلد Year بصورت صعودی مرتب مینماییم. حال برای آنکه بتوانیم سیاست جمع، روی فیلد Revenue، را پیاده سازی نماییم ، قطعه کد زیر را در Script بالا اضافه کردیم.
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3
برای شرح چگونگی استفاده از PRECEDING،فقط به شرح گروه اول بسنده میکنیم. مقدار جمع فیلد Revenue سطر اول، که قبل از آن سطری وجود ندارد، برابر است با مقدار خود، یعنی 10030، مقدار جمع فیلد Revenue سطر دوم برابر است با حاصل جمع مقدار فیلدRevenue سطر اول و دوم ، یعنی 30030 . این روند تا سطر چهار ادامه دارد، اما برای بدست آوردن مقدار جمع فیلدRevenue سطر پنجم، مقدار جمع فیلد Revenue سطر دوم،سوم،چهارم و پنجم در نظر گرفته میشود، و مقدار فیلدRevenue سطر اول در حاصل جمع در نظر گرفته نمیشود،بنابراین مقدار جمع فیلد Revenue سطر پنجم برابر است با 180000. در صورت مسئله گفته بودیم، مقدار جمع فیلد Revenue هر سطر جاری برابر است با حاصل جمع مقدارسطر جاری و مقادیر سه سطر ماقبل خود.
مثال سوم: نحوه استفاده از ROWS FOLLOWING، این مثال عکس مثال دوم است، یعنی حاصل جمع مقدار فیلد Revenue هر سطر برابر است با حاصل جمع سطر جاری با سه سطر بعد از خود. بنابراین Script زیر را اجرا نمایید:
select Year, DepartmentID, Revenue, sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR] ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) as Next3 From REVENUE order by departmentID, year;
خروجی :
مطابق شکل مقدار جمع فیلد اول برابراست با حاصل جمع مقدار سطر جاری و سه سطر بعد از آن.
نکته ای که در مثالهای دوم و سوم،می بایست به آن توجه نمود، این است که در زمان استفاده از Row یا Range ، استفاده از Order by در Partition الزامی است، در غیر این صورت با خطا مواجه میشوید.
نحوه استفاده از UNBOUNDED PRECEDING ، این امکان در T-SQL Server 2012 افزوده شده است.
مثال چهار: در این مثال میخواهیم کمترین سود بدست آمده در چند سال را بدست آوریم:
ابتدا Script زیر را اجرا نمایید:
select Year, DepartmentID, Revenue, min(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR] ROWS UNBOUNDED PRECEDING) as MinRevenueToDate From REVENUE order by departmentID, year;
خروجی:
طبق تعریف UNBOUNDED PRECEDING اولین سطر هر Partition را مشخص مینماید، و چون از PRECEDING استفاده کرده ایم، بنابراین مقایسه همیشه بین سطر جاری و سطرهای قبل از آن انجام میپذیرد. بنابراین خواهیم داشت، کمترین مقدار فیلد Revenue در سطر اول، برابر با مقدار خود میباشد، چون هیچ سطری ماقبل از آن وجود ندارد. در سطر دوم مقایسه کمترین مقدار، بین 20000 و 10030 انجام میگیرد، که برابر است با 10030، در سطر سوم، مقایسه بین مقادیر سطر اول،دوم و سطر سوم صورت میگیرد، یعنی کمترین مقدار بین 40000،20000 و 10030، بنابراین کمترین مقدار سطر سوم برابر است با 10030.
به بیان سادهتر برای بدست آوردن کمترین مقدار هر سطر، مقدار سطر جاری با مقادیر همه سطرهای ماقبل خود مقایسه میگردد.
برای بدست آوردن کمترین مقدار در سطر ششم، مقایسه بین مقادیر سطرهای اول،دوم،سوم،چهارم،پنجم و ششم صورت میگیرد که عدد 10000 بدست میآید و الی آخر...
نکنه: اگر در Over Clause شرط Order by را اعمال نماییم، اما از Row یا Range استفاده نکنیم، SQL Server بصورت پیش فرض از قالب زیر استفاده مینماید:
RANGE UNBOUNDED PRECEDING AND CURRENT ROW
برای روشنتر شدن مطلب فوق مثالی میزنیم:
ابتدا Script زیر را اجرا نمایید، که شامل ایجاد یک جدول و درج چند رکورد در آن میباشد:
CREATE TABLE Employees ( EmployeeId INT IDENTITY PRIMARY KEY, Name VARCHAR(50), HireDate DATE NOT NULL, Salary INT NOT NULL ) GO INSERT INTO Employees (Name, HireDate, Salary) VALUES ('Alice', '2011-01-01', 20000), ('Brent', '2011-01-15', 19000), ('Carlos', '2011-02-01', 22000), ('Donna', '2011-03-01', 25000), ('Evan', '2011-04-01', 18500) GO
سپس Script زیر را اجرا نمایید:
SELECT Name, Salary, AVG(Salary) OVER(ORDER BY HireDate) AS avgSalary FROM Employees GO
خروجی :
حال اگر Script زیر را نیز اجرا نمایید، خروجی آن مطابق شکل بالا خواهد بود:
SELECT Name, Salary, AVG(Salary) OVER(ORDER BY HireDate RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS avgSalary FROM Employees GO
توضیح درباره Script بالا، در این روش برای بدست آوردن میانگین هر سطر، مقدار سطر جاری با مقادیر سطرهای ماقبل خود جمع و تقسیم بر تعداد سطر میشود.
سطر دوم 20000 + 19000 تقسیم بر دو برابر است با 19500
میانگین سطر پنجم، حاصل جمع فیلد Salary همه مقادیر سطرها تقسیم بر 5
*** اگر بخواهید بوسیله Over Clause ، میانگین همه سطرها یکسان باشد میتوانید از Script زیر استفاده نمایید:
SELECT Name, Salary, AVG(Salary) OVER(ORDER BY HireDate RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS avgSalary FROM Employees GO
خروجی :
منظور از ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING یعنی در محاسبه میانگین برای هر سطر تمامی مقادیر سطرهای دیگر در نظر گرفته شود.
پایان بخش اول
امیدوارم مفید واقع شده باشد.