Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Rangkaian yang Dikelompokkan:Memesan dan Menghapus Duplikat

Dalam posting terakhir saya, saya menunjukkan beberapa pendekatan yang efisien untuk penggabungan yang dikelompokkan. Kali ini, saya ingin berbicara tentang beberapa aspek tambahan dari masalah ini yang dapat kita selesaikan dengan mudah dengan FOR XML PATH pendekatan:memesan daftar, dan menghapus duplikat.

Ada beberapa cara yang saya lihat orang-orang menginginkan daftar yang dipisahkan koma untuk diurutkan. Terkadang mereka ingin item dalam daftar diurutkan menurut abjad; Saya sudah menunjukkannya di posting saya sebelumnya. Tapi terkadang mereka ingin itu diurutkan berdasarkan beberapa atribut lain yang sebenarnya tidak diperkenalkan di output; misalnya, mungkin saya ingin mengurutkan daftar berdasarkan item terbaru terlebih dahulu. Mari kita ambil contoh sederhana, di mana kita memiliki tabel Karyawan dan tabel CoffeeOrders. Mari kita isi pesanan satu orang selama beberapa hari:

CREATE TABLE dbo.Employees
(
  EmployeeID INT PRIMARY KEY,
  Name NVARCHAR(128)
);
 
INSERT dbo.Employees(EmployeeID, Name) VALUES(1, N'Jack');
 
CREATE TABLE dbo.CoffeeOrders
(
  EmployeeID INT NOT NULL REFERENCES dbo.Employees(EmployeeID),
  OrderDate DATE NOT NULL,
  OrderDetails NVARCHAR(64)
);
 
INSERT dbo.CoffeeOrders(EmployeeID, OrderDate, OrderDetails)
  VALUES(1,'20140801',N'Large double double'),
        (1,'20140802',N'Medium double double'),
        (1,'20140803',N'Large Vanilla Latte'),
        (1,'20140804',N'Medium double double');

Jika kita menggunakan pendekatan yang ada tanpa menentukan ORDER BY , kami mendapatkan urutan arbitrer (dalam hal ini, kemungkinan besar Anda akan melihat baris dalam urutan yang dimasukkan, tetapi jangan bergantung padanya dengan kumpulan data yang lebih besar, lebih banyak indeks, dll.):

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Hasil (ingat, Anda mungkin mendapatkan hasil *berbeda* kecuali Anda menentukan ORDER BY ):

Nama | Pesanan
Jack | Double besar, Double sedang, Latte Vanilla Besar, Double sedang

Jika kita ingin mengurutkan daftar berdasarkan abjad, caranya sederhana; kita tinggal menambahkan ORDER BY c.OrderDetails :

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  ORDER BY c.OrderDetails  -- only change
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Hasil:

Nama | Pesanan
Jack | Double double besar, Latte Vanilla Besar, Double sedang, Double sedang, Double sedang

Kami juga dapat memesan berdasarkan kolom yang tidak muncul di kumpulan hasil; misalnya kita bisa memesan kopi berdasarkan pesanan kopi terbaru dulu:

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  ORDER BY c.OrderDate DESC  -- only change
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Hasil:

Nama | Pesanan
Jack | Double sedang, Latte Vanilla Besar, Double sedang, Double besar, Double besar

Hal lain yang sering ingin kita lakukan adalah menghapus duplikat; lagi pula, ada sedikit alasan untuk melihat "Medium double double" dua kali. Kita bisa menghilangkannya dengan menggunakan GROUP BY :

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  GROUP BY c.OrderDetails  -- removed ORDER BY and added GROUP BY here
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Sekarang, *kebetulan* ini mengurutkan output berdasarkan abjad, tetapi sekali lagi Anda tidak dapat mengandalkan ini:

Nama | Pesanan
Jack | Double double besar, Large Vanilla Latte, Double double sedang

Jika Anda ingin menjamin bahwa memesan dengan cara ini, Anda cukup menambahkan ORDER BY lagi:

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  GROUP BY c.OrderDetails
  ORDER BY c.OrderDetails  -- added ORDER BY
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Hasilnya sama (tapi saya ulangi, ini hanya kebetulan dalam kasus ini; jika Anda menginginkan pesanan ini, selalu katakan demikian):

Nama | Pesanan
Jack | Double double besar, Large Vanilla Latte, Double double sedang

Tetapi bagaimana jika kita ingin menghilangkan duplikat *dan* mengurutkan daftar berdasarkan pesanan kopi terbaru terlebih dahulu? Kecenderungan pertama Anda mungkin untuk mempertahankan GROUP BY dan cukup ubah ORDER BY , seperti ini:

SELECT e.Name, Orders = STUFF((SELECT N', ' + c.OrderDetails
  FROM dbo.CoffeeOrders AS c
  WHERE c.EmployeeID = e.EmployeeID
  GROUP BY c.OrderDetails
  ORDER BY c.OrderDate DESC  -- changed ORDER BY
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Itu tidak akan berhasil, karena OrderDate tidak dikelompokkan atau dikumpulkan sebagai bagian dari kueri:

Msg 8127, Level 16, State 1, Line 64
Kolom "dbo.CoffeeOrders.OrderDate" tidak valid dalam klausa ORDER BY karena tidak terkandung dalam fungsi agregat atau klausa GROUP BY.

Solusinya, yang memang membuat kueri sedikit lebih buruk, adalah mengelompokkan pesanan secara terpisah terlebih dahulu, lalu hanya mengambil baris dengan tanggal maksimum untuk pesanan kopi tersebut per karyawan:

;WITH grouped AS
(
  SELECT EmployeeID, OrderDetails, OrderDate = MAX(OrderDate)
   FROM dbo.CoffeeOrders
   GROUP BY EmployeeID, OrderDetails
)
SELECT e.Name, Orders = STUFF((SELECT N', ' + g.OrderDetails
  FROM grouped AS g
  WHERE g.EmployeeID = e.EmployeeID
  ORDER BY g.OrderDate DESC
  FOR XML PATH, TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.Employees AS e
GROUP BY e.EmployeeID, e.Name;

Hasil:

Nama | Pesanan
Jack | Double double sedang, Latte Vanilla Besar, Double double besar

Ini menyelesaikan kedua tujuan kami:kami telah menghilangkan duplikat, dan kami telah mengurutkan daftar dengan sesuatu yang sebenarnya tidak ada dalam daftar.

Kinerja

Anda mungkin bertanya-tanya seberapa buruk kinerja metode ini terhadap kumpulan data yang lebih kuat. Saya akan mengisi tabel kami dengan 100.000 baris, melihat bagaimana mereka melakukannya tanpa indeks tambahan, dan kemudian menjalankan kueri yang sama lagi dengan sedikit penyetelan indeks untuk mendukung kueri kami. Jadi pertama, dapatkan 100.000 baris yang tersebar di 1.000 karyawan:

-- clear out our tiny sample data
DELETE dbo.CoffeeOrders;
DELETE dbo.Employees;
 
-- create 1000 fake employees
INSERT dbo.Employees(EmployeeID, Name) 
SELECT TOP (1000) 
  EmployeeID = ROW_NUMBER() OVER (ORDER BY t.[object_id]),
  Name = LEFT(t.name + c.name, 128)
FROM sys.all_objects AS t
INNER JOIN sys.all_columns AS c
ON t.[object_id] = c.[object_id];
 
-- create 100 fake coffee orders for each employee
-- we may get duplicates in here for name
INSERT dbo.CoffeeOrders(EmployeeID, OrderDate, OrderDetails)
SELECT e.EmployeeID, 
  OrderDate = DATEADD(DAY, ROW_NUMBER() OVER 
    (PARTITION BY e.EmployeeID ORDER BY c.[guid]), '20140630'),
  LEFT(c.name, 64)
 FROM dbo.Employees AS e
 CROSS APPLY 
 (
   SELECT TOP (100) name, [guid] = NEWID() 
     FROM sys.all_columns 
     WHERE [object_id] < e.EmployeeID
     ORDER BY NEWID()
 ) AS c;

Sekarang mari kita jalankan setiap kueri kita dua kali, dan lihat seperti apa waktunya pada percobaan kedua (kita akan mengambil lompatan keyakinan di sini, dan berasumsi bahwa – di dunia yang ideal – kita akan bekerja dengan cache prima ). Saya menjalankan ini di SQL Sentry Plan Explorer, karena ini adalah cara termudah yang saya ketahui dari waktu ke waktu dan membandingkan sekelompok kueri individual:

Durasi dan metrik waktu proses lainnya untuk pendekatan FOR XML PATH yang berbeda

Pengaturan waktu ini (durasi dalam milidetik) sebenarnya tidak terlalu buruk sama sekali IMHO, ketika Anda memikirkan apa yang sebenarnya dilakukan di sini. Rencana yang paling rumit, setidaknya secara visual, tampaknya adalah rencana tempat kami menghapus duplikat dan mengurutkan berdasarkan urutan terbaru:

Rencana eksekusi untuk kueri yang dikelompokkan dan diurutkan

Tetapi bahkan operator yang paling mahal di sini – fungsi bernilai tabel XML – tampaknya semuanya adalah CPU (walaupun saya akan dengan bebas mengakui bahwa saya tidak yakin berapa banyak pekerjaan sebenarnya yang diekspos dalam detail rencana kueri):

Properti operator untuk fungsi bernilai tabel XML

"Semua CPU" biasanya baik-baik saja, karena sebagian besar sistem terikat I/O dan/atau terikat memori, bukan terikat CPU. Seperti yang sering saya katakan, di sebagian besar sistem saya akan menukar beberapa ruang kepala CPU saya dengan memori atau disk setiap hari dalam seminggu (salah satu alasan saya menyukai OPTION (RECOMPILE) sebagai solusi untuk masalah sniffing parameter yang meluas).

Karena itu, saya sangat menganjurkan Anda untuk menguji pendekatan ini terhadap hasil serupa yang bisa Anda dapatkan dari pendekatan CLR GROUP_CONCAT di CodePlex, serta melakukan agregasi dan pengurutan pada tingkat presentasi (terutama jika Anda menyimpan data yang dinormalisasi dalam beberapa jenis lapisan cache).


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL CASE:Ketahui dan Hindari 3 Kerumitan yang Kurang Diketahui

  2. Cara Mengatur Ulang Kata Sandi Pengguna Master Amazon RDS

  3. SQL Cross Gabung

  4. Apa batasan SQL dan jenisnya yang berbeda?

  5. Menyesuaikan Pasokan Dengan Permintaan — Solusi, Bagian 1