Quantcast
Channel: 黑暗執行緒
Viewing all 2337 articles
Browse latest View live

提防臉書帳號釣魚網站

$
0
0

在臉書專頁留言區看到一則帳號停權通知,心頭一驚:

Last Warning, Your account will be disabled permanent because your accouts have been reported by other users,And another reason is not allowed on Facebook… 留言者取名Facebook Pages,又配上臉書Logo,留言內容大意是我的帳號因為被人檢舉即將被永久停權,下方有一個連到Facebook Pages臉書專頁文章的連結:

這裡面有更詳細的說明,提到我的專頁被人檢舉違反使用協議,將導致停用,需要驗證電子郵件信箱以示清白,並在底下列了網址驗證帳號以證明所有權,若不依指示,專頁將被永久刪除並無法還原。循著網址出現以下輸入帳號、密碼跟生日的登入頁面:

一路上都是臉書的網址、名稱跟標示,加上被警告將要被停權,當時人在外面從平板收到訊息,差點下意識就要敲帳號,不過大腦讀取帳號、密碼時觸發了人體資安模組,警鈴大作,仔細一看,這網址有問題呀!pages-help. tuars.com不是臉書的網域,莫非是「釣魚網站」!(要騙取訪客輸入帳號及密碼,以竊取臉書資料或冒用身分)

回家後再仔細查證,愈看愈有問題。首先,官方的停用通知怎麼會用留言方式遞交?而且說明文字有些文法及排版錯誤。再者連到所謂的Facebook Pages專頁,專頁今天才成立,第一篇文章就是第二張圖的停權通告,之後有幾篇五花八門的分享文章,都指向第一篇停權公告,猜想它還在別人的專頁上留言,目的在引誘專頁所有者或網友看到停權公告連到上圖的假登入畫面以騙取帳號。

猜想這種做法很快就會被官方發現下架,有可能是想打閃電戰,或者關閉後依然可以用同樣手法另起爐灶再戰,很難杜絕。

在此提醒大家:在Facebook上看到你帳號將被停權的警告訊息或電子郵件通知,請仔細求證,不要胡里胡塗地看到網頁有帳號、密碼輸入欄就敲,以免中計帳號被盗。


【茶包射手日記】由TFS取得專案發生System.Web.Mvc.dll參照無效

$
0
0

接到任務,要在前人的專案新増一個小功能。由TFS下載原始碼準備編譯時,出現奇怪現象。

System.Web.Mvc參照失敗:

但System.Web.Razor及其他System.Web.*系列是好的:

同一專案在同事機器開啟一切正常。由於是參照問題,加上專案有點歷史,一度以為專案是因為最早使用Visual Studio 2012開發,System.Web.Mvc參照來自VS2012或另外安裝的ASP.NET MVC套件,而我的電腦沒裝過VS2012才出狀況。直到想起該檢查csproj,真相大白!

<Reference Include="System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <Private>True</Private>
  <HintPath>..\packages\Microsoft.AspNet.Mvc.4.0.20710.0\lib\net40\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.Web.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <Private>True</Private>
  <HintPath>..\packages\Microsoft.AspNet.Razor.2.0.20710.0\lib\net40\System.Web.Razor.dll</HintPath>
</Reference>
…略…
<Reference Include="WebGrease">
  <HintPath>..\..\packages\WebGrease.1.5.2\lib\WebGrease.dll</HintPath>
</Reference>   

由csproj可知,不管System.Web.Mvc或System.Web.Razor都來自NuGet Packages,而我另外注意到,HintPath有..\packages及..\..\packages兩種,直接宣佈破案!而本宗案件需要綜合兩篇舊文章的知識:

  • 【茶包射手日記】相同專案在另一台機器出現元件版本不合錯誤
    剖析:問題專案搬過家,sln檔向上搬了一層目錄,packages理應也上搬一層,但csproj中部分HintPath仍指向原本的路徑,後來才用NuGet安裝的指向新路徑。同事電腦上仍留有搬家前的packages目錄,故可編譯無誤;我的原始碼取自TFS,NuGet還原時只會寫進.sln層的packages補足了程式庫,csprojq指向舊的packages路徑,導致Visual Stuio找不到System.Web.*。
  • 【茶包射手日記】Visual Studio編譯成功的專案在IIS發生組件版本不合
    剖析:既然System.Web.*都指錯地方,為什麼只有System.Web.Mvc參照出錯?這就不得不提「Visual Studio可以編譯IIS無法編譯專案」的特異功能(VS會額外搜索可能路徑)。在HintPath指錯的狀況下,VS2013努力補齊缺少的System.Web.*參照,獨漏System.Web.Mvc。

有了這次經驗,下回再有類似某幾個參照找不到的狀況,就不用傻找了,應依以下SOP處理:

  1. 優先確認csproj檔中的參照HintPath
  2. 若參照檔的屬性視窗與HintPath所指不符,請立即檢查是否路徑有誤或檔案遺失。

ASP市佔消失之謎

$
0
0

早上讀到一則PHP7相關報導,雖然跟PHP沒什麼緣份,但文章裡附的一張後端語言市佔統計圖引起我濃厚的興趣。統計圖裡PHP佔了81.7%,ASP.NET則是16.1%,同時使用兩種語言的網站會重覆計算(假設為N%,很多PHP應是來自LAMP,Linux+Apache+MySQL+PHP,我認為N不會太高),故二者吃下81.7%+16.1%-N%的市場(說不定超過95%),而第三名Java只有3%。分佈數據裡PHP跟ASP.NET加起來就橫掃全球讓我意外,分享到專頁時順便提出ASP連0.1%都擠不上的疑問,莫非被混入ASP.NET計算?

同事J看到貼文認真去查了資料,指出 w3techs.com 網站的統計ASP.NET可以再展開成.NET 1.1, .NET 2.0跟.NET 4.0,推測並沒有納入ASP。

這不科學呀!ASP的市佔小於0.1%?我不信、我不信、我不信… (地上打滾)

用inurl:index.asp及inurl:default.asp Google可以分別查到八千萬及三千五百萬筆資料,其中以學校機關組織為大宗,也不乏銀行、樂透彩、機場等日常服務,也可找到應是最近才上線的超商、線上遊戲網站,來源更是遍佈海內外,單憑這個簡單抽樣,就很難說服我ASP的市佔低於0.1%。

深入研究 w3techs 的統計方式,試著解釋數字的由來:

  • 以網站為單位,而非計算網頁數,只要全站有一個網頁用到,就認定該網站使用該技術。
  • 統計對象限定在Alexa(Amazon.com的子公司)排名前一千萬名的大網站(2013六月前只統計前一百萬),取三個月的平均排名,Alexa有時會有流量統計誤差,但應已具足夠代表性。 (順便查了我的部落格,目前排名全球第15萬大,台灣區則排2343 :P)
  • 不考慮子網域,例如 sub1.example.com 及 sub2.example.com 都被歸為 example.com 只算一個網站(同理:*.blogger.com, *.wordpress.com 也都只算一個)
  • 不考慮導向他處的網域,例如:Sun.com 實際上被導到 Oracle.com,不計入考量。
  • 經上述標準,前一千萬名網站篩選合併後只剩不到10萬個,但不影響統計有效性。
  • 統計數字每天更新。

所以這個調查只發生在全球前10萬名的大網站,0.1%為100個,ASP.NET有16000個,ASP.NET與ASP混用實務上蠻常見的,難道其中找不到100個ASP.NET與ASP混用的樣本?進一步,我又在網站上找到一個重要線索「w3techs接受使用者的建議新增語言」,意味 w3techs 只鎖定注特定的語言(就技術面限制比對範圍是合理的),會不會ASP根本沒列入調查項目呢?

最後,我在後端語言介紹看到一段說明:…Shows a list of all 20 server-side programming languages monitored for our surveys…,在統計頁列出的語言有:PHP, ASP.NET, Java, static files, ColdFusion, Ruby, Perl, Python, JavaScript, Erlang, Miva Script, Lasso, Scala, Lua, Tcl, Smalltalk, C++, Haskell, Lisp, Ada,不多不少,剛好20個。

因此,在 w3techs 統計看不到 ASP 的原因是-身為被ASP.NET取代的前朝語言,它被 w3techs 無視了,儘管它很有可能還會再戰十年~

全案終結。

黃色小鴨除錯之原力升級版

$
0
0

大家有聽過黃色小鴨除錯法嗎?

黃色小鴨除錯的概念起源於一本書,故事裡的程式大師隨身攜帶一隻黃色小鴨,遇到要除錯或射茶包時,就把小鴨放在桌上詳細地向小鴨解釋每一行程式。

實際體驗過幾次(當然,不一定要跟鴨子聊天,跟你的同事,甚至滑鼠講一下心事都成),還真的因此停止鬼打牆。黃色小鴨除錯法的奧妙在於當你向別人(甚至是完全不懂程式的人)詳細解釋的過程,無形中會強迫自己從頭釐清細節,逐一梳理邏輯,往往因此突破盲點,自己發現問題或想出解決方案。

如果拿出黃色小鴨還不能斬妖除魔怎麼辦?

薑!薑!薑!薑~~ 試試「黄色小鴨原力升級版」吧?

抓不出Bug,抓到猶豫徬徨懷憂喪志萬念俱灰了無生趣之際,當尤達大師的影象浮現眼前,再搭配經典台詞-Use the source, Jeffrey!立刻能感受任督二脈原力泉湧(原力內力傻傻搞不清楚?),Bug再多也不怕 XD

搞笑搞夠了,介紹這件程式魔人工藝品背後的故事。一切要從去年收到的奇妙聖誕節禮物說起:

超有創意的「尤達大師立體公仔著色組」!

不過衡量在3D列印塑料塗顏料與表呈現綠皮膚皺摺的難度,深信在我拙手著色之後,尤達變身史瑞克的機率極高,對原力大師是極嚴重的褻瀆,還是維持潔白原色為宜。

前陣子讀到「電蝕刻DIY」文章,決定藉機會玩一下。身為程式魔人,想到尤達當然少不了「Use the souce, Luke!」,順理成章成為我個人化金屬牌上的金句。

電流計廢紙盒跟做萬聖節服裝用剩的不織布廢物利用,簡單黏貼後,程式魔人的精神激勵神器就完成了~

等等,好像少了點什麼…

拿出塵封已久的Espruino板子,接上三顆LED,再寫一小段JavaScript:

var c = 0;
var res = 256;
digitalWrite(D4, 0);
digitalWrite(D5, 0);
digitalWrite(D6, 0);
setInterval(function() {
  c = (c + 1) % (res * 2);
var v = Math.abs(c - res) / res;
  v = v * 0.95 + 0.05;
  analogWrite(D0, v);
  analogWrite(D1, v);
  analogWrite(D3, v);
}, 10);

發光吧!尤達大師。

影片

May the force be with you!

【茶包射手日記】ScriptBundle與自動部署時間差

$
0
0

同事報案,舉發古怪茶包一枚。

ASP.NET MVC專案新増了moment.js,在BundleConfig將其加入打包範圍[延伸閱讀]:

            bundles.Add(new ScriptBundle("~/bundles/common").Include(
"~/Scripts/jquery-2.1.3.js",
"~/Scripts/jquery.blockUI.js",
"~/Scripts/moment.js",
"~/Scripts/angular.js",
"~/Scripts/angular-animate.js",
"~/Scripts/angular-sanitize.js"));

更新簽入TFS,使用先前設計好的Build Definition,按個鈕自動部署到測試台[延伸閱讀]。不過,在DLL及JS檔案都更新後,網站卻未如預期載入moment.js。初步研判有兩種可能:moment.js沒部署成功、網站應用程式的DLL沒更新。

用瀏覽器直接連httq://test-server/myweb/scripts/moment.js確認moment.js存在,查看\bin\MyWeb.dll,檔案更新時間正確,不放心,反組譯BundleConfig確認RegisterBundles()已納入moment.js,而覆寫MyWeb.dll會重啟Application Pool,不可能還保留更新前的Cache。試著重新儲存web.config迫使網站應用程式重啟,moment.js就現身了,完全排除MyWeb.dll沒更新到的可能。至此,案情陷入膠著…

福爾摩斯轉過身,重新坐下來,在煙斗裡填上煙絲後開始吞雲吐霧,他的四周隨即飄起雲一般的青煙。彷彿過了一世紀那麼久,他終於開口說:「華生,我想我們忘了時間差…」

覆寫\bin\MyApp.dll會重啟Application Pool無庸置疑,但ScriptBundle有個特性,會自動略過不存在的js不傳回錯誤,而ScriptBundle會記住缺檔不會每次重查,補上js也不會馬上出現,需等到Cache逾時或網站程序重啟。另一方面,自動部署複製檔案是有順序的,會先複製\bin\MyApp.dll,陸續更新\Content\…,\Html\才輪到\Scripts\moment.js。這裡構成了一個時間差陷阱:若複製完MyApp.dll到moment.js寫入這段期間有使用者連上網站,ScriptBundle判定「沒有moment.js」並記憶下來,由於部署程序不包含覆寫web.config(我們採取web.config不進版控的策略,需要修改時由OP人員手動調整),於是部署結束時也更新了moment.js,但ScriptBundle停留在忽略moment.js的狀態,如此解釋前述觀察到的現象。

說到這裡,華生同事一拍大腿:「沒錯,部署期間同事D就開始用測試台驗證修正結果,的確有此可能」

為了驗證,做了個實驗:

  1. 把moment.js更名為moment_.js,修改web.config讓網站應用程式重啟,ScriptBundle吐回結果不含moment.js
  2. 將moment_.js改回moment.js,重新整理網頁,ScriptBundle仍未補上moment.js
  3. 修改web.config重啟網站應用程式,moment.js出現惹

結論:下回遇到更新ScriptBundle項目後如變更未如期出現,可再次重啟網站應用程式以排除前述時間差茶包。

蛤,微軟停止.NET 4.0/4.5/4.5.1的技術支援?會對我的系統造成影響嗎?

$
0
0

2016/1/12是個大日子,有好幾項微軟產品結束技術支援,包含:

  • Windows 8
  • IE 8/9/10 (狂賀)
  • .NET 4.0/4.5/4.5.1

生活周遭的Windows 8都已免費升級到Windows 8.1或Windows 10,受影響者應該有限。而官方停止支援舊版IE是好消息,以後可以大聲客戶說我們不支援IE 8/9/10,最後要不要支援是另一回事,但至少可以下巴抬高45度「好吧!看你可憐,我再幫你一次,下不為例哦~」,網頁開發者等這一天應該很久了… (謎:接洗謀摳零A代誌,快醒醒呀孩子)

但,第三項讓人毛毛的。啥?微軟不再支援.NET 4.0/4.5/4.5.1?那還沒升級到.NET 4.5.2或4.6的專案會受影響嗎?同事問了這個好問題,直覺微軟不致讓.NET技術支援週期政策嚴重衝擊現行系統,但我沒把握說不會,還是得查證才安心。

MSDN部落格去年底在這篇文章提到.NET 4、4.5、4.5.1中止技術支援的細節:1/12起將不再為.NET 4.0/4.5/4.5.1提供安全更新、程式修正(Hotfix)及技術支援,而.NET 3.5/4.6/4.6.1的技術支援週期可參考時間表。另外,這篇FAQ對.NET支援週期有更詳細的說明,摘要重點如下:

  • 2010年3月起,微軟宣佈自.NET 3.5 SP1起,.NET被定義為附屬元件(Component)而非獨立產品,技術支援週期依其支援的作業系統制訂。
    2014/8/7,微軟宣佈.NET 4.0/4.5/4.5.1自2016/1/12停止支援,建議開發人員升級至4.5.2。而.NET 4.5.2比照.NET 3.5 SP1,支援週期向其支援的作業系統看齊。
  • 元件的定義:隨附於微軟產品的一組檔案或特性功能,可以與產品一起發行,也可能包含於Service Pack、更新,透過網路下載發佈。
  • .NET 4.x採行就地升級In-Place Update政策,基本上.NET 4/4.5/4.5.1會隨Windows Update升級到.NET 4.6.2。
  • .NET 4/4.5/4.5.1停止支援不影響.NET 3.5 SP1的支援政策。
  • .NET 4.5.2向前相容,故升級不影響既有.NET 4.x程式,不需要重新編譯或做任何調整。僅有的例外是.NET 4.5.2存在極少數不相容修改,目的多半是為了符合業界標準或修正錯誤,真的遇上,除了調程式好像也不能怎麼樣。:P 不相容的項目可以看這裡
    另外,有些修改必須針對4.5.2編譯才會生效,稱為Retargeting Change。清單看這裡
  • 針對Exchange、SQL Server、Dynamics CRM、SharePoint、Lync等使用.NET 4或4.5的產品,依據前述原理,更新.NET 4.5.2後產品不需配合更新。
  • 隨Visual Studio安裝的.NET Framework,其技術支援週期仍跟著Windows作業系而不是Visual Studio。

【結論1】.NET 4.x可就地升級成4.5.2,而原有的.NET 4.x程式基本上不需做任何修改。

很好,那我們需不需要做什麼確保.NET 4.x更新到4.5.2?

這篇文章,2015/1/6起.NET 4.5.2已透過Windows Update開始部署, 使用WSUS的企業如暫時不想安裝,IT需修改Registry阻擋自動部署。(這篇KB提到修改Registry暫不升級的做法,亦為佐證)另外在這篇討論裡,網友提到.NET更新會被標成「重要」但不會主動強迫安裝。

【結論2】使用者只要「定期安裝"該裝"的Windows Update」應該都已升級.NET 4.5.2

總結:只要有定期執行Windows Update,Windows應已更新至.NET 4.5.2,而絕大部分.NET 4.x程式不需配合修改,故不用擔心.NET 4/4.5/4.5.1停止技術支援對系統造成影響。

快速將C#型別轉成TypeScript介面定義

$
0
0

使用TypeScript處理AJAX呼叫時,常需要在前端定義與C#物件屬性一致的TypeScript型別,以便將後端傳來的JSON資料還原成強型別物件。針對較正式的資料模型,我會用CodeGen方式同步C#與TypeScript端的型別定義(順便處理多語系問題)。但蠻多時候處理對象只是零散的小型別,不必殺雞用牛刀,針對這類需求,推薦一個好用工具-TypeLITE

在NuGet查關鍵字"typelite",很快就可找到TypeLite套件。

安裝後,專案會加入TypeLite.dll、TypeLIte.Net4.dll兩顆DLL,並在Scripts目錄下新増TypeLite.Net4.tt。接著就可以修改TypeLite.Net4.tt內容,決定為哪些型別產生TypeScript定義。

TypeLITE預設做法是執行.ForLoadedAssemblies()自動尋找組件標註[TsClass] Attribute的型別,但實務上用.For<型別名稱>()列舉要轉換的型別(如紅框所示)比較方便;若型別來自網站專案以外的組件,記得要加上<#@ assembly #>宣告(如黃框所示)。

修改TypeLite.Net4.tt並存檔,T4工具將自動產生或更新TypeLite.Net4.d.ts,在其中可看到我們用.For<型別名稱>()所列舉型別的TypeScript介面定義,若型別有參照到其他型別,TypeLITE會自動包含進來,非常方便。(在範例中用到Reflection的MethodInfo,相關介面就像提棕子一般整串被拉進來。)

TypeScript定義檔僅在開發編譯階段提供強型別約束及提示,不會產生任何JavaScript,不用擔心TypeLite.Net4.d.ts的內容太複雜。就這樣,輕輕鬆鬆幾個步驟,就把C#端的型別定義搬到TypeScript端囉~

又到了呼口號時間:TypeLITE,好用!好用!好用!

善用VS2015 NuGet Manager解決方案管理功能

$
0
0

NuGet已經是Visual Studio寫專案時的柴米油鹽醬醋茶了,但有些人可能沒發現在解決方案(.sln,Solution)層也可以開啟NuGet Manager。如下圖,在Solution Explorer的.sln開右鍵選單,有一個「Manage NuGet Packages for Solution…」:

在解決方案開啟NuGet Manager可以一次檢視所有專案的Package安裝狀況,但在VS2013時代,這功能有點雞肋。如以下例子,解決方案裡總共有三個專案安裝兩種不同版本HelperSharp,於是NuGet Manager的Package清單會看到兩筆HelperSharp,點選其中一項,右側則會顯示它被安裝在哪幾個專案(Currently installed in projects),點下Manage鈕可以批次移除。

Visual Studio 2015對NuGet Manager介面做了大幅改版,解決方案的套件管理介面變得更貼心好用。首先,同一套件的新舊版本被整合成單一項目,在右側以清單方式呈現套件在各專案的安裝狀況,並直接顯示安裝的版本號碼。如果你常常處理元件版本衡突,一定能了解這張彙總清單有多珍貴。:P

而處理元件版本衝突時,更新到最新版有時不是最佳解,降版牽就某些既有程式庫反而比較省事。過去NuGet Manager UI只提供解除安裝或升級到最新版,要安裝特定版本需要使用命令列工具下指令。VS2015的NuGet Manager安裝套件時可由版本清單選取最新版或歷史版本,還可透過勾選一次安裝或更新多個專案,解決版本衡突一次到位。

身為三不五時要處理元件版本衝突的苦手,發現這個功能的當下,我彷彿看到Visual Studio開發人員對我說「你的痛苦,我懂!」XD 再次為地表最強的開發工具按個讚!


CTE應用-將多筆查詢結果合併成逗號分隔字串

$
0
0

一對多關聯是常見的資料庫應用情境,有時我會遇到將多筆關聯資料特定欄立合併成逗號分隔字串的需求。聽起來有點抽象,用個實例說明。

假設有兩個資料表,應用系統清單及負責該系統的工程師名字,用以下指令建立模擬資料:

CREATETABLE AppSystem (
  Name VARCHAR(16),
  CodeName VARCHAR(16)
)
INSERTINTO AppSystem VALUES ('HR', 'Mars');
INSERTINTO AppSystem VALUES ('ERP', 'Jupiter');
INSERTINTO AppSystem VALUES ('POS', 'Venus');
CREATETable AppSupport (
   AppName VARCHAR(16),
   Engineer VARCHAR(16)
)
INSERTINTO AppSupport VALUES ('HR', 'Jeffrey');
INSERTINTO AppSupport VALUES ('ERP', 'Jeffrey');
INSERTINTO AppSupport VALUES ('POS', 'Jeffrey');
INSERTINTO AppSupport VALUES ('ERP', 'Darkthread');
INSERTINTO AppSupport VALUES ('POS', 'Darkthread');
INSERTINTO AppSupport VALUES ('HR', 'SecrectSound');
INSERTINTO AppSupport VALUES ('ERP', 'MouthCannon');

如下圖所示,資料顯示有HR、ERP、POS三個系統,而工程師共四位,每個人各負責1到3個系統。

用簡單的JOIN查詢就能列出每個系統負責的工程師,但有個缺點,每個工程師配一個系統會出現一次,故系統資料會重複,例如:HR、POS各出現兩次,ERP出現三次。

若目的在產生報表,理想的呈現方式是將同一系統的工程師名字合併成以逗號分隔的字串,像這樣:

要轉換成上述格式,我試過幾種做法:

  1. 建Temp Table跑Cursor統整資料
    我用T-SQL寫Cursor的功力很鳥,有馬拉松跑者被抓去游泳的fu,而建Temp Table跑Cursor迴圈的程序感覺有點繁瑣沒效率。
  2. 將JOIN結果拉回.NET端整理
    寫C#彙整資料這類雕蟲小技是我的強項,幾乎是信手拈來,開發跟執行效率都不會太差。但拉到.NET端處理有個缺點,JOIN完的重複資料需透過網路傳輸傳到網站伺服器後才能加工,若主資料的欄位又多又長,同一筆重覆次數又高,浪費的頻寬很可觀。

爬文找到更多做法,包含FOR XML PATH、自訂函式、SQLCLR函式、CTE… 等。之前體驗過CTE(Common Table Expresion)的特異功能,讓我頗為驚喜,當發現CTE也可解決這類需求,便決定動手試試。

CodeProject上有一篇好文章,照方煎藥,這個案例的合併欄位CTE寫法如下:

;WITH SupportCTE (RowNum, Name, CodeName, Engineer,Engineers) AS
(
SELECT 1, A.Name, A.CodeName, MIN(B.Engineer), 
CAST(MIN(B.Engineer) ASVARCHAR(MAX)) AS Engineers
FROM AppSystem A JOIN AppSupport B ON A.Name = B.AppName
GROUPBY A.Name, A.CodeName
 
UNIONALL
 
SELECT A.RowNum + 1, A.Name, A.CodeName, B.Engineer, 
CAST(A.Engineers + ', ' + B.Engineer ASVARCHAR(MAX)) As Supports
FROM SupportCTE A JOIN AppSupport B 
ON A.Name = B.AppName AND B.Engineer > A.Engineer 
)
 
SELECT * FROM SupportCTE

上次一樣,我們要善用 CTE 主查詢 UNION ALL 遞迴查詢的原理解決問題。程式有幾個重點,第一段為主查詢,JOIN AppSystem及AppSupport兩個資料表,除了Name及CodeName、Engineer外,又多加RowNum及Engineers兩個欄位。Engineers存放工程師名字串接結果,RowNum統計Engineers包含的工程師資料筆數。一個系統可能有多位工程師,Engineer要用GROUP BY及MIN()找出名字排序最前者(稍後遞迴時要靠它排除處理過的項目,防止陷入無窮遞迴,這個環節很重要)。UNION ALL接的第二段查詢將被遞迴呼叫(故一定要設計跳出遞迴的邏輯),原理是利用現有結果反覆JOIN AppSupport找出同一系統的其他工程師,將其串接於Engineers欄位後方,同時將RowNum+1,Engineer則換成本次串接的工程師名稱。JOIN條件限定工程師名字排序要在既有資料之後,可排除已串接過的項目,避免無窮迴圈。

直接SELECT SupportCTE可以看到遞迴查詢的結果,比較能體會其運作原理。

如上圖所示,每個系統都有多筆資料,Engineers分別由1-3位工程師名字串接而成,RowNum即為串接資料的個數。如此,我們只需找出每個系統RowNum最大的一筆,即為最終結果。這應該難不倒冰雪聰明的你,用以下寫法就可輕鬆搞定:

SELECT A.Name, A.CodeName, A.Engineers FROM SupportCTE A
JOIN (SELECT Name, Max(RowNum) AS MaxRowNum FROM SupportCTE GROUPBY Name) B
ON A.Name = B.Name AND A.RowNum = B.MaxRowNum

補充:針對找出RowNum最大的一筆,路人乙提供一個更棒的解法,不用JOIN就可以搞定,特此感謝!

SELECT Name,CodeName,Engineers
FROM (
SELECT *,ROW_NUMBER() OVER (PARTITION BY Name ORDERBY RowNum Desc) AS Pos
FROM SupportCTE
) T
WHERE Pos = 1 
ORDERBY Name

薑!薑!薑!薑~

實做一趟的感想:用CTE實現多筆資料串接需要動用遞迴,設計起來要費點腦筋,有點呼應那句「遞迴只應天上有,凡人應當用迴圈」的俗諺,但只要悟透原理,也不到難以駕御的程度,而且試出來的那一刻,還有種自己通過智力測驗的成就感(笑),我想我會開始在專案上試用這種寫法。

欄位合併效能比較:CTE vs FOR XML

$
0
0

前一篇文章介紹用CTE實現SQL一對多關聯欄位合併的方法,找資料期間曾發現另一個替代做法,利用SQL Server的FOR XML PATH('')技巧,可將多筆資料轉成單一字串(參考),看來也相當簡潔。但當下覺得子查詢加FOR XML轉換的做法有效能疑慮,理應不如CTE(事實不然!),加上搞CTE比較有挑戰性,故選了CTE研究嚐鮮。

網友ChoeChin留言提到FOR XML寫法,讓我重新思考:如果要大量重度應用,效能議題不可忽視,CTE效能是否真的比FOR XML方法好?有待實驗證實。經一番測試後,結論是我錯估了CTE與FOR XML兩種做法的效能表現。

原本的測試樣本太小,很難看出效能差異。我改用以下指令生出10萬筆糸統,並將AppSystem的Nam欄位與AppSupport的AppName, Engineer欄位設成Primary Key確保查詢效能。

DECLARE @I INT, @NOVARCHAR(6);
SET @I = 0;
TRUNCATETABLE AppSystem; 
TRUNCATETABLE AppSupport;
WHILE @I < 25000
BEGIN
SET @NO = CONVERT(VARCHAR(6), @I);
INSERTINTO AppSystem VALUES ('HR-' + @NO, 'Mars-' + @NO);
INSERTINTO AppSystem VALUES ('ERP-' + @NO, 'Jupiter-' + @NO);
INSERTINTO AppSystem VALUES ('POS-' + @NO, 'Venus-' + @NO);
INSERTINTO AppSystem VALUES ('MAIL-' + @NO, 'Apolo-' + @NO);
INSERTINTO AppSupport VALUES ('HR-' + @NO, 'Jeffrey-' + @NO);
INSERTINTO AppSupport VALUES ('ERP-' + @NO, 'Jeffrey-' + @NO);
INSERTINTO AppSupport VALUES ('POS-' + @NO, 'Jeffrey-' + @NO);
INSERTINTO AppSupport VALUES ('ERP-' + @NO, 'Darkthread-' + @NO);
INSERTINTO AppSupport VALUES ('POS-' + @NO, 'Darkthread-' + @NO);
INSERTINTO AppSupport VALUES ('HR-' + @NO, 'Alice-' + @NO);
INSERTINTO AppSupport VALUES ('ERP-' + @NO, 'Bob-' + @NO);
INSERTINTO AppSupport VALUES ('MAIL-' + @NO, 'Jeffrey-' + @NO);
SET @I = @I + 1;
END

我設計了四種查詢方式:

  1. CTE寫法的最終改良版,採用路人乙建議的ROW_NUMBER()法挑出最終合併結果
  2. 我最早的CTE寫法,用JOIN找出最終合併結果
  3. 標準FOR XML PATH做法
  4. 將重複出現的FOR XML PATH子查詢轉成CTE,看效能是否因此提升
;WITH SupportCTE (RowNum, Name, CodeName, Engineer,Engineers) AS
(
SELECT 1, A.Name, A.CodeName, MIN(B.Engineer), 
CAST(MIN(B.Engineer) ASVARCHAR(MAX)) AS Engineers
FROM AppSystem A JOIN AppSupport B ON A.Name = B.AppName
GROUPBY A.Name, A.CodeName
UNIONALL
SELECT A.RowNum + 1, A.Name, A.CodeName, B.Engineer, 
CAST(A.Engineers + ', ' + B.Engineer ASVARCHAR(MAX)) As Supports
FROM SupportCTE A JOIN AppSupport B 
ON A.Name = B.AppName AND B.Engineer > A.Engineer 
)
 
--Trail 1
SELECT Name,CodeName,Engineers
FROM (
SELECT *,ROW_NUMBER() OVER (PARTITION BY Name ORDERBY RowNum Desc) AS Pos
FROM SupportCTE
) T
WHERE Pos = 1 
ORDERBY Name
 
--Trial 2
--SELECT A.Name, A.CodeName, A.Engineers FROM SupportCTE A
--JOIN (SELECT Name, Max(RowNum) AS MaxRowNum FROM SupportCTE GROUPBY Name) B
--ON A.Name = B.Name AND A.RowNum = B.MaxRowNum
--ORDERBY A.Name
 
GO
 
--Trail 3
SELECTDISTINCT d.Name ,d.CodeName,
(
SELECT c.Engineer +','FROM
 (
SELECT a.Name, a.CodeName, b.Engineer 
FROM AppSystem a LEFTJOIN AppSupport b ON a.Name = b.AppName
  ) c
WHERE c.Name = d.Name FOR XML PATH('')
) AS Engineer
FROM
(
SELECT a.Name, a.CodeName, b.Engineer 
FROM AppSystem a LEFTJOIN AppSupport b ON a.Name = b.AppName
) d
ORDERBY Name
GO
 
--Trail 4
;WITH SupportCTE (Name, CodeName, Engineer) AS
(
SELECT a.Name, a.CodeName, b.Engineer 
FROM AppSystem a LEFTJOIN AppSupport b ON a.Name = b.AppName
)
SELECTDISTINCT d.Name ,d.CodeName,
(
SELECT c.Engineer + ','FROM SupportCTE c
WHERE c.Name = d.Name FOR XML PATH('')
) AS Engineer
FROM SupportCTE d
ORDERBY Name

執行結果如下。以Total execution time欄位做為比較標的,CTE+ROW_NUMBER()法 2.96秒、CTE+JOIN找結果法7.14秒,而FOR XML PATH法只花了0.59秒,FOR XML PATH+CTE法0.64秒(沒有變快)。

CTE慢了近5倍,顛覆我原本的想像…

由執行計劃推敲效能差異來源。FOR XML PATH法的執行計劃很單純,絕大部分的時間用在JOIN查詢。

CTE做法的執行計劃相對複雜許多,而高達84%消耗在Sort上!由Sort出現位置推測約略發生在用ROW_NUMBER()找最終合併結果時,猜想問題出在CTE結果暫存於記憶體,無法仰賴Index等機制加速,資料量一變大即成瓶頸。

若要以此推論CTE效能不好也有失公允,這次合併欄位的CTE動用了MIN、GROUP BY、ROW_NUMBER(),相對FOR XML PATH笨重複雜許多,而CTE的遞迴邏輯有其強大難以被取代的場合,隨便換個例子就不是FOR XML PATH可以搞定的。只能說,在這個案例用CTE加遞迴有殺雞用牛刀之嫌。如果只是在SQL要將一對多欄位合併成單一字串,FOR XML PATH方法有較好的效能表現。

最後補充一點,前述的FOR XML PATH寫法有個小瑕疵,Engineer欄位結尾會多一個逗號。

這個小問題在C#端可以用TrimEnd(',')輕鬆搞定,若要在T-SQL做掉稍微囉嗦一點,但也不難,例如以下範例。

SELECT Name, CodeName, 
CASEWHEN LEN(Engineer) > 1 THENSUBSTRING(Engineer, 1, LEN(Engineer)-1) ELSE Engineer ENDAS Engineer
FROM (
SELECTDISTINCT d.Name ,d.CodeName,
    (
SELECT c.Engineer +','FROM
     (
SELECT a.Name, a.CodeName, b.Engineer 
FROM AppSystem a LEFTJOIN AppSupport b ON a.Name = b.AppName
      ) c
WHERE c.Name = d.Name FOR XML PATH('')
    ) AS Engineer
FROM
    (
SELECT a.Name, a.CodeName, b.Engineer 
FROM AppSystem a LEFTJOIN AppSupport b ON a.Name = b.AppName
    ) d
) X
ORDERBY Name

在SharePoint網站檢視RDLC報表發生錯誤

$
0
0

冷門茶包一枚。在SharePoint網站安裝網站應用程式,在其中使用ReportViewer檢視RDLC報表,發生以下錯誤:

The type 'Microsoft.SharePoint.Portal.Analytics.UI.ReportViewerMessages, Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' does not implement IReportViewerMessages

SharePoint大幅更動預設網站web.config設定,導致掛在同一網站的網站應用程式出現各種奇幻現象,向來惡名眧彰,而錯誤訊息出現SharePoint字樣更是鐵證如山。

以錯誤訊息爬文查到將以下appSetting設定註解掉的做法:

<!–  <add key="ReportViewerMessages" value="Microsoft.SharePoint.Portal.Analytics…… /> –>

不過,想當然爾,問題網站的web.config並沒有這個設定可以註解,猜想是從根網站繼承來的。不想去更動根網站的SharePoint的設定,試了以下這招,appSettings既然可以<add key="…">,自然也可以<remove key="…">,在問題網站的appSettings加入以下設定:

<remove key="ReportViewerMessages" />

問題排除!

【茶包射手日記】IIS不接受TCP連線

$
0
0

公司有一台主機連續兩天出現異象。剛上班的尖峰時間出現IIS網站無回應,使用遠端桌面登入觀察到桌面反應遲緩,但幾個系統服務與網路磁碟機功能正常,IISRESET後問題仍未消除。進一步用 telnet 127.0.0.1 80 測試,得到的結果是TCP連線一建立就被切斷,主機上有另一個走8080 Port的PHP網站,telnet 127.0.0.1 8080 也是一接通馬上斷線,感覺問題在比IIS更底層的程序。時處尖峰不便重開機,而沒回應的網站有替代主機可用,就先按兵不動,約莫過了半小時,IIS自己醒來又一切如常。

不料,同樣的狀況隔天再上演一次,一樣出現 telnet 127.0.0.1 80 跟 8080 一連上就斷線,等了一個小時狀況仍未恢復,感覺狀況不太妙,呼叫同事進場協助。

同事依經驗砍了 Task Manager 中的mssearch程序(Sharepoint的檢索功能),此時發現有個 DW20.exe 執行中,涉嫌重大。

DW20(Dr.Watson)是 Windows 2003/XP 的錯誤報告程序,負責在程式發生嚴重當機時跳出來蒐集資訊(例如:錯誤代碼、呼叫堆疊、當時的記憶體內容等),差不多像是法醫寫驗屍報告。傾印記憶體等程序會耗用大量CPU及磁碟資源,有可能就是導致IIS 異常、甚至 TCP 連線無法建立的元凶。

IIS 與 PHP 在砍除 DW20 後,隨即恢復正常。因此學到一項經驗:DW20或Windows Error Reporting(Vista之後WER取代Dr.Watson)的錯誤報告產生程序可能耗用大量系統資源導致系統運作異常,故偵錯時可優先識別工作管理員中是否有DW20.exe或WerFault.exe出沒,排除記憶體傾印程序導致異常的狀況,特此筆記。

後記:今天剛好在我的PC也遇到一起Windows桌面凍結無反應的狀況,工作管理員顯示CPU不高,但發現WerFault.exe的蹤跡,凍結狀態在WerFault.exe結束後消失,相似案例+1。

2016台北渣打馬拉松

$
0
0

攝氏4度。

曾以為這是跑東京馬或京都馬才有幸體驗的跑馬溫度,今天我們終於能驕傲地說:台灣,做到了!(補聲暗)

報名渣打馬原本期望靠著路平天冷好催速,振作起來挑戰PB。天曉得遇上44年難得一見的霸王級寒流(專業名詞是負北極振盪),全台處處雪花飄,台北市不只陽明山,連木柵附近的猴山岳、阿柔洋、二格山甚至指南宮都下起來,面對此生難逢的寒流,把握氣溫最低的清晨時分,在戶外奔跑徹底體驗4度到底有多冷,好盡興呀~(淚)

4:40趕到總統前,雨勢甚大,濕冷又有風,一整個寒徹骨。實在不想脫掉保暖的大外套,但因起終點不同,5:00前就結束寄物發車,只好摸摸鼻子,脫掉外套早早寄物,穿著排汗衫、短褲,靠著薄風衣在寒風發抖至少半小時。關鍵時刻,跑者們發揮動物本能,不約而同在原地跳抗寒,起跑區數千人原地跳真是壯觀,活像緒勢待發的僵屍大軍來著。XD

5:30準時鳴槍起跑,好久沒參加數千人的大比賽,跑了兩公里人潮才漸漸散開。隨著四肢活動身體漸暖,氣溫低不再是困擾,如果天氣乾爽,這種氣溫跑來倒也舒適,但下雨壞了好事。雨勢大到地面積水,落腳得小心翼翼避開水坑,耗神又無法盡興跨步,但如此仍難逃鞋襪濕透的下場,一路都在提心吊膽會起水泡。而天冷四肢僵硬跑不順,Pace只在540到六分速間游移。

這場比賽,我最不爭氣的器官,不是腿腳膝蓋,不是心臟,而是膀胱!明明寄物後才上過廁所,在起跑區就開始尿急,搞得我一出拱門就沿途張望,眼睛除了要閃積水還要找廁所,好忙。要命的是,一直到5公里水站才看到流動廁所(曾有衝動要不要靠邊就地解放,想想還是當個文明人吧),乖乖排隊,花兩分鐘解決人生大事後,總算一身輕鬆,繼續上路。依照過往經驗,擔心水分流失傷身,依循慣例每個水站兩杯的頻率補充水分,但顯然低溫環境的身體代謝效率大不同,沒將天冷因素納入考量的下場是-整場比賽我上了五次廁所!五次!還創下五公里不到上兩次廁所的記錄,為我的馬拉松生涯再創新頁… orz

跑到16K處,對向跑完30K的第一名選手已折返,再往前行陸續看到的清一色是黑人選手,前十名幾乎被包辦(事後看報導第八名起才是台灣的何盡文)。令我印象深刻的是第三名老兄,沒戴帽子,倒是戴著旅館的拋棄式浴帽,大概是不用就抛想省累贅吧!頭頂透明香菇帽飛奔而來,專業跑姿搭配詼諧造型,害我嘴角忍不住上揚。

決定戴手套是對的,進水站要脫手套拿餅乾剝香蕉頗麻煩,還一度跑出水站才發現手套不見回頭撿,幾度試著脫下來放口袋,都在十分鐘內默默戴回去。雙手位處四肢末梢運動量又低,不太經得起寒風吹,全身要僵硬發痛的地方多的是,手部還是乖乖保暖別多找麻煩。

前30K勉強守住3小時,但天冷腳泡水,姿勢僵硬怎麼都跑不順,酸痛逐漸上身,30K後開始掉速,一路滑到630。估計破PB無望後便自暴自棄起來,陷入跑跑走走的墮落模式,原本捨不得停水站(卻花了好多時間上廁所 orz),不敢停下拍照,破PB夢碎後解禁,盡情吃香蕉亂拍照。

最後4K被430配速列車碾過,激起小小危機感,拖著沈重僵硬的大腿連滾帶爬,勉強保住430。

領回衣保袋趕緊換上乾衣服加長褲,停下來不動一吹風就起雞皮疙瘩,還是早早回家好。踼了約一公里馬路去搭接駁車,途中身旁大哥唸了兩句「搭車的路也太遠了,比跑馬還累」我就說「這是大會用心良苦,讓我們排乳酸呢」,兩個人就這麼開了話匣子聊起來。原來大哥早在1999年就跑完百馬,是跑了150馬的老前輩,半年前脊椎開刀,本次是靜養數月後的復出賽,無奈成績一落千丈,跑得很差,只跑出34X… 聽得我下巴都快掉下來。我使盡吃奶力氣也摸不到SUB 4,在高手眼中34X已上不了檯面,這是一種「打進SBL」對上「國小操場拍皮球」的概念~臨下車前,前輩分享了他的LSD跑法:從大佳河濱出發,向東跑到南港,走南深路越過山頭銜接木柵河濱道,一路往北跑到社子島再接回大佳,總長約70公里,八小時跑完,是很好很盡興的耐力訓練…

拾起掉到地上的下巴,懷著無限崇敬告別前輩,結束今天難忘的冰馬勇戰記。

GUID Primary Key資料庫避雷守則

$
0
0

【聲明】該不該用GUID當Primary Key是可以讓開發人員大戰三百回合的好題材,由標題可知我屬於GUID陣營,這篇文章不打算花時間論證該不該用GUID PK,假設讀者已接受使用GUID當PK,只聚焦如何避免GUID PK導致資料庫效能悲劇。

故事源起MVP James最近寫的幾篇GUID鬼故事(包含一起寫入資料3秒變40秒的案例,也實證了GUID作為叢集索引造成索引破裂現象,值得一看),讓我有所警覺,身為一個偏好GUID Primary Key的開發者,有必要正視這個問題,避免掉進資料庫效能陷阱。

簡單歸納我愛用GUID當Primary Key的理由:

  1. 不仰賴資料庫就能取得唯一鍵值
    建立新資料的當下就能決定PK,放心地使用它衍生相關應用。即使程式執行當下資料庫還沒建好、三個月後才會寫入資料庫,或是一年後來自十個資料庫的資料要合併成一個資料表,都不必擔心鍵值重複的問題。(註:傳說GUID仍有重複的可能,但機率極低,在此忽略)
  2. 無法被猜測,用於參數時內含安全防護效果
    GetTradeData.aspx?no=617ad98a-c010-4cd2-bc07-9a64d907154f 比 GetTradeData.aspx?no=6386 安全,可避免惡意人士修改參數嘗試存取非授權範圍資料的風險。

但使用GUID當Key也有缺點,例如:

  1. 不利於人工查詢或偵錯
    例如:GetTradeData.aspx?no=617ad98a-c010-4cd2-bc07-9a64d907154f,SELECT * FROM MyTable WHERE Id = '617ad98a-c010-4cd2-bc07-9a64d907154f',大多得靠複製貼上處理參數,難以手工輸入。
  2. 增加儲存空間
    每個GUID有16 Byte,相較於整數4 Byte,多耗用4倍的儲存空間。
  3. 衝擊資料庫效能
    GUID的非連續特性,易導致索引破碎(Index Fragement),降低系統效能。

依我的看法,人工查詢不便頂多費工不會致命,12 Byte vs 4 Byte在資料筆數很龐大時才有顯著影響,而第3點可能嚴重影響效能,輕忽不得。

由James提供的案例可知,在資料庫使用GUID PK,稍有不慎便會發生悲劇。所以該好好思索,如果你打算使用GUID PK,要怎麼樣才能避免掉下效能陷阱?

首先,鬼故事裡有一個共同關鍵-「GUID PK被設成叢集索引(Clustered Index)」。我們都知道,資料庫的索引有兩種,Clustered Index與Nonclustered Index,依據MSDN,二者比較如下:

  • Clustered Index
    叢集索引決定資料表儲存資料的實際順序,每個資料表最多只能有一個叢集索引,而叢集索引不需額外空間儲存鍵值。有叢集索引的資料表稱為叢集資料表(Clustered Table)。找到索引鍵時一併找得到資料列,查詢效能比非叢集索引好,用在最常用的鍵值(例如:Primary Key)可以產生最大效益。若資料表沒設叢集索引,資料會以沒有固定排序的方式儲存,這種儲存結構稱之為Heap。
  • Nonclustered Index
    非叢集索引需在實際資料列之外另行建立資料結構,每筆索引除了索引包含欄位外,還需要一個指標(Pointer)指向資料位置,若資料表有叢集索引,指標指向叢集索引的鍵值,若資料表使用Heap結構,指標直接指向資料列本身。
    非叢集索引可以附加非鍵值欄位以突破16欄900Bytes的索引鍵值上限,完全涵蓋查詢條件提升效能。(參考:Create Indexes with Included Columns.)

PK使用頻率很高,設成叢集索引對效能最有利,故慣例上會設成叢集索引以提升效能。然而,因為GUID具有不連續的隨機性,即使循序寫入資料,常常後寫的資料GUID排序較前,依叢集索引特性,實體儲存位置應擺在前段,造成每次寫入資料都需挪動調整既有資料造成索引破碎,拖累寫入與查詢效能。

由此看來,將GUID PK設為叢集索引的缺點蓋過優點。轉念一想,沒人規定PK一定要設成叢集索引呀!只要將PK改為非叢集索引就能避開GUID導致索引破碎的危機。當然,非叢集索引的效能不如叢集索引,但減損幅度不致太嚴重,相較於其降低的風險是划算的。

但這衍生另一個議題,PK不設成叢集索引,資料表就只有非叢集索引可行嗎?前面提到,沒有叢集索引的資料表稱為Heap Table。

依據MSDN,Heap Table只適合資料極少(例如數十筆)的場合,即使不設任何Index,Table Scan也很有效率,或者某些資料架構師會巧妙利用非叢集索引配合Row Identifier (RID,由File Number, Data Page Number, Slot on The Page組成,長度不長),達到比叢集索引還好的效率。但絕大部分的情況下,設定叢集索引有好處:

  1. 循序讀取一段資料時,叢集索引可以節省排序動件
  2. GROUP BY時,分群前必須先排序,叢集索引可以省去排序作業
  3. 記得避免資料多又無非叢集索引可用的狀況,如此永遠只能Table Scan,包準慢死你

而我覺得缺乏叢集索引的最致命點是-Heap Table也會產生破碎現象,一旦出現,依MSDN的建議是建個Clustered Index再砍掉,網路上提到的其他做法還有把資料先搬出來再重新塞回去、匯出到新資料表再更名,不管哪一種做法,聽起來都好沒效率,好可怕。

至此,我得到兩點結論:1.將GUID PK設成非叢集索引利大於弊 2.資料表欠缺叢集索引就會形成Heap Table,弊大於利。所以,最好的折衷方案就是「GUID PK設成非叢集索引,並另外增設叢集索引」,而這個額外的叢集索引,自動跳號整數會是首選。如此我們降低GUID PK導致索引破碎的風險,自動跳號叢集索引避免新增資料造成索引破碎,而叢集索引讓資料表可以透過索引重建重組改善破碎狀況,同時避開索引破碎及Heap Table陷阱。

綜合以上,來段CREATE SCRIPT示意:

CREATETABLE [dbo].[MiniFlow](
     [SeqNo] [int] IDENTITY(1,1) NOTNULL,
     [FlowId] [uniqueidentifier] NOTNULL,
     [FormCode] [varchar](4) NOTNULL,
     [FormNo] [varchar](16) NOTNULL,
     [Subject] [nvarchar](256) NOTNULL,
CONSTRAINT [PK_MiniFlow] PRIMARYKEYNONCLUSTERED
(
     [FlowId] ASC
)
GO
CREATECLUSTEREDINDEX [IX_MiniFlow] ON [dbo].[MiniFlow]
(
     [SeqNo] ASC
)
GO

有幾個重點:

  1. PK外外增設SeqNo INT,以IDENTITY(1,1)設定自動跳號
  2. FlowId為GUID是MiniFlow資料表的Primary Key,但設定時加上NONCLUSTERED指定為非叢集索引
  3. 利用CREATE CLUSTERED INDEX將SeqNo建為叢集索引

依我的看法,這是可以克服GUID PK負作用的最佳設計,在Stackoverflow上也獲得印證,大家如有不同看法,歡迎提出討論。手邊一些使用GUID PK的現有系統效能都還OK,不打算積極調整,但日後開發使用GUID PK新系統,我應該會採用這種設計方式。

最後提一下NEWSEQUENTIALID,有不少人建議用它取代GUID避免索引破碎,但為NEWSEQUENTIALID只能用於資料庫INSERT時自動產生,又可以被預測,並不符合我期待GUID PK應提供的隨時可取及防猜保密要求,我認為只適合用在處理跨資料庫合併用的鍵值。

【茶包射手日記】VS2015卡在專案初始化無回應

$
0
0

用Visual Studio 2015開啟最近每天在寫的解決方案,突然卡在「解決方案呈現載入中(loading…),多個專案處於初始化中(initializing…)」狀態,Visual Studio 2015 UI也凍結無回應,只能從工作管理員強行砍掉devenv.exe重新啟動,再次開啟問題依舊。為了對照,用VS2015改載入其他專案,一切正常,縮小範圍,確認問題只出在特定解決方案。改用VS2015安全模式(不載入外掛套件)開啟該解決方案,問題依舊。進一步測試,VS2013開啟該解決方案是正常的,至此,問題再縮小到「VS2015開啟特定解決方案時卡在專案初始化階段無回應」。試了Windows重開機,沒出現奇蹟。

由於同一專案VS2015有問題、VS2013 OK,推論可能與VS設定有關係,試著刪除 .vs\AfaV4\v14\.suo檔案(VS2013時代,解決方案所屬VS設定儲存在AfaV4.v12.suo,VS2015起則改放在.vs資料夾下),問題消失。

心得:若VS2015/VS2013只在開啟特定解決方案發生異常,可試著刪除該解決方案的.suo設定看能否排除問題。(會遺失一些設定,例如中斷點,但總好過專案開不起來)


漫談尾牙抽獎程式的公正性

$
0
0

上週看到以程序員生活為題材的漫畫作者西喬的創作:「年会上的程序员们……」,改編自一個「CTO覺得抽獎程式有點問題,程式作者被迫在旺年會場接受上千人Code Review」的奇妙真實故事,令人不禁莞爾。

很巧地,看到漫畫的前一天剛好才參加完資訊部門尾牙,而永遠的重頭戲-摸彩,自然是跑程式抽獎。(每天寫程式的人做不出電腦抽獎,傳出去像話嗎?)隨著抽獎號碼一一抽出,眾人希望落空,台下沒中獎的程式設計師們開始鼓噪議論:「XX號為什麼出現這麼多次?」「靠!程式絕對有鬼」「Open the source, Luke!」「程式沒做Code Review,肯定有Bug」… 話是這麼說,但大家心知肚明,程式被動手腳或真出錯的機率甚微,都是或然率使然,純粹是拿Coding哏瞎起閧,沒中獎也尋開心唄。

不過,我很無聊地思考,抽獎程式是否可以做到讓程式設計師無法挑剔呢?仔細分析:在大家眼裡,傳統的抽籤、摸彩球還是較有公信力,即使沒做到樂透開獎等級:公證律師監督,準備多組彩球、抽獎機隨機採用,大家還是習慣眼見為憑,寧可相信人手在箱子裡攪和後抽出的號碼,而不是程式不知依據什麼冒出的結果。

針對這點疑慮,我覺得唯一的解決辦法是「讓程式碼公開,演算結果能被反覆驗證」。抽獎程式的核心在於亂數產生器,依前述要求,讓亂數元件抓執行時間當亂數種子的做法是不可行的,每次執行的結果永遠不同,那你如何證明當天當時按鈕就該跑出這個結果?所以,抽獎程式要經得起考驗一定要能反覆驗證,故「以其他具公信力方式決定亂數種子,再依據其產生亂數決定抽獎結果」是較理想做法。至於亂數種子怎麼決定?就抽籤、摸彩球囉… XD

看到這裡,可能會有人想翻桌了,繞了一圈又要抽籤,為什麼不從頭到尾抽籤就好?依我的看法,如果可行,用抽籤、摸彩球取代程式更不易起爭議,因為它原理簡單,不需要專業背景就能懂,如果有人對開獎結果不服氣,就把箱子倒出來,找出他的籤條打他臉,爭議立解。而程式演算法較難理解,把電腦拆開也沒法當場驗票,被人抹黑也只能往肚吞,實在悲情。但抽籤並非萬能,很多情況只有電腦抽獎才能勝任,例如:十萬名員工的抽獎得做十萬枝籤(又不是十萬枝箭可以開草船出去借),包準工作人員忙上好幾天;要當場抽出三百名普獎,從上烏魚子海蜇皮拼盤就要開始抽,抽到水果西米露登場都沒還抽完。抽籤公平,電腦選號有效率,那就「用抽籤結果衍生一組任意數量的隨機結果」,魚與熊掌兼得。

最後,把焦點拉回「如何選一個公平且夠隨機的亂數種子」。不同語言、平台的亂數元件規格不同,以C#為例,Random型別支援亂數種子,參數型別為Int32,故最多產生20億種不同結果,可滿足絕大多數的應用。(註:JavaScript內建的Math.random()不支援亂數種子,但有現成程式庫救援,可比照辦理)

20億種變化夠大,但勞煩長官一連抽十個數字,過程偏長且會冷場。我有個好點子,不妨摻雜一些「大家公認無法人為操控又會隨機跳動的數字」,例如:前期樂透開獎號碼、氣象預報氣溫、當天收盤的道瓊工業/那斯達克/台股指數、日圓/美元/歐元匯率… 做成一堆數字牌(注意:亂數種子取用規則要事前公佈),讓長官從中抽出幾個再洗牌重排順序,就可以得到夠多位數的亂數種子。如果嫌麻煩,買三副撲克牌只留1-9給長官抽出10張再洗牌排序比較簡單(如果長官不介意演一下賭神的話)。總之,方法很多,有辦法湊出沒人質疑的數字即可,數字位數愈多隨機性愈高但愈費工且可能會冷場或讓長官不悅不爽加碼(這個很嚴重),請大會自行拿捏。

還有一種有效率的替代做法,亂數種子不用數字,改取一段文字再用MD5或SHA1雜湊轉成數字,文字到處可得(報紙標題、人名、翻字典),中文字元數目眾多,常用的就有數千個,短短幾個字經過雜湊演算就能產生足夠的變化性。缺點是由字元產生雜湊數字的概念較不直覺,抽獎對象要有相關背景才易理解,需考慮參與者的接受度。

有了亂數種子,就能得到可重複驗算的一連串隨機數字,抽獎候選清單多半儲存於陣列,最直覺的寫法是取 0 到 Array.Length-1的亂數挑選中獎者,將其自陣列移出再用同樣邏輯抽下一個。我個人則偏好另一種做法,為陣列的每一個元素產生一個亂數,再依此亂數由小到大排序,演算法較簡單,一口氣取出數百上千名抽獎者都不是問題。

借用先前500萬人次抽獎的程式當範例,程式差不是長這様,其中的12345就是亂數種子,不用的亂數種子取出的得獎者不同,且可反覆驗證:

class Program
    {
staticvoid Main(string[] args)
        {
string raw = @"1.Jeffrey
2.Darkthread
3.球證
4.旁證
5.主辦
6.協辦
7.全都是我的人";
            List<string> candidates = new List<string>(raw.Split('\n'));
            Random rnd = new Random(12345);
            Console.Write(candidates.OrderBy(o => rnd.Next()).First());
            Console.Read();
        }
    }

順手也寫了一個JavaScript版:Live Demo

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<metaname="viewport"content="width=device-width">
<title>尾牙抽獎範例</title>
</head>
<body>
亂數種子=<inputvalue="12345"/><inputtype="button"value="抽獎"/>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.2/seedrandom.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.4.js"></script>
<script>
var candidates = 
"1.Jeffrey,2.Darkthread,3.球證,4.旁證,5.主辦,6.協辦,7.全都是我的人".split(',');
    $(":button").click(function() {
      Math.seedrandom($(":text").val());
var list = $.map(candidates, function(n) {
return { name: n, rand: Math.random() }
                 });
      list.sort(function(a, b) { return a.rand - b.rand; });
      alert("得獎的是:" + list[0].name);
    });
</script>
</body>
</html>

抽獎程式碼先公開,預先公告亂數種子取碼規則,只待當天填入亂數種子就能抽出任意數量的得獎者,抽獎結果可以反覆驗證,在我心中,這就稱得上是公平公正公開的抽獎程式囉~

小技巧-自動刪除App_Data過期暫存檔

$
0
0

寫網站時有時需要產生暫存檔,過去我慣用的做法是透過System.IO.Path.GetTempPath(),將暫存檔寫入Windows系統暫存資料夾,如意算盤是暫存資料夾本來就是放暫存檔的地方,而Windows有機制可在磁碟空間不足時刪除暫存檔釋放空間,如果檔案沒有機密或敏感性,放在暫存資料夾基本上可以Fire and Forget,用過就放著不管,很省事。

後來幾次部署經驗告訴我,「ASP.NET程式不一定有寫入暫存資料夾的權限」(登楞!)。針對Temp資料夾開放ASP.NET寫入權限是一種解法,但每個ASP.NET應用程式的執行身分不同,搞下來Temp資料夾需維護一堆權限有點複雜。相形之下,App_Data隸屬各ASP.NET應用程式,可部署時一併設定權限統一管理,相對簡便。(註:適合放暫存檔是因為App_Data具有隱身特性,請不要將暫存檔放在一般網站目錄下,以免被使用者不當存取)

但暫存檔放App_Data就不像系統暫存資料夾可以靠Windows機制回收空間,得自行清理過期暫存檔。最直覺做法是寫個排程每天定時刪檔,但最近我想出另一個更省事的做法-取得暫存目錄時自動清理過期檔案。程式範例如下,分享兼請大家Code Review:P

//記憶上次清理執行日期
staticstring lastAttTempCleanDate = null;
//非同步鎖定
staticobject cleanLock = new Object();
 
staticstring GetTempPath()
    {
//使用App_Data暫存檔案,避免ASPX沒有寫入系統TEMP權限
string tempPath = Path.Combine(
            HttpContext.Current.Server.MapPath("~/App_Data"), "UploadAtts");
//若目錄不存在,建立之
        Directory.CreateDirectory(tempPath);
//每次啟動網站或每天清理一次過期Cache
string todayDate = DateTime.Today.ToString("yyyyMMdd");
//防止多執行緒同時觸發
lock (cleanLock)
        {
//比對上次執行日期,原則上每天只執行一次
if (todayDate != lastAttTempCleanDate)
            {
//先儲存執行日期記錄,避免重複執行
                lastAttTempCleanDate = todayDate;
//另開Thread執行刪除,以免卡住目前的暫存作業
                Task.Factory.StartNew(() =>
                {
//決定逾期判斷基準
                    DateTime expireTime = DateTime.Today;
foreach (var file in Directory.GetFiles(tempPath))
                    {
//若檔案建立時間早於今天,刪除之
if (File.GetCreationTime(file).CompareTo(expireTime) == -1)
                        {
                            File.Delete(file);
                        }
                    }
                });
            }
        }
return tempPath;
    }

小技巧-為text-overflow: ellipsis增加完整文字顯示

$
0
0

CSS 的 text-overflow: ellipsis 刪節號效果可讓長度不一的文字等寬顯示,遇到版面空間有限又必須整齊排列時很好用,但套用刪節號樣式後看不到完整文字,尤其遇上文字前半截相同時更是難以區別,是一大困擾。為此,我的慣用解法是為套用 ellipsis 的文字元素加上 title Attribute 存入完整文字,將滑鼠移到文字上停留就能看到原始文字,問題迎刃而解。

每次套用 ellipsis 還要額外加 title 有點囉嗦,我寫了一小段  jQuery 讓程序自動化,範例如下提供大家參考:Live Demo

<!DOCTYPEhtml>
<html>
<head>
 
<metacharset="utf-8">
<metaname="viewport"content="width=device-width">
<title>自動為text-overflow ellipsis加上title顯示</title>
<style>
    .a-ellipsis {
      width: 100%;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    div { padding: 3px; }
</style>
</head>
<body>
<divstyle="width: 120px">
<divclass="a-ellipsis">如何讓你遇見我</div>
<divclass="a-ellipsis">在我最美麗的時刻 為這</div>
<divclass="a-ellipsis">我已在佛前求了五百年</div>
<divclass="a-ellipsis">求他讓我們結一段塵緣</div>
</div>
<scriptsrc="https://code.jquery.com/jquery-2.1.4.js"></script>
<script>
    $(function() {
      $("body").on("mouseenter", ".a-ellipsis", function() {
if (!this.title) this.title = this.innerText;
      });
    });
</script>
</body>
</html>

效果展示

LINQ寫法:類SQL查詢語法 vs 方法串接

$
0
0

前陣子跟同事討論到LINQ查詢寫法。

記得在LINQ剛推出時以「可以在C#裡寫SQL語法查資料」為號召,範例裡常看到這種長得有點像SQL語法的LINQ查詢運算式(LINQ Query Expression,我習慣叫它「類SQL查詢語法」):

    IEnumerable<int> scoreQuery = //query variable
        from score in scores //required
where score > 80 // optional
        orderby score descending // optional
        select score; //must end with select or group

不過,我在熟悉LINQ後(甚至演變成「少了LINQ幾乎不會寫程式」),卻很少再用這種寫法,大都是用LINQ方法配上Lambda運算式:

    IEnumerable<int> scoreQuery = //query variable
        scores //required
        .Where(o => o.score > 80) // optional
        .OrderByDescending(o => o.score) // optional
        .Select(o => o.score); //must end with select or group

那一種寫法比較好?好問題,甚至我也想不起來自己選擇第二種寫法的理由。既然聊到了,順手對二者做了粗淺的分析比較。

首先,有一個重要觀念:兩種寫法編譯出來的結果是相同的!

用個範例驗證:

class Run
        {
publicint No;
publicstring Name;
public TimeSpan Time;
publicbyte Road;
public Run(int no, string name, TimeSpan time, byte road)
            {
                No = no;
                Name = name;
                Time = time;
                Road = road;
            }
        }
class RoadType
        {
publicbyte Id;
publicstring Name;
        }
 
static Run[] Runs = new Run[]
        {
new Run(1, "櫻花馬", new TimeSpan(5,50,16), 1),
new Run(2, "櫻木花道馬", new TimeSpan(5,27,50), 1),
new Run(3, "國道馬", new TimeSpan(4,15,38), 2),
new Run(4, "鳳梨馬", new TimeSpan(5,26,03), 1),
new Run(5, "三重馬", new TimeSpan(4,47,32), 2),
new Run(6, "台北星光馬", new TimeSpan(5,30,25), 2),
new Run(7, "海山馬", new TimeSpan(5,42,18), 2),
new Run(8, "石碇馬", new TimeSpan(6,07,54), 1),
new Run(9, "五分山馬", new TimeSpan(5,13,32), 1),
new Run(10, "土地公馬", new TimeSpan(5,26,51), 1),
new Run(11, "觀音山馬", new TimeSpan(5,47,07), 1)
        };
 
static RoadType[] RoadTypes = new RoadType[]
        {
new RoadType() { Id=1, Name="山路" },
new RoadType() { Id=2, Name="平地" }
        };
 
staticvoid Main(string[] args)
        {
            var queryBySql =
                from r in Runs
where r.Time.Hours < 5
                orderby r.Time
                select new { r.Name, r.Time };
 
            var r1 = queryBySql.Skip(1).Single();
 
            var queryByLambda =
                Runs
                .Where(o => o.Time.Hours < 5)
                .OrderBy(o => o.Time)
                .Select(o => new { o.Name, o.Time });
 
            var r2 = queryByLambda.Skip(1).Single();
 
            Console.WriteLine("Test {0} {1}", r1.Name, r2.Name);
            Console.Read();
        }

程式裡我分別用「類SQL查詢」及「方法串接」找出我去年馬拉松成績跑出Sub5的第二筆,使用JustDecompile反組譯可發現兩段程式碼除了變數名稱有別,程式結構一模一樣。(以類SQL查詢呈現是JustDecompile的偏好)

由此可知,不管你使用哪一種寫法,都不會產生效能或結果上的差異,大家可依自己的偏好選擇。

那麼,我為什麼偏好方法串接呢?認真想過,整理出我選擇方法串接的理由:

  1. 符合程式運作原理,複雜應用時較易思考

    類SQL查詢提供熟悉SQL語法開發者另一種較親切的表達方式,背後運作其實是採方法串接的邏輯,先.Where(),再執行.OrderBy(),最後.Select(),每個方法都傳回IEnumerable<T>故可前後串接。在一些較複雜的動態邏輯串接情境,方法串接寫法就顯得較直覺,容易思考,例如動態依條件加上Where篩選:
                var q = Runs.ToList();
    if (filterSub5)
                    q = q.Where(o => o.Time.Hours < 5).ToList();
    if (filterMountain)
                    q = q.Where(o => o.Road == 1).ToList();

  2. LINQ查詢未必與DB有關,對陣列、集合下SQL不如呼叫方法來得直覺

    平日寫專案,LINQ我用得極多,但超過90%是用在一般的IEnumerable<T>與資料庫沒有任何關係。在這種情況下,不太會有動機刻意寫SQL,套用Where()、Select()得到處理過的IEnumerable<T>再繼續處理比較直覺。
  3. 有些邏輯只能靠LINQ方法達成

    類SQL查詢最終還是得串接如 Count()、Any()、SingleOrDefault() 等方法才能得到結果。使用方法串接可以一氣喝成,用類SQL查詢簡潔度差一些,例如:
                var countSub5BySql = 
                    (from r in Runs where r.Time.Hours < 5 select r).Count();
                var countSub5ByLambda =
                    Runs.Where(o => o.Time.Hours < 5).Count();
  4. 統一做法,減少腦力負擔

    承上點,既然寫類SQL查詢免不了動用LINQ方法,乾脆只學只記一種做法,人老堪用記憶體漸少,少記點東西又避免切換做法消耗腦力,省力有效率。 (謎:你的大腦是有多不堪啦?)

但類SQL查詢有一強項可以狂電方法串接-其JOIN語法比.Join()方法簡潔直覺!如下例,差異很明顯吧?

            var queryBySql =
                from r in Runs
                join t in RoadTypes on r.Road equals t.Id
                select new { r.Name, RoadType = t.Name };
 
            var queryByLambda =
                Runs.Join(
                    RoadTypes,
                    r => r.Road,
                    t => t.Id,
                    (r, t) => new { r.Name, RoadType = t.Name });
 
            Console.WriteLine(JsonConvert.SerializeObject(queryBySql.ToArray()));
            Console.WriteLine(JsonConvert.SerializeObject(queryByLambda.ToArray()));
            Console.Read();

我一直覺得類SQL查詢語法在LINQ技術的定位,有點像VB.NET在.NET技術扮演的角色,都是藉由開發者已經熟悉的語法(SQL、VB、VBScript)增加熟悉感與親和力,引領開發者進入全新世界。而在上手後,開發者依語法簡潔性、市場主流趨勢、自己的偏好,漸漸就會自動流向不同陣營,很有趣。

親愛的.NET開發者們,大家慣用哪一種LINQ寫法呢?

讀書筆記-思考的藝術

$
0
0

前陣子跟同事聊到邏輯謬誤,同事提到德國作家的相似作品,談非受迫性的思考邏輯錯誤及行為偏誤,邏輯控對這些知識沒什麼抵抗力,趁著剛好拿到博客來Coupon就入手了,下單隔天書就送抵小七門市,效率令人驚喜。(線上書店品項完整折扣多再加上交貨神速,實體書店只剩能深入試閱及可搭訕知性正妹等優勢,這場仗註定艱辛。)

書薄不及300頁又算淺顯好消化,沒兩下就能翻完,但衡量自己不會有耐性再翻一次,又不想讀過的內容太快隨風而逝,唯有寫成筆記才能長記活用。這篇算是私人筆記,貼在部落格只為方便日後查詢引用,但有興趣的讀者可以加減看,或許也能有所得。

  • 存活者偏誤

    同一件事,成功者獲得的關注與轉述遠遠超過背後數量龐大的失敗者,讓人過分高估成功機率。
  • 游泳選手身材錯覺

    由為游泳選手身材健美推論游泳對於美化身材有神奇的效果,但事實上很多選手是因為擁有那種身材才成為游泳選手。
  • 過度自信效應

    蒲隆地共和國有多少人口?受試者可選擇任意兩個數字當範圍,但必須有把握實際答案超出這兩個數字的機率小於2%(回答到1到10億也可以),有40%的受試者答錯。人們常高估自己在預測方面的知識與能力,某些時候是為了塑造自己專業形象謀取利益。
  • 社會認同

    誤認「大家都在做的事就是對的事」。源於物種演化,在草原上看到其他同類轉身逃跑跟著跑的人類存活率高,基因就被保留下來。
  • 沈沒成本謬誤

    發現是部爛電影,但都已經買了票就只好看完,結果浪費錢還浪費時間。
    男友不斷出軌但每每跪求原諒,預期他未來仍會再犯,卻基於已投入的感情、歲月無法割捨。
    美國的越戰的「我們已經犠牲了這麼多官兵性命,若就此停戰將是錯誤決定…」
    投資出現損失時不是不能加碼,但必須基於「繼續投入會成功」的預期,而非「我們已經投入這麼多」。
    注:若身體狀況不允許,馬拉松已跑了40K也不該是硬撐下去的理由。(知道是對的,但好難做到…)
  • 互惠(基於虧欠感的補償行為)

    非營利組織寄來免費明信片,言明有無捐款都能保留。招待潛在客戶看足球賽,一個月後拿到該客戶的訂單。朋友請客送禮,習慣上要禮尚往來回請回送。
    互惠也來自基因演化,打到獵物時跟伙伴分享,運氣不好時分享伙伴的獵物不致餓死,是種風險分擔。
    注:採行互惠是融入社會的必須手段,如禮尚往來之俗是人之常情,不可能捨棄,唯必須僅防別人藉此架構陷阱。
  • 確認偏誤

    遇到砥觸既有觀念的否定證據,自動將其視為特殊案例不列入考慮,錯失修正既有觀念的機會。
    性善論與性惡論都可以找到支持事證,但主張者常忽略無法解釋的反面例證,最後兩種論述存在並行,各有擁護者。確認偏誤在宗教上尤其明顯,宗教經典裡有許多無法解釋或有反面例證的陳述被信徒無視。
    在臉書封鎖與自己意見相左的朋友,訂閱與自己信仰相同的社群或粉絲專頁,也是向確認偏誤傾斜的行為。
  • 權威偏誤

    面對權威時,我們的獨立思考不自覺婑一截。飛機副駕駛不敢質疑正駕駛的錯誤決策造成空難,無法律專業的大學校長評論食安案件審判結果被大肆報導。
  • 對比效應

    當被擺在反差很大的對照物旁邊,我們會誤判事物的好壞、美醜、大小。
    人們在買車時對於配件、買房子的裝潢花費不手軟,相對於車款房價,偏貴產生的痛覺不明顯。
    願意多走十分鐘買菜便宜一百塊,卻覺得多走十分鐘買三萬塊的筆電便宜一百塊很呆<--完全不理性,但聽起來好有道理
  • 現成偏誤

    我們會選擇一些較易取得的現成事證進行推論。例如:以R開頭的英文字多還是以R結尾的英文字多?R開頭的字比較好想。
    戲劇性原則優於量化原則,因而高估墜機車禍致死率,低估糖尿病、憂鬱症的致死率。
    近期反覆出現的字眼也會在做判斷時優先被拿來參考<--洗腦原理
  • 好轉前會先惡化

    號稱專業者遇到無法掌控狀況,主張情況惡化是轉好前的必經歷程,藉以安撫對方,若運氣好撐到谷底反彈或自己變好,就撿到「妙手回春」之名。
  • 故事偏誤

    以故事解釋事實時扭曲並簡化事實。發生橋斷汽車落河,報導描述苦主生平,訪問歷刧心情,卻未探究重點-事故發生原因。
  • 事後諸葛亮偏誤

    事後解釋分析事物成功的原因,試圖將已存在的事實羅織成一套說辭,看似合理但與實實不符。商業雜誌的企業家報導跟股市名嘴對市場趨勢的詮釋是經典,不管樓起樓塌,上漲下跌都能頭頭是道。
  • 司機的知識

    量子力學大師普朗克的司機耳濡目染,對於量子力學理論朗朗上口,有一天跟普朗克約好交換身分假冒他公開演講,表現還挺稱職,但中途有位物理學家提問,司機不慌不忙地說:「萬萬沒想到居然有人會提這麼簡單的問題,不然這様好了,就讓我的司機來回答…」
    以此典故,用以諷刺那些空有具說服力外表,雖然知識空洞,卻表演得頭頭是道的人。
    巴菲特主張每個人都該知道自己「能力的範圍」,在範圍內成為專家,不要試圖在範圍之外尋求成功。
    知之為知之,不知為不知,具有真才實學的人會很大方地承認我不知道,但司機的知識型的名嘴,什麼都講上兩句(從外太空到內子宮),永遠不會聽到他說「我不知道」。(大家可以在電視談話節目裡找樣本)
  • 控制的錯覺

    我們相信自己可以控制某些事情(事實上無能為力),聊以自慰。 例如:每次打開電視球隊就丟分數、穿紅內褲會贏錢。
  • 激勵過敏傾向

    要撲滅鼠患,鼔勵交死老鼠換獎金,結果民眾開始養老鼠賺獎金。
    人們對激勵會有反應,但反應可能變質未必符合激勵原意。
  • 均質迴歸

    在不干預的情況下,事物本來就有起有伏,人們在遭逢谷底時執行一些「儀式」,隨後剛好轉好,就判定儀式有效。有時醫生、教練、顧問並不如表面上所認為的那麼有效。
  • 公地悲劇

    公用草地免費供農民放牧,農民爭相增加在公地放牧的牛隻數目,終將導致公地牧草源耗盡。對個別農夫而言,多放一隻牛增加的利潤為1,增加1分風險由全部農民承擔,分析後傾向增牛決策。當全部的農民都做出增牛決策,註定走向悲劇。
    近代的二氧化碳排放、濫墾濫伐、太空垃圾議題都有類似特性。
    公地悲劇是亞當史密斯「看不見的手」的反面,在特定情況下市場機制可能導向崩潰。
    唯一的解決之道只有兩條:私有化或管理(針對無法私有化的東西:大氣層、海洋、衛星軌道)。
  • 結果偏誤

    數百萬隻猴子參與股市投資,每日決定做空或做多,一個月後人們發現某隻猴子的操作完全命中漲跌趨勢,被奉為股神,人們開始研究其吃香蕉的習慣,抓癢的姿勢,期望挖掘其成功訣竅。
    人們傾向用結果評斷決定,而不是根據當時的決定過程評斷決定,又稱為史學家謬誤。
    勿單以成敗論英雄,成功有可能是歪打著,失敗可能是非戰之罪。
  • 選擇的吊詭

    太多的選項讓人茫然:提供6種果醬試吃的銷售量比24種果醬試吃多十倍,太多選擇無法決定,索性不買。
    選擇太多導致用無法仔細客觀評量,反而用偏頗的決定原則(電風扇吹考卷?)
    選項太多,做完決定後易存在不確定感及不滿(這個選項在XX上不如A,在OO上不如B)
  • 討喜偏誤

    當別人讓我們有好感或表現得對我們有好感,我們愈容易向這些人買東西或伸出援手。 人正真好/同鄉三分情/恭維灌迷湯
    鏡像-學對方的動作、語調、習慣。廣告要找正妹、小孩、可愛動物當主角。「請投入你神聖的一票」「因為你值得」「我們在地人」
  • 稟賦效應

    沒吃到葡萄說葡萄酸,吃到葡萄後把它吹捧到比實際甜。同一件東西,我們擔任賣方時認定的價格高於當買方時認定的價格。
    當我們擁有某件東西,主觀上會顯著為它加值。甚至準擁有者也會中箭,拍賣時不理性加碼。
    當你該放手時,不要因為稟賦效應遲疑錯失機會。
  • 奇蹟

    十五個唱詩班成員因不同的原因遲到躲過教會爆炸,被認定是神蹟。
    AMD與Intel公司的同名員工同一天入住加州一家飯店,飯店人員將機密文件交錯人,引爆一起專利案。
    看似奇蹟的事,其出現的機率可被計算而得,並非不可能發生。
  • 團體迷思

    一群精英組成團體,因刻意迎合被信以為真的共識,最後做出愚蠢的決定。例:古巴豬儸灣事件
    團隊精神:如果大家都堅信會成功,代表我的憂慮是多餘的,異議是錯的
    註:我想起末日之戰裡提到以色列的「十聖人」理論,政府為了預視危機成立10人小組,一旦有9個人對事件有一致意見,第10個就必須持相反意見去追查以發掘危機的可能性。
  • 輕忽機率偏誤

    告知受試者稍後會有多高機率被電擊,無論機率高低100%-1%,受試者的緊張程度相近,換言之,受試者無視機率因素。
    草率地投資新創公司,無視其成功機率。因墜機事件寧可損失機票也不坐飛機,無視其發生機率渺茫。
    一支突擊隊選擇解救A村,可將村民的傷亡由5%降到2%,解決B村可將傷亡由1%降到0%,全村都會生還。大部分的人會選B,稱之為零風險偏誤。
    除了風險為零的情況外,我們很難辦別不同機率的風險程度。
  • 零風險偏誤

    俄羅斯輪盤,已知彈匣四枚子彈,付錢可以拿掉其中兩顆 vs 已知只有一顆子彈,付錢拿掉唯一的一顆。人們願意為後者付出更多代價,前者降低六分之二死亡率,後者只降低六分之一<--零風險偏差
    人們對99%-1%致命率感受到的恐懼接近,只有零風險可以治好莫名的恐懼,因而不理性付出過高代價。(註:這可以解釋大家在食安議題上的過度反應)
  • 稀少性謬誤

    小孩子去搶一堆彈珠裡唯一的藍色彈珠;大人爭向申請限量的Gmail試用申請。物以稀為貴,餅乾愈少愈可口,跳樓大拍賣最後一天。
    抗拒(Reactance)-得不到的最美。註:白玫瑰與紅玫瑰?
  • 忽視基本比率

    李先生是一名載眼鏡的瘦高中年人,喜歡聽莫扎特。A)李先生是一位計程車司機 B)李先生是一位住在台東的中文系教授
    大部人選B,雖然不限地域計程車司機的數量遠大於台東中文系教授,符合事實的機率高出許多,但精確描述誤導了我們,讓我們不能冷眼面對統計事實。
    醫生的訓練,頭痛可能是感冒也可能是腦瘤,但前者機率高很多,要優先朝感冒方向診斷。如果你在俄懷明州聽到馬蹄聲,又彷彿看到黑白條紋,相信我,大部分的情況下那東西就是一匹馬。
  • 賭徒謬誤

    參見邏輯謬誤
  • 錨定

    錨定:對於未知答案的數字,先找出熟知的事物當參考點,再由該點推算正確答案。例如:101的高度,已知101層,用一層4米推算,推測高度至少400米以上。
    當我們無所憑藉時,會用不相干的數字錨定,例如:轉輪盤後猜聯合國會員國數字,轉到大數字的人猜的數字偏大。
    老師打成績時,會被學生過去的成績錨定;商品貼上原價標籤影響顧客對商品價值的評判。
  • 歸納法

    人們傾向由個別觀察推得普遍有效的確信,但這麼做往往很危險。股票過去漲不代表未來一定漲,極限玩家說極限運動不危險,出一次意外就足以推翻先前上百次證明。
    歸納法是人類預測未來的重要原則,但要辨明原委,小心變異。
  • 損失規避

    相同的獲利及損失規模相同的狀態下(獲得1萬塊跟損失1萬塊),損失造成的情緒反應是獲利的兩倍。
    這點也來自演化,小心翼翼的基因較有機會延續香火。
    DM寫鬼故事恐嚇可能發生的損失,效果比鼔吹效益來得有效。
    壞事對我們的影響總是強過好事。
  • 社會性懈怠

    兩個人一起拉繩的出力只有單人拉繩的93%,三人同拉時每人出力只有85%,當增加到八人,每人出力只剩49%。
    只要個人可以藉團體混水摸魚不被發現,就會出現社會性懈怠。注:濫竽充數效應
    接力賽不會發生是因為每個成員的績效會被單獨檢視。人們只會偷懶不致完全不出力,以免被發現。
    責任分擔帶來社會懈怠,但也讓團體比個人更勇於冒險:「萬一失敗責任不用我一個人扛」
  • 指數成長

    連續30天,每天給一百萬 vs 第一天給1塊、第2天給2塊、第3天給4塊、第4天給8塊…
    人類對線型增長較有概念,對幾何成長(指數成長)的感覺不夠強烈。欠缺原因:遠古時代,自然界觀察不到幾何成長的例子。
  • 嬴家詛咒

    拍賣會的嬴家往往是真正的輸家。招標、網拍。標的物實際價值不確定時,過多競爭者導致不合理出價,嬴家的求勝心造成不理性。
  • 基本歸因謬誤

    解釋事情時,高估人物對事情造成的影響。
    在某某球員或教練帶領下,某球隊封王。業績不佳,某CEO黯然下台。二次大戰是希特勒一手挑起。聽音樂會時討論指揮家或獨唱表現,不討論樂曲。 源起我們思考時需要一張「臉」聚焦,當成標的。
  • 錯誤的因果關係

    頭蝨離開宿主,宿主就會生病發燒,所以生病時要在頭上放頭蝨。(蝨子不耐高溫,人一發燒蝨子就會跑走)
    研究指出,家中藏書多的學生功課較好,引發家長購書潮。(父母的教育程度影響孩子成績,而父母背景形響藏書數量)
    關聯性並非因果關係。
  • 月暈效應

    .COM泡沫前夕的網路設備需求讓Cisco成為全球市值最高的公司,獲得報章雜誌無止盡的吹捧。一年後股價跌至20%,原先吹捧有加的客服、經營策略、行銷技巧、執行長被嫌到一無是處。Cisco其實沒什麼改變,造成截然不同的評價。
    人們迷失在某個觀點上,用它設想事物的全貌。長相好看的人容易事業有成,我們普遍認定他們比較親切、誠實而且聰明。(人正真好,Again)
    月暈效應會障蔽我們的視野,讓我們看不清事物全貌。
  • 替代路徑

    人們常常只看到某個人現在擁有的財富名聲,卻未考慮他是透過極高風險的過程獲得成就(九死一生),過程中替代路徑的後果極為可怕(破產、喪命)。富貴險中求
    注:有一句簡單但傳神的俗諺「只看賊吃肉,沒看賊挨揍」
  • 預測的錯覺

    柏克萊大學研究,檢驗十年內各行各業284位專家做的82,361預測,應驗率與隨機預測相近。
    只有兩種人會預測未來:一無所知的人、不知道自己一無所知的人
    檢驗預測:預測者處於何種激勵機制?(預測錯會丟飯碗?純粹要博知名度,矇對就賺到?)預測者過往準確率有多高?
  • 聯結謬誤

    以下描述何者可能性較高? A)松山機場關閉,所有班機取消 B)因天候不佳,松山機場關閉,所有班機取消
    很多人會選B,但關閉原因不只天候,A限制少,顯然成立的機率更高。
    原因:理性思考運作前,直覺思考先啟動,它無法抗拒合情合理的故事。
  • 框架效能

    同一件事,因陳述方式不同,產生不同反應。「喂,垃圾埇滿了」vs「寶貝,要是你趕快去倒垃圾,我會愛死你的」
    六百個人命在旦夕。對策A預估可救回200條性命 vs 對策B有1/3機率讓600人生還,但有2/3機率無人倖免<=大家選A
    對策A預估會犠牲400條性命 vs 對策B有1/3機率無人犠牲,但有2/3機率全部死亡=>大家選B
    美化修辭:重挫是修正、撤退是轉進、被開除是重新出發…
    沒有框架,你什麼都沒法陳述,而日常生活裡則要小心框架建構的陷阱
  • 行為偏誤

    足球PK時,對方射左中右的機率各為1/3,但守門員不是向左撲就是向右撲,很少會站著等中路來球。即使行動根本沒用,先做了再說,至少別人覺得我很努力。
    遠古時代,看到黑影就跑的基因存活率高,當我們遇到情況不明,內心會泛起馬上採取行動的衝動,也算原始本能。
  • 不作為偏誤

    登山同伴落崖,未伸手援,最後同伴喪命 vs 將同伴推下懸崖致死。何者較惡劣?大部人覺得見死不救比較沒那麼嚴重,此為不作為偏誤。當作為與不作為都會導致損失,便會出現不作為偏誤。
    新藥會引發20%絕症病患立刻致死,但有80%機率治癒,大部分的人會選擇不核准。(出事官位不保)
    即使病患有強烈求死意願,協助安樂死仍被判刑。
  • 自利偏誤

    成功全是自己的功勞,失敗統統是外在因素影響。將失敗原因推到其他事物身上,才能舒緩內咎怠。
    詢問五個同住學生負責倒垃圾的比例,總和為320%,大家高估自己的貢獻。
  • 享樂跑步機

    中樂透的喜悅只能維持三個月,事業更上層樓的人三個月後幸福感消失,忘記豪宅的好,只記得每天通勤時間變長。這些人必須不斷換新車,學界稱這種效應為「享樂跑步機」-不斷工作,追求升遷,藉此享有更多更美好的事物,但仍無法感到幸福。
    處在不幸時,也會高估情緒持續的長度與強度。失戀時覺得天崩地裂,世界毁滅。
  • 自我選擇偏誤

    為什麼我最倒楣,我排隊的櫃檯處理速度最慢、一路都遇到紅燈。抽樣來源不客觀:排隊快時、一路綠燈時不會留意
    沒來的舉手。電訪每個家庭電話+手機的支數,發現沒有家庭是沒有電話的。
  • 聯想偏誤

    把無關的東西聯想在一起,因經驗相信穿紅內褲球運較佳、喝可樂時聯想到廣告裡年輕活力的形象。因瓦斯漏氣按電鈴被炸傷的推銷員,再也無法按電鈴。貓跳過一次熱爐子後就不會再犯了,但也再不敢上冷卻的爐子。一朝被蛇咬,十年怕井繩
  • 新手的運氣

    新手嚐到甜頭只是運氣或大勢使然,卻高估了自已的實力。如何辨別?把時間拉長,與其他參與者比較。
  • 認知失調

    吃不到葡萄的狐狸就說葡萄是酸的不屑吃。原本所想所做跟最後結果不同,是一種失調。
    買到的車引擎吵座位不舒服,安慰自己絕不會開到睡著,很棒。
    人會對自己撒點小謊,讓自己好過一點。
  • 雙曲貼現

    一年後拿一萬 vs 13個月後拿1萬1千 <= 大部分的人選後者,都等一年,晚一個月沒什麼差
    今天拿一萬 vs 一個月後拿1萬1千 <= 很多人選前者
    雙曲貼現:做決定時點與獲利時點愈近,「情緒利率」就愈高。(注:24小時到貨超邪惡 XD)
    雙曲貼現是本能,延遲享樂是種被教育訓練出來的自制能力
Viewing all 2337 articles
Browse latest View live