Vừa thấy mấy tranh cãi về vụ bảng 200 cột với tách thành nhiều bảng (mình đoán chắc ý là kĩ thuậtvertical partition) rất hay.
Mình sẽ giải thích về vấn đề này. Lưu ý là những thứ mình sẽ nói ở đây là với SQL Server của Microsoft, có thể nó sẽ không đúng với những database khác.
Có 1 số database (thường là các database cho application), có khuynh hướng seek key (Tìm 1 dòng hoặc 1 số dòng nào đó bằng 1 ID. Đánh index trên 1 bảng để seek row bằng key đó). Với những bảng/index này thì sẽ là dạng rowstore.
Mà rowstore thì dù bạn có select 1, 2 cột trên 1 row thì khi đọc 1 row thì bạn cũng phải đọc hết 1 row mà chính xác là phải đọc hết 1 page chứa row đó (IO của SQL Server thực hiện ở page level). 1 page trong SQL Server database có thể chứa tối đa là 8KB. Nếu 1 row có độ lớn hơn 8kb thì phải có 1 pointer dẫn đến 1 page khác,
https://docs.microsoft.com/en-us/sq...ents-architecture-guide?view=sql-server-ver15
Nếu 1 để 1 row quá dài thì bạn sẽ phải cần nhiều hơn 1 page để lưu 1 row. Bạn phải đọc row chính rồi dùng pointer để tìm đến các phần data còn lại.
Nếu khi bạn seek mà kết quả trả về nhiều row (VD bạn đánh nonclustered index lên cột ngày của 1 bảng rồi bạn seek range theo ngày chẳng hạn), hoặc bạn thực hiện table/index scan. Thì page càng nhiều thì cost IO càng lớn. Bạn có select 1 cột thì disk IO (có thể check bằng SET Statistics IO ON) vẫn thế. (Vì nó phải đọc nguyên 1 page).
Như vậy, người ta có thể nghĩ đến việc dùng kĩ thuật vertical partitioning để tách 1 thực thể ra thành nhiều bảng. VD là Email đi, và bạn có 1 non clustered index theo ngày. Nếu bạn ko tách làm 2 thì khi bạn select EmailID, SenderID rồi where theo ngày, SQL Server khi read phải read toàn bộ page của các dòng email. Rồi sau đó chỉ nhặt ra mỗi EmailID và SenderID. Tưởng tượng IO sẽ khủng khiếp thế nào nếu email nào cũng có header và content trung bình dài cỡ chừng tổng cộng 1000 kí tự.
Thì có thể người ta sẽ tách. VD tiêu đề và nội dung sẽ ra 1 bảng và có thêm EmailID, VD bảng đó sẽ là EmailContent. Còn 1 bảng chỉ có các key thường được truy cập thì có 1 bảng riêng VD bảng đó sẽ là EmailGeneral. Hầu hết trên mọi trường hợp thì sẽ chỉ làm việc trên bảng EmailGeneral. Còn khi nào cần hiện nội dung email thì mới vô bảng EmailContent và dùng EmailID để seek.
Tất nhiên để khắc phục tình trạng này người ta có thể dùng Filtered Index. Tuy nhiên Filtered Index thì có thể sẽ ra tình trạng tăng dung lượng ổ cứng và thêm index thì rõ ràng sẽ làm việc insert/update vào bảng đó lâu hơn. (Do nó còn phải maintain các nonclustered index nữa.)
Tuy nhiên với 1 số mục đích database, nó lại có đặc thù là không cần seek key, mà lại là scan toàn bảng hoặc 1 partition, rồi filter và group by trên 1 số field nào đó. (VD như các database về các mục đích analytics).
Với các database kiểu này, thường người ta sẽ sử dụng columnstore. Với columnstore, thay vì phải đọc nguyên 1 row hay nguyên 1 page chứa row đó, data sẽ được tổ chức theo dạng column, khi câu query cần column nào, nó sẽ chỉ đọc các page liên quan đến column đó.
https://docs.microsoft.com/en-us/sq...nstore-indexes-overview?view=sql-server-ver15
Như vậy vấn đề row có lớn hay không giờ không quan trọng nữa. Tuy nhiên, do yêu cầu của query trên các database này thường ko phải là seek key mà lại lại filter aggregate.
Giả sử ta chia đơn hàng hành 2 bảng, là Order Header và OrderDetail.
- Bảng OrderHeader có OrderID, OrderDate, BuyerName
- Bảng OrderDetail có OrderDetailID, OrderID, ProductID, Amount
Giả sử ta có query là tìm ra top 10 Buyer theo Amount của những đơn hàng được thực hiện vào tháng 1 năm 2021. Bạn sẽ có query kiểu
SELECT TOP 10 BuyerName, SUM(Amount)
FROM OrderHeader
LEFT JOIN OrderDetail ON OrderHeader.OrderID = OrderDetail.OrderID
WHERE OrderDate> '2021-01-01' AND OrderDate < '2021-02-01'
GROUP BY BuyerName
Vấn đề cái trên chỉ là logical query. Còn SQL thực sự bên dưới sẽ phải: (Giả sử là columnstore index, không có partition)
- Quét toàn bộ (columnstore index scan) bảng Order, lấy ra tất cả các row có orderdate đạt chuẩn, project ra column BuyerName và OrderID. Số lượng row có sẽ giảm nhưng cũng vẫn sẽ lớn vì số đơn hàng trong 1 tháng không thể ít.
- Quét Lấy toàn bộ (columnstore index scan) bảng Detail. (project ra column OrderID, Amount). Bảng có bao nhiêu row là ra hết bấy nhiêu. (Vì có filter bớt được cái gì đi đâu, do điều kiện filter là OrderDate chỉ có ở bảng Header chứ ko có ở bảng detail). SQL Server có thể áp dụng Bitmap operator để giảm số lượng row trước khi vào Hash join operator, tuy nhiên bitmap chỉ xuất hiện khi query chạy parallel và tất nhiên vẫn sẽ tốn kém resource.
- Join 2 bảng. Physical join operator là giả sử là hash join (trường hợp này thường sẽ là hash join vì nhanh nhât O(n) so với nested loop (O(n^2) - Ở đây ko có rowstore index có sort nên không thể là nested loop - index seek O(nlogn). Còn merge join thì lại chậm hơn hash (O(nlog(n))))). Thì hash join mọi người cũng biết có độ phức tạp là O(n). Nghĩa là 2 bảng càng lớn thì join càng khủng. Ngoài ra hash có space complexity là O(n). Khi hash join thì 1 trong 2 bảng sẽ được chọn ra để build hash table. Thường thì SQL Server sẽ thông minh và sẽ chọn bảng nhỏ hơn. Nhưng vấn đề là khi vertitcal partition hay kiểu bảng cha con Order-Detail thì 2 bên cái nào cũng lớn cả. => Xài rất nhiều ram -> Spill - Nguyên nhân chủ yếu nhất khiến 1 câu query chạy cực chậm.
- Join xong lấy đủ các data cần thiết (BuyerName từ bảng Header và Amount từ bảng Detail) rồi thì mới thực hiện aggregate (thường là hash aggregate vì nhanh hơn 0(n) so với Stream Aggregate (cần sort, sort thì chậm O(nlogn)).
Kinh nghiệm của mình khi làm việc với database, query cực chậm chậm thường chỉ có 2 nguyên nhân spill ram vì sort/hash (được tạo ra từ join (nghĩa là bao gồm cả In và Exists, vì thực tế 2 thằng này là semi và anti join), window function, aggregate) và naive nested loop O(n^2) (gây ra từ inequal join hoặc SQL Server chọn nhầm plan ngu) với cả inner và outer đều cực lớn

. Nested loop thường ko có spill, vì space complexity chỉ là O(1), vấn đề là nó cả inner với outer chỉ cần cỡ 1.000.000 row ở 1 mỗi bảng thì đảm bảo là nó chạy đến tết. (1.000.000x1.000.000 = 1000 tỉ thì phải)
Thế nên là với mấy database dạng analytics datawarehouse gì đó thì người ta de-normalize xuống 1 bảng vài trăm cột là vì vậy. Và với database application người ta dùng vertical partitioning hay bảng cha con kiểu Header-Detail cũng là vì vậy.
Mình hiện ko có database có thể chạy được columnstore để test nên ko demo execution plan được. Mong các bạn thông cảm