Sql Server gagal menggunakan indeks pada bijection sederhana

11

Ini adalah teka-teki pengoptimal permintaan lainnya.

Mungkin saya hanya memperkirakan pengoptimal permintaan yang berlebihan, atau mungkin saya melewatkan sesuatu - jadi saya meletakkannya di sana.

Saya punya meja sederhana

CREATE TABLE [dbo].[MyEntities](
  [Id] [uniqueidentifier] NOT NULL,
  [Number] [int] NOT NULL,
  CONSTRAINT [PK_dbo.MyEntities] PRIMARY KEY CLUSTERED ([Id])
)

CREATE NONCLUSTERED INDEX [IX_Number] ON [dbo].[MyEntities] ([Number])

dengan indeks dan beberapa ribu baris di sana, Numberdidistribusikan secara merata dalam nilai 0, 1 dan 2.

Sekarang pertanyaan ini:

SELECT * FROM
    (SELECT
        [Extent1].[Number] AS [Number],
        CASE
        WHEN (0 = [Extent1].[Number]) THEN 'one'
        WHEN (1 = [Extent1].[Number]) THEN 'two'
        WHEN (2 = [Extent1].[Number]) THEN 'three'
        ELSE '?'
        END AS [Name]
        FROM [dbo].[MyEntities] AS [Extent1]
        ) P
WHERE P.Number = 0;

apakah indeks mencari IX_Numberseperti yang diharapkan.

Jika klausa di mana

WHERE P.Name = 'one';

Namun, itu menjadi pemindaian.

Klausa kasus jelas merupakan suatu bujukan, sehingga dalam teori optimasi harus dimungkinkan untuk mengurangi rencana kueri pertama dari kueri kedua.

Itu juga tidak sepenuhnya bersifat akademis: Permintaan diilhami dengan menerjemahkan nilai enum ke nama masing-masing yang bersahabat.

Saya ingin mendengar dari seseorang yang tahu apa yang bisa diharapkan dari pengoptimal permintaan (dan khususnya yang ada di Sql Server): Apakah saya hanya berharap terlalu banyak?

Saya bertanya karena ada beberapa kasus sebelumnya di mana beberapa variasi sedikit kueri akan membuat pengoptimalan tiba-tiba terungkap.

Saya menggunakan Sql Server 2016 Developer Edition.

John
sumber

Jawaban:

18

Apakah saya hanya berharap terlalu banyak?

Iya. Setidaknya dalam versi produk saat ini.

SQL Server tidak akan memisah-misahkan CASEpernyataan dan merekayasa baliknya untuk mengetahui bahwa jika hasil dari kolom yang dihitung 'one'itu [Extent1].[Number]harus 0.

Anda perlu memastikan bahwa Anda menulis predikat Anda menjadi mahal. Yang hampir selalu melibatkannya dalam bentuk. basetable_column_name comparison_operator expression.

Bahkan penyimpangan kecil mematahkan sargabilitas.

WHERE P.Number + 0 = 0;

tidak akan menggunakan pencarian indeks meskipun itu bahkan lebih mudah untuk disederhanakan daripada CASEekspresi.

Jika Anda ingin mencari pada nama string dan mencari nomor yang Anda butuhkan tabel pemetaan dengan nama dan angka dan bergabung ke dalamnya dalam permintaan, maka rencana tersebut mungkin memiliki pencarian di tabel pemetaan diikuti oleh pencarian berkorelasi terus [dbo].[MyEntities]dengan nomor yang dikembalikan dari pencarian pertama.

Martin Smith
sumber
6

Jangan proyeksikan enum Anda sebagai pernyataan kasus. Proyeksikan sebagai tabel turunan seperti ini:

SELECT * FROM
   (SELECT
      [Extent1].[Number] AS [Number],
      enum.Name
   FROM
      [dbo].[MyEntities] AS [Extent1]
      LEFT JOIN (VALUES
         (0, 'one'),
         (1, 'two'),
         (2, 'three')
      ) enum (Number, Name)
         ON Extent1.Number = enum.Number
   ) P
WHERE
   P.Name = 'one';

Saya curiga Anda akan mendapatkan hasil yang lebih baik. (Saya tidak mengonversi Nama menjadi ?ketika hilang karena ini kemungkinan akan mengganggu kemungkinan peningkatan kinerja. Namun, Anda bisa memindahkan WHEREklausa di dalam kueri luar untuk menempatkan predikat di atas enummeja, atau Anda bisa mengembalikan dua kolom dari permintaan dalam, satu untuk predikat dan satu untuk tampilan, di mana predikatnya adalah NULLketika tidak ada nilai enum yang cocok.)

Saya menduga, karena itu [Extent1]di sana, Anda menggunakan ORM seperti Entity Framework atau Linq-To-SQL. Saya tidak bisa membimbing Anda bagaimana menyelesaikan proyeksi seperti itu secara asli, tetapi, Anda bisa menggunakan teknik yang berbeda.

Dalam satu proyek saya, saya merefleksikan nilai enum kode dalam tabel nyata dalam database, melalui kelas custom-build yang menggabungkan nilai enum ke dalam database. (Anda harus menghormati aturan bahwa Anda harus secara eksplisit mencantumkan nilai enum Anda, tidak pernah dapat menghapusnya tanpa meninjau tabel Anda, dan tidak pernah, pernah dapat mengubahnya, meskipun Anda harus mengamati setidaknya sebagian dari ini dengan pengaturan Anda saat ini) .

Sekarang, saya menggunakan enumerable dari Identifierkelas dasar yang memiliki banyak subkelas beton yang berbeda, tetapi tidak ada alasan itu tidak dapat dilakukan dengan vanum enum biasa. Berikut ini contoh penggunaannya:

new EnumOrIdentifierProjector<CodeClassOrEnum, PrivateDbDtoObject>(
   _sqlConnector.Connection,
   "dbo.TableName",
   "PrimaryKeyId",
   "NameColumnName",
   dtoObject => dtoObject.PrimaryKeyId,
   dtoObject => dtoObject.NameField,
   EnumerableOfIdentifierOrTypeOfEnum
)
   .Populate();

Anda dapat melihat bahwa saya menyampaikan semua informasi yang diperlukan untuk menulis dan membaca nilai basis data. (Saya memiliki situasi di mana permintaan saat ini mungkin tidak mengandung semua nilai yang masih ada, jadi diperlukan untuk mengembalikan tambahan apa pun dari database serta set yang saat ini dimuat. Saya juga membiarkan database menetapkan ID, meskipun untuk enum Anda mungkin tidak akan menginginkan itu.)

Idenya adalah bahwa sekali Anda memiliki tabel yang dibaca / ditulis hanya sekali pada saat startup yang andal akan memiliki semua nilai enum, Anda cukup bergabung dengannya seperti tabel lainnya, dan kinerja harus bagus.

Saya harap ide-ide ini cukup bagi Anda untuk melakukan perbaikan.

ErikE
sumber
Ya, saya menggunakan EntityFramework dan di sanalah solusinya seharusnya berada di dunia yang optimal. Sebelum itu terjadi, saran Anda adalah salah satu solusi terbaik yang saya percaya.
John
5

Saya menafsirkan pertanyaan sebagai bahwa Anda tertarik pada pengoptimal secara umum, tetapi dengan minat khusus untuk SQL Server. Saya menguji skenario Anda dengan db2 LUW V11.1:

]$ db2 "create table myentities ( id int not null, number int not null )"
]$ db2 "create index ix_number on myentities (number)"
]$ db2 "insert into myentities (id, number) with t(n) as ( values 0 union all select n+1 from t where n<10000) select n, mod(n,3) from t"

Pengoptimal dalam DB2 menulis ulang kueri kedua ke yang pertama:

Original Statement:
------------------
SELECT 
  * 
FROM 
  (SELECT 
     number,

   CASE 
   WHEN (0 = Number) 
   THEN 'one' 
   WHEN (1 = Number) 
   THEN 'two' 
   WHEN (2 = Number) 
   THEN 'three' 
   ELSE '?' END AS Name 
   FROM 
     MyEntities
  ) P 
WHERE 
  P.name = 'one'


Optimized Statement:
-------------------
SELECT 
  Q1.NUMBER AS "NUMBER",

CASE 
WHEN (0 = Q1.NUMBER) 
THEN 'one' 
WHEN (1 = Q1.NUMBER) 
THEN 'two' 
WHEN (2 = Q1.NUMBER) 
THEN 'three' 
ELSE '?' END AS "NAME" 
FROM 
  LELLE.MYENTITIES AS Q1 
WHERE 
  (0 = Q1.NUMBER)

Rencananya terlihat seperti:

Access Plan:
-----------
        Total Cost:             33.5483
        Query Degree:           1


      Rows 
     RETURN
     (   1)
      Cost 
       I/O 
       |
      3334 
     IXSCAN
     (   2)
     33.1861 
     4.66713 
       |
      10001 
 INDEX: LELLE   
    IX_NUMBER
       Q1

Saya tidak tahu banyak tentang pengoptimal lain, tetapi saya merasa bahwa pengoptimal DB2 dianggap cukup baik bahkan di antara pesaing.

Lennart
sumber
Itu menyenangkan. Bisakah Anda menjelaskan dari mana "pernyataan yang dioptimalkan" berasal? Apakah db2 sendiri mengembalikannya kepada Anda? - Juga, saya kesulitan membaca rencananya. Saya bawa "IXSCAN" tidak berarti pemindaian indeks dalam kasus ini?
Yohanes
1
Anda bisa memberi tahu DB2 untuk menjelaskan pernyataan untuk Anda. Informasi yang dikumpulkan disimpan dalam satu set tabel, dan Anda dapat menggunakan penjelasan visual atau seperti dalam kasus ini utilitas db2exfmt (atau buat util Anda sendiri). Selain itu, Anda dapat memonitor pernyataan dan membandingkan perkiraan kardinalitas dalam rencana dengan rencana yang sebenarnya. Dalam rencana ini kita dapat melihat bahwa itu memang indexscan (IXSCAN) dan output yang diperkirakan dari operator ini adalah 3334 baris. Apakah ini buruk di server SQL? Ia mengetahui startkey dan stopkey sehingga hanya memindai baris yang relevan di DB2.
Lennart
Jadi apa yang disebut pemindaian melibatkan pencarian, dan sejujurnya, penjelasan paket setara Sql Server juga terkadang menyebut sesuatu pemindaian yang melibatkan pencarian, dan kadang-kadang menyebutnya pemindaian. Saya selalu perlu melihat jumlah baris untuk memahami apa itu. Karena jelas ada 3334 dalam output db2, itu pasti melakukan apa yang saya harapkan. Sangat menarik.
John
Ya, saya juga kadang-kadang membingungkan. Kita harus melihat informasi yang lebih rinci untuk setiap operator untuk benar-benar memahami apa yang sedang terjadi.
Lennart
0

Dalam permintaan khusus ini, cukup konyol bahkan memiliki CASEpernyataan. Anda memfilter ke satu case tertentu! Mungkin ini hanya detail dari contoh kueri tertentu yang Anda berikan, tetapi jika tidak, Anda dapat menulis kueri ini untuk mendapatkan hasil yang setara:

SELECT
    [Extent1].[Number] AS [Number],
    'one' AS [Name]
FROM [dbo].[MyEntities] AS [Extent1]
WHERE [Extent1].[Number] = 0;

Ini akan memberi Anda set hasil yang persis sama, dan karena Anda sudah mengkodekan nilai dalam sebuah CASEpernyataan, Anda tidak kehilangan perawatan apa pun di sini.

jpmc26
sumber
1
Saya pikir Anda melewatkan intinya — ini dihasilkan SQL dari basis kode back-end yang bekerja dengan enum melalui representasi string mereka. Kode yang memproyeksikan SQL adalah melakukan kekerasan terhadap kueri. Saya yakin si penanya, jika dia menulis SQL sendiri, akan dapat menulis kueri yang lebih baik. Jadi, sama sekali tidak konyol memiliki CASEpernyataan, karena ORM melakukan hal semacam itu. Apa yang konyol adalah bahwa Anda tidak mengenali aspek sederhana dari masalah ini ... (bagaimana itu karena secara tidak langsung disebut tanpa otak?)
ErikE
@ErikE Masih agak konyol, karena Anda bisa menggunakan nilai numerik dari enum, dengan asumsi C #. (Asumsi yang cukup aman mengingat bahwa kita sedang berbicara SQL Server.)
jpmc26
Tetapi Anda tidak tahu apa itu use case. Mungkin itu akan menjadi perubahan besar untuk beralih ke nilai numerik. Mungkin enum di-retrofit menjadi basis kode raksasa yang ada. Mengkritik tanpa pengetahuan itu konyol.
ErikE
@ErikE Jika ini konyol, lalu mengapa Anda melakukannya? =) Saya hanya menjawab untuk menunjukkan bahwa jika use case sesederhana contoh dalam pertanyaan (yang jelas ditentukan dalam kata pengantar jawaban saya), CASEpernyataan tersebut dapat dihilangkan seluruhnya tanpa kekurangan. Tentu saja mungkin ada faktor yang tidak diketahui, tetapi tidak ditentukan.
jpmc26
Saya tidak keberatan dengan bagian faktual dari jawaban Anda, hanya bagian-bagian yang menjadi ciri subyektif. Mengenai apakah saya mengkritik tanpa sepengetahuan, saya sepenuhnya memahami cara saya gagal menggunakan logika yang sangat bersih atau membuat asumsi yang terbukti salah ...
ErikE