2010年12月30日 星期四

UNION VS. UNION ALL 的效能

以前並沒有特別在意 UNION 與 UNION ALL 對於 T-SQL 效能的影響,直到最近 UNION 上萬筆資料後,才深深發現這小小的差異,竟還有很深的學問。

如果將 TableA UNION TableB,得到的結果,會類似將 TableA 與 TableB 的資料匯總之後再進行 Distinct 。所以 UNION 後的結果是不會重複的。

至於另一個 UNION ALL 呢? TableA UNION ALL TableB ,所得到的會是 TableA 與 TableB 的資料匯總,即便有重複資料,也都會被保留。

解釋過 UNION 與 UNION ALL 後,可以知道 UNION 比人家多了一個 Distinct 的動作,而這一額外動作就會對整個查詢效能造成影響,尤其是當你的資料量很大時,差異就會更明顯。

舉例來說,我建了2個暫存資料表,分別各放幾筆測試資料。

create table #tmp1 (t_Name varchar(30))
create table #tmp2 (t_Name varchar(30))
 
insert into #tmp1(t_Name) values ('paladin')
insert into #tmp1(t_Name) values ('hugo')
insert into #tmp1(t_Name) values ('ken')
insert into #tmp1(t_Name) values ('lillian')
insert into #tmp1(t_Name) values ('panda')
 
insert into #tmp2(t_Name) values ('polly')
insert into #tmp2(t_Name) values ('keen')
insert into #tmp2(t_Name) values ('hana')
insert into #tmp2(t_Name) values ('gemma')

select t_Name from #tmp1
union
select t_Name from #tmp2
 
 
select t_Name from #tmp1
union all
select t_Name from #tmp2

透過「顯示估計執行計畫」,可以得到下面的結果:


透過執行計畫可以瞭解, UNION 多了一個相異排序(Distinct Sort) 動作,這額外的動作,致使他的執行效能比 UNION ALL 慢了些。但是自己實際遇到的資料量,遠比這幾筆測試資料多太多了。於是修改了測試資料的筆數,並將測試數據列述如後。



declare @i_count int
set @i_count=0
 
while (@i_count<100000)  -- 分別用 1000,10000,100000
begin
 insert into #tmp1(t_Name) values ('paladin@@')
 insert into #tmp2(t_Name) values ('panda@@')
 set @i_count=@i_count+1
end


當測試資料達10萬筆之後,兩種語法的效能相差了 3 倍之多。

所以,如果您所要 UNION 的兩個資料表並不會有重複資料或是本身您並不在意有重複資料,改用 UNION ALL 會大大提昇整體 SQL 的查詢效率。

參考:

01.SQL SERVER – Union vs. Union All – Which is better for performance?

02.The effects UNION in a SQL query



你的網頁已經下載完畢了?

在撰寫 JavaScript 時,我們可能會透過 getElemnetById 或 getElemnetByName 來取得頁面上的物件,或者你想去呼叫某個已經事先定義好的函數。可是,如果網頁上的 DOM 尚未完全載入完畢時,你所執行的 JavaScript 語法,就很有可能會出現錯誤。我參考「Are you ready for this」這篇文章,作者提了一個問題,「你怎麼確定你的網頁已經下載完畢了?」

對於這個問題,首先要清楚網頁對於「下載完畢」的定義有分兩種:第一種:window.onready,是真的所有東西都下載完畢,包括:頁面所參考引用的所有 CSS、Script、Image、Flash。第二種:DOM-ready,則是網頁的DOM階層樹建置完畢,包括頁面上所引用的 CSS、Script。可想而知,第一種 window.onready 是所謂網頁真正完全下載完畢,但是目前的網頁越來越花俏,可能你的頁面有一個非常巨大的圖片連結,而要達到 window.onready 的地步,勢必要花不少時間。比較合適的作法,是第二種 DOM-ready,只需要確保我們執行的 JavaScript 所需的 DOM 階層架構都已經完整,就可以先執行了,不需要去等待圖片或是 flash 的下載。


在我們常見的一個 JavaScript 語法, window.onload=function(){ //Do something. };
他的效果比像上面所提到的第一種 window.onready,所以在我們使用它用的很開心的時候,應該好好想想,這種寫法會不會讓你的程式效能或是給使用者的感觀受影響。「jQuery in Action」一書的作者,在他書裡的第一章節,也提到為了避免讓使用者長時間的等待頁面圖片的下載,建議使用:

$(document).ready(function(){ // Do something. });

因為 jQuery.ready( )的效果,是我們介紹的第二種 DOM-ready 模式 ,可以讓使用者減少等待的時間。不過附帶一提的是, 作者認為使用 window.onload 的另一個缺點,就是他只能使用一次,如果被除重複呼叫或是你程式裡還有其他 third party 函式庫也使用了 window.onload ,就會造成被蓋來蓋去的問題。其實,這部份可以參考「JavaScript 小陷阱 (5) – window.onload()」這篇文章,透過一個小技巧,也是可以讓你重複呼叫多次的 window.onload 。

以下程式碼節錄自JavaScript 小陷阱 (5) – window.onload()

var oldOnload = window.onload || function () {};
window.onload = function ()
{
oldOnload();
// Do Something...
}

透過事先將舊的 onload 事件存放在一個變數,並於自行定義的 onload 事件中,找個適當位置觸發舊的事件。

在上一篇紀錄:「Internet Explorer 無法開啟網際網路網站」,除了使用者自己的瀏覽器問題之外,程式撰寫本身也是有可能會造成這問題。程式的部份,就是 DOM 有沒有完全下載完畢所導致。透過比較清楚的瞭解後,至少下次發生問題時,可以降低因為程式撰寫不良而讓系統出錯的機率。

參考:


01.Are you ready for this
http://www.hunlock.com/blogs/Are_you_ready_for_this
02.JavaScript 小陷阱 (5) – window.onload()
http://www.jaceju.net/blog/archives/160
03.Internet Explorer 無法開啟網際網路網站
http://paladinprogram.blogspot.com/2010/12/internet-explorer.html
04.The window.onload Problem
http://peter.michaux.ca/articles/the-window-onload-problem-still

2010年12月21日 星期二

Internet Explorer 無法開啟網際網路網站

最近有頁程式,壓下一個按鈕後,會執行一段 JavaScript 去作 window.open( ) 的動作。但不知為何,就有一位 user 反應他會出現「Internet Explorer 無法開啟網際網路網站」的錯誤訊息。


在 Google 找了好幾篇文章,建議把 IE 的 plug-in 套件移除或關閉,甚至蠻多人說是安裝了 skype 的 plug-in,但自己試了,還是沒用。最後在一篇文章「IE無法開啟網際網路網站,操作已中止」,有提到:如果出現這個畫面,代表在IE在DOM尚未載入完全的時候,就嘗試存取DOM而產生的訊息。所以,我猜想可能是我執行 window.open( ) 時,頁面上的 DOM 還沒完全載入所造成。既然如此,為了確保頁面上的 DOM 能完全載入,我想起透過 jQuery 的

$(document).ready(function(){  //Do Some ... });

透過這種寫法,可以確保所有 DOM 都載入後,才開始執行裡面的程式。

所以我將原先 window.open( ) 的動作,移到  $(document).ready(function(){  //Do Some ... }); 裡試試,果然就沒在出現原先的錯誤訊息了。

private void btnPrintSignNew_Click(object sender, System.EventArgs e)
{  
  string scriptString=string.Empty;
  scriptString = string.Format(@"
  <script language=JavaScript>
  <!-- begin 
  $(function(){{
  window.open('xxx');
  }});
  //end -->
  </script> 
"));

  this.RegisterStartupScript("MsgDownLoad",scriptString);
}


參考:
01:IE無法開啟網際網路網站,操作已中止

2010年12月16日 星期四

透過 Proxy 產生 Reporting Service 2008R2 報表

透過 proxy 呼叫 Reporting Service 所提供的 Web Service,可以提供我們更有彈性的報表管理。在 Reporting Service 2000 時,如果要產生 pdf 報表,使用 http://Repsdev/ReportServer/ReportService.asmx 的 Render( ) 方法來產生報表(Repsdev 是伺服器名稱)。隨著物換星移,當初的 Reporting Service 2000 現今也已換成 Reporting Service 2008R2 了,伴隨著微軟打著無痛升級、效率更好、功能更強的口號,系統也一一升級了,直到... user 打電話通知,報表不能 Run 之後,才發現事情並不是我想的這麼簡單。


版本升級對目前的設計所造成的衝擊,有以下幾點:

1.原先使用的 WebService http://Repsdev/ReportServer/ReportService.asmx ,現在已經不能用了,取而代之的,是可以選擇使用 http://Repsdev/ReportServer/ReportService2005.asmx 或 http://Repsdev/ReportServer/ReportService2010.asmx。確切來說,要知道你的主機上可以使用哪些 WebService,主要是看主機上安裝了哪些版本的 SQL Server。將各個版本的 Reporting Service 列出來比較,就能明瞭了。

SQL Server 2000 Reporting Server >>ReportService.asmx
SQL Server 2005 Reporting Server >>ReportService2005.asmx
SQL Server 2008 Reporting Server >>ReportService2006.asmx
SQL Server 2008R2 Reporting Server >>ReportService2010.asmx

目前我可以使用 ReportService2005.asmx 與 ReportService2010.asmx,是因為主機上裝了 SQL 2005 與 SQL 2008R2,由此解釋,應該可以通吧!


2. Render( ) 方法的入口點,已由 ReportService.asmx 改為 ReportExecutionService2005.asmx,這改變真的也讓人理不出個所以然來。只能靠網友間口耳相傳才會知道,自己則是在 Rendering a report using web services with 2008 R2 (ReportService2010) 這篇文章找到原因的。

3. Render( ) 引數的改變。 Render( ) 所接受的引數,在 ReportExecutionService2005.asmx 是跟 ReportService.asmx 不一樣的。新的 Render( ) 內容,可以參考 ReportExecutionService.Render Method

折騰一陣子後,終於把透過 Proxy 來產生 Reporting Service 報表的問題解決了。最後將完整的測試程式整理於後,以便有興趣的朋友或未來的自己參考參考。

範例是用 VS2003 所撰寫,所以有點懷舊的味道...只差程式沒辦法強調是黑白的而已。


using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Configuration;

namespace WebApplication1
{
 /// <summary>
 /// Summary description for WebForm1.
 /// </summary>
 public class WebForm1 : System.Web.UI.Page
 {
  protected System.Web.UI.WebControls.Button btnPDF;
 
  private void Page_Load(object sender, System.EventArgs e)
  {
   // Put user code to initialize the page here
  }

 

  private void btnPDF_Click(object sender, System.EventArgs e)
  {
   MakePDF();
  }

  private void MakePDF()
  {
   //PDF 暫存檔路徑(必須已經存在的檔案路徑)
   string filepath = "D:\\upload\\engage_upd\\PDF\\";
   string filename = filepath+"Quote.PDF";


   //取得 WebService 服務(SQL 2008R2 的 webservice 要使用 ReportExecution2005.asmx )   
   string strRSTmp="http://Repsdev/ReportServer/ReportExecution2005.asmx";   

   
   repsdev.ReportExecutionService rs=new WebApplication1.repsdev.ReportExecutionService();

   //設定 windows 帳號認證(正式使用時,採用此行)   
   //rs.Credentials=System.Net.CredentialCache.DefaultCredentials;

   //測試資料【連接Reporting Server】(測試時,採用此行,固定測試帳號)
   rs.Credentials=new System.Net.NetworkCredential("52xxxx","passwd","network");


   //重新指派 WebService 服務位址
   rs.Url=strRSTmp;
   
   Byte [] result=null;

   //參數收集(呼叫報表時必須傳入的參數,目前範例要傳入 seqsn,ver,isdraft 三個參數)
   repsdev.ParameterValue [] parameter=new WebApplication1.repsdev.ParameterValue[3] ;
   parameter[0]=new WebApplication1.repsdev.ParameterValue();
   parameter[0].Name="seqsn";
   parameter[0].Value="545";
   parameter[1]=new WebApplication1.repsdev.ParameterValue();
   parameter[1].Name="ver";
   parameter[1].Value="";
   parameter[2] = new WebApplication1.repsdev.ParameterValue();
   parameter[2].Name="isdraft";
   parameter[2].Value="1";


   repsdev.ExecutionInfo execInfo=new WebApplication1.repsdev.ExecutionInfo();
   repsdev.ExecutionHeader execHeader=new WebApplication1.repsdev.ExecutionHeader();

   //報表位置
   string reportPath = "/EPath/rptTest";
   string historyID = null;

   rs.ExecutionHeaderValue = execHeader;
   execInfo = rs.LoadReport(reportPath, historyID);
   rs.SetExecutionParameters(parameter, "en-us");


   string mimeType;   
   repsdev.Warning [] warnings=null;
   string [] streamIDs=null;
   string devInfo=@"<DeviceInfo><Toolbar>False</Toolbar></DeviceInfo>";
   string extension;
   string encoding;
   try
   {
    result=rs.Render("PDF",devInfo,out extension,out mimeType,out encoding,out warnings,out streamIDs);
   }
   catch(Exception e)
   {
    if(e.Message.IndexOf("401")>-1)
    {
     //HTTP 401:沒通過授權
     String scriptString = @"
<script language=JavaScript>
<!-- begin 
alert('您必須登入網域,才能有權限讀取本報表!') ;
//end -->
</script> 
";
     this.RegisterStartupScript("AccessDeny",scriptString);

     return;
    }
    else
     return;
   }

   FileStream fs=new FileStream(filename,FileMode.Create);
   fs.Write(result,0,result.Length);
   fs.Close();
   rs.Dispose();       
  }


 }
}



參考:

01.ReportExecutionService.Render Method

02.What is reportexecution2005.asmx i used this as webreference

03.Rendering a report using web services with 2008 R2 (ReportService2010)

04.SSRS 2008, C# and ReportingService.Render()

05.SSRS 亂七八糟的SOAP API 配對

2010年12月8日 星期三

一看就懂的圖解心經


站在書店的門口,在一個顯眼處的地方,發現一本圖文並茂,可愛到不行的書。翻翻封面,於背後有著一段文字:「心是人的主宰,心是認識一切的開始,宇宙的萬象變化,都是內心意識的變化所致。修行的重點是心,心無所執著心就解脫了,心一解脫一切就解脫了。就像風鈴一樣,不管是涼風、熱風、冷風吹來,風鈴都一樣搖曳作響毫不執著,凡人同樣也能得到大自在而就心無罣礙了。」

作個小實驗,請記得你現在的心情是什麼樣子,接著看一段下面影片:




現在再把你的心拿出來檢視一下,是否跟一開始的心境有所不同?

影片的內容,可以看作是一陣暖風,你的心,則是風鈴。你看影片時,隨著劇情的演進而感動,讓你心中的風鈴噹噹作響。但當影片結束時,你的心,是否也可以跟風鈴一樣靜止下來呢?

此刻的你我,也許正吹著冷風,也許吹著暖風,不管是東西南北風,風一停,都試著讓你的心也停。

如果你想更有智慧一點,可以看看這本書,他會告訴你一切的事物,都是緣生則聚、緣滅則散,目前自己會有所執著,多半是你的心有所貪求、心有所怨恨、心有所愚昧所致。正如華嚴經所云:「心如工畫師,能畫諸世間。」喜怒哀樂既然由心而起,自然也就由心而去囉!

2010年12月7日 星期二

ASP.Net 取得使用者真實 IP

ASP.Net 如何抓取 User 的 IP ? 於 Request.UserHostAddress 屬性,可以看到微軟的解釋為:取得遠端用戶端的 IP 主機位址。這句話沒錯,只是現在許多使用者的電腦與網站主機之間,常常會透過 Proxy 伺服器來存取主機上的網頁,也許是你自己設定的,也許是公司網管設定的,或是 ISP 業者,都有可能讓 Request.UserHostAddress 抓不到使用者的真實 IP。


更進一步來說,可能會聽到資安專家,直接要你死了這條心,在網路世界裡,是無法找到真實 IP的,因為網路封包的標頭都是可以假冒的,也就是說,你抓到的 IP ,有可能是假的。有許多論壇的網站,都很習慣將發表文章的作者 IP 連同文章內容一起公開,也就是說,該網站的運作方式,會去抓 User 的 IP ,然後存進資料庫。這動作,曾經讓不安好心的人起了個念頭。假設這個站台不注重資安,是用了

string sql = "INSERT INTO (IP) VALUE ('" + IP + "')";

這寫法, 會讓有心人士將 HTTP 標頭裡將原先紀錄 IP 位址的地方改成惡意的SQL 指令碼,那當你的系統在執行儲存 IP 動作時,也同時執行了惡意程式碼。而這一點,是所有準備要留下 IP 紀錄的工程師,必須要考慮的一個安全議題。當然,養成好習慣,驗證你所要傳給 SQL 的參數內容,是自保的不二法門。

以下提供網友提供的驗證是否為合法 IP 的 Regular :

^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$

用 C# 寫成 CheckIP( )

///
/// 檢查 IP 是否合法
/// 
/// strPattern:需檢測的 IP
/// true:合法 false:不合法
private bool CheckIP(string strPattern)
{
// regular: ^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$
Regex regex = new Regex("^\\d{1,3}[\\.]\\d{1,3}[\\.]\\d{1,3}[\\.]\\d{1,3}$");
Match m = regex.Match(strPattern);

return m.Success;
}

撇開特別的情況,抓真實 IP 的功能,還是得寫,總不能讓你無法跟老闆交待吧!但記得要先跟他打預防針,讓他理解到,無法保證百分百取得真實 IP,因為 IP 有可能被假冒,但正常情況下,取得大多數的使用者 IP 則是可行的。

假設使用者沒有透過代理伺服器,則使用 Request.ServerVariables["REMOTE_ADDR"] 就可以抓到 IP;但如果有透過代理伺服器,則要改成使用 Request.ServerVariables["HTTP_X_FORWARDED_FOR"]。而在kingwkb的专栏 的一篇文章「[hidotnet]真正的取真实IP地址及利弊」提到,如果使用者的環境是使用多重代理伺服器時,則使用Request.ServerVariables["HTTP_X_FORWARDED_FOR"] 會得到「真實IP,第一層代理IP,第二層代理IP,...」(ex:「140.134.4.4,140.134.4.250,140.128.2.10」)的結果。而 kingwkb 文章也進一步去將所謂內部 IP 的資訊事先在程式中進行了篩選,只留下相對可信的資訊。


最後整理了測試程式,結果如下:

撰寫了 GetIP( ) 函式來取得真實 IP,並搭配 CheckIP( ) 來驗證所抓的 IP 是否合法。

protected void Page_Load(object sender, EventArgs e)
{      
    //自己定義的 Label,用來顯示 IP 訊息
    lbIP.Text = GetIP();
       
}

private string GetIP()
{
    string ip;
    string trueIP=string.Empty;

    //先取得是否有經過代理伺服器
    ip=Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

    if (!string.IsNullOrEmpty(ip))
    {
        //將取得的 IP 字串存入陣列
        string[] ipRange = ip.Split(',');

        //比對陣列中的每個 IP
        for (int i = 0; i < ipRange.Length; i++)
        {
            //剔除內部 IP 及不合法的 IP 後,取出第一個合法 IP
            if (ipRange[i].Trim().Substring(0, 3) != "10." &&
                ipRange[i].Trim().Substring(0, 7) != "192.168" &&
                ipRange[i].Trim().Substring(0, 7) != "172.16." && 
                CheckIP(ipRange[i].Trim()))
            {
                trueIP = ipRange[i].Trim();
                break;
            }
        }
            
    }
    else
    {
        //沒經過代理伺服器,直接使用 ServerVariables["REMOTE_ADDR"]
        //並經過 CheckIP( ) 的驗證
        trueIP = CheckIP(Request.ServerVariables["REMOTE_ADDR"])?
            Request.ServerVariables["REMOTE_ADDR"]:"";
    }

    return trueIP;
}

/// <summary>
/// 檢查 IP 是否合法
/// </summary>
/// <param name="strPattern">需檢測的 IP</param>
/// <returns>true:合法 false:不合法</returns>
    
private bool CheckIP(string strPattern)
{
    // 繼承自:System.Text.RegularExpressions
    // regular: ^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$
    Regex regex = new Regex("^\\d{1,3}[\\.]\\d{1,3}[\\.]\\d{1,3}[\\.]\\d{1,3}$");
    Match m = regex.Match(strPattern);

    return m.Success;
}


參考文章:
01:[hidotnet]真正的取真实IP地址及利弊
02:使用HTTP_X_FORWARDED_FOR获取客户端IP的严重后果
03:PHP利用HTTP_X_FORWARDED_FOR抓取訪客ip
04:How to get client “IP Address” using Asp.net /C#

2010年12月1日 星期三

VS2010 使用 Source Control 的小細節

當使用 VS2010 搭配 Source Control 時,自己覺得有些地方用起來很不習慣。大致上將兩個地方說明如下:

第一:當使用「Split」設計模式時,很奇怪的都會自動將檔案簽出,而我只是想看看而已,並沒有打算修改,這樣檔案也莫名的被我簽出了。為了解決這問題,可以到 Tools / Options 的設定裡,在左邊分頁的 Source Control / Environment ,可以在右邊找到 Checked-in 區塊,然後把 Saving 與 Editing 的「Prompt for check out」挑選起來,日後有要簽出時,就會跳出視窗詢問你了。


第二:當我將檔案 check out 出來時,VS2010並不會自動幫我抓取最新的程式,所以有可能我現在即將要改得程式,並不是最新的。這時候,可以讓 VS2010每次 check out 程式時,就自動抓最新版程式回來。設定方式,可以在 Tools / Options ,於左邊分頁選擇 Source Control / Visual Studio Team Fundation Server ,並在右邊將 「Get latest version of item on check out」打勾,這樣就可以確保每次編輯都是最新的程式了。


非常感謝熱心的 Hugo 救我於水火,功德物量啊!!