UTF-8支持對SQL Server 2019的影響

概述

  SQL Server長期以來一直以nchar,nvarchar和ntext數據類型的形式支持Unicode字符,這些字符僅限於UTF-16。可以將UTF-8數據放入nchar和nvarchar列中,但這 通常很乏味,即使 在SQL Server 2014 SP2中添加了通過BCP和BULK INSERT的UTF-8支持之後。最終結果是要支付Unicode的存儲和內存需求,因爲即使部分或全部爲ASCII,仍然必須將所有數據存儲爲Unicode。

  在SQL Server 2019中,有新的UTF-8排序規則,可讓您節省存儲空間,同時仍可享受兼容性和原生存儲UTF-8數據的好處。與Unicode壓縮類似(但不完全相同),您只需爲實際需要該字符的字符支付額外的存儲空間。但是,實際的存儲影響是什麼?這如何影響內存授予和查詢性能?

測試過程

  各種歸類,代碼頁和UTF格式都有許多含義。我覺得專家可以撰寫20個部分的文章系列,但仍然沒有完成。實際上,所羅門·魯茲基(Solomon Rutzky)已經寫了很多有關這些主題的文章,最近 一篇有關SQL Server 2019中對UTF-8支持的文章 –這表明您可能不應該使用此功能,並且您應該專注於使用UTF-8列的排序規則應該主要是關於兼容性,而不是存儲空間或性能。

  因爲我知道盡管有 Solomon的建議,人們仍然會使用它,所以我只想專注於特定的UTF-8排序規則,以及與傳統Unicode列中存儲的UTF-16數據相比,空間和內存要求有何不同。我將比較壓縮與不壓縮以及列值的各種百分比(以及表中行的百分比)與非ASCII數據的比較。

   首先,讓我們看一下一個表,該表包含具有三個不同排序規則的列,並查看當我們向其中插入數據時的外觀。我爲該查詢拍攝了屏幕截圖,因爲我知道其中一些Unicode字符在到達您的設備時無法很好地轉換:

共有三列,第一列使用標準Latin1_General歸類,第二列包含具有補充字符(SC)的Latin1_General,第三列使用新的Latin1_General UTF-8歸類。我分別插入了希臘字符,亞洲字符和表情符號(當然是加拿大國旗!),然後再插入一些其他ASCII字符。這是每個值的LEN()和DATALENGTH()的結果:

   顯然,您可以看到長度基本相同,唯一的不同是表情符號在第一次排序時需要四個字節(請參閱 Greg Low的這篇文章,以瞭解爲什麼這是字節而不是字符)。但是,使用UTF-8歸類時,實際存儲幾乎總是相同或更低(再次,除了一個例外,這次亞洲字符需要一個額外的字節)。我爲您省去了一個懸念:通過行和頁面壓縮以及類似的#temp表,所有結果都是相同的。

另外,上面的代碼示例中的註釋表明,即使目標類型是varchar,您仍然需要在字符串文字上使用N前綴。原因是SQL Server將首先嚐試解釋字符串的值,如果N不存在,則部分Unicode數據會丟失。

嘗試這個:

DECLARE @t TABLE (t varchar(10) COLLATE Latin1_General_100_CI_AI_SC_UTF8);
INSERT @t(t) VALUES('h'),(N'h');
SELECT t FROM @t;
t
----
?
h

在玩這個遊戲的過程中,我還發現了另一種現象,可能與排序規則完全無關,但仍然很有趣。當使用Unicode字符串的varbinary表示形式時(例如一堆poo表情符號,0x3DD8A9DC),可以根據語句中的其他內容來不同地解釋它們。在此示例中,我要執行三個不同的批處理:

(1)直接插入varbinary值;

(2)直接插入值,並在單獨的語句中,將值轉換爲nvarchar後插入;

(3)將值和轉換後的值插入同一條語句中:

DECLARE @t TABLE (t varchar(10) COLLATE Latin1_General_100_CI_AI_SC_UTF8);
INSERT @t(t) VALUES(0x3DD8A9DC);
SELECT t FROM @t;
GO -- 1
 
DECLARE @t TABLE (t varchar(10) COLLATE Latin1_General_100_CI_AI_SC_UTF8);
INSERT @t(t) VALUES(0x3DD8A9DC);
INSERT @t(t) VALUES(CONVERT(nvarchar(10),0x3DD8A9DC));
SELECT t FROM @t;
GO -- 2
 
DECLARE @t TABLE (t varchar(10) COLLATE Latin1_General_100_CI_AI_SC_UTF8);
INSERT @t(t) VALUES(0x3DD8A9DC),(CONVERT(nvarchar(10),0x3DD8A9DC));
SELECT t FROM @t;
GO -- 3

結果讓我感到困惑:

在使用不同的語句執行插入的情況下,兩個解釋都正確。但是,當使用VALUES()將兩行插入在一起時,兩者都以某種方式轉換爲nvarchar。涉及VALUES()的行爲,可能與歸類無關,但在以後的技巧中,我將不得不對此進行研究。同時,如果要將腳本從一種形式更改爲另一種形式,請注意這一點。

回到原來的調查;如果我們大規模嘗試該怎麼辦?我編寫了一個腳本,該腳本爲一堆表生成CREATE TABLE語句,這些表具有用於校對,壓縮和實際存儲多少非ASCII數據的各種設置。具體來說,這將創建81個表,這些表具有以下組合:

  • 壓縮(行,頁,無);
  • 排序規則(Latin1_General_100_CI_AI,Latin1_General_100_CI_AI _SC和Latin1_General_100_CI_AI _SC_UTF8);
  • 包含UTF-8數據的行百分比(0%,50%,100%);和,
  • 每行是UTF-8數據的字符數(0個字符,25個字符和50個字符):
CREATE TABLE #cmp(cmp varchar(4));
INSERT #cmp VALUES('ROW'),('PAGE'),('NONE');
CREATE TABLE #coll(coll varchar(8));
INSERT #coll VALUES(''),('_SC'),('_SC_UTF8');
CREATE TABLE #row(rowconf varchar(9));
INSERT #row VALUES('0  % UTF8'),('50 % UTF8'),('100% UTF8');
CREATE TABLE #char(charconf varchar(7));
INSERT #char VALUES('0 UTF8'),('25 UTF8'),('50 UTF8');
SELECT N'CREATE TABLE dbo.' + QUOTENAME(N'UTF8Test' + coll.coll + N'_' 
  + cmp.cmp + N'_' + rowconf + N'_' + charconf) + N'
(
    id int IDENTITY(1,1) NOT NULL,
    the_column ' + CASE coll.coll WHEN '_SC_UTF8' THEN N'' ELSE N'n' END + N'varchar(512)' END 
    + N' COLLATE Latin1_General_100_CI_AI' + coll.coll + N',
    CONSTRAINT ' + QUOTENAME(N'pk_UTF8Test_' + coll.coll + N'_' + cmp.cmp 
    + N'_' + rowconf + N'_' + charconf) + N' PRIMARY KEY CLUSTERED(id) 
    WITH (DATA_COMPRESSION = ' + cmp.cmp + N')
);' FROM #cmp AS cmp, #coll AS coll, #row AS rowconf, #char AS charconf;

該腳本產生81行輸出,並具有如下表定義(當然,它們不是漂亮的腳本):

CREATE TABLE dbo.[UTF8Test_ROW_0  % UTF8_0 UTF8]
(
    id int IDENTITY(1,1) NOT NULL,
    the_column nvarchar(200) COLLATE Latin1_General_100_CI_AI,
    CONSTRAINT [pk_UTF8Test__ROW_0  % UTF8_0 UTF8] PRIMARY KEY CLUSTERED(id)
      WITH (DATA_COMPRESSION = ROW)
);
CREATE TABLE dbo.[UTF8Test_SC_ROW_0  % UTF8_0 UTF8]
(
    id int IDENTITY(1,1) NOT NULL,
    the_column nvarchar(200) COLLATE Latin1_General_100_CI_AI_SC,
    CONSTRAINT [pk_UTF8Test__SC_ROW_0  % UTF8_0 UTF8] PRIMARY KEY CLUSTERED(id)
      WITH (DATA_COMPRESSION = ROW)
);
CREATE TABLE dbo.[UTF8Test_SC_UTF8_ROW_0  % UTF8_0 UTF8]
(
    id int IDENTITY(1,1) NOT NULL,
    the_column varchar(200) COLLATE Latin1_General_100_CI_AI_SC_UTF8,
    CONSTRAINT [pk_UTF8Test__SC_UTF8_ROW_0  % UTF8_0 UTF8] PRIMARY KEY CLUSTERED(id)
      WITH (DATA_COMPRESSION = ROW)
);

… 78 more tables …

 

複製,粘貼,執行,現在您有81個表,可以生成INSERT語句以類似的方式進行填充。這裏涉及更多邏輯,因此腳本更加醜陋-我們希望在每個表中插入10,000行,但是這些行是部分或全部填充(或未填充)Unicode數據的值的混合。我在這裏有加拿大國旗,並在該位置添加了註釋,以防它無法在您的瀏覽器中正確顯示:

DECLARE @sql nvarchar(max) = N'SET NOCOUNT ON;';
SELECT @sql += N'
WITH n AS (SELECT n = 1 UNION ALL SELECT n+1 FROM n WHERE n < 10000)
INSERT dbo.' + QUOTENAME(N'UTF8Test' + coll.coll + N'_' + cmp.cmp 
  + N'_' + rowconf + N'_' + charconf) + N'(the_column) SELECT b FROM (SELECT 
  b = REPLICATE(N''🇨🇦'',' + LEFT(charconf.charconf,2) + N')
  -----------------^ Canada flag is here
  + REPLICATE(N''.'',' + RTRIM(50-LEFT(charconf.charconf,2)) + N')) AS a
  CROSS APPLY (SELECT TOP (' + CONVERT(varchar(11),CONVERT(int,10000 
  * LEFT(rowconf.rowconf,3)/100.0)) + N') n FROM n) AS b OPTION (MAXRECURSION 10000); 
WITH n AS (SELECT n = 1 UNION ALL SELECT n+1 FROM n WHERE n < 10000)
INSERT dbo.' + QUOTENAME(N'UTF8Test' + coll.coll + N'_' + cmp.cmp 
  + N'_' + rowconf + N'_' + charconf) + N'(the_column) SELECT b FROM (SELECT 
  b = REPLICATE(N''.'',50)) AS a 
  CROSS APPLY (SELECT TOP (' + CONVERT(varchar(11),10000-CONVERT(int,10000 
  * LEFT(rowconf.rowconf,3)/100.0)) + N') n FROM n) AS b OPTION (MAXRECURSION 10000);'
FROM #cmp AS cmp, #coll AS coll, #row AS rowconf, #char AS charconf;
PRINT @sql;
--EXEC sys.sp_executesql @sql;

打印不會顯示所有腳本(除非您具有 SSMS 18.2或使用本文所述的其他 措施),而是成對的insert語句。每對中的第一對代表包含UTF-8數據的行,第二對代表不包含數據的行:

WITH n AS (SELECT n = 1 UNION ALL SELECT n+1 FROM n WHERE n < 10000)
INSERT dbo.[UTF8Test_ROW_0  % UTF8_0 UTF8](the_column) SELECT b FROM (SELECT 
  b = REPLICATE(N'🇨🇦',0 )
  ----------------^ Canada flag is here
  + REPLICATE(N'.',50)) AS a
 CROSS APPLY (SELECT TOP (0) n FROM n) AS b OPTION (MAXRECURSION 10000); 
WITH n AS (SELECT n = 1 UNION ALL SELECT n+1 FROM n WHERE n < 10000)
INSERT dbo.[UTF8Test_ROW_0  % UTF8_0 UTF8](the_column) SELECT b FROM (SELECT 
  b = REPLICATE(N'.',50)) AS a 
 CROSS APPLY (SELECT TOP (10000) n FROM n) AS b OPTION (MAXRECURSION 10000);

在第一個示例中,我們希望0%的行包含UTF-8數據,並且希望任何行內的0個字符包含UTF-8數據。這就是爲什麼我們不插入包含加拿大國旗的行,也不插入10,000行(包含50個句點)的原因。(我承認50個週期的壓縮會受到不公平的壓縮,但是更具代表性的數據更難以自動化,而GUID則相反。)

如果我們從腳本後面的示例中選取一個任意示例,我們可以看到行的分佈方式不同–一半的行包含UTF-8數據,而那些行包含25個Unicode字符和25個句點:

WITH n AS (SELECT n = 1 UNION ALL SELECT n+1 FROM n WHERE n < 10000)
INSERT dbo.[UTF8Test_ROW_50 % UTF8_25 UTF8](the_column) SELECT b FROM (SELECT 
  b = REPLICATE(N'🇨🇦',25)
  ----------------^ Canada flag is here
  + REPLICATE(N'.',25)) AS a
 CROSS APPLY (SELECT TOP (5000) n FROM n) AS b OPTION (MAXRECURSION 10000);
WITH n AS (SELECT n = 1 UNION ALL SELECT n+1 FROM n WHERE n < 10000)
INSERT dbo.[UTF8Test_ROW_50 % UTF8_25 UTF8](the_column) SELECT b FROM (SELECT 
  b = REPLICATE(N'.',50)) AS a 
  CROSS APPLY (SELECT TOP (5000) n FROM n) AS b OPTION (MAXRECURSION 10000);

如果您確信我不會炸燬您的磁盤,請更改以下內容:

PRINT @sql;
--EXEC sys.sp_executesql @sql;
--PRINT @sql;
EXEC sys.sp_executesql @sql;

然後執行它。在我的系統上,這花費了20到40秒的時間,數據和日誌文件分別爲400 MB和140 MB(從相當標準的AdventureWorks示例數據庫開始)。

現在,我們準備進行抽查和分析!首先,讓我們確保所有表的行數均正確:

SELECT t.name, p.rows
  FROM sys.tables AS t
  INNER JOIN sys.partitions AS p
  ON t.object_id = p.object_id
  WHERE t.name LIKE N'UTF8%';
-- 81 rows, all with 10,000 rows

然後我們可以對任何我們希望會有差異的表進行檢查:

SELECT TOP (2) * FROM dbo.[UTF8Test_ROW_50 % UTF8_50 UTF8] ORDER BY id;
SELECT TOP (2) * FROM dbo.[UTF8Test_ROW_50 % UTF8_50 UTF8] ORDER BY id DESC;
SELECT TOP (2) * FROM dbo.[UTF8Test_SC_UTF8_ROW_50 % UTF8_25 UTF8] ORDER BY id;
SELECT TOP (2) * FROM dbo.[UTF8Test_SC_UTF8_ROW_50 % UTF8_25 UTF8] ORDER BY id DESC;

果然,我們看到了我們期望看到的結果(這對排序規則沒有任何滿足,只是證明我的腳本做了我認爲會做的事情):

現在,存儲空間如何?我想看看頁面分配DMV,sys.dm_db_database_page_allocations,尤其是相對比較。我從模板中提取了以下簡單查詢:

SELECT t.name,PageCount = COUNT(p.allocated_page_page_id) 
FROM sys.tables AS t CROSS APPLY 
sys.dm_db_database_page_allocations(DB_ID(), t.object_id, 1, NULL, 'LIMITED') AS p
WHERE t.name LIKE N'UTF8%'
GROUP BY t.name
ORDER BY PageCount DESC;

 

我將輸出移到Excel中,幾乎任意地將其分爲三列。左側的列是每個需要100頁以上的表,而右側的列是每個使用頁壓縮的表。中間一欄是包含81或89頁的所有內容。現在,我可能已經堆疊了甲板以便於壓縮,因爲任何給定頁面上的所有值都可能是相同的。這意味着壓縮所涉及的頁數可能比真實世界中更多的數據要少得多。但這確實表明,在給定相同數據的情況下,頁面壓縮是絕對均衡器。剩下的是一團糟,沒有實際可觀察​​的趨勢,除了說明當更多數據是Unicode時,頁數會增加,而不管排序規則如何(而且大部分情況下,

性能如何?在這種情況下,我通常關心的事情-除了必須在掃描中讀取的頁面數之外-是要分配的內存授權,尤其是對於具有排序的查詢。持續時間也總是讓人感興趣的,但是我總是覺得自由記憶比耐心更稀缺。我編寫了一個腳本來生成針對每個表運行的查詢,共10次:

DECLARE @sql nvarchar(max) = N'DBCC FREEPROCCACHE;
GO
';
;WITH x AS (SELECT name FROM sys.tables WHERE name LIKE N'UTF8%')
SELECT @sql += N'
SELECT TOP 1 c FROM (SELECT TOP 9999 the_column FROM dbo.' 
  + QUOTENAME(name) + ' ORDER BY 1) x(c);
GO 10'
FROM x;
PRINT @sql;

在這種情況下,我使用PRINT輸出(複製並粘貼到新窗口中)而不是sys.sp_executesql,因爲後者不能接受GO 10之類的命令 。運行查詢後,我轉到sys.dm_exec_query_stats檢查內存授予和查詢持續時間。我本可以單獨分析這82個查詢,但是我決定簡單地按排序規則和壓縮將它們分組。我運行的查詢:

WITH x AS 
(
  SELECT coll = CASE WHEN t.name LIKE '%SC_UTF8%' THEN 'UTF8'
      WHEN t.name LIKE '%_SC%' THEN 'SC' ELSE '' END,
    comp = CASE WHEN t.name LIKE N'%_PAGE_%' THEN 'Page'
      WHEN t.name LIKE N'%_ROW_%' THEN 'Row' ELSE 'None' END,
      max_used_grant_kb,max_ideal_grant_kb,max_elapsed_time
  FROM sys.dm_exec_query_stats AS s
  CROSS APPLY sys.dm_exec_sql_text(s.plan_handle) AS st
  INNER JOIN sys.tables AS t
  ON st.[text] LIKE N'SELECT TOP%' + t.name + N'%'
  WHERE t.name LIKE N'UTF8%'
)
SELECT coll, comp, 
  max_used_grant = AVG(max_used_grant_kb*1.0),
  ideal_grant    = AVG(max_ideal_grant_kb*1.0),
  max_time       = AVG(max_elapsed_time*1.0) 
FROM x GROUP BY coll,comp
ORDER BY coll, comp;

這產生了兩個有趣的圖表。第一個顯示UTF-8數據的內存授予量略小:

不幸的是,第二個圖表顯示UTF-8查詢的平均持續時間高出50%或更多:

摘要

  新的UTF-8歸類可以提供存儲空間方面的好處,但是如果使用頁面壓縮,則該好處並不比舊歸類更好。儘管內存授予量可能會略低,從而可能允許更多的併發性,但是這些查詢的運行時間卻明顯更長。經過一小段調查之後,我不會說有明顯的情況,我會急於改用UTF-8歸類。

 

 

 

 

 

 

 

 

 

 

 

 

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章