SQL Server hasil yang tidak dapat diprediksi terpilih (kesalahan dbms?)

37

Di bawah ini adalah contoh sederhana, yang mengembalikan hasil aneh, yang tidak dapat diprediksi dan kami tidak dapat menjelaskannya di tim kami. Apakah kita melakukan sesuatu yang salah atau kesalahan SQL Server?

Setelah beberapa penyelidikan, kami mengurangi area pencarian menjadi klausa gabungan dalam subquery , yang memilih satu catatan dari tabel "men"

Ini berfungsi seperti yang diharapkan dalam SQL Server 2000 (mengembalikan 12 baris), tetapi pada 2008 dan 2012 hanya mengembalikan satu baris.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Ini hanya mengembalikan satu baris: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Batalkan komentar pada baris terakhir dan isinya: 2 2 2

Ada banyak perilaku aneh:

  • Setelah serangkaian tetesan, buat, potong, dan sisipkan pada tabel "men" , terkadang berhasil (mengembalikan 12 baris)
  • Ketika Anda mengubah "union select men.wife_id" menjadi "union all select men.wife_id" atau "union select isnull (men.wife_id, null)" (!!!) ia mengembalikan 12 baris (seperti yang diharapkan).
  • Perilaku aneh tampaknya tidak terkait dengan tipe data kolom "wife_id". Kami mengamatinya pada sistem pengembangan dengan set data yang jauh lebih besar.
  • "where wife_id> 0" mengembalikan 6 baris
  • kami juga mengamati perilaku pandangan aneh dengan pernyataan semacam ini. SELECT * mengembalikan bagian dari baris, SELECT TOP 1000 mengembalikan semua
Ryszard Bocian
sumber

Jawaban:

35

Apakah kita melakukan sesuatu yang salah atau kesalahan SQL Server?

Ini adalah bug hasil yang salah, yang harus Anda laporkan melalui saluran dukungan biasa. Jika Anda tidak memiliki perjanjian dukungan, mungkin membantu untuk mengetahui bahwa insiden berbayar biasanya dikembalikan jika Microsoft mengkonfirmasi perilaku tersebut sebagai bug.

Bug ini membutuhkan tiga bahan:

  1. Loop bersarang dengan referensi luar (berlaku)
  2. Spool indeks sisi malas yang mencari referensi luar
  3. Operator Rangkaian sisi dalam

Misalnya, kueri dalam pertanyaan menghasilkan rencana seperti berikut:

Rencana beranotasi

Ada banyak cara untuk menghapus salah satu elemen ini, sehingga bug tidak lagi mereproduksi.

Misalnya, seseorang dapat membuat indeks atau statistik yang berarti pengoptimal memilih untuk tidak menggunakan Malas Indeks Spool. Atau, orang dapat menggunakan petunjuk untuk memaksa hash atau menggabungkan serikat daripada menggunakan Rangkaian. Orang juga bisa menulis ulang kueri untuk mengekspresikan semantik yang sama, tetapi yang menghasilkan bentuk rencana yang berbeda di mana satu atau lebih elemen yang diperlukan hilang.

Keterangan lebih lanjut

Spool Indeks Malas dengan malas mem-cache baris hasil sisi dalam, dalam tabel kerja yang diindeks oleh nilai referensi terluar (parameter berkorelasi). Jika Spool Indeks Malas diminta untuk referensi luar yang telah dilihat sebelumnya, itu mengambil baris hasil di-cache dari tabel kerjanya ("mundur"). Jika spool diminta untuk nilai referensi luar yang belum pernah dilihat sebelumnya, ia menjalankan subtree dengan nilai referensi luar saat ini dan menyimpan hasilnya ("rebind"). Predikat pencarian di Spool Indeks Malas menunjukkan kunci untuk tabel kerjanya.

Masalah terjadi dalam bentuk rencana khusus ini ketika spool memeriksa untuk melihat apakah referensi luar baru sama dengan yang telah dilihat sebelumnya. Nested Loops Join memperbarui referensi luarnya dengan benar, dan memberi tahu operator tentang input dalamnya melalui PrepRecomputemetode antarmuka mereka . Pada awal pemeriksaan ini, operator sisi dalam membaca CParamBounds:FNeedToReloadproperti untuk melihat apakah referensi luar telah berubah dari terakhir kali. Contoh jejak tumpukan ditampilkan di bawah ini:

CParamBounds: FNeedToReload

Ketika subtree yang ditunjukkan di atas ada, khususnya di mana Concatenation digunakan, ada yang salah (mungkin masalah ByVal / ByRef / Copy) dengan binding yang CParamBounds:FNeedToReloadselalu mengembalikan false, terlepas dari apakah referensi luar benar-benar berubah atau tidak.

Ketika subtree yang sama ada, tetapi Gabung Union atau Hash Union digunakan, properti penting ini diatur dengan benar pada setiap iterasi, dan Spool Indeks Malas menggulung atau mem-rebind setiap kali sesuai kebutuhan. Agak Mengurutkan dan Stream Agregat tidak bersalah, omong-omong. Kecurigaan saya adalah bahwa Merge dan Hash Union membuat salinan dari nilai sebelumnya, sedangkan Concatenation menggunakan referensi. Sayangnya hampir tidak mungkin untuk memverifikasi ini tanpa akses ke kode sumber SQL Server.

Hasil akhirnya adalah bahwa Spool Indeks Malas dalam bentuk rencana yang bermasalah selalu berpikir telah melihat referensi luar saat ini, mundur dengan mencari ke dalam tabel kerjanya, umumnya tidak menemukan apa pun, sehingga tidak ada baris yang dikembalikan untuk referensi luar itu. Melangkah melalui eksekusi dalam debugger, spool hanya pernah menjalankannyaRewindHelper metodenya, dan tidak pernah ReloadHelpermetodenya (reload = rebind dalam konteks ini). Ini terbukti dalam rencana eksekusi karena semua operator di bawah spool memiliki 'Jumlah Eksekusi = 1'.

RewindHelper

Pengecualian, tentu saja, adalah untuk referensi luar pertama Spool Indeks Malas diberikan. Ini selalu mengeksekusi subtree dan cache baris hasil di tabel kerja. Semua iterasi berikutnya menghasilkan mundur, yang hanya akan menghasilkan baris (baris cache tunggal) ketika iterasi saat ini memiliki nilai yang sama untuk referensi luar seperti yang pertama kali dilakukan.

Jadi, untuk input apa pun yang ditetapkan di sisi luar Nested Loops Join, kueri akan mengembalikan sebanyak mungkin baris karena ada duplikat dari baris pertama yang diproses (ditambah satu untuk baris pertama itu sendiri tentunya).

Demo

Tabel dan data sampel:

CREATE TABLE #T1 
(
    pk integer IDENTITY NOT NULL,
    c1 integer NOT NULL,

    CONSTRAINT PK_T1
    PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6);

Kueri (sepele) berikut menghasilkan hitungan yang benar dari dua untuk setiap baris (total 18) menggunakan Gabungan Gabungan:

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C;

Merge Union Plan

Jika sekarang kita menambahkan petunjuk kueri untuk memaksa Rangkaian:

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C
OPTION (CONCAT UNION);

Rencana eksekusi memiliki bentuk yang bermasalah:

Rencana penggabungan

Dan hasilnya sekarang salah, hanya tiga baris:

Hasil tiga baris

Meskipun perilaku ini tidak dijamin, baris pertama dari Pemindaian Indeks Clustered memiliki c1nilai 1. Ada dua baris lain dengan nilai ini, sehingga total tiga baris dihasilkan.

Sekarang pangkas tabel data dan muat dengan duplikat dari baris 'pertama':

TRUNCATE TABLE #T1;

INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (1), (1), (1), (1), (1);

Sekarang rencana Penggabungan adalah:

8 baris Rencana Penggabungan

Dan, seperti yang ditunjukkan, 8 baris diproduksi, semua c1 = 1tentu saja dengan:

8 hasil baris

Saya perhatikan Anda telah membuka item Connect untuk bug ini tetapi sebenarnya itu bukan tempat untuk melaporkan masalah yang memiliki dampak produksi. Jika itu masalahnya, Anda harus menghubungi Dukungan Microsoft.


Bug hasil salah ini diperbaiki pada tahap tertentu. Tidak lagi mereproduksi untuk saya di versi SQL Server mulai 2012 dan seterusnya. Itu repro pada SQL Server 2008 R2 SP3-GDR build 10.50.6560.0 (X64).

Paul White mengatakan GoFundMonica
sumber
-3

Mengapa Anda menggunakan subquery tanpa pernyataan dari? Saya pikir itu dapat menyebabkan perbedaan pada server 2005 dan 2008. Mungkin Anda bisa bergabung dengan eksplisit bergabung?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

sumber
3
Ya, ini berfungsi, tetapi versi saya juga harus berfungsi. Contoh abstrak di atas adalah versi yang lebih sederhana dari kueri produksi kami, yang jauh lebih masuk akal.