Grup başına n satır alınıyor


88

Sonuç setinde her gruptan birkaç satır seçmem gerekiyor.

Örneğin, müşteri başına 'n' en yüksek veya en düşük son sipariş değerlerini listelemek isteyebilirim.

Daha karmaşık durumlarda, listelenecek satırların sayısı grup başına değişebilir (gruplandırma / ana kaydın bir niteliği ile tanımlanır). Bu bölüm kesinlikle isteğe bağlıdır / fazladan kredi için ve insanları yanıtlamaktan caydırmak için tasarlanmamıştır.

SQL Server 2005 ve sonraki sürümlerinde bu tür sorunları çözmek için ana seçenekler nelerdir? Her yöntemin ana avantajları ve dezavantajları nelerdir?

AdventureWorks örnekleri (netlik için, isteğe bağlı)

  1. En son beş işlem tarihini ve TransactionHistoryM'den R'ye bir harf ile başlayan her ürün için tablodaki kimlikleri listele .
  2. Yine aynı, ancak nürün başına geçmiş çizgileri ile n, DaysToManufactureÜrün özelliğinin beş katıdır .
  3. Aynı şekilde, ürün başına tam bir geçmiş çizgisinin gerekli olduğu özel durum için (en son yapılan tek giriş TransactionDate, beraberlik TransactionID.

Yanıtlar:


70

Temel senaryo ile başlayalım.

Tablodaki bazı satırları almak istersem iki ana seçeneğim var: sıralama işlevleri; veya TOP.

Öncelikle, tüm kümeyi Production.TransactionHistorybelirli bir amaç için düşünelim ProductID:

SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;

Bu, 418 satır döndürür ve plan, tablodaki tüm satırları, bunun için filtreyi sağlamak için bir Öngörü ile sınırlandırılmamış Kümelenmiş Dizin Taraması'nı kontrol ettiğini gösterir. 797, burada okur, bu çirkin.

'Artık' ile Pahalı Tarama Tahmini

Öyleyse ona karşı adil olalım ve daha faydalı olacak bir dizin oluşturalım. Koşullarımız eşitlik eşleşmesi ve ProductIDsonrasında en sonuncular için yapılan bir arama yapılması çağrısında bulunuyor TransactionDate. Biz gerek TransactionIDde döndü, o yüzden birlikte gidelim: CREATE INDEX ix_FindingMostRecent ON Production.TransactionHistory (ProductID, TransactionDate) INCLUDE (TransactionID);.

Bunu yaptıktan sonra planımız önemli ölçüde değişiyor ve okurları sadece 3'e düşürüyor. Bu yüzden zaten 250x'in üzerinde bir şeyi geliştiriyoruz ...

Geliştirilmiş plan

Şimdi oyun alanını seviyeledik, en üstteki seçeneklere bakalım - sıralama fonksiyonları ve TOP.

WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
)
SELECT TransactionID, ProductID, TransactionDate
FROM Numbered
WHERE RowNum <= 5;

SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
ORDER BY TransactionDate DESC;

İki plan - temel TOP \ RowNum

Second ( TOP) sorgusunun hem sorguda hem de planda birinciden çok daha basit olduğunu fark edeceksiniz . Fakat çok anlamlı bir şekilde, her ikisi TOPde dizinden çekilmekte olan satır sayısını sınırlamak için kullanılır . Maliyetler yalnızca tahmindir ve görmezden gelmeye değerdir, ancak iki plan arasında çok fazla benzerlik görülebilir, ROW_NUMBER()sürüm sayıları atamak ve buna göre filtrelemek için küçük bir miktar fazladan çalışma yapar ve her iki sorgu da sadece 2 okuma yapar onların işleri. Query Optimizer, bir ROW_NUMBER()alanda filtreleme fikrini kesinlikle kabul eder ve ihtiyaç duyulmayacak satırları yoksaymak için bir Top işleci kullanabileceğini fark eder. Her iki sorgu da yeterince iyi - TOPkod değiştirmeye değecek kadar iyi değil, ama yeni başlayanlar için daha basit ve muhtemelen daha net.

Yani bu tek bir üründe çalışır. Ancak bunu birden fazla üründe yapmamız gerekirse ne olacağını düşünmemiz gerekir.

Yinelemeli programcı, ilgilenilen ürünler arasında döngü oluşturma ve bu sorguyu birçok kez arama fikrini göz önüne alacaktır ve aslında bu formda bir sorgu yazmaktan kurtulabiliriz - imleç kullanarak değil, kullanarak APPLY. Ben kullanıyorum OUTER APPLY, bunun için hiçbir İşlemler varsa, NULL ile Ürünü dönmek isteyebilirsiniz endam.

SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM 
Production.Product p
OUTER APPLY (
    SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
    FROM Production.TransactionHistory h
    WHERE h.ProductID = p.ProductID
    ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';

Bunun planı yinelemeli programcıların yöntemidir - Yuvalanmış Döngü, Her Ürün için bir En İyi İşlem ve Arama (daha önce okuduğumuz 2 okuma). Bu, Ürüne karşı 4 okuma ve TransactionHistory'ye karşı 360 verir.

UYGULAMA planı

Kullanılması ROW_NUMBER(), yöntem kullanmaktır PARTITION BYiçinde OVERher Ürün için numaralandırma yeniden böylece maddesi. Bu daha sonra olduğu gibi filtre edilebilir. Plan oldukça farklı hale geldi. Mantıksal okumalar TransactionHistory'de yaklaşık% 15 daha düşüktür ve tam İndeks Taraması satırları almaya devam eder.

WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;

ROW_NUMBER plan

Yine de, bu planın pahalı bir Sort işletmecisi var. Birleştirme Birleştirmesi TransactionHistory'deki satırların sırasını koruyor gibi görünmüyor, kabadayıları bulmak için verilerin yeniden gönderilmesi gerekiyor. Daha az okunuyor, ancak bu engelleme Sıralaması acı verici olabilir. Kullanarak APPLY, Yuvalanmış Döngü ilk satırları çok hızlı bir şekilde döndürür, sadece birkaç okuduktan sonra, ancak Sırala ile ROW_NUMBER()yalnızca işlerin çoğu bittikten sonra satırları döndürür.

İlginçtir, ROW_NUMBER()sorgu INNER JOINyerine kullanırsa LEFT JOIN, farklı bir plan ortaya çıkar.

INNER JOIN ile ROW_NUMBER ()

Bu planda olduğu gibi İç İçe Döngü kullanılır APPLY. Ancak Top operatörü yoktur, bu nedenle her ürün için tüm işlemleri çeker ve öncekinden çok daha fazla okuma kullanır - 492 TransactionHistory'a karşı okur. Burada Birleştirme Birleştir seçeneğini seçmemek için iyi bir neden yok, bu nedenle planın 'Yeterince İyi' olarak kabul edildiğini düşünüyorum. Yine de - engellemiyor, ki bu güzel - sadece kadar güzel değil APPLY.

PARTITION BYBen için kullanılan sütun ROW_NUMBER()oldu h.ProductIDben qo Ürün masaya katılmadan önce ROWNUM değer üretme seçeneği vermek istediğini çünkü, her iki durumda da. Kullanırsam p.ProductID, INNER JOINvaryasyonla aynı şekil planını görüyoruz .

WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY p.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;

Ancak Join operatörü 'Inner Join' yerine 'Left Outer Join' diyor. TransactionHistory tablosuna karşı okumaların sayısı hala 500'ün altında.

BÖLÜM İKİ h.ProductID yerine p.ProductID'de

Her neyse - eldeki soruya dönelim ...

Soru 1'i seçip seçebileceğiniz iki seçenekle cevapladık . Şahsen ben APPLYseçeneği seviyorum .

Bunu değişken bir sayı kullanacak şekilde genişletmek için ( soru 2 ), 5sadece uygun şekilde değiştirilmesi gerekir. Oh, ve ben başka bir indeks ekledim, böylece sütunu Production.Product.Nameiçeren bir indeks vardı DaysToManufacture.

WITH Numbered AS
(
SELECT p.Name, p.ProductID, p.DaysToManufacture, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5 * DaysToManufacture;

SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM 
Production.Product p
OUTER APPLY (
    SELECT TOP (5 * p.DaysToManufacture) h.TransactionID, h.ProductID, h.TransactionDate
    FROM Production.TransactionHistory h
    WHERE h.ProductID = p.ProductID
    ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';

Ve her iki plan da öncekilerle neredeyse aynı!

Değişken satırlar

Yine, tahmini maliyetleri göz ardı edin - ama yine de ÜST senaryoyu seviyorum, çünkü çok daha basit ve planın engelleyici bir operatörü yok. Okumak, işlem sırasındaki yüksek sıfır sayısından dolayı TransactionHistory'de daha az DaysToManufacture, ancak gerçek hayatta bu sütunu seçeceğimizden şüpheliyim. ;)

Bloktan kaçınmanın bir yolu ROW_NUMBER(), birleşimin sağındaki (plandaki) ucu işleyen bir plan bulmaktır. Bunu CTE dışındaki birleşme yaparak ikna edebiliriz.

WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
)
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM Production.Product p
LEFT JOIN Numbered t ON t.ProductID = p.ProductID
    AND t.RowNum <= 5 * p.DaysToManufacture
WHERE p.Name >= 'M' AND p.Name < 'S';

Buradaki plan daha basit görünüyor - engelleme değil, ancak gizli bir tehlike var.

CTE dışına katılmak

Ürün tablosundan veri çeken Hesaplama Skalerine dikkat edin. Bu 5 * p.DaysToManufacturedeğer çalışıyor . Bu değer TransactionHistory tablosundan veri çeken şubeye aktarılmıyor, Birleştirme Birleşmesinde kullanılıyor. Kalıntı olarak.

Sinsi Artık!

Bu yüzden Birleştirme Birleşmesi, TÜM satırları tüketiyor, yalnızca ihtiyaç duyulan ilk kişi olmakla kalmıyor, hepsini de ve ardından artık bir kontrol yapıyor. İşlem sayısı arttıkça bu tehlikelidir. Bu senaryonun hayranı değilim - Merge Joins'deki artık tahminler hızla yükselebilir. APPLY/TOPSenaryoyu tercih etmemin bir diğer nedeni .

Tam olarak bir satırın olduğu özel durumda, soru 3 için , açıkça aynı sorguları kullanabiliriz, 1bunun yerine 5. Fakat daha sonra düzenli toplama araçlarını kullanmak için fazladan bir seçeneğimiz var.

SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;

Bunun gibi bir sorgulama yararlı bir başlangıç ​​olacaktır ve İşlem Kimliği'ni koparma amacıyla (daha sonra kırılacak olan bir birleştirme kullanarak) çıkarması için kolayca değiştirebiliriz, ancak tüm dizine bakarız veya Ürüne ürün olarak dalıyoruz ve bu senaryoda daha önce sahip olduklarımız üzerinde gerçekten büyük bir iyileşme elde etmiyoruz.

Fakat burada belirli bir senaryoya baktığımıza dikkat etmeliyim. Gerçek verilerle ve ideal olmayabilecek bir endeksleme stratejisiyle, kilometre oldukça değişebilir. APPLYBurada güçlü olduğunu gördüğümüz gerçeğine rağmen, bazı durumlarda daha yavaş olabilir. Ancak, nadiren engeller, çünkü birçok insanın (kendim dahil) çok çekici bulduğu Yuvalanmış Döngüler kullanma eğilimi vardır.

Burada paralelliği keşfetmeyi denemedim ya da insanların bir araya gelip bölmenin karmaşıklığına dayanarak nadiren istedikleri özel bir durum olarak gördüğüm 3. soruya çok daldım. Burada dikkate alınması gereken en önemli şey, bu iki seçeneğin her ikisinin de çok güçlü olmasıdır.

Ben tercih APPLY. Açıktır, Top operatörünü iyi kullanır ve nadiren engellemeye neden olur.


44

Bunu SQL Server 2005 ve daha yeni sürümlerde yapmanın tipik yolu CTE ve pencereleme işlevlerini kullanmaktır. Grup başına üstteki n için sadece ROW_NUMBER()bir PARTITIONcümle ile kullanabilir ve dış sorguda buna karşı süzebilirsiniz. Örneğin, müşteri başına en son 5 sipariş şu şekilde gösterilebilir:

DECLARE @top INT;
SET @top = 5;

;WITH grp AS 
(
   SELECT CustomerID, OrderID, OrderDate,
     rn = ROW_NUMBER() OVER
     (PARTITION BY CustomerID ORDER BY OrderDate DESC)
   FROM dbo.Orders
)
SELECT CustomerID, OrderID, OrderDate
  FROM grp
  WHERE rn <= @top
  ORDER BY CustomerID, OrderDate DESC;

Bunu ayrıca yapabilirsiniz CROSS APPLY:

DECLARE @top INT;
SET @top = 5;

SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY 
(
    SELECT TOP (@top) OrderID, OrderDate 
    FROM dbo.Orders AS o
    WHERE CustomerID = c.CustomerID
    ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;

Ek Paul seçeneği ile birlikte, Müşteriler tablosunun müşteri başına kaç satır ekleneceğini belirten bir sütunu olduğunu söyleyin:

;WITH grp AS 
(
   SELECT CustomerID, OrderID, OrderDate,
     rn = ROW_NUMBER() OVER
     (PARTITION BY CustomerID ORDER BY OrderDate DESC)
   FROM dbo.Orders
)
SELECT c.CustomerID, grp.OrderID, grp.OrderDate
  FROM grp 
  INNER JOIN dbo.Customers AS c
  ON grp.CustomerID = c.CustomerID
  AND grp.rn <= c.Number_of_Recent_Orders_to_Show
  ORDER BY c.CustomerID, grp.OrderDate DESC;

Ve yine, CROSS APPLYbir müşterinin satır sayısının müşteriler tablosundaki bazı sütunlar tarafından dikte edilmesi için eklenmiş seçeneklerin kullanılması ve dahil edilmesi:

SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY 
(
    SELECT TOP (c.Number_of_Recent_Orders_to_Show) OrderID, OrderDate 
    FROM dbo.Orders AS o
    WHERE CustomerID = c.CustomerID
    ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;

Bunların veri dağılımına ve destekleyici dizinlerin kullanılabilirliğine bağlı olarak farklı performans göstereceğini unutmayın; bu nedenle performansı optimize etmek ve en iyi planı elde etmek gerçekten yerel faktörlere bağlı olacaktır.

Şahsen, mantığı daha iyi ayırdıkları ve benim için daha sezgisel oldukları için CROSS APPLY/ üzerinde CTE ve pencereleme çözümlerini tercih ediyorum TOP. Genel olarak (hem bu durumda hem de benim genel deneyimimde), CTE yaklaşımı daha verimli planlar üretmektedir (aşağıdaki örnekler), ancak bu evrensel bir gerçek olarak görülmemelidir - senaryolarınızı her zaman, özellikle endeksler değiştiyse veya veri kayda değer bir şekilde eğildi.


AdventureWorks örnekleri - değişiklik yapmadan

  1. En son beş işlem tarihini ve TransactionHistoryM'den R'ye bir harf ile başlayan her ürün için tablodaki kimlikleri listele .
-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn <= 5;

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (5) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

Bu ikisinin çalışma zamanı ölçümlerinde karşılaştırılması:

görüntü tanımını buraya girin

CTE / OVER()plan:

görüntü tanımını buraya girin

CROSS APPLY plan:

görüntü tanımını buraya girin

CTE planı daha karmaşık görünüyor, ancak aslında çok daha verimli. Tahmini maliyet% rakamlarına çok az dikkat edin, ancak daha az okuma ve çok daha düşük bir süre gibi daha önemli gerçek gözlemlere odaklanın . Bunları paralellik olmadan da yaptım ve bu fark değildi. Çalışma zamanı metrikleri ve CTE planı ( CROSS APPLYplan aynı kaldı):

görüntü tanımını buraya girin

görüntü tanımını buraya girin

  1. Yine aynı, ancak nürün başına geçmiş çizgileri ile n, DaysToManufactureÜrün özelliğinin beş katıdır .

Burada çok küçük değişiklikler yapılması gerekiyor. CTE için iç sorguya bir sütun ekleyebilir ve dış sorguya filtre uygulayabiliriz; için, CROSS APPLYkorelasyon içinde hesaplamayı yapabiliriz TOP. Bunun CROSS APPLYçözüme biraz verim katacağını düşünebilirsiniz , ancak bu durumda olmaz. Sorguları:

-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, p.DaysToManufacture, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn <= (5 * DaysToManufacture);

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (5 * p.DaysToManufacture) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

Çalışma zamanı sonuçları:

görüntü tanımını buraya girin

Paralel CTE / OVER()plan:

görüntü tanımını buraya girin

Tek iş parçacıklı CTE / OVER()plan:

görüntü tanımını buraya girin

CROSS APPLY plan:

görüntü tanımını buraya girin

  1. Aynı şekilde, ürün başına tam bir geçmiş çizgisinin gerekli olduğu özel durum için (en son yapılan tek giriş TransactionDate, beraberlik TransactionID.

Yine, burada küçük değişiklikler. CTE çözümde, biz eklemek TransactionIDiçin OVER()fıkra ve dış filtreyi değiştirmek rn = 1. İçin CROSS APPLY, biz değiştirmek TOPiçin TOP (1), ve eklemek TransactionIDiç için ORDER BY.

-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC, TransactionID DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn = 1;

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (1) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC, TransactionID DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

Çalışma zamanı sonuçları:

görüntü tanımını buraya girin

Paralel CTE / OVER()plan:

görüntü tanımını buraya girin

Tek iş parçacıklı CTE / OVER () planı:

görüntü tanımını buraya girin

CROSS APPLY plan:

görüntü tanımını buraya girin

Pencereleme işlevleri her zaman en iyi alternatif değildir (devam etme şansı vardır COUNT(*) OVER()) ve bunlar grup problemi başına n satırı çözme konusundaki tek iki yaklaşım değildir, ancak bu özel durumda - şema, mevcut indeksler ve veri dağılımı göz önüne alındığında - CTE anlamlı hesaplar tarafından daha iyi çalıştı.


AdventureWorks örnekleri - dizin ekleme esnekliği

Ancak, bir destekleyici dizin eklerseniz , bir yorumda belirtilen Paul’a benzer, ancak 2. ve 3. sütunlar sipariş edilmişse DESC:

CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory 
  (ProductID, TransactionDate DESC, TransactionID DESC);

Aslında her yerde çok daha uygun planlar elde edersiniz ve ölçütler bu CROSS APPLYüç durumda da yaklaşımı lehine çevirir:

görüntü tanımını buraya girin

Eğer bu benim üretim ortamım olsaydı, muhtemelen bu durumun süresinden memnun olurdum ve daha fazla optimize etmek için uğraşmazdım.


Bunların hepsi, SQL Server 2000'de desteklenmeyen APPLYya da OVER()yan tümce çok daha çirkindi .


24

DBMS'de, MySQL gibi, pencere işlevi olmayan veya CROSS APPLYbunu yapmanın yolu standart SQL kullanmaktır (89). Yavaş yol, agrega ile üçgen bir çapraz birleşme olacaktır. Daha hızlı yolu (ama yine de ve çapraz uygulamak veya satırnum işlevini kullanarak muhtemelen kadar verimli değildir) ben dediğimiz olurdu "yoksul adamın CROSS APPLY" . Bu sorguyu diğerleriyle karşılaştırmak ilginç olurdu:

Varsayım: Orders (CustomerID, OrderDate)bir UNIQUEkısıtlaması vardır:

DECLARE @top INT;
SET @top = 5;

SELECT o.CustomerID, o.OrderID, o.OrderDate
  FROM dbo.Customers AS c
    JOIN dbo.Orders AS o
      ON  o.CustomerID = c.CustomerID
      AND o.OrderID IN
          ( SELECT TOP (@top) oi.OrderID
            FROM dbo.Orders AS oi
            WHERE oi.CustomerID = c.CustomerID
            ORDER BY oi.OrderDate DESC
          )
  ORDER BY CustomerID, OrderDate DESC ;

Grup başına özelleştirilmiş üst satırların ek sorunu için:

SELECT o.CustomerID, o.OrderID, o.OrderDate
  FROM dbo.Customers AS c
    JOIN dbo.Orders AS o
      ON  o.CustomerID = c.CustomerID
      AND o.OrderID IN
          ( SELECT TOP (c.Number_of_Recent_Orders_to_Show) oi.OrderID
            FROM dbo.Orders AS oi
            WHERE oi.CustomerID = c.CustomerID
            ORDER BY oi.OrderDate DESC
          )
  ORDER BY CustomerID, OrderDate DESC ;

Not: MySQL'de AND o.OrderID IN (SELECT TOP(@top) oi.OrderID ...)bir tane kullanmak yerine AND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1)). SQL Server FETCH / OFFSET, 2012 sürümünde sözdizimi ekledi . Buradaki sorgular IN (TOP...)önceki sürümlerle çalışmak üzere ayarlandı .


21

Seçeneklerin iyi olması doğru, çünkü bu tekniğin diğerleriyle nasıl karşılaştırılacağını görmek için biraz farklı bir yaklaşım izledim.

Test

Neden sadece çeşitli yöntemlerin birbirleriyle nasıl istiflendiğine bakarak başlamıyoruz. Üç takım test yaptım:

  1. İlk sette DB değişiklik yapılmadı
  2. İkinci set, TransactionDatekarşı gelen sorguları desteklemek için bir endeks oluşturulduktan sonra koştu Production.TransactionHistory.
  3. Üçüncü set, biraz farklı bir varsayımda bulundu. Üç testin tümü aynı Ürün listesine karşı çıktığından, bu listeyi önbelleğe alırsak ne olur? Benim yöntemim bellek içi bir önbellek, diğer yöntemler ise eşdeğer bir geçici tablo kullanıyordu. İkinci test grubu için oluşturulan destek endeksi bu test grubu için hala mevcuttur.

Ek test detayları:

  • Testler AdventureWorks2012, SQL Server 2012, SP2'de (Geliştirici Sürümü) yapıldı.
  • Her test için sorgusunu kimin yanıtını aldığımı ve hangi sorgunun olduğunu etiketledim.
  • Query Options'ın "Yürütmeden sonra sonuçları sil" seçeneğini kullandım. Sonuçlar.
  • Lütfen ilk iki test kümesi RowCountsiçin benim yöntemim için "kapalı" göründüğünü unutmayın . Bunun sebebi benim yöntemimin ne CROSS APPLYyaptığının manuel bir uygulaması olması : İlk sorguyu karşı çalıştırır Production.Productve daha sonra karşı sorgularda kullandığı 161 satır geri alır Production.TransactionHistory. Bu nedenle, RowCountgirişlerimin değerleri her zaman diğer girişlerden 161 daha fazladır. Üçüncü test setinde (önbellekleme ile) satır sayıları tüm yöntemler için aynıdır.
  • Yürütme planlarına güvenmek yerine istatistikleri yakalamak için SQL Server Profilcisi kullandım. Aaron ve Mikael zaten sorgularının planlarını gösteren harika bir iş çıkardılar ve bu bilgiyi tekrarlamaya gerek yoktu. Ve yöntemimin amacı, sorguları gerçekten önemli olmayacağı kadar basit bir forma indirmektir. Profiler'ı kullanmak için ek bir neden var, ancak daha sonra bahsedilecek.
  • Yapıyı kullanmak yerine, kullanmayı Name >= N'M' AND Name < N'S'seçtim Name LIKE N'[M-R]%'ve SQL Server onlara aynı şekilde davranıyor.

Sonuçlar

Destekleyici Dizin Yok

Bu, esasen kullanıma hazır AdventureWorks2012'dir. Her durumda benim yöntemim açıkça diğerlerinden daha iyi, ancak hiçbir zaman ilk 1 veya 2 yöntem kadar iyi değil.

Test 1 Test 1 Sonuçları-indekssiz
Aaron's CTE açıkça burada kazanmıştır.

Test 2 Test 2 Sonuçları - indekssiz
Aaron'un CTE (tekrar) ve Mikael'in ikinci apply row_number()yöntemi yakın bir ikinci.

Test 3 Test 3 Sonuçları - indekssiz
Aaron's CTE (yine) kazanır.

Sonuç
Destekleyici bir indeks bulunmadığında TransactionDate, yöntemim standart yapmaktan daha iyidir CROSS APPLY, ancak yine de, CTE yöntemini kullanmak açık bir yoldur.

Destek Endeksli (Önbellek Yok)

Bu testler TransactionHistory.TransactionDateiçin, bu alandaki tüm sorguların sıralanmasından bu yana bariz indeksi ekledim . "Açık" diyorum, çünkü çoğu cevap bu noktada hemfikir. Sorguların tümü en son tarihleri ​​istediği için, TransactionDatealan sipariş edilmeli DESC, bu yüzden CREATE INDEXMikael'in cevabının altındaki ifadeyi aldım ve açık bir şekilde şunu ekledim FILLFACTOR:

CREATE INDEX [IX_TransactionHistoryX]
    ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
    WITH (FILLFACTOR = 100);

Bu dizin bir kez yerleştiğinde, sonuçlar biraz değişir.

Test 1 Test 1 Sonuçları - destek dizini ile
Bu kez, en azından Mantıksal Okumalar açısından öne çıkan yöntemim. Daha CROSS APPLYönce Test 1 için en kötü performans gösteren yöntem, Süre kazanır ve hatta Mantıksal Okumalar'daki CTE yöntemini yener.

Test 2 Test 2 Sonuçları - destek endeksi ile
Bu kez Mikael'in Reads'e apply row_number()bakarken kazanan ilk yöntem olduğu, oysa daha önce en kötü performans gösterenlerden biriydi. Ve şimdi yöntemim, Reads'a bakarken çok yakın bir yerde geliyor. Aslında, CTE yönteminin dışında kalanların hepsi Okumalar açısından oldukça yakındır.

Test 3 Test 3 Sonuçları - destek endeksi ile
Burada CTE hala kazanıyor, ancak şimdi diğer yöntemler arasındaki fark, endeksi oluşturmadan önce varolan şiddetli farkla karşılaştırıldığında zar zor farkediliyor.

Sonuç
Benim yöntemimin uygulanabilirliği şimdi daha belirgindir, ancak yerinde uygun indekslerin bulunmaması daha az dirençlidir.

Destek Dizini ve Önbelleğe Alma ile

Bu testler için önbelleklemeyi kullandım, çünkü neden olmasın ki? Benim yöntemim, diğer yöntemlerin erişemediği bellek içi önbelleklemeyi kullanmaya izin veriyor. Dürüst olmak gerekirse, Product.Productüç testin tamamında bu diğer yöntemlerde tüm referansların yerine kullanılan aşağıdaki geçici tabloyu oluşturdum . DaysToManufactureAlan sadece Testi Number 2'de kullanılan, ancak SQL komut dosyaları aynı tabloyu kullanmak tutarlı olmak daha kolaydı ve orada ona sahip olmak incitmedi edilir.

CREATE TABLE #Products
(
    ProductID INT NOT NULL PRIMARY KEY,
    Name NVARCHAR(50) NOT NULL,
    DaysToManufacture INT NOT NULL
);

INSERT INTO #Products (ProductID, Name, DaysToManufacture)
    SELECT  p.ProductID, p.Name, p.DaysToManufacture
    FROM    Production.Product p
    WHERE   p.Name >= N'M' AND p.Name < N'S'
    AND    EXISTS (
                    SELECT  *
                    FROM    Production.TransactionHistory th
                    WHERE   th.ProductID = p.ProductID
                );

ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);

Sınama 1 Test 1 Sonuçları - Destek dizini ve önbelleğe alma ile
Tüm yöntemler önbelleğe alma işleminden eşit ölçüde yarar görüyor ve benim yöntemim hala ön plana çıkıyor.

Test 2 Test 2 Sonuçları - destekleyici dizin ve önbelleğe alma ile
Burada, yöntemim henüz öne çıkmadığında, dizilişe göre bir fark görüyoruz, sadece 2 Mikael'in ilk apply row_number()yönteminden daha iyi , oysa önbellek olmadan yöntemim 4 Okuyucunun arkasında kaldı.

Test 3 Test 3 Sonuçları - destekleyici dizin ve önbelleğe alma ile
Lütfen aşağıya doğru güncellemeye bakınız (satırın altında) . Burada yine bazı farklılıklar görüyoruz. Yöntemimin "parametreleştirilmiş" lezzeti, şimdi Aaron'un CROSS APPLY yöntemine kıyasla 2 Okuma ile önderlik ediyor (önbellekleme olmadan eşit değildi). Ancak gerçekten garip olan şey, ilk defa önbellekten olumsuz etkilenen bir yöntem görmemiz: Aaron'un CTE yöntemi (önceden Test Numarası 3 için en iyisiydi). Ancak, ödenmediği durumlarda kredi almayacağım ve önbelleğe alma Aaron'ın CTE yöntemi hala benim yöntemim burada önbelleğe alma ile daha hızlı olduğundan, bu özel durum için en iyi yaklaşım Aaron CTE yöntemi gibi görünüyor.

Sonuç Lütfen aşağıdan yukarıya doğru (satırın altındaki) güncellemeye bakın.
İkincil bir sorgunun sonuçlarını tekrarlanan şekilde kullanan durumlar, genellikle bu sonuçları önbelleğe almaktan yararlanabilir. Ancak, önbelleğe alma bir avantaj olduğunda, söz konusu önbelleğe alma için belleği kullanmak, geçici tabloları kullanmaktan bazı avantajlar sağlar.

Yöntem

genellikle

"Header" sorgusunu (yani ProductIDs'yi ve bir durumda da, belirli harflerle başlayanları DaysToManufacturetemel alarak Name) "detay" sorgularından (yani TransactionIDs ve s'lerini almak TransactionDate) ayırdım. Buradaki konsept çok basit sorgular yapmaktı ve optimizer’in KATILDIĞINDA kafasını karıştırmasına izin vermemek. Açıkçası, bu her zaman avantajlı değildir , çünkü optimize ediciyi optimizasyondan da devre dışı bırakır. Ancak sonuçlarda gördüğümüz gibi, sorgu türüne bağlı olarak, bu yöntemin yararları vardır.

Bu yöntemin çeşitli tatları arasındaki fark:

  • Sabitler: Değiştirilebilir değerleri, parametre olmak yerine satır içi sabitler olarak gönderin. Bu, ProductIDüç testin hepsinde ve aynı zamanda " DaysToManufactureÜrün özelliğinin beş katı" işlevi olarak Test 2'ye döndürülecek satır sayısını ifade eder . Bu alt yöntem, her birinin ProductIDveri dağıtımında geniş bir değişiklik olması durumunda faydalı olabilecek kendi yürütme planına sahip olacağı anlamına gelir ProductID. Ancak veri dağılımında çok az değişiklik olursa, ek planlar oluşturma maliyeti buna değmeyecektir.

  • Parametreli: En az Gönder ProductIDolarak @ProductIDyürütme planı önbelleğe alma ve yeniden kullanım için izin. Test 2 için parametre olarak döndürülecek değişken satır sayısını da tedavi etmek için ek bir test seçeneği vardır.

  • Bilinmeyen Optimize: başvururken ProductIDolarak @ProductID, veri dağıtım geniş varyasyon varsa o zaman diğer olumsuz etkisi vardır bir plan önbelleğe mümkündür ProductIDSorgu İpucu kullanarak herhangi yardımcı olup olmadığını bilmek iyi olurdu böylece değerler.

  • Önbellek Ürünleri:Production.Product Tabloyu her seferinde sorgulamak yerine , yalnızca tam olarak aynı listeyi elde etmek için sorguyu bir kez çalıştırın (ve bizdeyken ProductID, TransactionHistorymasada bile olmayan herhangi bir öğeyi filtreleyin, böylece hiçbirini israf etmeyin) kaynakları var) ve bu listeyi önbelleğe al. Liste DaysToManufacturealanı içermelidir . Bu seçeneği kullanarak, ilk yürütme için Mantıksal Okumalara biraz daha yüksek bir başlangıç ​​vuruşu gelir, ancak bundan sonra yalnızca TransactionHistorysorgulanan tablo olur.

özellikle

Tamam, ama öyleyse, tüm alt sorguları CURSOR kullanmadan ve her bir sonuç kümesini geçici tabloya veya tablo değişkenine dökmeden ayrı sorgular olarak düzenlemek nasıl mümkün olabilir? Açıkça CURSOR / Temp Table yönteminin yapılması, Açıkça Okunanlar ve Yazma bölümlerine oldukça açık bir şekilde yansıyacaktır. Peki, SQLCLR :) kullanarak. Bir SQLCLR saklı yordamı oluşturarak, bir sonuç kümesini açabildim ve esas olarak her bir alt sorgunun sonuçlarını sürekli bir sonuç kümesi (ve birden fazla sonuç kümesi olarak değil) olarak yayınladım. Ürün bilgilerinin dışında (yani ProductID, NameveDaysToManufacture), alt sorgu sonuçlarından hiçbiri herhangi bir yerde (bellek veya disk) saklanmak zorunda değildi ve SQLCLR saklı yordamının ana sonuç kümesi olarak geçti. Bu, Ürün bilgisini almak için basit bir sorgu yapmamı ve daha sonra da sorgulama yapmamı sağladı TransactionHistory.

Ve bu yüzden istatistikleri yakalamak için SQL Server Profiler kullanmak zorunda kaldım. SQLCLR saklı yordamı, "Gerçek Yürütme Planını Dahil Et" Sorgu Seçeneği ayarlayarak veya yayınlayarak bir yürütme planı döndürmedi SET STATISTICS XML ON;.

Ürün Bilgisi önbelleğe almak için, readonly staticGenel Bir Liste kullandım (örneğin _GlobalProducts, aşağıdaki kodda). Koleksiyonlarına eklemek ihlal etmediğini görünüyor readonlymontaj bir sahip olduğunda dolayısıyla bu kod çalışır seçeneği PERMISSON_SETait SAFEolduğunu sezgilere bile :).

Üretilen Sorgular

Bu SQLCLR saklı yordam tarafından üretilen sorgular aşağıdaki gibidir:

Ürün bilgisi

Test Numaraları 1 ve 3 (Önbellekleme Yok)

SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM   Production.Product prod1
WHERE  prod1.Name LIKE N'[M-R]%';

Test Numarası 2 (Önbellek Yok)

;WITH cte AS
(
    SELECT prod1.ProductID
    FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
    WHERE  prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM   Production.Product prod2
INNER JOIN cte
        ON cte.ProductID = prod2.ProductID;

Test Numaraları 1, 2 ve 3 (Önbellekleme)

;WITH cte AS
(
    SELECT prod1.ProductID
    FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
    WHERE  prod1.Name LIKE N'[M-R]%'
    AND    EXISTS (
                SELECT *
                FROM Production.TransactionHistory th
                WHERE th.ProductID = prod1.ProductID
                  )
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM   Production.Product prod2
INNER JOIN cte
        ON cte.ProductID = prod2.ProductID;

İşlem Bilgisi

Test Numaraları 1 ve 2 (Sabitler)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = 977
ORDER BY th.TransactionDate DESC;

Test Numaraları 1 ve 2 (Parametreli)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;

Test Numaraları 1 ve 2 (Parametreli + OPTIMIZE UNKNOWN)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

Test Numarası 2 (Her ikisi de Parametrelenmiş)

SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;

Test Numarası 2 (Her ikisi de Parametrelenmiş + OPTIMIZE UNKNOWN)

SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

Test Numarası 3 (Sabitler)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;

Test Numarası 3 (Parametreli)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;

Test Numarası 3 (Parametreli + OPTIMIZE UNKNOWN)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

Kod

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public class ObligatoryClassName
{
    private class ProductInfo
    {
        public int ProductID;
        public string Name;
        public int DaysToManufacture;

        public ProductInfo(int ProductID, string Name, int DaysToManufacture)
        {
            this.ProductID = ProductID;
            this.Name = Name;
            this.DaysToManufacture = DaysToManufacture;

            return;
        }
    }

    private static readonly List<ProductInfo> _GlobalProducts = new List<ProductInfo>();

    private static void PopulateGlobalProducts(SqlBoolean PrintQuery)
    {
        if (_GlobalProducts.Count > 0)
        {
            if (PrintQuery.IsTrue)
            {
                SqlContext.Pipe.Send(String.Concat("I already haz ", _GlobalProducts.Count,
                            " entries :)"));
            }

            return;
        }

        SqlConnection _Connection = new SqlConnection("Context Connection = true;");
        SqlCommand _Command = new SqlCommand();
        _Command.CommandType = CommandType.Text;
        _Command.Connection = _Connection;
        _Command.CommandText = @"
   ;WITH cte AS
   (
     SELECT prod1.ProductID
     FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
     WHERE  prod1.Name LIKE N'[M-R]%'
     AND    EXISTS (
                     SELECT *
                     FROM Production.TransactionHistory th
                     WHERE th.ProductID = prod1.ProductID
                   )
   )
   SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
   FROM   Production.Product prod2
   INNER JOIN cte
           ON cte.ProductID = prod2.ProductID;
";

        SqlDataReader _Reader = null;

        try
        {
            _Connection.Open();

            _Reader = _Command.ExecuteReader();

            while (_Reader.Read())
            {
                _GlobalProducts.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
                                                    _Reader.GetInt32(2)));
            }
        }
        catch
        {
            throw;
        }
        finally
        {
            if (_Reader != null && !_Reader.IsClosed)
            {
                _Reader.Close();
            }

            if (_Connection != null && _Connection.State != ConnectionState.Closed)
            {
                _Connection.Close();
            }

            if (PrintQuery.IsTrue)
            {
                SqlContext.Pipe.Send(_Command.CommandText);
            }
        }

        return;
    }


    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void GetTopRowsPerGroup(SqlByte TestNumber,
        SqlByte ParameterizeProductID, SqlBoolean OptimizeForUnknown,
        SqlBoolean UseSequentialAccess, SqlBoolean CacheProducts, SqlBoolean PrintQueries)
    {
        SqlConnection _Connection = new SqlConnection("Context Connection = true;");
        SqlCommand _Command = new SqlCommand();
        _Command.CommandType = CommandType.Text;
        _Command.Connection = _Connection;

        List<ProductInfo> _Products = null;
        SqlDataReader _Reader = null;

        int _RowsToGet = 5; // default value is for Test Number 1
        string _OrderByTransactionID = "";
        string _OptimizeForUnknown = "";
        CommandBehavior _CmdBehavior = CommandBehavior.Default;

        if (OptimizeForUnknown.IsTrue)
        {
            _OptimizeForUnknown = "OPTION (OPTIMIZE FOR (@ProductID UNKNOWN))";
        }

        if (UseSequentialAccess.IsTrue)
        {
            _CmdBehavior = CommandBehavior.SequentialAccess;
        }

        if (CacheProducts.IsTrue)
        {
            PopulateGlobalProducts(PrintQueries);
        }
        else
        {
            _Products = new List<ProductInfo>();
        }


        if (TestNumber.Value == 2)
        {
            _Command.CommandText = @"
   ;WITH cte AS
   (
     SELECT prod1.ProductID
     FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
     WHERE  prod1.Name LIKE N'[M-R]%'
   )
   SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
   FROM   Production.Product prod2
   INNER JOIN cte
           ON cte.ProductID = prod2.ProductID;
";
        }
        else
        {
            _Command.CommandText = @"
     SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
     FROM   Production.Product prod1
     WHERE  prod1.Name LIKE N'[M-R]%';
";
            if (TestNumber.Value == 3)
            {
                _RowsToGet = 1;
                _OrderByTransactionID = ", th.TransactionID DESC";
            }
        }

        try
        {
            _Connection.Open();

            // Populate Product list for this run if not using the Product Cache
            if (!CacheProducts.IsTrue)
            {
                _Reader = _Command.ExecuteReader(_CmdBehavior);

                while (_Reader.Read())
                {
                    _Products.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
                                                  _Reader.GetInt32(2)));
                }

                _Reader.Close();

                if (PrintQueries.IsTrue)
                {
                    SqlContext.Pipe.Send(_Command.CommandText);
                }
            }
            else
            {
                _Products = _GlobalProducts;
            }

            SqlDataRecord _ResultRow = new SqlDataRecord(
                new SqlMetaData[]{
                    new SqlMetaData("ProductID", SqlDbType.Int),
                    new SqlMetaData("Name", SqlDbType.NVarChar, 50),
                    new SqlMetaData("TransactionID", SqlDbType.Int),
                    new SqlMetaData("TransactionDate", SqlDbType.DateTime)
                });

            SqlParameter _ProductID = new SqlParameter("@ProductID", SqlDbType.Int);
            _Command.Parameters.Add(_ProductID);
            SqlParameter _RowsToReturn = new SqlParameter("@RowsToReturn", SqlDbType.Int);
            _Command.Parameters.Add(_RowsToReturn);

            SqlContext.Pipe.SendResultsStart(_ResultRow);

            for (int _Row = 0; _Row < _Products.Count; _Row++)
            {
                // Tests 1 and 3 use previously set static values for _RowsToGet
                if (TestNumber.Value == 2)
                {
                    if (_Products[_Row].DaysToManufacture == 0)
                    {
                        continue; // no use in issuing SELECT TOP (0) query
                    }

                    _RowsToGet = (5 * _Products[_Row].DaysToManufacture);
                }

                _ResultRow.SetInt32(0, _Products[_Row].ProductID);
                _ResultRow.SetString(1, _Products[_Row].Name);

                switch (ParameterizeProductID.Value)
                {
                    case 0x01:
                        _Command.CommandText = String.Format(@"
   SELECT TOP ({0}) th.TransactionID, th.TransactionDate
   FROM   Production.TransactionHistory th
   WHERE  th.ProductID = @ProductID
   ORDER BY th.TransactionDate DESC{2}
   {1};
", _RowsToGet, _OptimizeForUnknown, _OrderByTransactionID);

                        _ProductID.Value = _Products[_Row].ProductID;
                        break;
                    case 0x02:
                        _Command.CommandText = String.Format(@"
   SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
   FROM   Production.TransactionHistory th
   WHERE  th.ProductID = @ProductID
   ORDER BY th.TransactionDate DESC
   {0};
", _OptimizeForUnknown);

                        _ProductID.Value = _Products[_Row].ProductID;
                        _RowsToReturn.Value = _RowsToGet;
                        break;
                    default:
                        _Command.CommandText = String.Format(@"
   SELECT TOP ({0}) th.TransactionID, th.TransactionDate
   FROM   Production.TransactionHistory th
   WHERE  th.ProductID = {1}
   ORDER BY th.TransactionDate DESC{2};
", _RowsToGet, _Products[_Row].ProductID, _OrderByTransactionID);
                        break;
                }


                _Reader = _Command.ExecuteReader(_CmdBehavior);

                while (_Reader.Read())
                {
                    _ResultRow.SetInt32(2, _Reader.GetInt32(0));
                    _ResultRow.SetDateTime(3, _Reader.GetDateTime(1));

                    SqlContext.Pipe.SendResultsRow(_ResultRow);
                }
                _Reader.Close();
            }

        }
        catch
        {
            throw;
        }
        finally
        {
            if (SqlContext.Pipe.IsSendingResults)
            {
                SqlContext.Pipe.SendResultsEnd();
            }

            if (_Reader != null && !_Reader.IsClosed)
            {
                _Reader.Close();
            }

            if (_Connection != null && _Connection.State != ConnectionState.Closed)
            {
                _Connection.Close();
            }

            if (PrintQueries.IsTrue)
            {
                SqlContext.Pipe.Send(_Command.CommandText);
            }
        }


    }
}

Test Sorguları

Testleri buraya göndermek için yeterli alan yok, bu yüzden başka bir yer bulacağım.

Sonuç

Bazı senaryolar için, SQLCLR, T-SQL'de yapılamayan sorguların belirli yönlerini değiştirmek için kullanılabilir. Ve geçici tablolar yerine önbellekleme için bellek kullanma yeteneği vardır, ancak bellek otomatik olarak sisteme geri bırakılmadığından dikkatli ve dikkatli yapılmalıdır. Bu yöntem aynı zamanda geçici sorgulamalara yardımcı olacak bir şey değildir, ancak yürütülen sorguların daha fazla yönünü uyarlamak için parametreler ekleyerek burada gösterdiğimden daha esnek hale getirmek mümkündür.


GÜNCELLEME

Ek Test
Üzerine destekleyici bir dizin içeren orijinal testlerim TransactionHistoryaşağıdaki tanımı kullandı:

ProductID ASC, TransactionDate DESC

TransactionId DESCSonunda dahil olmaktan vazgeçmeye karar verdim, Test Numarası 3’ün (en yeni TransactionIdolanı bağladığını belirten - ki, “en son” un açıkça belirtilmediğinden beri varsayıldığını varsayıyordu, ancak herkes öyle görünüyordu) Bu varsayım üzerinde hemfikir olmak için), muhtemelen bir fark yaratmak için yeterli bağ olmayacaktı.

Ancak, daha sonra Aaron TransactionId DESC, CROSS APPLYyöntemin her üç testte de kazanan olduğunu içeren ve içeren bir destekleyici endeksiyle yeniden test etti. Bu, CTE yönteminin Test Numarası 3 için en iyisi olduğunu gösteren testlerimden farklıydı (önbellekleme kullanılmadığında, Aaron'un testini yansıtan). Test edilmesi gereken ek bir varyasyon olduğu açıktı.

Geçerli destek dizinini kaldırdım, yenisini oluşturdum TransactionIdve plan önbelleğini temizledim (emin olmak için):

DROP INDEX [IX_TransactionHistoryX] ON Production.TransactionHistory;

CREATE UNIQUE INDEX [UIX_TransactionHistoryX]
    ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC, TransactionID DESC)
    WITH (FILLFACTOR = 100);

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

Test Numarası 1'i yeniden yaptım ve sonuçlar beklendiği gibi aynıydı. Daha sonra Test Numarası 3'ü tekrar değiştirdim ve sonuçlar gerçekten değişti:

Test 3 Sonuçları - destek endeksi ile (TransactionId DESC ile)
Yukarıdaki sonuçlar standart, önbelleğe almayan test içindir. Bu sefer, sadece CROSS APPLYCTE'yi yenmekle kalmıyor (aynı Aaron'un testinde belirtildiği gibi), ancak SQLCLR proc'u 30 Reads (woo hoo) götürdü.

Test 3 Sonuçları - destekleyici endeksi ile (TransactionId DESC ile)
Yukarıdaki sonuçlar, önbellekleme etkinleştirilmiş test içindir. Bu sefer CTE'nin performansı düşmese de, yine CROSS APPLYde atıyor. Ancak, şimdi SQLCLR proc 23 okur (woo hoo, yine).

Uzaklaş

  1. Kullanmak için çeşitli seçenekler var. Her birinin güçlü yanları olduğu için birkaçını denemek en iyisidir. Burada yapılan testler, tüm testlerde (destekleyici bir indeksle) en iyi ve en kötü performans gösterenler arasında hem Okuma hem de Süre'de oldukça küçük bir farklılık göstermektedir; Okurlardaki değişim yaklaşık 350 ve Süre 55 ms'dir. SQLCLR proc, 1 test haricinde (Okurlar açısından) hepsinde kazanmış olsa da, sadece birkaç Okur kaydetmek genellikle SQLCLR yoluna gitmenin bakım maliyetine değmez. Ancak AdventureWorks2012'de, Producttablo sadece 504 satıra ve TransactionHistoryyalnızca 113.443 satıra sahiptir. Bu yöntemler arasındaki performans farkı, satır sayıları arttıkça muhtemelen daha belirgin hale gelir.

  2. Bu soru, belirli bir satır dizisini elde etmeye özgüyken, performanstaki tek en büyük faktörün, belirli bir SQL değil, dizin oluşturduğu göz ardı edilmemelidir. Hangi yöntemin gerçekten en iyi olduğuna karar vermeden önce iyi bir endeksin yer alması gerekir.

  3. Burada bulunan en önemli ders CROSS APPLY - CTE - SQLCLR hakkında değil: TESTING hakkında. Farz etme. Birkaç kişiden fikir alın ve mümkün olduğu kadar çok senaryoyu test edin.


2
Uygulamayla ilgili fazladan mantıksal okumaların nedeni için Mikael'in cevabına bakınız.
Paul Beyaz

18

APPLY TOPya ROW_NUMBER()? Bu konuda söylenebilecek daha çok ne olabilir?

Farklılıkların kısa bir özeti ve gerçekten kısa tutulması için sadece seçenek 2 için planları göstereceğim ve endeksi ekledim Production.TransactionHistory.

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate)

row_number()Sorgu :.

with C as
(
  select T.TransactionID,
         T.TransactionDate,
         P.DaysToManufacture,
         row_number() over(partition by P.ProductID order by T.TransactionDate desc) as rn
  from Production.Product as P
    inner join Production.TransactionHistory as T
      on P.ProductID = T.ProductID
  where P.Name >= N'M' and
        P.Name < N'S'
)
select C.TransactionID,
       C.TransactionDate
from C
where C.rn <= 5 * C.DaysToManufacture;

görüntü tanımını buraya girin

apply topversiyon:

select T.TransactionID, 
       T.TransactionDate
from Production.Product as P
  cross apply (
              select top(cast(5 * P.DaysToManufacture as bigint))
                T.TransactionID,
                T.TransactionDate
              from Production.TransactionHistory as T
              where P.ProductID = T.ProductID
              order by T.TransactionDate desc
              ) as T
where P.Name >= N'M' and
      P.Name < N'S';

görüntü tanımını buraya girin

Bunların arasındaki temel fark apply top, iç içe geçmiş döngülerin altındaki üst ifadedeki row_numberfiltrelerin birleştirmeden sonra sürüm filtrelerinin bulunduğu alanlardır . Bu, Production.TransactionHistorygerçekten gerekenden daha fazla okuma olduğu anlamına gelir .

Yalnızca birleştirme işleminden önce sıraları sıralamaktan sorumlu operatörleri itmek için bir yol varsa, row_numbersürüm daha iyi olabilir.

Yani apply row_number()versiyonunu girin .

select T.TransactionID, 
       T.TransactionDate
from Production.Product as P
  cross apply (
              select T.TransactionID,
                     T.TransactionDate
              from (
                   select T.TransactionID,
                          T.TransactionDate,
                          row_number() over(order by T.TransactionDate desc) as rn
                   from Production.TransactionHistory as T
                   where P.ProductID = T.ProductID
                   ) as T
              where T.rn <= cast(5 * P.DaysToManufacture as bigint)
              ) as T
where P.Name >= N'M' and
      P.Name < N'S';

görüntü tanımını buraya girin

Gördüğünüz gibi apply row_number()hemen hemen apply topbiraz daha karmaşık olduğu gibi aynı . Uygulama süresi de aynı ya da biraz daha yavaş.

Peki neden sahip olduğumuzdan daha iyi olmayan bir cevap bulmak için uğraştım? Gerçek dünyada denemek için bir şey daha var ve aslında okurlarda bir fark var. Biri için açıklama yapmam *.

APPLY - ROW_NUMBER
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 230, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

APPLY - TOP
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 268, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Ben de buna devam ederken, ikinci bir row_number()versiyonda bazı durumlarda böyle devam edebileceğini söyleyebilirim. Bu belirli durumlar, aslında satırların çoğuna ihtiyaç duymanızı beklediğiniz zaman olacaktır Production.TransactionHistoryçünkü burada bir birleşim birleşmesiyle Production.Productnumaralandırılmışsınız Production.TransactionHistory.

with C as
(
  select T.TransactionID,
         T.TransactionDate,
         T.ProductID,
         row_number() over(partition by T.ProductID order by T.TransactionDate desc) as rn
  from Production.TransactionHistory as T
)
select C.TransactionID,
       C.TransactionDate
from C
 inner join Production.Product as P
      on P.ProductID = C.ProductID
where P.Name >= N'M' and
      P.Name < N'S' and
      C.rn <= 5 * P.DaysToManufacture;

görüntü tanımını buraya girin

Yukarıdaki şekli bir sıralama operatörü olmadan almak için, TransactionDateaşağı inerek sıralamak için destek endeksini de değiştirmeniz gerekir .

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate desc)

* Düzenleme: Ekstra mantıksal okumalar , uygulama topuyla kullanılan yuvalama döngüler ön beslemesinden kaynaklanmaktadır . Aynı sayıdaki mantıksal okumaları almak için bunu undoc'd TF 8744 (ve / veya 9115) ile devre dışı bırakabilirsiniz. Önceden ayarlama, doğru koşullarda uygulama alternatifinin bir avantajı olabilir. - Paul White


11

Genellikle CTE'lerin ve pencereleme fonksiyonlarının bir kombinasyonunu kullanırım. Aşağıdaki gibi bir şey kullanarak bu cevabı elde edebilirsiniz:

;WITH GiveMeCounts
AS (
    SELECT CustomerID
        ,OrderDate
        ,TotalAmt

        ,ROW_NUMBER() OVER (
            PARTITION BY CustomerID ORDER BY 
            --You can change the following field or sort order to whatever you'd like to order by.
            TotalAmt desc
            ) AS MySeqNum
    )
SELECT CustomerID, OrderDate, TotalAmt
FROM GiveMeCounts
--Set n per group here
where MySeqNum <= 10

Farklı grupların farklı sayıda satır döndürmek isteyebileceği fazladan kredi kısmı için ayrı bir tablo kullanabilirsiniz. Devlet gibi coğrafi kriterleri kullanarak diyelim:

+-------+-----------+
| State | MaxSeqnum |
+-------+-----------+
| AK    |        10 |
| NY    |         5 |
| NC    |        23 |
+-------+-----------+

Değerlerin farklı olabileceği bu noktaya ulaşmak için CTE'nizi buna benzer Durum tablosuna eklemeniz gerekir:

SELECT [CustomerID]
    ,[OrderDate]
    ,[TotalAmt]
    ,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
    AND gmc.MySeqNum <= st.MaxSeqNum
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.