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

Bersenang-senang dengan kompresi (penyimpanan kolom) di atas meja yang sangat besar – bagian 3

[ Bagian 1 | Bagian 2 | Bagian 3 ]

Di bagian 1 dari seri ini, saya mencoba beberapa cara untuk mengompresi tabel 1TB. Sementara saya mendapatkan hasil yang layak dalam upaya pertama saya, saya ingin melihat apakah saya dapat meningkatkan kinerja di bagian 2. Di sana saya menguraikan beberapa hal yang saya pikir mungkin menjadi masalah kinerja, dan menjelaskan bagaimana saya akan lebih baik mempartisi tabel tujuan untuk kompresi penyimpanan kolom yang optimal. Saya sudah:

  • mempartisi tabel menjadi 8 partisi (satu per inti);
  • meletakkan file data setiap partisi pada filegroupnya sendiri; dan,
  • mengatur kompresi arsip pada semua kecuali partisi "aktif".

Saya masih perlu membuatnya agar setiap penjadwal menulis secara eksklusif ke partisinya sendiri.

Pertama, saya perlu membuat perubahan pada tabel batch yang saya buat. Saya memerlukan kolom untuk menyimpan jumlah baris yang ditambahkan per batch (semacam pemeriksaan kewarasan yang mengaudit sendiri), dan waktu mulai/berakhir untuk mengukur kemajuan.

ALTER TABLE dbo.BatchQueue ADD 
  RowsAdded int,
  StartTime datetime2, 
  EndTime   datetime2;

Selanjutnya, saya perlu membuat tabel untuk menyediakan afinitas – kami tidak pernah ingin lebih dari satu proses berjalan pada penjadwal apa pun, bahkan jika itu berarti kehilangan waktu untuk mencoba kembali logika. Jadi, kita memerlukan tabel yang akan melacak sesi apa pun pada penjadwal tertentu dan mencegah penumpukan:

CREATE TABLE dbo.OpAffinity
(
  SchedulerID int NOT NULL,
  SessionID   int NULL,
  CONSTRAINT  PK_OpAffinity PRIMARY KEY CLUSTERED (SchedulerID)
);

Idenya adalah saya akan memiliki delapan contoh aplikasi (SQLQueryStress) yang masing-masing akan berjalan pada penjadwal khusus, hanya menangani data yang ditujukan untuk partisi / filegroup / file data tertentu, ~ 100 juta baris sekaligus (klik untuk memperbesar) :

Aplikasi 1 mendapatkan penjadwal 0 dan menulis ke partisi 1 pada filegroup 1, dan seterusnya …

Selanjutnya kita memerlukan prosedur tersimpan yang akan memungkinkan setiap instance aplikasi untuk memesan waktu pada satu penjadwal. Seperti yang saya sebutkan di posting sebelumnya, ini bukan ide asli saya (dan saya tidak akan pernah menemukannya di panduan itu jika bukan karena Joe Obbish). Berikut adalah prosedur yang saya buat di Utility :

CREATE PROCEDURE dbo.DoMyBatch
  @PartitionID   int,    -- pass in 1 through 8
  @BatchID       int     -- pass in 1 through 4
AS
BEGIN
  DECLARE @BatchSize       bigint, 
          @MinID           bigint, 
          @MaxID           bigint, 
          @rc              bigint,
          @ThisSchedulerID int = 
          (
            SELECT scheduler_id 
	      FROM sys.dm_exec_requests 
    	      WHERE session_id = @@SPID
          );
 
  -- try to get the requested scheduler, 0-based
  IF @ThisSchedulerID <> @PartitionID - 1 
  BEGIN
    -- surface the scheduler we got to the application, but force a delay
    RAISERROR('Got wrong scheduler %d.', 11, 1, @ThisSchedulerID);
    WAITFOR DELAY '00:00:05';
    RETURN -3;
  END
  ELSE
  BEGIN
    -- we are on our scheduler, now serializibly make sure we're exclusive
    INSERT Utility.dbo.OpAffinity(SchedulerID, SessionID)
      SELECT @ThisSchedulerID, @@SPID
        WHERE NOT EXISTS 
        (
          SELECT 1 FROM Utility.dbo.OpAffinity WITH (TABLOCKX) 
            WHERE SchedulerID = @ThisSchedulerID
        );
 
    -- if someone is already using this scheduler, raise roar:
    IF @@ROWCOUNT <> 1
    BEGIN
      RAISERROR('Wrong scheduler %d, try again.',11,1,@ThisSchedulerID) WITH NOWAIT;
      RETURN @ThisSchedulerID;
    END
 
    -- checkpoint twice to clear log
    EXEC OCopy.sys.sp_executesql N'CHECKPOINT; CHECKPOINT;';
 
    -- get our range of rows for the current batch
    SELECT @MinID = MinID, @MaxID = MaxID
      FROM Utility.dbo.BatchQueue 
      WHERE PartitionID = @PartitionID
        AND BatchID = @BatchID
        AND StartTime IS NULL;
 
    -- if we couldn't get a row here, must already be done:
    IF @@ROWCOUNT <> 1
    BEGIN
      RAISERROR('Already done.', 11, 1) WITH NOWAIT;
      RETURN -1;
    END
 
    -- update the BatchQueue table to indicate we've started:
    UPDATE msdb.dbo.BatchQueue 
      SET StartTime = sysdatetime(), EndTime = NULL
      WHERE PartitionID = @PartitionID
        AND BatchID = @BatchID;
 
    -- do the work - copy from Original to Partitioned
    INSERT OCopy.dbo.tblPartitionedCCI 
      SELECT * FROM OCopy.dbo.tblOriginal AS o
        WHERE o.CostID >= @MinID AND o.CostID <= @MaxID
        OPTION (MAXDOP 1); -- don't want parallelism here!
 
    /*
        You might think, don't I want a TABLOCK hint on the insert, 
        to benefit from minimal logging? I thought so too, but while 
        this leads to a BULK UPDATE lock on rowstore tables, it is a 
        TABLOCKX with columnstore. This isn't going to work well if 
        we want to have multiple processes inserting into separate 
        partitions simultaneously. We need a PARTITIONLOCK hint!
    */
 
    SET @rc = @@ROWCOUNT;
 
    -- update BatchQueue that we've finished and how many rows:
    UPDATE Utility.dbo.BatchQueue 
      SET EndTime = sysdatetime(), RowsAdded = @rc
      WHERE PartitionID = @PartitionID
        AND BatchID = @BatchID;
 
    -- remove our lock to this scheduler:
    DELETE Utility.dbo.OpAffinity 
      WHERE SchedulerID = @ThisSchedulerID 
        AND SessionID = @@SPID;
  END
END

Sederhana, bukan? Jalankan 8 instance SQLQueryStress, dan masukkan batch ini ke masing-masing:

EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 1;
EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 2;
EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 3;
EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 4;

Paralelisme orang malang

Kecuali itu tidak sesederhana itu, karena tugas scheduler seperti sekotak coklat. Butuh banyak percobaan untuk mendapatkan setiap contoh aplikasi pada penjadwal yang diharapkan; Saya akan memeriksa pengecualian pada setiap contoh aplikasi yang diberikan, dan mengubah PartitionID untuk mencocokkan. Inilah sebabnya saya menggunakan lebih dari satu iterasi (tetapi saya masih hanya menginginkan satu utas per instance). Sebagai contoh, instance aplikasi ini diharapkan berada di scheduler 3, tetapi mendapat scheduler 4:

Jika pada awalnya Anda tidak berhasil…

Saya mengubah 3s di jendela kueri menjadi 4s, dan mencoba lagi. Jika saya cepat, tugas penjadwal cukup "lengket" sehingga akan mengambilnya dengan benar dan mulai menenggak. Tapi saya tidak selalu cukup cepat, jadi itu seperti memukul-a-mol untuk pergi. Saya mungkin bisa merancang rutinitas coba ulang/loop yang lebih baik untuk membuat pekerjaan lebih sedikit manual di sini, dan mempersingkat penundaan sehingga saya langsung tahu apakah itu berhasil atau tidak, tetapi ini cukup baik untuk kebutuhan saya. Itu juga membuat waktu mulai yang mengejutkan secara tidak sengaja untuk setiap proses, saran lain dari Mr. Obbish.

Pemantauan

Saat salinan affinitized sedang berjalan, saya bisa mendapatkan petunjuk tentang status saat ini dengan dua pertanyaan berikut:

SELECT r.session_id, r.[status], r.scheduler_id, partition_id = o.SchedulerID + 1, 
  r.logical_reads, r.total_elapsed_time, r.last_wait_type, longest_wait_type = 
  (
    SELECT TOP (1) wait_type 
      FROM sys.dm_exec_session_wait_stats
      WHERE session_id = r.session_id AND wait_type <> 'WAITFOR' 
      ORDER BY wait_time_ms - signal_wait_time_ms DESC
  )
  FROM sys.dm_exec_requests AS r 
  INNER JOIN Utility.dbo.OpAffinity AS o
      ON o.SessionID = r.session_id
  WHERE r.command = N'INSERT'
  ORDER BY r.scheduler_id;
 
SELECT SchedulerID = PartitionID - 1, Duration = DATEDIFF(SECOND, StartTime, EndTime), *
  FROM Utility.dbo.BatchQueue WITH (NOLOCK) 
  WHERE StartTime IS NOT NULL -- AND EndTime IS NULL
  ORDER BY PartitionID;

Jika saya melakukan semuanya dengan benar, kedua kueri akan mengembalikan 8 baris, dan menunjukkan peningkatan pembacaan logis dan durasi. Jenis tunggu akan dibalik antara PAGEIOLATCH_SH , SOS_SCHEDULER_YIELD , dan terkadang RESERVED_MEMORY_ALLOCATION_EXT. Ketika batch selesai (saya dapat meninjau ini dengan membatalkan komentar -- AND EndTime IS NULL , saya akan mengonfirmasi bahwa RowsAdded = RowsInRange .

Setelah semua 8 contoh SQLQueryStress selesai, saya bisa melakukan SELECT INTO <newtable> FROM dbo.BatchQueue untuk mencatat hasil akhir untuk analisis nanti.

Pengujian Lainnya

Selain menyalin data ke indeks penyimpanan kolom berkerumun terpartisi yang sudah ada, menggunakan afinitas, saya juga ingin mencoba beberapa hal lain:

  • Menyalin data ke tabel baru tanpa mencoba mengontrol afinitas. Saya mengeluarkan logika afinitas dari prosedur dan membiarkan seluruh hal "harap-Anda-mendapatkan-penjadwal-yang-benar" secara kebetulan. Ini membutuhkan waktu lebih lama karena, tentu saja, penumpukan penjadwal melakukannya terjadi. Misalnya, pada titik spesifik ini, penjadwal 3 menjalankan dua proses, sementara penjadwal 0 tidak aktif mengambil istirahat makan siang:

    Di mana Anda, penjadwal nomor 0?

  • Menerapkan laman atau baris kompresi (baik online/offline) ke sumber sebelum salinan affinitized (offline), untuk melihat apakah mengompresi data terlebih dahulu dapat mempercepat tujuan. Perhatikan bahwa penyalinan juga dapat dilakukan secara online, tetapi, seperti int Andy Andy Mallon ke bigint konversi, itu membutuhkan beberapa senam. Perhatikan bahwa dalam kasus ini kami tidak dapat memanfaatkan afinitas CPU (meskipun kami dapat melakukannya jika tabel sumber sudah dipartisi). Saya pintar dan mengambil cadangan dari sumber aslinya, dan membuat prosedur untuk mengembalikan database kembali ke keadaan awal. Jauh lebih cepat dan lebih mudah daripada mencoba kembali ke keadaan tertentu secara manual.

    -- refresh source, then do page online:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = PAGE, ONLINE = ON);
    -- then run SQLQueryStress
     
    -- refresh source, then do page offline:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = PAGE, ONLINE = OFF);
    -- then run SQLQueryStress
     
    -- refresh source, then do row online:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = ROW, ONLINE = ON);
    -- then run SQLQueryStress
     
    -- refresh source, then do row offline:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = ROW, ONLINE = OFF);
    -- then run SQLQueryStress
  • Dan terakhir, membangun kembali indeks berkerumun ke skema partisi terlebih dahulu, lalu membangun indeks penyimpanan kolom tergugus di atasnya. Kelemahan dari yang terakhir adalah, di SQL Server 2017, Anda tidak dapat menjalankan ini secara online… tetapi Anda akan dapat melakukannya pada tahun 2019.

    Di sini kita perlu menghilangkan batasan PK terlebih dahulu; Anda tidak dapat menggunakan DROP_EXISTING , karena batasan unik asli tidak dapat diterapkan oleh indeks clustered columnstore, dan Anda tidak dapat mengganti indeks clustered unik dengan indeks clustered non-unik.

    Msg 1907, Level 16, Status 1
    Tidak dapat membuat ulang indeks 'pk_tblOriginal'. Definisi indeks baru tidak cocok dengan batasan yang diberlakukan oleh indeks yang ada.

    Semua detail ini menjadikan ini proses tiga langkah, hanya langkah kedua online. Langkah pertama saya hanya secara eksplisit menguji OFFLINE; yang berjalan dalam tiga menit, sementara ONLINE Saya berhenti setelah 15 menit. Salah satu hal yang mungkin tidak boleh menjadi operasi ukuran data dalam kedua kasus tersebut, tetapi saya akan membiarkannya di lain hari.

    ALTER TABLE dbo.tblOriginal DROP CONSTRAINT PK_tblOriginal WITH (ONLINE = OFF);
    GO
     
    CREATE CLUSTERED INDEX CCI_tblOriginal -- yes, a bad name, but only temporarily
      ON dbo.tblOriginal(OID)
      WITH (ONLINE = ON)
      ON PS_OID (OID); -- this moves the data
     
     
    CREATE CLUSTERED COLUMNSTORE INDEX CCI_tblOriginal
      ON dbo.tblOriginal
      WITH                 
      (
        DROP_EXISTING = ON,
        DATA_COMPRESSION = COLUMNSTORE_ARCHIVE ON PARTITIONS (1 TO 7),
        DATA_COMPRESSION = COLUMNSTORE ON PARTITIONS (8)
        -- in 2019, CCI can be ONLINE = ON as well
      )
      ON PS_OID (OID);
    GO

Hasil

Pengaturan waktu dan tingkat kompresi:

Beberapa opsi lebih baik daripada yang lain

Perhatikan bahwa saya membulatkan ke GB karena akan ada perbedaan kecil dalam ukuran akhir setiap kali dijalankan, bahkan dengan menggunakan teknik yang sama. Juga, pengaturan waktu untuk metode afinitas didasarkan pada rata-rata runtime penjadwal/batch individual, karena beberapa penjadwal selesai lebih cepat daripada yang lain.

Sulit untuk membayangkan gambaran yang tepat dari spreadsheet seperti yang ditunjukkan, karena beberapa tugas memiliki ketergantungan, jadi saya akan mencoba menampilkan info sebagai garis waktu dan menunjukkan berapa banyak kompresi yang Anda dapatkan dibandingkan dengan waktu yang dihabiskan:

Waktu yang dihabiskan (menit) vs. laju kompresi

Beberapa pengamatan dari hasil, dengan peringatan bahwa data Anda mungkin dikompres secara berbeda (dan bahwa operasi online hanya berlaku untuk Anda jika Anda menggunakan Edisi Perusahaan):

  • Jika prioritas Anda adalah menghemat ruang secepat mungkin , taruhan terbaik Anda adalah menerapkan kompresi baris di tempatnya. Jika Anda ingin meminimalkan gangguan, gunakan online; jika Anda ingin mengoptimalkan kecepatan, gunakan offline.
  • Jika Anda ingin memaksimalkan kompresi tanpa gangguan , Anda dapat mendekati pengurangan penyimpanan 90% tanpa gangguan sama sekali, menggunakan kompresi halaman online.
  • Jika ingin memaksimalkan kompresi dan gangguan boleh saja , salin data ke versi tabel baru yang dipartisi, dengan indeks penyimpanan kolom tergugus, dan gunakan proses afinitas yang dijelaskan di atas untuk memigrasikan data. (Dan sekali lagi, Anda dapat menghilangkan gangguan ini jika Anda adalah perencana yang lebih baik dari saya.)

Opsi terakhir bekerja paling baik untuk skenario saya, meskipun kita masih harus mengatasi beban kerja (ya, jamak). Perhatikan juga bahwa di SQL Server 2019 teknik ini mungkin tidak bekerja dengan baik, tetapi Anda dapat membuat indeks toko kolom terklaster secara online di sana, jadi mungkin tidak terlalu penting.

Beberapa dari pendekatan ini mungkin lebih atau kurang dapat diterima oleh Anda, karena Anda mungkin lebih menyukai "tetap tersedia" daripada "menyelesaikan secepat mungkin", atau "meminimalkan penggunaan disk" daripada "tetap tersedia", atau hanya menyeimbangkan kinerja baca dan tulis. .

Jika Anda ingin detail lebih lanjut tentang aspek apa pun dari ini, tanyakan saja. Saya memangkas beberapa lemak untuk menyeimbangkan detail dengan daya cerna, dan saya telah salah tentang keseimbangan itu sebelumnya. Pikiran perpisahan adalah bahwa saya ingin tahu seberapa linier ini – kami memiliki tabel lain dengan struktur serupa yang lebih dari 25 TB, dan saya ingin tahu apakah kami dapat membuat dampak serupa di sana. Sampai saat itu, selamat mengompres!

[ Bagian 1 | Bagian 2 | Bagian 3 ]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Bagaimana cara membuat kolom unik dalam SQL?

  2. Memulihkan Contoh DW Database AdventureWorksDW2019

  3. Bagian Dalam DENGAN ENKRIPSI

  4. Pendekatan terbaik untuk total lari berkelompok

  5. Menghubungkan ke Sage dari Jawa