بررسی گزارش آماری Crosstab (بخش اول)
  کمتر برنامه نویسی پیدا می شود که به این نوع گزارش گیری احتیاج پیدا نکرده باشد. امروز شما را با روش ها و ایده های جالبی برای پیاده سازی این گزارش آشنا خواهم کرد.
   SQL Server
   ۱۹۸۶۴
   این مقاله حاوی فایل ضمیمه نمی باشد
   محمد سلیم آبادی
   ۱۳۸۹/۱/۲۲
ارسال لینک صفحه برای دوستان ارسال لینک صفحه برای دوستان  اضافه کردن به علاقه مندیها اضافه کردن به علاقه مندیها   نسخه قابل چاپ نسخه قابل چاپ

 

مقدمه

Cross Tabulations یا بطور خلاصه Crosstabs (که به PivotTable نیز شناخته می شود) یک گزارش آماری رایج است. خیلی از spreadsheets هایی مثل Excel می توانند نتایج پرس و جوهای SQL را بارگذاری کنند و عمل Crosstab را روی آن انجام دهند. شرکت ماکروسافت  در MS SQL Server 2005 برای اولین بار عملگر جدولی غیر استانداردی به نام PIVOT را برای این منظور معرفی کرد(البته در کنار عملگر UNPIVOT). در این مقاله به راه حل ها یا روشهای موثری برای تولید Crosstab که همگی قابل استفاده در تمام نسخه های SQL Server و مطابق با استانداردهای زبان مجموعه گرای SQL اند را خواهم پرداخت. و امید وارم در مقالات بعدی فرصتی ایجاد شود تا به عملگر جدولی غیر استاندارد PIVOT در  SQL Server 2005 و Dynamic Pivot بپردازم.
کدها تست و اجرا شده اند و
syntax مربوط به درج داده ها تنها در SQL Server 2008 پشتیبانی می شوند.

(عملگرهایی که در ماده ی FROM استفاده می شوند را عملگر های جدولی گویند. تنها یک عملگر جدولی استاندارد وجود دارد آن هم JOIN می باشد. ولی در SQL Server عملگرهای جدولی دیگری معرفی شده اند که همگی غیر استاندارد هستند از جمله APPLY و PIVOT)

محیط عملیاتی

فرض کنید جدولی به نام فروش (sales) ایجاد کرده ایم که وضعیت فروش هر محصول (قیمت و تعداد فروش) را در هر سال مشخص می کند و جدولی دیگر با نام محصولات (products) مشخصات هر محصول را در بر می گیرد (مثل نام، وزن و...). ساختار جداول همراه با انتشار داده های آزمایشی به این شکل می باشند.

BEGIN TRAN;
CREATE TABLE Products 
(product_name VARCHAR(15) NOT NULL
   PRIMARY KEY CLUSTERED,
 [weight] INTEGER NOT NULL,
    CHECK (weight > 0));
GO

INSERT INTO Products (product_name, [weight])
VALUES ('P1', 5),
       ('P2', 25),
       ('P3', 12),
       ('P4', 1);
GO

CREATE TABLE Sales
(product_name  VARCHAR(15)  NOT NULL
   REFERENCES Products (product_name),
 product_price DECIMAL(5,2) NOT NULL,
 qty           INTEGER      NOT NULL,
 sales_year    INTEGER      NOT NULL);
GO 
INSERT Sales (product_name, product_price, qty, sales_year)
VALUES('P1', 85.5, 4,  1990),
      ('P1', 100, 10, 1990),
      ('P1', 121, 7,  1991),
      ('P2', 50, 15, 1993),
      ('P2', 85.5, 13, 1994),
      ('P3', 12, 1,  1990),   
      ('P3', 7, 25, 1991),
      ('P3', 7.5, 14, 1991),
      ('P3', 5, 3,  1993);
GO
COMMIT TRAN

تصویر داده های موجود در جداول (که با یکدیگر JOIN شده اند):



گزارشی که پس از پردازش داده های فوق انتظارش می رود:

همانطوری که در تصویر فوق نتیجه مورد نظر مشاهده می شود، برای هر محصول در جدول Product ما جمع کل فروش را نیاز داریم (totals) البته در کنار آن تفکیک فروش در سال های مختلف که در مثال فوق جمع کل فروش محصول در سال 1990 در ستون year1 و جمع فروش محصول در سال 1991 برای ستون year2 و همینطور به ترتیب تا سال 1994 برای ستون year4.

بطور مثال محصول P4 اصلا هیچ فروشی نداشته است. و محصول P3 در سال 1990 یک نمونه ی آن با مبلغ 12 واحد به فروش رسیده است و ...

برای تولید Crosstab راه حل های متعددی وجود دارد که استفاده از عبارت CASE یکی از بهترین، ساده ترین و شناخته ترین روش ها است. آقای Joe Celko در کتاب معروف خود با نام JOE CELKO'S SQL FOR SMARTIES: Advanced SQL Programming به روش های استاندارد تولید Crosstab پرداخته است. که در ادامه آنها را مورد بررسی قرار خواهم داد. در بخش های بعدی این مقاله بعد از معرفی PIVOT به مقایسه ی عملکرد (Performance) دو تا از بهترین روش ها در یک سناریوی خاص می پردازم.

اشاره به این نکته ضروری است که برخی از روشهای موجود در این مقاله موثر (Efficient) و کاربردی (Practical) نخواهند بود و تنها برای تکامل یافتن و معرفی تمام راه حل ها پیشنهاد شده اند. مثل روش subquery و outer apply.

انواع روشهای تولید crosstab (بدون روش های ویژه ی SQL Server)

*    Matrix Table
*    Multiple Outer Joins
*    Case Expression
*    Subquery
*    David Rozenshtein Method

Crosstab با OUTER JOINS

اگر در یکسری جداول موقت یا VIEW و یا عبارت های جدولی جمع کل فروش محصولات را در سال های مختلف بدست آوریم سپس می توانیم  آن جداول را با یکدیگر اتصال داده و جمع فروش محصولات در سالهای مختلف را در یک جدول به نمایش در آوریم. و برای اینکه ممکن است یک محصول در تمام سالها فروش نداشته باشد از اتصال خارجی استفاده می کنیم. در اتصال خارجی مقادیری که match نشدن با مقدار NULL نمایش داده می شود در این حالت می توانیم از تابع COALESCE برای تبدیل مقادیر NULL به عدد صفر استفاده کنیم.

من در این مورد از CTE یا Common Table Expression استفاده کرده ام (شما می توانید از Derived table یا حتی VIEW استفاده کنید) از CTE استفاده کردم تا کپسوله سازی اتفاق افتد و امکان درک Query به حد اکثر ممکنه برسد. ممکن است خیلی از کاربران از CTE بندرت استفاده کرده باشند یا حتی آشنا نباشد ولی CTE یک جایگزین بسیار مناسبی برای Derived Table است. زمانی که چند عبارت جدولی پشت سرهم تعریف می شوند بایستی از کاراکتر کاما (comma) برای تفکیک جداول استفاده شود.

;WITH C1 AS
(SELECT product_name,
        SUM(qty * product_price) AS year1
   FROM Sales 
  WHERE sales_year = 1990
  GROUP BY product_name),
   C2 AS
(SELECT product_name,
        SUM(qty * product_price) AS year2
   FROM Sales
  WHERE sales_year = 1991
  GROUP BY product_name),
   C3 AS
(SELECT product_name,
        SUM(qty * product_price) AS year3
   FROM Sales
  WHERE sales_year = 1992
  GROUP BY product_name),
   C4 AS
(SELECT product_name,
        SUM(qty * product_price) AS year4
   FROM Sales
  WHERE sales_year = 1993
  GROUP BY product_name),
   C5 AS
(SELECT product_name,
        SUM(qty * product_price) AS year5
   FROM Sales
  WHERE sales_year = 1994
  GROUP BY product_name),
   totals AS 
(SELECT product_name,
        SUM(qty * product_price) AS totals 
   FROM Sales 
  GROUP BY product_name)  
SELECT P1.product_name, 
       COALESCE(year1, 0) AS year1, 
       COALESCE(year2, 0) AS year2, 
       COALESCE(year3, 0) AS year3, 
       COALESCE(year4, 0) AS year4, 
       COALESCE(year5, 0) AS year5,
       COALESCE(totals, 0) AS totals
  FROM Products P1 
       LEFT OUTER JOIN C1 
            ON P1.product_name = C1.product_name 
       LEFT OUTER JOIN C2 
            ON P1.product_name = C2.product_name 
       LEFT OUTER JOIN C3 
            ON P1.product_name = C3.product_name 
       LEFT OUTER JOIN C4 
            ON P1.product_name = C4.product_name 
       LEFT OUTER JOIN C5 
            ON P1.product_name = C5.product_name
       LEFT OUTER JOIN totals 
            ON P1.product_name = totals.product_name;

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

 

Crosstab با کمک Correlated Scalar Subqueries

Subquery و مخصصوصا پرس و جوهایی تودرتوی وابسته (correlated nested query) یکی از قابلیت هایی است که به زبان SQL قدرت و انعطاف پذیری فوق العاده ای داده است. و اما متاسفانه جدا از مرحله ی نظری در بعد عملی بایستی داده ها از جداول استخراج شوند و query هایی می توانند سرعت بالاتری داشته باشد که نیاز کمتری به خواندن داده ها از روی حافظه ی مقیم در cache یا دیسک سخت (hard disk) داشته باشند. به هر حال این دیدگاه و سبک نوشتن کوئری ها از نظر تئوری و زبان شیرین SQL جایگاه ویژه ای دارد.

یکی از مزایای این سبک این است که نیاز ما را به به هر نوع اتصال (داخلی یا خارجی) برطرف می کند.
این روش بر خلاف سادگی بسیاری که دارد (تنها از سه ماده ی
SELECT و FROM و WHERE استفاده شده است!) سرعت بسیار نامناسب و غیر کارامدی داشته و همچنین هزینه ی Read بالایی هم دارد.
توجه داشته باشید که از علامت تساوی (=) برای دادن نام مستعار استفاده شده است که ممکن است در نرم افزارهایی غیر از
SQL Server مورد استفاده قرار نگیرد. شما می توانید از AS برای این منظور استفاده کنید.

SELECT product_name,
       year1 = COALESCE((SELECT SUM(qty * product_price)
                           FROM Sales
                          WHERE sales_year = 1990
                            AND product_name = P.product_name), 0),
       year2 = COALESCE((SELECT SUM(qty * product_price)
                           FROM Sales
                          WHERE sales_year = 1991
                            AND product_name = P.product_name), 0),
       year3 = COALESCE((SELECT SUM(qty * product_price)
                           FROM Sales
                          WHERE sales_year = 1992
                            AND product_name = P.product_name), 0),
       year4 = COALESCE((SELECT SUM(qty * product_price)
                           FROM Sales
                          WHERE sales_year = 1993
                            AND product_name = P.product_name), 0),
       year5 = COALESCE((SELECT SUM(qty * product_price)
                           FROM Sales
                          WHERE sales_year = 1994
                            AND product_name = P.product_name), 0),
       totals = COALESCE((SELECT SUM(qty * product_price)
                            FROM Sales 
                           WHERE product_name=P.product_name), 0)
  FROM Products P;

Crosstab با CASE Expression

عبارت CASE در SELECT Statement یک قابلیت فوق العاده موثری را بوجود آورده است. از CASE حتی در داخل توابع تجمعی مثل MAX و SUM می توانیم استفاده کنیم.
یکی از مزیت های این روش بسیار کم بودن هزینه ی
I/O آن است و هچنین خوانایی (readability) آن بسیار بالا بوده و بسادگی می توان آن را گسترش داده یا اصلاح کرد.

SELECT P1.product_name,
       year1 = SUM(CASE WHEN sales_year = 1990 THEN qty * product_price ELSE 0 END),
       year2 = SUM(CASE WHEN sales_year = 1991 THEN qty * product_price ELSE 0 END),
       year3 = SUM(CASE WHEN sales_year = 1992 THEN qty * product_price ELSE 0 END),
       year4 = SUM(CASE WHEN sales_year = 1993 THEN qty * product_price ELSE 0 END),
       year5 = SUM(CASE WHEN sales_year = 1994 THEN qty * product_price ELSE 0 END),
       totals = COALESCE(SUM(qty * product_price), 0)
  FROM Sales AS S1
       RIGHT OUTER JOIN Products AS P1
             ON S1.product_name = P1.product_name
 GROUP BY P1.product_name;

 

مراجع و اطلاعات بیشتر:

Advanced SQL Programming By Joe CELKO 3rd edition

Plamen Ratchev (SQL Server MVP)