it-swarm.asia

Melewati parameter array ke prosedur tersimpan

Saya punya proses yang mengambil banyak catatan (1000-an) dan beroperasi pada mereka, dan ketika saya selesai, saya perlu menandai sejumlah besar dari mereka sebagai diproses. Saya dapat menunjukkan ini dengan daftar besar ID. Saya mencoba untuk menghindari pola "pembaruan dalam satu lingkaran", jadi saya ingin menemukan cara yang lebih efisien untuk mengirim tas ID ini ke dalam proc MS SQL Server 2008 yang disimpan.

Proposal # 1 - Parameter Tabel Bernilai. Saya bisa mendefinisikan tipe tabel tanpa bidang ID dan mengirim tabel yang penuh dengan ID untuk memperbarui.

Proposal # 2 - Parameter XML (varchar) dengan OPENXML () di badan proc.

Proposal # 3 - Daftar parsing. Saya lebih suka menghindari ini, jika mungkin, karena tampaknya sulit dan rawan kesalahan.

Adakah preferensi di antara ini, atau ide yang saya lewatkan?

54
D. Lambert

Artikel terbaik yang pernah ada tentang masalah ini adalah oleh Erland Sommarskog:

Dia mencakup semua opsi dan menjelaskan dengan cukup baik.

Maaf atas jawaban yang singkat, tetapi artikel Erland tentang Array adalah seperti buku-buku Joe Celko tentang pohon dan suguhan SQL lainnya :)

44
Marian

Ada diskusi hebat tentang ini pada StackOverflow yang mencakup banyak pendekatan. Yang saya sukai untuk SQL Server 2008+ adalah untuk menggunakan parameter tabel-nilai. Ini pada dasarnya adalah solusi SQL Server untuk masalah Anda - mengirimkan daftar nilai ke prosedur tersimpan.

Kelebihan dari pendekatan ini adalah:

  • buat satu panggilan prosedur tersimpan dengan semua data Anda dimasukkan sebagai 1 parameter
  • input tabel terstruktur dan sangat diketik
  • tidak ada pembuatan string/parsing atau penanganan XML
  • dapat dengan mudah menggunakan input tabel untuk memfilter, bergabung, atau apa pun

Namun, perhatikan: Jika Anda memanggil prosedur tersimpan yang menggunakan TVPs melalui ADO.NET atau ODBC dan ambil lihat aktivitas dengan SQL Server Profiler, Anda akan melihat bahwa SQL Server menerima beberapa pernyataan INSERT untuk memuat TVP, satu untuk setiap baris di TVP , diikuti dengan panggilan ke procedure.Ini dengan desain . Batch INSERTs ini perlu dikompilasi setiap kali prosedur dipanggil, dan merupakan overhead yang kecil. Namun, bahkan dengan overhead ini, TVPs masih - blow away pendekatan lain dalam hal kinerja dan kegunaan untuk sebagian besar kasus penggunaan.

Jika Anda ingin mempelajari lebih lanjut, Erland Sommarskog telah yang sepenuhnya kurus tentang cara kerja parameter yang dihargai tabel dan memberikan beberapa contoh.

Berikut adalah contoh lain yang saya buat:

CREATE TYPE id_list AS TABLE (
    id int NOT NULL PRIMARY KEY
);
GO

CREATE PROCEDURE [dbo].[tvp_test] (
      @param1           INT
    , @customer_list    id_list READONLY
)
AS
BEGIN
    SELECT @param1 AS param1;

    -- join, filter, do whatever you want with this table 
    -- (other than modify it)
    SELECT *
    FROM @customer_list;
END;
GO

DECLARE @customer_list id_list;

INSERT INTO @customer_list (
    id
)
VALUES (1), (2), (3), (4), (5), (6), (7);

EXECUTE [dbo].[tvp_test]
      @param1 = 5
    , @customer_list = @customer_list
;
GO

DROP PROCEDURE dbo.tvp_test;
DROP TYPE id_list;
GO
23
Nick Chammas

Seluruh subjek dibahas pada the artikel definitif oleh Erland Sommarskog: "Array dan Daftar di SQL Server" . Pilih versi mana yang akan dipilih.

Ringkasan, untuk pra SQL Server 2008 di mana TVP mengalahkan sisanya

  • CSV, bagilah sesuka Anda (biasanya saya menggunakan tabel Angka)
  • XML dan parse (lebih baik dengan SQL Server 2005+)
  • Buat tabel sementara di klien

Artikel ini layak dibaca untuk melihat teknik dan pemikiran lain.

Sunting: jawaban terlambat untuk besar daftar di tempat lain: Melewati parameter array ke prosedur tersimpan

21
gbn

Saya tahu saya terlambat untuk pesta ini, tapi saya punya masalah di masa lalu, harus mengirim hingga 100 ribu angka bigint, dan melakukan beberapa tolok ukur. Kami akhirnya mengirim mereka dalam format biner, sebagai gambar - yang lebih cepat dari yang lainnya hingga 100 ribu angka.

Ini kode lama saya (SQL Server 2005):

SELECT  Number * 8 + 1 AS StartFrom ,
        Number * 8 + 8 AS MaxLen
INTO    dbo.ParsingNumbers
FROM    dbo.Numbers
GO

CREATE FUNCTION dbo.ParseImageIntoBIGINTs ( @BIGINTs IMAGE )
RETURNS TABLE
AS RETURN
    ( SELECT    CAST(SUBSTRING(@BIGINTs, StartFrom, 8) AS BIGINT) Num
      FROM      dbo.ParsingNumbers
      WHERE     MaxLen <= DATALENGTH(@BIGINTs)
    )
GO

Kode berikut mengemas integer ke dalam gumpalan biner. Saya membalik urutan byte di sini:

static byte[] UlongsToBytes(ulong[] ulongs)
{
int ifrom = ulongs.GetLowerBound(0);
int ito   = ulongs.GetUpperBound(0);
int l = (ito - ifrom + 1)*8;
byte[] ret = new byte[l];
int retind = 0;
for(int i=ifrom; i<=ito; i++)
{
ulong v = ulongs[i];
ret[retind++] = (byte) (v >> 0x38);
ret[retind++] = (byte) (v >> 0x30);
ret[retind++] = (byte) (v >> 40);
ret[retind++] = (byte) (v >> 0x20);
ret[retind++] = (byte) (v >> 0x18);
ret[retind++] = (byte) (v >> 0x10);
ret[retind++] = (byte) (v >> 8);
ret[retind++] = (byte) v;
}
return ret;
}
14
A-K

Saya terpecah antara merujuk Anda ke SO atau menjawabnya di sini, karena ini hampir merupakan pertanyaan pemrograman. Tetapi karena saya sudah punya solusi, saya menggunakan ... Saya akan memposting itu;)

Cara ini bekerja adalah Anda memberi string yang dibatasi koma (split sederhana, tidak melakukan pemisahan gaya CSV) ke dalam prosedur tersimpan sebagai varchar (4000) dan kemudian mengumpankan daftar itu ke dalam fungsi ini dan mendapatkan tabel yang berguna kembali, meja varchars yang adil.

Ini memungkinkan Anda mengirimkan nilai hanya id yang ingin Anda proses, dan Anda bisa melakukan penggabungan sederhana pada titik itu.

Bergantian Anda bisa melakukan sesuatu dengan CLR DataTable dan memasukkan itu, tapi itu sedikit lebih banyak untuk mendukung dan semua orang mengerti daftar CSV.

USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER FUNCTION [dbo].[splitListToTable] (@list      nvarchar(MAX), @delimiter nchar(1) = N',')
      RETURNS @tbl TABLE (value     varchar(4000)      NOT NULL) AS
/*
http://www.sommarskog.se/arrays-in-sql.html
This guy is apparently THE guy in SQL arrays and lists 

Need an easy non-dynamic way to split a list of strings on input for comparisons

Usage like thus:

DECLARE @sqlParam VARCHAR(MAX)
SET @sqlParam = 'a,b,c'

SELECT * FROM (

select 'a' as col1, '1' as col2 UNION
select 'a' as col1, '2' as col2 UNION
select 'b' as col1, '3' as col2 UNION
select 'b' as col1, '4' as col2 UNION
select 'c' as col1, '5' as col2 UNION
select 'c' as col1, '6' as col2 ) x 
WHERE EXISTS( SELECT value FROM splitListToTable(@sqlParam,',') WHERE x.col1 = value )

*/
BEGIN
   DECLARE @endpos   int,
           @startpos int,
           @textpos  int,
           @chunklen smallint,
           @tmpstr   nvarchar(4000),
           @leftover nvarchar(4000),
           @tmpval   nvarchar(4000)

   SET @textpos = 1
   SET @leftover = ''
   WHILE @textpos <= datalength(@list) / 2
   BEGIN
      SET @chunklen = 4000 - datalength(@leftover) / 2
      SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
      SET @textpos = @textpos + @chunklen

      SET @startpos = 0
      SET @endpos = charindex(@delimiter, @tmpstr)

      WHILE @endpos > 0
      BEGIN
         SET @tmpval = ltrim(rtrim(substring(@tmpstr, @startpos + 1,
                                             @endpos - @startpos - 1)))
         INSERT @tbl (value) VALUES(@tmpval)
         SET @startpos = @endpos
         SET @endpos = charindex(@delimiter, @tmpstr, @startpos + 1)
      END

      SET @leftover = right(@tmpstr, datalength(@tmpstr) / 2 - @startpos)
   END

   INSERT @tbl(value) VALUES (ltrim(rtrim(@leftover)))
   RETURN
END
9
jcolebrand

Saya secara teratur menerima set 1000-an baris dan 10.000-an baris yang dikirim dari aplikasi kita untuk diproses oleh berbagai prosedur yang tersimpan SQL Server.

Untuk memenuhi tuntutan kinerja, kami menggunakan TVP, tetapi Anda harus menerapkan abstrak dbDataReader Anda sendiri untuk mengatasi beberapa masalah kinerja dalam mode pemrosesan standarnya. Saya tidak akan membahas bagaimana dan mengapa mereka berada di luar ruang lingkup untuk permintaan ini.

Saya tidak mempertimbangkan pemrosesan XML karena saya belum menemukan implementasi XML yang tetap memiliki lebih dari 10.000 "baris".

Pemrosesan daftar dapat ditangani dengan pemrosesan tabel penghitungan satu dimensi dan dua dimensi. Kami telah berhasil menggunakan ini di berbagai bidang, tetapi TVP yang dikelola dengan lebih baik lebih berkinerja ketika ada lebih dari beberapa ratus "baris".

Seperti semua pilihan tentang pemrosesan SQL Server, Anda harus menentukan pilihan berdasarkan model penggunaan.

5
Robert Miller

Saya akhirnya mendapat kesempatan untuk melakukan beberapa TableValuedParameters dan mereka bekerja dengan baik, jadi saya akan menempelkan seluruh kode lotta yang menunjukkan bagaimana saya menggunakannya, dengan sampel dari beberapa kode saya saat ini: (catatan: kami menggunakan ADO .BERSIH)

Juga perhatikan: Saya menulis beberapa kode untuk layanan, dan saya punya banyak bit kode yang sudah ditentukan sebelumnya di kelas lain, tapi saya menulis ini sebagai aplikasi konsol sehingga saya bisa men-debug-nya, jadi saya ripping semua ini dari aplikasi konsol. Maafkan gaya pengkodean saya (seperti string koneksi hardcoded) karena itu semacam "membangun satu untuk membuang". Saya ingin menunjukkan bagaimana saya menggunakan List<customObject> dan Dorong ke dalam database dengan mudah sebagai tabel, yang dapat saya gunakan dalam prosedur tersimpan. Kode C # dan TSQL di bawah ini:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using a;

namespace a.EventAMI {
    class Db {
        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static void Update(List<Current> currents) {
            const string CONSTR = @"just a hardwired connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );
            cmd.Parameters.Add( "@CurrentTVP", SqlDbType.Structured ).Value = Converter.GetDataTableFromIEnumerable( currents, typeof( Current ) ); //my custom converter class

            try {
                using ( con ) {
                    con.Open();
                    cmd.ExecuteNonQuery();
                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }
        }
    }
    class Current {
        public string Identifier { get; set; }
        public string OffTime { get; set; }
        public DateTime Off() {
            return Convert.ToDateTime( OffTime );
        }

        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static List<Current> GetAll() {
            List<Current> l = new List<Current>();

            const string CONSTR = @"just a hardcoded connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );

            try {
                using ( con ) {
                    con.Open();
                    using ( SqlDataReader reader = cmd.ExecuteReader() ) {
                        while ( reader.Read() ) {
                            l.Add(
                                new Current {
                                    Identifier = reader[0].ToString(),
                                    OffTime = reader[1].ToString()
                                } );
                        }
                    }

                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }

            return l;
        }
    }
}

-------------------
the converter class
-------------------
using System;
using System.Collections;
using System.Data;
using System.Reflection;

namespace a {
    public static class Converter {
        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable) {
            return GetDataTableFromIEnumerable( aIEnumerable, null );
        }

        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable, Type baseType) {
            DataTable returnTable = new DataTable();

            if ( aIEnumerable != null ) {
                //Creates the table structure looping in the in the first element of the list
                object baseObj = null;

                Type objectType;

                if ( baseType == null ) {
                    foreach ( object obj in aIEnumerable ) {
                        baseObj = obj;
                        break;
                    }

                    objectType = baseObj.GetType();
                } else {
                    objectType = baseType;
                }

                PropertyInfo[] properties = objectType.GetProperties();

                DataColumn col;

                foreach ( PropertyInfo property in properties ) {
                    col = new DataColumn { ColumnName = property.Name };
                    if ( property.PropertyType == typeof( DateTime? ) ) {
                        col.DataType = typeof( DateTime );
                    } else if ( property.PropertyType == typeof( Int32? ) ) {
                        col.DataType = typeof( Int32 );
                    } else {
                        col.DataType = property.PropertyType;
                    }
                    returnTable.Columns.Add( col );
                }

                //Adds the rows to the table

                foreach ( object objItem in aIEnumerable ) {
                    DataRow row = returnTable.NewRow();

                    foreach ( PropertyInfo property in properties ) {
                        Object value = property.GetValue( objItem, null );
                        if ( value != null )
                            row[property.Name] = value;
                        else
                            row[property.Name] = "";
                    }

                    returnTable.Rows.Add( row );
                }
            }
            return returnTable;
        }

    }
}

USE [Database]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROC [dbo].[Event_Update]
    @EventCurrentTVP    Event_CurrentTVP    READONLY
AS

/****************************************************************
    author  cbrand
    date    
    descrip I'll ask you to forgive me the anonymization I've made here, but hope this helps
    caller  such and thus application
****************************************************************/

BEGIN TRAN Event_Update

DECLARE @DEBUG INT

SET @DEBUG = 0 /* test using @DEBUG <> 0 */

/*
    Replace the list of outstanding entries that are still currently disconnected with the list from the file
    This means remove all existing entries (faster to truncate and insert than to delete on a join and insert, yes?)
*/
TRUNCATE TABLE [database].[dbo].[Event_Current]

INSERT INTO [database].[dbo].[Event_Current]
           ([Identifier]
            ,[OffTime])
SELECT [Identifier]
      ,[OffTime]
  FROM @EventCurrentTVP

IF (@@ERROR <> 0 OR @DEBUG <> 0) 
BEGIN
ROLLBACK TRAN Event_Update
END
ELSE
BEGIN
COMMIT TRAN Event_Update
END

USE [Database]
GO

CREATE TYPE [dbo].[Event_CurrentTVP] AS TABLE(
    [Identifier] [varchar](20) NULL,
    [OffTime] [datetime] NULL
)
GO

Juga, saya akan menerima kritik konstruktif pada gaya pengkodean saya jika Anda memiliki itu untuk ditawarkan (kepada semua pembaca yang menemukan pertanyaan ini) tetapi tolong tetap konstruktif;) ... Jika Anda benar-benar menginginkan saya, temukan saya di ruang obrolan di sini . Semoga dengan potongan kode ini orang dapat melihat bagaimana mereka dapat menggunakan List<Current> seperti yang saya definisikan sebagai tabel di db dan List<T> di aplikasi mereka.

5
jcolebrand

Saya akan pergi dengan proposal # 1 atau, sebagai alternatif, membuat tabel awal yang hanya menampung id yang diproses. Masukkan ke dalam tabel itu selama pemrosesan, kemudian setelah selesai, panggil proc seperti di bawah ini:

BEGIN TRAN

UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;

TRUNCATE TABLE processedIds

COMMIT TRAN

Anda akan melakukan banyak sisipan, tetapi mereka akan ke meja kecil, jadi itu harus cepat. Anda juga dapat mengelompokkan sisipan Anda menggunakan ADO.net atau adaptor data apa pun yang Anda gunakan.

Judul pertanyaan mencakup tugas untuk mengirimkan data dari aplikasi ke dalam prosedur tersimpan. Bagian itu dikecualikan oleh badan pertanyaan, tetapi izinkan saya mencoba untuk menjawab ini juga.

Dalam konteks sql-server-2008 seperti yang ditentukan oleh tag ada artikel hebat lain oleh E. Sommarskog Array dan Daftar di SQL Server 2008 . BTW saya menemukannya di artikel yang dimaksud Marian dalam jawabannya.

Alih-alih hanya memberikan tautan, saya mengutip daftar isinya:

  • Pengantar
  • Latar Belakang
  • Parameter Table-Valued di T-SQL
  • Passing Table-Valued Parameters dari ADO .NET
    • Menggunakan Daftar
    • Menggunakan DataTable
    • Menggunakan DataReader
    • Keterangan Terakhir
  • Menggunakan Parameter Table-Valued dari API Lain
    • ODBC
    • OLE DB
    • ADO
    • LINQ dan Kerangka Entitas
    • JDBC
    • PHP
    • Perl
    • Bagaimana Jika API Anda Tidak Mendukung TVP
  • Pertimbangan Kinerja
    • Sisi server
    • Sisi klien
    • Kunci Utama atau Tidak?
  • Ucapan Terima Kasih dan Umpan Balik
  • Riwayat Revisi

Di luar teknik yang disebutkan di sana, saya merasa bahwa dalam beberapa kasus, bulkcopy dan bulk insert layak disebutkan dalam lingkup kasus umum.

2
bernd_k

Melewati parameter array ke prosedur tersimpan

ntuk MS SQL 2016 versi terbar

Dengan MS SQL 2016 mereka memperkenalkan fungsi baru: SPLIT_STRING () untuk mem-parsing beberapa nilai.

Ini dapat memecahkan masalah Anda dengan mudah.

ntuk MS SQL Versi Lebih Lama

Jika Anda menggunakan versi yang lebih lama, ikuti langkah ini:

Pertama Buat satu fungsi:

 ALTER FUNCTION [dbo].[UDF_IDListToTable]
 (
    @list          [varchar](MAX),
    @Seperator     CHAR(1)
  )
 RETURNS @tbl TABLE (ID INT)
 WITH 

 EXECUTE AS CALLER
 AS
  BEGIN
    DECLARE @position INT
    DECLARE @NewLine CHAR(2) 
    DECLARE @no INT
    SET @NewLine = CHAR(13) + CHAR(10)

    IF CHARINDEX(@Seperator, @list) = 0
    BEGIN
    INSERT INTO @tbl
    VALUES
      (
        @list
      )
END
ELSE
BEGIN
    SET @position = 1
    SET @list = @list + @Seperator
    WHILE CHARINDEX(@Seperator, @list, @position) <> 0
    BEGIN
        SELECT @no = SUBSTRING(
                   @list,
                   @position,
                   CHARINDEX(@Seperator, @list, @position) - @position
               )

        IF @no <> ''
            INSERT INTO @tbl
            VALUES
              (
                @no
              )

        SET @position = CHARINDEX(@Seperator, @list, @position) + 1
    END
END
RETURN
END

Setelah membuat ini, sampaikan string Anda ke fungsi ini dengan pemisah.

Saya harap ini dapat membantu anda. : -)

1
Ankit Bhalala