Subquery menggunakan Exists 1 atau Exists *

89

Saya biasa menulis cek EXISTS saya seperti ini:

IF EXISTS (SELECT * FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Where Columns=@Filters
END

Salah satu DBA dalam kehidupan sebelumnya mengatakan kepada saya bahwa ketika saya melakukan EXISTSklausul, gunakan SELECT 1bukanSELECT *

IF EXISTS (SELECT 1 FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Columns=@Filters
END

Apakah ini benar-benar membuat perbedaan?

Raj More
sumber
1
Anda lupa EXISTS (SELECT NULL FROM ...). Ini ditanyakan baru-baru ini btw
OMG Ponies
17
ps dapatkan DBA baru. Takhayul tidak memiliki tempat dalam TI, terutama dalam manajemen basis data (dari DBA sebelumnya !!!)
Matt Rogish

Jawaban:

136

Tidak, SQL Server pintar dan tahu itu digunakan untuk yang sudah ada, dan mengembalikan NO DATA ke sistem.

Quoth Microsoft: http://technet.microsoft.com/en-us/library/ms189259.aspx?ppud=4

Daftar pilihan subkueri yang diperkenalkan oleh EXISTS hampir selalu terdiri dari tanda bintang (*). Tidak ada alasan untuk mencantumkan nama kolom karena Anda hanya menguji apakah baris yang memenuhi kondisi yang ditentukan dalam subkueri ada.

Untuk memeriksa diri Anda sendiri, coba jalankan yang berikut ini:

SELECT whatever
  FROM yourtable
 WHERE EXISTS( SELECT 1/0
                 FROM someothertable 
                WHERE a_valid_clause )

Jika itu benar-benar melakukan sesuatu dengan daftar SELECT, itu akan melempar div dengan kesalahan nol. Tidak.

EDIT: Catatan, Standar SQL sebenarnya berbicara tentang ini.

ANSI SQL 1992 Standard, hal 191 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) Kasus:
a) Jika <select list>"*" hanya terkandung dalam a <subquery> yang langsung terkandung dalam <exists predicate>, maka <select list> ekuivalen dengan a <value expression> itu adalah sewenang-wenang <literal>.

Matt Rogish
sumber
1
yang EXISTSCaranya dengan 1/0 bahkan dapat diperluas untuk ini SELECT 1 WHERE EXISTS(SELECT 1/0)... tampaknya langkah yang lebih abstrak kemudian sebagai yang kedua SELECTtidak memiliki FROMklausul
whytheq
1
@why - Atau SELECT COUNT(*) WHERE EXISTS(SELECT 1/0). A SELECTtanpa FROMdi SQL Server diperlakukan seolah-olah mengakses tabel baris tunggal (misalnya mirip dengan memilih dari dualtabel di RDBMS lain)
Martin Smith
@MartinSmith bersulang - jadi intinya adalah SELECTmembuat tabel 1 baris sebelum melakukan hal lain jadi meskipun 1/0sampah tabel 1 baris masih EXISTS?
whytheq
Apakah ini selalu terjadi, atau apakah ini pengoptimalan yang diperkenalkan di versi SQL Server tertentu?
Martin Brown
1
@ArtinSUMPULAN "quoth". Terima kasih telah memperbaikinya kembali.
Gurwinder Singh
113

Alasan kesalahpahaman ini mungkin karena keyakinan bahwa ia akan membaca semua kolom. Sangat mudah untuk melihat bahwa bukan ini masalahnya.

CREATE TABLE T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)

CREATE NONCLUSTERED INDEX NarrowIndex ON T(Y)

IF EXISTS (SELECT * FROM T)
    PRINT 'Y'

Memberikan rencana

Rencana

Ini menunjukkan bahwa SQL Server dapat menggunakan indeks tersempit yang tersedia untuk memeriksa hasil meskipun pada kenyataannya indeks tidak menyertakan semua kolom. Akses indeks berada di bawah operator semi-join yang berarti dapat menghentikan pemindaian segera setelah baris pertama dikembalikan.

Jadi jelas sekali keyakinan di atas salah.

Namun Conor Cunningham dari tim Pengoptimal Kueri menjelaskan di sini bahwa ia biasanya menggunakan SELECT 1dalam kasus ini karena dapat membuat perbedaan kinerja kecil dalam kompilasi kueri.

QP akan mengambil dan memperluas semua yang ada *di awal pipeline dan mengikatnya ke objek (dalam hal ini, daftar kolom). Ini kemudian akan menghapus kolom yang tidak dibutuhkan karena sifat kueri.

Jadi untuk EXISTSsubkueri sederhana seperti ini:

SELECT col1 FROM MyTable WHERE EXISTS (SELECT * FROM Table2 WHERE MyTable.col1=Table2.col2)Ini *akan diperluas ke beberapa daftar kolom yang berpotensi besar dan kemudian akan ditentukan bahwa semantik EXISTStidak memerlukan salah satu kolom tersebut, jadi pada dasarnya semuanya dapat dihapus.

" SELECT 1" akan menghindari pemeriksaan metadata yang tidak diperlukan untuk tabel itu selama kompilasi kueri.

Namun, pada waktu proses, kedua bentuk kueri tersebut akan sama dan akan memiliki waktu proses yang identik.

Saya menguji empat kemungkinan cara untuk mengekspresikan kueri ini pada tabel kosong dengan berbagai jumlah kolom. SELECT 1vs SELECT *vs SELECT Primary_Keyvs SELECT Other_Not_Null_Column.

Saya menjalankan kueri dalam satu putaran menggunakan OPTION (RECOMPILE)dan mengukur jumlah rata-rata eksekusi per detik. Hasil di bawah

masukkan deskripsi gambar di sini

+-------------+----------+---------+---------+--------------+
| Num of Cols |    *     |    1    |   PK    | Not Null col |
+-------------+----------+---------+---------+--------------+
| 2           | 2043.5   | 2043.25 | 2073.5  | 2067.5       |
| 4           | 2038.75  | 2041.25 | 2067.5  | 2067.5       |
| 8           | 2015.75  | 2017    | 2059.75 | 2059         |
| 16          | 2005.75  | 2005.25 | 2025.25 | 2035.75      |
| 32          | 1963.25  | 1967.25 | 2001.25 | 1992.75      |
| 64          | 1903     | 1904    | 1936.25 | 1939.75      |
| 128         | 1778.75  | 1779.75 | 1799    | 1806.75      |
| 256         | 1530.75  | 1526.5  | 1542.75 | 1541.25      |
| 512         | 1195     | 1189.75 | 1203.75 | 1198.5       |
| 1024        | 694.75   | 697     | 699     | 699.25       |
+-------------+----------+---------+---------+--------------+
| Total       | 17169.25 | 17171   | 17408   | 17408        |
+-------------+----------+---------+---------+--------------+

Seperti yang dapat dilihat, tidak ada pemenang yang konsisten antara SELECT 1dan SELECT *dan perbedaan antara kedua pendekatan dapat diabaikan. The SELECT Not Null coldan SELECT PKtampil sedikit lebih cepat sekalipun.

Keempat kueri tersebut menurunkan kinerja saat jumlah kolom dalam tabel meningkat.

Karena tabel kosong, hubungan ini tampaknya hanya dapat dijelaskan dengan jumlah metadata kolom. Karena COUNT(1)mudah untuk melihat bahwa ini akan ditulis ulang COUNT(*)di beberapa titik dalam proses dari bawah.

SET SHOWPLAN_TEXT ON;

GO

SELECT COUNT(1)
FROM master..spt_values

Yang memberikan rencana berikut

  |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1004],0)))
       |--Stream Aggregate(DEFINE:([Expr1004]=Count(*)))
            |--Index Scan(OBJECT:([master].[dbo].[spt_values].[ix2_spt_values_nu_nc]))

Melampirkan debugger ke proses SQL Server dan secara acak melanggar saat menjalankan di bawah ini

DECLARE @V int 

WHILE (1=1)
    SELECT @V=1 WHERE EXISTS (SELECT 1 FROM ##T) OPTION(RECOMPILE)

Saya menemukan bahwa dalam kasus di mana tabel memiliki 1.024 kolom sebagian besar waktu tumpukan panggilan terlihat seperti di bawah ini menunjukkan bahwa memang menghabiskan sebagian besar waktu memuat metadata kolom bahkan ketika SELECT 1digunakan (Untuk kasus di mana tabel memiliki 1 kolom yang secara acak melanggar tidak mencapai sedikit tumpukan panggilan ini dalam 10 upaya)

sqlservr.exe!CMEDAccess::GetProxyBaseIntnl()  - 0x1e2c79 bytes  
sqlservr.exe!CMEDProxyRelation::GetColumn()  + 0x57 bytes   
sqlservr.exe!CAlgTableMetadata::LoadColumns()  + 0x256 bytes    
sqlservr.exe!CAlgTableMetadata::Bind()  + 0x15c bytes   
sqlservr.exe!CRelOp_Get::BindTree()  + 0x98 bytes   
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_FromList::BindTree()  + 0x5c bytes  
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_QuerySpec::BindTree()  + 0xbe bytes 
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CScaOp_Exists::BindScalarTree()  + 0x72 bytes  
... Lines omitted ...
msvcr80.dll!_threadstartex(void * ptd=0x0031d888)  Line 326 + 0x5 bytes C
kernel32.dll!_BaseThreadStart@8()  + 0x37 bytes 

Upaya pembuatan profil manual ini didukung oleh profiler kode VS 2012 yang menunjukkan pilihan fungsi yang sangat berbeda yang memakan waktu kompilasi untuk dua kasus ( kolom 15 Fungsi Teratas 1024 vs kolom 15 Fungsi Teratas 1 ).

Baik versi SELECT 1dan SELECT *akhirnya memeriksa izin kolom dan gagal jika pengguna tidak diberikan akses ke semua kolom dalam tabel.

Contoh yang saya kutip dari percakapan di heap

CREATE USER blat WITHOUT LOGIN;
GO
CREATE TABLE dbo.T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)
GO

GRANT SELECT ON dbo.T TO blat;
DENY SELECT ON dbo.T(Z) TO blat;
GO
EXECUTE AS USER = 'blat';
GO

SELECT 1
WHERE  EXISTS (SELECT 1
               FROM   T); 
/*  ↑↑↑↑ 
Fails unexpectedly with 

The SELECT permission was denied on the column 'Z' of the 
           object 'T', database 'tempdb', schema 'dbo'.*/

GO
REVERT;
DROP USER blat
DROP TABLE T

Jadi orang mungkin berspekulasi bahwa perbedaan kecil yang terlihat saat menggunakan SELECT some_not_null_coladalah bahwa itu hanya berakhir memeriksa izin pada kolom tertentu (meskipun masih memuat metadata untuk semua). Namun ini tampaknya tidak sesuai dengan fakta karena perbedaan persentase antara kedua pendekatan jika ada yang semakin kecil karena jumlah kolom dalam tabel yang mendasarinya meningkat.

Bagaimanapun saya tidak akan terburu-buru dan mengubah semua pertanyaan saya ke formulir ini karena perbedaannya sangat kecil dan hanya terlihat selama kompilasi kueri. Menghapus OPTION (RECOMPILE)sehingga eksekusi selanjutnya dapat menggunakan rencana cache memberi berikut ini.

masukkan deskripsi gambar di sini

+-------------+-----------+------------+-----------+--------------+
| Num of Cols |     *     |     1      |    PK     | Not Null col |
+-------------+-----------+------------+-----------+--------------+
| 2           | 144933.25 | 145292     | 146029.25 | 143973.5     |
| 4           | 146084    | 146633.5   | 146018.75 | 146581.25    |
| 8           | 143145.25 | 144393.25  | 145723.5  | 144790.25    |
| 16          | 145191.75 | 145174     | 144755.5  | 146666.75    |
| 32          | 144624    | 145483.75  | 143531    | 145366.25    |
| 64          | 145459.25 | 146175.75  | 147174.25 | 146622.5     |
| 128         | 145625.75 | 143823.25  | 144132    | 144739.25    |
| 256         | 145380.75 | 147224     | 146203.25 | 147078.75    |
| 512         | 146045    | 145609.25  | 145149.25 | 144335.5     |
| 1024        | 148280    | 148076     | 145593.25 | 146534.75    |
+-------------+-----------+------------+-----------+--------------+
| Total       | 1454769   | 1457884.75 | 1454310   | 1456688.75   |
+-------------+-----------+------------+-----------+--------------+

Skrip pengujian yang saya gunakan dapat ditemukan di sini

Martin Smith
sumber
3
+1 Jawaban ini layak mendapatkan suara lebih atas upaya yang terlibat untuk mendapatkan data nyata.
Jon
1
Adakah yang tahu versi SQL Server yang mana statistik ini dihasilkan?
Martin Brown
3
@MartinBrown - IIRC awalnya tahun 2008 meskipun saya mengulangi tes baru-baru ini pada tahun 2012 untuk pengeditan terbaru dan menemukan hal yang sama.
Martin Smith
8

Cara terbaik untuk mengetahuinya adalah dengan menguji performa kedua versi dan memeriksa rencana eksekusi untuk kedua versi. Pilih tabel dengan banyak kolom.

HLGEM
sumber
2
+1. Tidak tahu mengapa ini ditolak. Saya selalu berpikir lebih baik mengajari seseorang memancing, daripada memberinya ikan. Bagaimana orang akan mempelajari sesuatu?
Ogre Mazmur33
5

Tidak ada perbedaan di SQL Server dan tidak pernah menjadi masalah di SQL Server. Pengoptimal tahu bahwa mereka sama. Jika Anda melihat rencana eksekusi, Anda akan melihat bahwa mereka identik.

Cade Roux
sumber
1

Secara pribadi saya merasa sangat, sangat sulit untuk percaya bahwa mereka tidak mengoptimalkan rencana kueri yang sama. Tetapi satu-satunya cara untuk mengetahui situasi khusus Anda adalah dengan mengujinya. Jika Anda melakukannya, laporkan kembali!

Larry Lustig
sumber
-1

Tidak ada perbedaan nyata tetapi mungkin ada kinerja yang sangat kecil. Sebagai aturan praktis, Anda tidak boleh meminta lebih banyak data daripada yang Anda butuhkan.

orjan
sumber