摘要
在SQL Server安全系列專題月報分享中,我們已經分享了:如何使用對稱密鑰實現SQL Server列加密技術、使用非對稱密鑰加密方式實現SQL Server列加密和使用混合密鑰實現SQL Server列加密技術三篇文章。本期月報我們分享列加密技術帶來的查詢性能問題以及相應的解決方案。
問題引入
根據SQL Server安全系列專題前三篇的月報分享,我們已經可以非常輕松的實現SQL Server的列加密,來保護我們關鍵數據列的安全性。但是,如果我們需要使用加密列來做為條件查詢的話,會導致SQL Server No-SARG查詢,進而導致查詢性能低下。比如:在我們場景中,使用電話號碼做為查詢、更新、刪除客戶信息的條件。
電話號碼條件查詢
在很多種場景中,業務系統需要通過電話號碼來查詢客戶詳細信息,但是由於我們已經將電話號碼加密存儲,於是查詢語句必須先將電話號碼密文解密后為明文后,再做查詢。比如,我們需要查找電話號碼為13487759293的客戶詳細信息,查詢語句會是:
USE [TestDb] GO OPEN SYMMETRIC KEY SymKey_TestDb DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy'; SELECT *, DescryptedCustomerPhone = CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) FROM dbo.CustomerInfo WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293' /** UPDATE A SET EncryptedCustomerPhone = EncryptByKey( Key_GUID('SymKey_TestDb'), '13487759293') FROM dbo.CustomerInfo AS A WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293' DELETE A FROM dbo.CustomerInfo AS A WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13402872514' **/ CLOSE SYMMETRIC KEY SymKey_TestDb; GO
查詢結果為: 這個使用電話號碼做為查詢條件的語句,會導致查詢語句存在非常大的性能問題: 在WHERE語句中使用函數運算解密電話號碼密文,即No-SARG查詢 電話號碼密文字段EncryptedCustomerPhone無法建立索引 類似導致查詢性能問題會同樣出現在客戶信息更細、客戶信息刪除等場景中,詳細的原理分析及解決方案,參加下一章節。
原理分析
為什么說使用電話號碼做為查詢條件,會帶來非常大的性能問題呢,這一章節將從以下兩個方面來進行分析:
No-SARG查詢
寬字段無法建立索引
No-SARG查詢
由於用戶只知道電話號碼的明文,即查詢條件的輸入端是明文,而在數據庫的表中,存儲的是電話號碼加密過的密文。如果要使用電話號碼做為查詢條件,必須解密電話號碼密文后,再與電話號碼明文匹配。即WHERE語句呈現如下的寫法: WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = ‘13487759293’ 這種在WHERE語句對表中正式字段進行函數運算的查詢是典型的No-SARG查詢,會導致SQL Server Scan表中該字段的所有值,導致IO,CPU資源的極大消耗,進而導致查詢時間消耗過長,性能低下。 在這里,可能會有人挑戰說,為什么不先加密電話號碼為密文后,再與數據庫表中的電話號碼密文進行對比呢?這樣就不會對表字段進行函數運算了嗎?這個方法提的非常好,這也是我們平時解決No-SARG查詢的思路。但是,在這個場景中變得行不通了,如下示例我們針對同一個電話號碼明文加密,出來的密文是完全不一致:
DECLARE @phone VARCHAR(11) = '13880975623' ; SELECT encrypted_phone_1 = EncryptByKey( Key_GUID('SymKey_TestDb'), @phone), encrypted_phone_2 = EncryptByKey( Key_GUID('SymKey_TestDb'), @phone) ;
如下結果,加密同一個電話號碼明文13880975623,加密密文encrypted_phone_1和encrypted_phone_2值卻完全不一樣。 因此,采用加密電話號碼明文后,再與表中字段數據匹配的方法不可行。
寬字段無法建立索引
由於創建索引的字段寬度,最大不允許超過900 bytes,但是EncryptByKey函數最多可能會返回8000個bytes,加之EncryptedCustomerPhone字段定義為varbinary(MAX)。因此,電話號碼密文字段不允許創建索引,嘗試創建索引。
USE [TestDb] GO CREATE INDEX ix_EncryptedCustomerPhone ON dbo.CustomerInfo(EncryptedCustomerPhone) ;
會報告如下錯誤:
解決方案
我們可以創建一個列,用於存放用戶電話號碼明文Hash值,然后再該Hash列上建立索引。查詢的時候,先將電話號碼明文計算Hash值,再與該Hash列進行匹配查找到對應的行即可。
解決方法
詳細的解決方法有如下幾個步驟:
添加Hash列
初始化Hash列數據
創建Hash列索引
新增數據行
添加Hash列
我們選擇CHECKSUM函數來計算電話號碼的Hash值,因此,添加一個INT數據類型的CustomerPhone_Hashkey列即可。
USE [TestDb] GO ALTER TABLE dbo.CustomerInfo ADD CustomerPhone_Hashkey INT NULL ; GO
初始化Hash列數據
初始化Hash列數據時,由於電話號碼已經加密為密文,需要先將其解密出來,然后再計算Hash值。
USE [TestDb] GO OPEN SYMMETRIC KEY SymKey_TestDb DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy'; WHILE EXISTS( SELECT TOP 1 * FROM dbo.CustomerInfo WITH(NOLOCK) WHERE CustomerPhone_Hashkey IS NULL ) BEGIN UPDATE TOP(10000) A SET CustomerPhone_Hashkey = CHECKSUM(CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone))) FROM dbo.CustomerInfo AS A WHERE CustomerPhone_Hashkey IS NULL WAITFOR DELAY '00:00:01' END CLOSE SYMMETRIC KEY SymKey_TestDb; GO
創建Hash列索引
在電話號碼Hash值列上,建立相應的索引。
USE [TestDb] GO CREATE INDEX IX_CustomerPhone_Hashkey ON dbo.CustomerInfo(CustomerPhone_Hashkey) WITH(FILLFACTOR=90, ONLINE=ON); GO
新增數據
添加了電話號碼Hash列后,新增數據時,需要計算電話號碼的Hash值,存儲在CustomerPhone_Hashkey列中。
USE [TestDb] GO OPEN SYMMETRIC KEY SymKey_TestDb DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy'; GO -- Performs the update of the record INSERT INTO dbo.CustomerInfo (CustomerName, CustomerPhone_Hashkey, EncryptedCustomerPhone) VALUES ('CustomerD', CHECKSUM('13880975623'), EncryptByKey( Key_GUID('SymKey_TestDb'), '13880975623')); -- Close the symmetric key CLOSE SYMMETRIC KEY SymKey_TestDb; GO
性能對比
以下是對優化前后的查詢語句寫法以及性能對比展示。
優化前
優化前,是對電話號碼密文在WHERE語句中解密出來,然后和用戶側輸入的電話號碼明文進行比較,獲取到相應的數據行。
查詢語句
查詢的語句寫法如下,關鍵點請注意WHERE語句:WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = ‘13487759293’
USE [TestDb] GO -- empty buffer cache DBCC DROPCLEANBUFFERS GO OPEN SYMMETRIC KEY SymKey_TestDb DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy'; SET STATISTICS TIME ON SET STATISTICS IO ON GO SELECT *, DescryptedCustomerPhone = CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) FROM dbo.CustomerInfo WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293' SET STATISTICS TIME OFF SET STATISTICS IO OFF CLOSE SYMMETRIC KEY SymKey_TestDb; GO
執行計划
從查詢語句的執行計划來看,走的Clustered Index Scan,幾乎等價於表掃描,SQL Server需要掃面這張表的所有數據,才能找到對應的數據行。
性能指標
從性能指標來看,Logical reads: 13957,CPU: 13010 ms,Duration: 41682 ms,性能消耗非常嚴重,查詢性能低下。
優化后
優化后的查詢,我們使用電話號碼明文Hash值列CustomerPhone_Hashkey進行查詢,去找到對應的數據行。
查詢語句
查詢的關鍵點在WHERE CustomerPhone_Hashkey = CHECKSUM(‘13487759293’)。
USE [TestDb] GO -- empty buffer cache DBCC DROPCLEANBUFFERS GO OPEN SYMMETRIC KEY SymKey_TestDb DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy'; GO SET STATISTICS TIME ON SET STATISTICS IO ON GO SELECT *, DescryptedCustomerPhone = CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) FROM dbo.CustomerInfo WITH(NOLOCK) WHERE CustomerPhone_Hashkey = CHECKSUM('13487759293') AND CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293' -- Close the symmetric key CLOSE SYMMETRIC KEY SymKey_TestDb; GO
執行計划
從執行計划我們也可以看出,SQL Server從Clustered Index Scan操作變成了Index Seek操作了,可以直接定位到具體的數據行。
性能指標
優化后的性能指標來看,性能天壤之別,Logical reads: 6,CPU: 0ms,Duration: 2ms。
注意: 為了防止Hash對撞,在WHERE語句中需要添加如下條件語句: AND CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = ‘13487759293’
性能對比圖
總結下優化前后的LogicalReads、CPU和Duration指標,如下表所示: 從此表格,我們可以看到查詢性能有質的飛躍,不論是從IO邏輯讀、CPU消耗還是從執行時間,性能都有非常大的提升。
最后總結
本文分享了使用SQL Server列加密技術后,應用端可能面臨的查詢性能問題以及完整的解決方案。通過此方案,我們可以很好兼顧:最大限度保證用戶數據安全性的前提下,還能最大程度的提升我們的查詢性能。