sql server: memperbarui bidang pada tabel besar dalam potongan kecil: bagaimana cara mendapatkan kemajuan / status?

10

Kami memiliki tabel (100 juta baris) yang sangat besar, dan kami perlu memperbarui beberapa bidang di atasnya.

Untuk pengiriman log, dll, kami juga, tentu saja, ingin menyimpannya untuk transaksi ukuran gigitan.

  • Akankah hal-hal di bawah ini berhasil?
  • Dan bagaimana kita bisa mencetak beberapa output, sehingga kita bisa melihat kemajuan? (kami mencoba menambahkan pernyataan PRINT di sana, tetapi tidak ada yang dihasilkan selama while)

Kode tersebut adalah:

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END
Jonesome Reinstate Monica
sumber

Jawaban:

12

Saya tidak mengetahui pertanyaan ini ketika saya menjawab pertanyaan terkait ( Apakah transaksi eksplisit diperlukan dalam loop sementara ini? ), Tetapi demi kelengkapan, saya akan membahas masalah ini di sini karena itu bukan bagian dari saran saya dalam jawaban terkait itu .

Karena saya menyarankan untuk menjadwalkan ini melalui pekerjaan SQL Agent (itu adalah 100 juta baris, setelah semua), saya tidak berpikir bahwa segala bentuk pengiriman pesan status ke klien (yaitu SSMS) akan ideal (meskipun jika itu adalah pernah dibutuhkan untuk proyek lain, maka saya setuju dengan Vladimir bahwa menggunakan RAISERROR('', 10, 1) WITH NOWAIT;adalah cara untuk pergi).

Dalam kasus khusus ini, saya akan membuat tabel status yang dapat diperbarui per setiap loop dengan jumlah baris yang diperbarui sejauh ini. Dan tidak ada salahnya untuk membuang waktu saat ini untuk memiliki detak jantung pada prosesnya.

Mengingat bahwa Anda ingin dapat membatalkan dan memulai kembali proses, Saya lelah membungkus UPDATE dari tabel utama dengan UPDATE dari tabel status dalam transaksi eksplisit. Namun, jika Anda merasa bahwa tabel status tidak sinkron karena pembatalan, mudah untuk menyegarkan dengan nilai saat ini hanya dengan memperbaruinya secara manual dengan COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL.dan ada dua tabel untuk DIPERBARUI (yaitu tabel utama dan tabel status), kita harus menggunakan transaksi eksplisit untuk menjaga kedua tabel tetap sinkron, namun kami tidak ingin mengambil risiko memiliki transaksi yatim jika Anda membatalkan proses di sebuah titik setelah memulai transaksi tetapi belum melakukan itu. Ini harus aman dilakukan selama Anda tidak menghentikan pekerjaan SQL Agent.

Bagaimana Anda bisa menghentikan proses tanpa, um, well, menghentikannya? Dengan memintanya untuk berhenti :-). Ya. Dengan mengirimkan proses "sinyal" (mirip dengan kill -3di Unix), Anda dapat meminta itu berhenti pada saat yang tepat berikutnya (yaitu ketika tidak ada transaksi aktif!) Dan membuatnya membersihkan semua yang bagus dan rapi.

Bagaimana Anda bisa berkomunikasi dengan proses yang sedang berjalan di sesi lain? Dengan menggunakan mekanisme yang sama yang kami buat untuk mengomunikasikan statusnya saat ini kembali kepada Anda: tabel status. Kita hanya perlu menambahkan kolom yang prosesnya akan memeriksa di awal setiap loop sehingga ia tahu apakah akan melanjutkan atau membatalkan. Dan karena tujuannya adalah untuk menjadwalkan ini sebagai pekerjaan SQL Agent (berjalan setiap 10 atau 20 menit), kita juga harus memeriksa di awal karena tidak ada gunanya mengisi tabel temp dengan 1 juta baris jika prosesnya hanya berjalan untuk keluar sebentar kemudian dan tidak menggunakan data itu.

DECLARE @BatchRows INT = 1000000,
        @UpdateRows INT = 4995;

IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
  CREATE TABLE dbo.HugeTable_TempStatus
  (
    RowsUpdated INT NOT NULL, -- updated by the process
    LastUpdatedOn DATETIME NOT NULL, -- updated by the process
    PauseProcess BIT NOT NULL -- read by the process
  );

  INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
  VALUES (0, GETDATE(), 0);
END;

-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
  PRINT 'Process is paused. No need to start.';
  RETURN;
END;

CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);

INSERT INTO #FullSet (KeyField1, KeyField2)
  SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
  FROM   dbo.HugeTable ht
  WHERE  ht.deleted IS NULL
  OR     ht.deletedDate IS NULL

WHILE (1 = 1)
BEGIN
  -- Check if process is paused. If yes, just exit cleanly.
  IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
  BEGIN
    PRINT 'Process is paused. Exiting.';
    BREAK;
  END;

  -- grab a set of rows to update
  DELETE TOP (@UpdateRows)
  FROM   #FullSet
  OUTPUT Deleted.KeyField1, Deleted.KeyField2
  INTO   #CurrentSet (KeyField1, KeyField2);

  IF (@@ROWCOUNT = 0)
  BEGIN
    RAISERROR(N'All rows have been updated!!', 16, 1);
    BREAK;
  END;

  BEGIN TRY
    BEGIN TRAN;

    -- do the update of the main table
    UPDATE ht
    SET    ht.deleted = 0,
           ht.deletedDate = '2000-01-01'
    FROM   dbo.HugeTable ht
    INNER JOIN #CurrentSet cs
            ON cs.KeyField1 = ht.KeyField1
           AND cs.KeyField2 = ht.KeyField2;

    -- update the current status
    UPDATE ts
    SET    ts.RowsUpdated += @@ROWCOUNT,
           ts.LastUpdatedOn = GETDATE()
    FROM   dbo.HugeTable_TempStatus ts;

    COMMIT TRAN;
  END TRY
  BEGIN CATCH
    IF (@@TRANCOUNT > 0)
    BEGIN
      ROLLBACK TRAN;
    END;

    THROW; -- raise the error and terminate the process
  END CATCH;

  -- clear out rows to update for next iteration
  TRUNCATE TABLE #CurrentSet;

  WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;

-- clean up temp tables when testing
-- DROP TABLE #FullSet; 
-- DROP TABLE #CurrentSet; 

Anda kemudian dapat memeriksa status kapan saja menggunakan kueri berikut:

SELECT sp.[rows] AS [TotalRowsInTable],
       ts.RowsUpdated,
       (sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
       ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE  sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND    sp.[index_id] < 2;

Ingin menghentikan proses, apakah itu berjalan dalam pekerjaan SQL Agent atau bahkan dalam SSMS di komputer orang lain? Lari saja:

UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;

Ingin proses untuk dapat memulai kembali? Lari saja:

UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;

MEMPERBARUI:

Berikut adalah beberapa hal tambahan untuk dicoba yang dapat meningkatkan kinerja operasi ini. Tidak ada yang dijamin untuk membantu tetapi mungkin layak untuk diuji. Dan dengan 100 juta baris untuk diperbarui, Anda punya banyak waktu / kesempatan untuk menguji beberapa variasi ;-).

  1. Tambahkan TOP (@UpdateRows)ke kueri UPDATE sehingga baris teratas terlihat seperti:
    UPDATE TOP (@UpdateRows) ht
    Terkadang ini membantu pengoptimal untuk mengetahui berapa banyak baris maks akan terpengaruh sehingga tidak membuang waktu mencari lebih.
  2. Tambahkan KUNCI UTAMA ke #CurrentSettabel sementara. Idenya di sini adalah untuk membantu pengoptimal dengan BERGABUNG ke tabel 100 juta baris.

    Dan hanya untuk menyatakan agar tidak ambigu, seharusnya tidak ada alasan untuk menambahkan PK ke #FullSettabel sementara karena itu hanya tabel antrian sederhana di mana urutannya tidak relevan.

  3. Dalam beberapa kasus, membantu menambahkan Indeks Filter untuk membantu SELECTumpan yang masuk ke #FullSettabel temp. Berikut adalah beberapa pertimbangan terkait dengan menambahkan indeks seperti itu:
    1. Kondisi WHERE harus cocok dengan kondisi WHERE dari permintaan Anda WHERE deleted is null or deletedDate is null
    2. Pada awal proses, sebagian besar baris akan cocok dengan kondisi WHERE Anda, jadi indeks tidak terlalu membantu. Anda mungkin ingin menunggu hingga sekitar 50% sebelum menambahkan ini. Tentu saja, seberapa besar hal itu membantu dan kapan yang terbaik untuk menambahkan indeks bervariasi karena beberapa faktor, jadi ini sedikit trial and error.
    3. Anda mungkin harus secara manual MEMPERBARUI STATS dan / atau MEMBANGUN KEMBALI indeks untuk tetap up to date karena data dasar berubah cukup sering
    4. Pastikan untuk mengingat bahwa indeks, sambil membantu SELECT, akan menyakiti UPDATEkarena itu adalah objek lain yang harus diperbarui selama operasi itu, maka lebih banyak I / O. Ini berperan baik menggunakan Filtered Indexed (yang menyusut saat Anda memperbarui baris karena lebih sedikit baris cocok dengan filter), dan menunggu sebentar untuk menambahkan indeks (jika itu tidak akan sangat membantu di awal, maka tidak ada alasan untuk mengeluarkan I / O tambahan).
Solomon Rutzky
sumber
1
Ini luar biasa. Saya menjalankannya sekarang, dan merokok bahwa kita dapat menjalankannya secara online, pada siang hari. Terima kasih!
Jonesome Reinstate Monica
@samsmith Silakan lihat bagian PEMBARUAN yang baru saja saya tambahkan karena ada beberapa ide untuk berpotensi membuat proses ini bekerja lebih cepat.
Solomon Rutzky
Tanpa peningkatan UPDATE, kami mendapatkan sekitar 8 juta pembaruan / jam ... dengan @BatchRows diatur ke 10000000 (sepuluh juta)
Jonesome Reinstate Monica
@ tukang kayu Itu bagus :) benar? Ingat dua hal: 1) Proses akan melambat karena ada lebih sedikit dan lebih sedikit baris yang cocok dengan klausa WHERE, karenanya mengapa ini saat yang tepat untuk menambahkan indeks yang difilter, tetapi Anda sudah menambahkan indeks yang tidak difilter di mulailah jadi saya tidak yakin apakah itu akan membantu atau menyakiti, tapi tetap saya akan mengharapkan throughput berkurang karena semakin dekat untuk dilakukan, dan 2) Anda dapat meningkatkan throughput dengan mengurangi WAITFOR DELAYsetengah atau lebih, tapi itu adalah trade-off dengan konkurensi dan mungkin berapa banyak dikirim melalui pengiriman log.
Solomon Rutzky
Kami senang dengan 8 juta baris / jam. Ya, kita bisa melihatnya melambat. Kami ragu untuk membuat indeks lagi (karena tabel dikunci untuk seluruh bangunan). Apa yang telah kami lakukan beberapa kali adalah melakukan reorg pada indeks yang ada (karena itu on line).
Jonesome Reinstate Monica
4

Menjawab bagian kedua: bagaimana cara mencetak beberapa output selama loop.

Saya memiliki beberapa prosedur pemeliharaan yang berjalan lama yang terkadang harus dijalankan oleh sys admin.

Saya menjalankannya dari SSMS dan juga memperhatikan bahwa PRINTpernyataan ditampilkan dalam SSMS hanya setelah seluruh prosedur selesai.

Jadi, saya menggunakan RAISERRORdengan tingkat keparahan rendah:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

Saya menggunakan SQL Server 2008 Standard dan SSMS 2012 (11.0.3128.0). Berikut ini adalah contoh kerja lengkap untuk dijalankan di SSMS:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

Ketika saya berkomentar RAISERRORdan hanya meninggalkan PRINTpesan di tab Pesan di SSMS hanya muncul setelah seluruh batch selesai, setelah 6 detik.

Ketika saya berkomentar PRINTdan menggunakan RAISERRORpesan di tab Pesan di SSMS, muncul tanpa menunggu selama 6 detik, tetapi seiring berjalannya waktu.

Menariknya, ketika saya menggunakan keduanya RAISERRORdan PRINT, saya melihat kedua pesan tersebut. Pesan pertama datang dari pertama RAISERROR, kemudian tunda selama 2 detik, lalu pertama PRINTdan kedua RAISERROR, dan seterusnya.


Dalam kasus lain saya menggunakan logtabel khusus yang terpisah dan cukup memasukkan baris ke tabel dengan beberapa informasi yang menggambarkan keadaan saat ini dan cap waktu dari proses jangka panjang.

Sementara proses panjang berjalan saya secara berkala SELECTdari logmeja untuk melihat apa yang terjadi.

Ini jelas memiliki overhead tertentu, tetapi meninggalkan log (atau sejarah log) yang dapat saya periksa dengan langkah saya sendiri nanti.

Vladimir Baranov
sumber
Pada SQL 2008/2014, kita tidak dapat melihat hasil dari raiserror .... apa yang kita lewatkan?
Jonesome Reinstate Monica
@samsmith, saya menambahkan contoh lengkap. Cobalah. Perilaku apa yang Anda dapatkan dalam contoh sederhana ini?
Vladimir Baranov
2

Anda dapat memantaunya dari koneksi lain dengan sesuatu seperti:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

untuk melihat berapa banyak yang tersisa untuk dilakukan. Ini bisa berguna jika suatu aplikasi memanggil proses, daripada menjalankannya secara manual di SSMS atau serupa, dan perlu menunjukkan kemajuan: jalankan proses utama secara tidak sinkron (atau di utas lain) dan kemudian loop memanggil "berapa banyak yang tersisa" "periksa setiap saat sampai panggilan async (atau utas) selesai.

Menetapkan level isolasi serendah mungkin berarti bahwa ini harus kembali dalam waktu yang wajar tanpa terjebak di belakang transaksi utama karena masalah penguncian. Itu bisa berarti nilai yang dikembalikan sedikit tidak akurat tentu saja, tetapi sebagai pengukur kemajuan sederhana ini seharusnya tidak masalah sama sekali.

David Spillett
sumber