轉自:微軟技術大會2016html
逐行插入:插入數據-》行組 超過100K 102400行纔會插入到壓縮行組-》壓縮行組-》移動元組-》列段mysql
大批量插入:插入數據 超過100K 102400行-》壓縮行組-》移動元組-》列段
數據插入到行組時,一旦併發度太高,會臨時生成多個臨時行組,臨時行組最後會合併爲一個行組算法
插入數據-》行組 (行組會有X鎖行鎖)影響併發sql
兩種方式
建表時候直接建立一個彙集列存儲索引表
建表時候先建一個普通匯集索引表,而後在這個普通匯集索引表上創建一個彙集列存儲索引(一個彙集索引表變成了兩個副本兩份數據,一個是彙集索引,一個是彙集列存儲索引)
SQL2014:彙集列存儲索引表不能創建主鍵,不支持有大對象數據類型nvarchar(max)等的表
SQL2016:彙集列存儲索引表能夠創建主鍵,支持有大對象數據類型nvarchar(max)等的表
數據庫
http://mysql.taobao.org/monthly/2017/03/04/緩存
delta store->列存儲session
看這篇文章以前能夠先看一下下面這兩篇文章:併發
列存儲索引ide
http://www.cnblogs.com/qanholas/archive/2013/03/08/2949205.html測試
非彙集索引
http://www.cnblogs.com/lyhabc/p/3196484.html
還有這一篇文章
https://www.sqlpassion.at/archive/2017/01/30/columnstore-segment-elimination/?awt_l=BJCrA&awt_m=3mFjBb8vbIYUUTS
行組-》加密,壓縮-》元數據,段-》blob段數據列存儲
東方瑞通
一張表水平切爲多個行組,各個行組有垂直切爲多個段,每一個小方塊爲一個段(一個表又水平切又垂直切成多個小方塊(段))
段列中的部分行的一個集合
包含相同行的段是一個行組
數據以段爲單位進行壓縮
每一個段存儲在不一樣lob頁中
段是IO訪問的最小單位
創建測試環境
先建立一張表
1 USE [pratice] 2 GO 3 IF (OBJECT_ID('TestTable') IS NOT NULL) 4 DROP TABLE [dbo].[TestTable] 5 GO 6 CREATE TABLE TestTable 7 ( 8 id INT , 9 c1 INT 10 ) 11 GO
插入1W條測試數據
1 DECLARE @i INT 2 SET @i=1 3 WHILE @i<10001 4 BEGIN 5 INSERT INTO TestTable (id,c1) 6 SELECT @i,@i 7 SET @i=@i+1 8 END 9 GO
看一下插入的記錄是否足夠
1 SELECT COUNT(*) FROM TestTable 2 SELECT TOP 10 * from TestTable
在C1列上建立一個列存儲索引
1 CREATE NONCLUSTERED columnstore INDEX PK__TestTable__ColumnStore ON TestTable(c1)
執行計劃
在上面給出的文章裏提到 http://www.cnblogs.com/qanholas/archive/2013/03/08/2949205.html
下面幾個SQL語句的執行計劃也顯示出列存儲索引不會seek
(2)列存儲索引不支持 SEEK
若是查詢應返回行的一小部分,則優化器不大可能選擇列存儲索引(例如:needle-in-the-haystack 類型查詢)。
若是使用表提示 FORCESEEK,則優化器將不考慮列存儲索引。
1 SELECT * FROM TestTable WHERE [C1]=60 --列存儲索引掃描 RID查找 2 SELECT id FROM TestTable WHERE [C1]=60 --列存儲索引掃描 RID查找 3 SELECT c1 FROM TestTable WHERE [C1]=60 --列存儲索引掃描 4 SELECT * FROM TestTable WHERE id=60 --全表掃描 5 SELECT c1 FROM TestTable --列存儲索引掃描 6 SELECT * FROM TestTable --全表掃描
列存儲索引的結構
先建立一張表保存DBCC的結果
1 USE [pratice] 2 GO 3 CREATE TABLE DBCCResult ( 4 PageFID NVARCHAR(200), 5 PagePID NVARCHAR(200), 6 IAMFID NVARCHAR(200), 7 IAMPID NVARCHAR(200), 8 ObjectID NVARCHAR(200), 9 IndexID NVARCHAR(200), 10 PartitionNumber NVARCHAR(200), 11 PartitionID NVARCHAR(200), 12 iam_chain_type NVARCHAR(200), 13 PageType NVARCHAR(200), 14 IndexLevel NVARCHAR(200), 15 NextPageFID NVARCHAR(200), 16 NextPagePID NVARCHAR(200), 17 PrevPageFID NVARCHAR(200), 18 PrevPagePID NVARCHAR(200) 19 )
咱們看一下列存儲索引在表中創建了一些什麼頁面
1 --TRUNCATE TABLE [dbo].[DBCCResult] 2 INSERT INTO DBCCResult EXEC ('DBCC IND(pratice,TestTable,-1) ') 3 4 SELECT * FROM [dbo].[DBCCResult] ORDER BY [PageType] DESC
先說明一下:DBCC IND的結果
PageType 頁面類型:1:數據頁面;2:索引頁面;3:Lob_mixed_page;4:Lob_tree_page;10:IAM頁面
IndexID 索引ID:0 表明堆, 1 表明彙集索引, 2-250 表明非彙集索引 大於250就是text或image字段
因爲表中的頁面太多,原本想truncate table並只插入1000行記錄到表,讓你們看清楚一下表中的頁面的,可是遇到下面錯誤
http://www.cnblogs.com/qanholas/archive/2013/03/08/2949205.html
文章中裏提到:
(14).有列存儲索引後,表變成只讀表,不能進行添加,刪除,編輯的操做。 insert into TestTable(c1,c2) select rand(),rand() 錯誤信息是這樣的:因爲不能在包含列存儲索引的表中更新數據,INSERT 語句失敗。
微軟提供了三種方式來解決這個問題,這裏簡單介紹兩種: 1) 若要更新具備列存儲索引的表,先刪除列存儲索引,執行任何所需的 INSERT、DELETE、UPDATE 或 MERGE 操做,而後從新生成列存儲索引。
2) 對錶進行分區並切換分區。對於大容量插入,先將數據插入到一個臨時表中,在臨時表上生成列存儲索引,而後將此臨時表切換到空分區。對於其餘更新,將主表外的一個分區切換到一個臨時表中,禁用或刪除臨時表上的列存儲索引,執行更新操做,在臨時表上從新生成或從新建立列存儲索引,而後將臨時表切換回主表。
只可以先刪除列存儲索引,再truncate table了
1 DROP INDEX PK__TestTable__ColumnStore ON TestTable
truncate table,再插入1000條記錄,從新創建列存儲索引,看到DBCC IND的結果以下:
表中有10000條記錄的話,表中的頁面類型又增長了一種,並且能夠看到,列存儲索引的表中是沒有索引頁面的,只有LOB頁面
10000條記錄的表比1000條記錄的表多了一種頁面類型:Lob_tree_page
爲了不篇幅過長,有關Lob_tree_page頁面的詳細內容請看個人另外一篇文章
這裏爲什麼要用LOB頁來存放索引數據呢?
本人認爲由於要將數據轉爲二進制並壓縮,因此用LOB頁來存放索引數據
測試和比較
下面創建一張非彙集索引表
1 USE [pratice] 2 GO 3 4 CREATE TABLE TestTable2 5 ( 6 id INT , 7 c1 INT , 8 c2 INT 9 ) 10 11 CREATE NONCLUSTERED INDEX NCL__TestTabl__C1 ON TestTable2(c1) 12 13 DECLARE @i INT 14 SET @i=1 15 WHILE @i<10001 16 BEGIN 17 INSERT INTO TestTable2 (id,c1) 18 SELECT @i,@i 19 SET @i=@i+1 20 END 21 22 SELECT COUNT(*) FROM TestTable2 23 SELECT TOP 10 * from TestTable2
爲何用非彙集索引表來比較?
你們能夠看一下列存儲索引的創建語句
1 CREATE NONCLUSTERED columnstore INDEX PK__TestTable__ColumnStore ON TestTable(c1)
在非彙集索引上加多了一個columnstore關鍵字
並且列存儲索引的表的頁面狀況跟非彙集索引表的頁面狀況幾乎是同樣的
除了LOB頁面,數據頁面仍是在堆裏面的
測試結果:
1 SET NOCOUNT ON 2 SET STATISTICS IO ON 3 SET STATISTICS TIME ON 4 SELECT id FROM TestTable WHERE [C1]=60 --列存儲索引掃描 RID查找 5 SQL Server 分析和編譯時間: 6 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 7 表 'TestTable'。掃描計數 1,邏輯讀取 37 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。 8 9 SQL Server 執行時間: 10 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 11 ------------------------------------------------------------- 12 SET NOCOUNT ON 13 SET STATISTICS IO ON 14 SET STATISTICS TIME ON 15 SELECT id FROM TestTable2 WHERE [C1]=60 --索引查找 RID查找 16 SQL Server 分析和編譯時間: 17 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 18 表 'TestTable2'。掃描計數 1,邏輯讀取 3 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。 19 20 SQL Server 執行時間: 21 CPU 時間 = 15 毫秒,佔用時間 = 17 毫秒。 22 ----------------------------------------------------------------------------------
CPU執行時間非彙集索引要多一些
而邏輯讀取非彙集索引表比列存儲索引表少了37-3=34次
由於非彙集索引使用的是索引查找,找到索引頁就能夠了,而列存儲索引還要掃描LOB頁面
----------------------------------------------------------------------------------
上面是沒有清空數據緩存和執行計劃緩存的狀況下的測試結果
下面是清空了數據緩存和執行計劃緩存的狀況下的測試結果
1 USE [pratice] 2 GO 3 DBCC DROPCLEANBUFFERS 4 DBCC freeproccache 5 GO 6 SET NOCOUNT ON 7 SET STATISTICS IO ON 8 SET STATISTICS TIME ON 9 SELECT id FROM TestTable2 WHERE [C1]=60 --索引查找 RID查找 10 11 12 SQL Server 分析和編譯時間: 13 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 14 15 SQL Server 執行時間: 16 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 17 SQL Server 分析和編譯時間: 18 CPU 時間 = 0 毫秒,佔用時間 = 1 毫秒。 19 DBCC 執行完畢。若是 DBCC 輸出了錯誤信息,請與系統管理員聯繫。 20 21 SQL Server 執行時間: 22 CPU 時間 = 0 毫秒,佔用時間 = 2 毫秒。 23 DBCC 執行完畢。若是 DBCC 輸出了錯誤信息,請與系統管理員聯繫。 24 25 SQL Server 執行時間: 26 CPU 時間 = 0 毫秒,佔用時間 = 18 毫秒。 27 SQL Server 分析和編譯時間: 28 CPU 時間 = 63 毫秒,佔用時間 = 95 毫秒。 29 30 SQL Server 執行時間: 31 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 32 33 SQL Server 執行時間: 34 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 35 36 SQL Server 執行時間: 37 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 38 SQL Server 分析和編譯時間: 39 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 40 表 'TestTable2'。掃描計數 1,邏輯讀取 28 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。 41 42 SQL Server 執行時間: 43 CPU 時間 = 0 毫秒,佔用時間 = 1 毫秒。 44 --------------------------------------------------------------------- 45 USE [pratice] 46 GO 47 DBCC DROPCLEANBUFFERS 48 DBCC freeproccache 49 GO 50 SET NOCOUNT ON 51 SET STATISTICS IO ON 52 SET STATISTICS TIME ON 53 SELECT id FROM TestTable WHERE [C1]=60 --列存儲索引掃描 RID查找 54 55 SQL Server 分析和編譯時間: 56 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 57 58 SQL Server 執行時間: 59 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 60 SQL Server 分析和編譯時間: 61 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 62 DBCC 執行完畢。若是 DBCC 輸出了錯誤信息,請與系統管理員聯繫。 63 64 SQL Server 執行時間: 65 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 66 DBCC 執行完畢。若是 DBCC 輸出了錯誤信息,請與系統管理員聯繫。 67 68 SQL Server 執行時間: 69 CPU 時間 = 0 毫秒,佔用時間 = 13 毫秒。 70 SQL Server 分析和編譯時間: 71 CPU 時間 = 0 毫秒,佔用時間 = 26 毫秒。 72 73 SQL Server 執行時間: 74 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 75 76 SQL Server 執行時間: 77 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 78 79 SQL Server 執行時間: 80 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 81 SQL Server 分析和編譯時間: 82 CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。 83 表 'TestTable'。掃描計數 1,邏輯讀取 40 次,物理讀取 1 次,預讀 68 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。 84 85 SQL Server 執行時間: 86 CPU 時間 = 0 毫秒,佔用時間 = 41 毫秒。
能夠看到列存儲索在執行時間上佔優點,可是在IO上比非彙集索引差一點點
列存儲索引所申請的鎖
1 USE [pratice] 2 GO 3 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 4 GO 5 6 BEGIN TRAN 7 SELECT id FROM TestTable WHERE [C1]=60 --列存儲索引掃描 RID查找 8 9 --COMMIT TRAN 10 11 USE [pratice] --要查詢申請鎖的數據庫 12 GO 13 SELECT 14 [request_session_id], 15 c.[program_name], 16 DB_NAME(c.[dbid]) AS dbname, 17 [resource_type], 18 [request_status], 19 [request_mode], 20 [resource_description],OBJECT_NAME(p.[object_id]) AS objectname, 21 p.[index_id] 22 FROM sys.[dm_tran_locks] AS a LEFT JOIN sys.[partitions] AS p 23 ON a.[resource_associated_entity_id]=p.[hobt_id] 24 LEFT JOIN sys.[sysprocesses] AS c ON a.[request_session_id]=c.[spid] 25 WHERE c.[dbid]=DB_ID('pratice') AND a.[request_session_id]=@@SPID ----要查詢申請鎖的數據庫 26 ORDER BY [request_session_id],[resource_type]
能夠看到雖然是列存儲索引掃描,可是也沒有在LOB頁面申請鎖,只是在普通數據頁面和真正的數據行上申請了鎖
使用列存儲索引,阻塞的機會也減小了
SQL Server 2012列存儲索引技術(雲棲社區)
https://yq.aliyun.com/articles/69254?spm=5176.100240.searchblog.59.hrBlST#
Batch Mode Processing 是SQL Server新的查詢處理算法,專門設計來高效地處理大量數據的批處理算法,以加快統計分析類查詢的效率。其實這個算法的原理實現很是簡單,SQL Server有一個專門的系統視圖sys.column_store_segments來存放列存儲索引的每一個列的每一個Segments的最小值和最大值,當SQL Server執行Batch Mode Processing查詢時,只須要和查詢篩選條件對比,
就能夠知道對應的Segment是否包含知足條件的數據,從而能夠實現快速的跳過哪些不知足條件的Segment
因爲每一個Segment中包含成千上萬條記錄,因此SQL Server篩選出知足條件的效率很是高,所以大大節約了磁盤I/O和所以帶來的CPU開銷。這種跳過不知足條件Segment的算法專業術語叫Segment Elimination。
Segment Elimination的作法:一個傳統彙集索引表,用drop_exist的方式建立彙集列存儲索引並用查詢提示maxdop=1
https://www.sqlpassion.at/archive/2017/01/30/columnstore-segment-elimination/?awt_l=BJCrA&awt_m=3ZdebPPtXEYUUTS
SET STATISTICS IO ON
能看到 Segment skipped 的數量
-- Now we create a traditional RowStore Clustered Index to sort our -- table data by the column "DateKey". CREATE CLUSTERED INDEX idx_ci ON FactOnlineSales_Temp(DateKey) GO -- "Swap" the Clustered Index through a Clustered ColumnStore Index CREATE CLUSTERED COLUMNSTORE INDEX idx_ci ON FactOnlineSales_Temp WITH (DROP_EXISTING = ON) GO
CREATE CLUSTERED COLUMNSTORE INDEX idx_ci ON FactOnlineSales_Temp WITH (DROP_EXISTING = ON, MAXDOP = 1) GO
--查詢列存儲的壓縮方式 USE ColumnStoreDB GO SELECT DISTINCT table_name = object_name(part.object_id) ,ix.name ,part.data_compression_desc FROM sys.partitions as part INNER JOIN sys.indexes as ix ON part.object_id = ix.object_id AND part.index_id = ix.index_id WHERE part.object_id = object_id('dbo.SalesOrder','U') AND ix.name = 'NCSIX_ALL_Columns'
--查詢每一個列存儲段的最大值和最小值 USE ColumnStoreDB GO SELECT table_name = object_name(part.object_id) ,col.name ,seg.segment_id ,seg.min_data_id ,seg.max_data_id ,seg.row_count FROM sys.partitions as part INNER JOIN sys.column_store_segments as seg ON part.partition_id = seg.partition_id INNER JOIN sys.columns as col ON part.object_id = col.object_id AND seg.column_id = col.column_id WHERE part.object_id = object_id('dbo.SalesOrder','U') AND seg.column_id = 1 ORDER BY seg.segment_id
row mode改成 batch mode
INNER JOIN使用Batch Mode而OUTER JOIN使用Row Mode -- Batch mode processing will be used when INNER JOIN SELECT at.Model ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.AutoType AS at INNER JOIN dbo.SalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY at.Model -- OUTER JOIN workaround ;WITH intermediateData AS ( SELECT at.AutoID ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.AutoType AS at INNER JOIN dbo.SalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY at.AutoID ) SELECT at.Model ,TotalAmount = ISNULL(itm.TotalAmount, 0) ,TotalQty = ISNULL(itm.TotalQty, 0) FROM dbo.AutoType AS at LEFT OUTER JOIN intermediateData AS itm ON itm.AutoID = at.AutoID ORDER BY itm.TotalAmount DESC ------------------------------------------------------------- IN & EXISTS使用Row Mode -- DEMO 2: IN & EXISTS both use row mode processing IF OBJECT_ID('dbo.HondaAutoTypes', 'U') IS NOT NULL BEGIN TRUNCATE TABLE dbo.HondaAutoTypes DROP TABLE dbo.HondaAutoTypes END SELECT * INTO dbo.HondaAutoTypes FROM dbo.AutoType WHERE make = 'Honda' -- IN use row mode SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.SalesOrder AS ord WHERE ord.AutoID IN(SELECT AutoID FROM dbo.HondaAutoTypes) GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ORDER BY 1 DESC -- EXISTS use row mode too. SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.SalesOrder AS ord WHERE EXISTS(SELECT TOP 1 * FROM dbo.HondaAutoTypes WHERE AutoID = ord.AutoID) GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ORDER BY 1 DESC -- IN & EXISTS workaround using INNER JOIN SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.SalesOrder AS ord INNER JOIN dbo.HondaAutoTypes AS hat ON ord.AutoID = hat.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ORDER BY 1 DESC -- or we also can use IN(<list of constants>) to make it use batch mode. SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.SalesOrder AS ord WHERE ord.AutoID IN(104,106) GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ORDER BY 1 DESC ------------------------------------------------------------- UNION ALL -- DEMO 3: UNION ALL usually use row mode IF OBJECT_ID('dbo.partSalesOrder', 'U') IS NOT NULL BEGIN TRUNCATE TABLE dbo.partSalesOrder DROP TABLE dbo.partSalesOrder END SELECT TOP 100 * INTO dbo.partSalesOrder FROM dbo.SalesOrder WHERE OrderID < 2500000; -- UNION ALL mostly use row mode ;WITH unionSalesOrder AS ( SELECT * FROM dbo.SalesOrder AS ord UNION ALL SELECT * FROM dbo.partSalesOrder AS pord ) SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.AutoType AS at INNER JOIN unionSalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ORDER BY 1 DESC -- UNION ALL workaround ;WITH unionSaleOrders AS( SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.AutoType AS at INNER JOIN SalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ), unionPartSaleOrders AS ( SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,TotalAmount = SUM(ord.UnitPrice) ,TotalQty = SUM(ord.OrderQty) FROM dbo.AutoType AS at INNER JOIN dbo.partSalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ), unionAllData AS ( SELECT * FROM unionSaleOrders UNION ALL SELECT * FROM unionPartSaleOrders ) SELECT OrderDay ,TotalAmount = SUM(TotalAmount) ,TotalQty = SUM(TotalQty) FROM unionAllData GROUP BY OrderDay ORDER BY OrderDay DESC ------------------------------------------------------------- Scalar Aggregates -- DEMO 4: Scalar Aggregates SELECT COUNT(*) FROM dbo.SalesOrder -- workaround ;WITH salesOrderByAutoId([AutoID], cnt) AS( SELECT [AutoID], count(*) FROM dbo.SalesOrder GROUP BY [AutoID] ) SELECT SUM(cnt) FROM salesOrderByAutoId -- END DEMO 4 ------------------------------------------------------------- Multiple DISTINCT Aggregates -- DEMO 5: Multiple DISTINCT Aggregates SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,AutoIdCount = COUNT(DISTINCT ord.[AutoID]) ,UserIdCount = COUNT(DISTINCT ord.[UserID]) FROM dbo.AutoType AS at INNER JOIN dbo.SalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) -- workaround ;WITH autoIdsCount(orderDay, AutoIdCount) AS( SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,AutoIdCount = COUNT(DISTINCT ord.[AutoID]) FROM dbo.AutoType AS at INNER JOIN dbo.SalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ), userIdsCount(orderDay, UserIdCount) AS( SELECT OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120) ,UserIdCount = COUNT(DISTINCT ord.[UserID]) FROM dbo.AutoType AS at INNER JOIN dbo.SalesOrder AS ord ON ord.AutoID = at.AutoID GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120) ) SELECT auto.orderDay ,auto.AutoIdCount ,ur.UserIdCount FROM autoIdsCount AS auto INNER JOIN userIdsCount AS ur ON auto.orderDay = ur.orderDay -- END DEMO 5
最後,若有不對的地方,歡迎你們拍磚哦o(∩_∩)o