简体   繁体   中英

How do I replicate in transact SQL the nested loops on recordsets I am using in vb.net?

Below is pseudocode that demonstrates what I can do in vb.net to update monthly paid insurance policy records with updated premiums from a rating engine table. However, I need to be able to do this inside a stored procedure in an SQL Server database. As far as I know I will have to use two cursors, one for the outer loop, and one for the inner loop. My problem is that I don't know enough about the syntax of transact SQL to be able to build the SQL query that will populate the inner cursor. Here is the pseudocode:

Dim rsouter as ADODB.Recordset = New ADODB.Recordset
Dim rsinner as ADODB.Recordset = New ADODB.Recordset
Dim strSQL as string
Dim Paycol as string

‘ first of all open a recordset to get the records for updating

Rsouter.open(Select area, [payment method], suminsured, scpremium from tblpolicy where [payment method] = ‘monthly’), CurrentProject.Connection

‘ then loop through this outer recordset

Do while not rsouter.eof

‘ set the Paycol variable value to being a column name concatenated from the string “SC” and the value of the field “suminsured” from the open recordset, e.g. "SC12000"

Paycol = ‘SC’ & rsouter.fields(‘suminsured’).value

‘ now build the string to create the inner recordset

strSQL = ‘Select area, ‘ & Paycol & ‘ as sc from re where area = ‘ & rsouter.fields(‘area’).value

' the string should read something like; "select area, sc12000 from re where area = 5"

‘ open the inner recordset against the query string

Rsinner.open(strSQL), CurrentProject.Connection

‘ update the outer recordset premium field with the value from the inner recordset

Rsouter.fields(‘scpremium’).value = rsinner.fields(‘sc’).value

‘ close the inner recordset

Rsinner.close

‘ commit the change to the outer recordset

rsouter.update()

‘ move to the next record in the outer recordset

If not rsouter.eof then rsouter.movenext()

loop

‘ close the outer recordset

rsouter.close()

I am sure there is a better way to do this than creating two cursors, but my SQL is not strong enough to be able to do it without help.


Yads,

Many thanks for your answer, it goes a long way to solving the problem I have. Unfortunately my problem is a bit more complex than the pseudocode I wrote in my original request. I actually need to get more than one value from the inner cursor to pass back to the outer cursor. I need to get the annual premium, monthly premium, fortnightly premium, weekly premium, and direct debit premium from the re table, and use all of them to update the corresponding fields in the policy table record in the outer cursor. From what I understand your code is creating a new procedure and passing in part of the column name and the area. The value of the passed in column is then returned when you execute the procedure, and used to update the value of that column in the policy table.

I guess I could alter the EXEC statement to read:

EXEC ('select ' & @premiumColName & ' from re where re.area = ''' & @area & ''')

I could then run the EXEC statement repeatedly, but passing in a different column name each time. I would then have to run the SET statement repeatedly to update each premium column in turn.

So my next questions are these:

  1. Would I have to create and drop the GetPremium procedure each time my outer cursor looped?
  2. How does the update statement know which records to update, or would it update all records in the table (as opposed to the outer cursor) that had a matching area?
  3. How can I validate the contents of the column being requested in GetPremium to ensure they are greater than zero before passing the value back to update the premium column in the outer cursor?

Damien,

I am very grateful for your help. Unfortunately I am converting someone else's VB6 program into VB.NET. In the process of doing this the original programmer, who also designed the data storage, has decided to concatenate the 6 original Access databases into a single SQL Server database. In so doing he has redesigned several of the tables, and concatenated the original annual premium, monthly premium, fortnightly premium, weekly premium and direct debit premium tables into a single table with all of the columns added separately. So all the annual premium columns are now in the new rating engine table with the prefix 'AC'. All the fortnightly premium columns are now prefixed with 'FC' and so on. I can't change the way this works as he is an outside software supplier, and owns the IPR to the program and its data structure. All I can do is advise wherever possible, and undertake the VB code conversion work, as he isn't up to speed on VB.NET himself, and our company needs his product.

As a way of trying to speed up the current very slow update process, I have been endeavouring to write a stored procedure that will replace a form in the VB program, that has code which currently chunders through the separate databases, looks for any changes in rates, then applies them to existing records in the policy table (called the 'core' table in the database). This is not only very inefficient, but means that whoever is the first person to open the program on the first day of the month has to wait (sometimes for several hours) for the update form to run through its code before being able to proceed. Furthermore, no-one else can do anything either while the updating process is running.

To replace this I intend to set up the stored procedure so that it can be scheduled to run overnight on the first day of each month, and apply any changes in premium to all existing pay as you go policies. Hopefully this will mean no more waiting all day before being able to do any work with the program and also prevent any unintended human intervention during the update process.

So far I have two stored procedures. The first one is called 'UpdateAll', and contains the code for filling a temporary table with only the policies that need updating. There are currently around 6000 records to update out of a total of around 103,000. This table is then queried to fill the cursor and for each record put the values for the collected columns into variables. I am then able to concatenate the prefixes 'AC', 'FC', etc, to the value from the suminsured column and put the resulting string into a variable. So for example, the code for building the annual premium column required is:

set @ansum = 'AC' + ltrim(str(@suminsured))

With this done I can now call a slightly modified version of the GetPremium stored procedure that Yads sent earlier. This fills the output variable, which in this case is a variable called '@ansumval'. The code for calling it is:

EXECUTE dbo.GetPremium @ansum, @area, @ansumval

I repeat this for each of the other columns in order to update the premium information for monthly, fortnightly, weekly policies, etc. After which I close the cursor. I can then open a new cursor, and loop through the 6000 or so records in the tmpcore2 table, and use each one to update the record matched on certificate no. (certno) in the core table and update the necessary columns with the values from the matching columns in the tmpcore2 record. After this I close the cursor, and everything is done.

Much as I realize that this is nowhere near as efficient as doing a table join, nevertheless, it allows me to build the column names dynamically and also allows me to check the update values and ensure that I am not overwriting a proper due amount with zero. An abbreviated listing of the working bits of each cursor are as follows:

open cursor1

select * from tmpcore2 order by area

Fetch Next From Cursor1 into @area, @HA, @cert, @status, @paymeth, @suminsured, @anprem, @monprem, @fortprem, @dprem, @wprem

    While @@Fetch_status = 0
        Begin

            set @ansum = 'AC' + ltrim(str(@suminsured))
            set @mnsum = 'SC' + ltrim(str(@suminsured))
            set @ftsum = 'FN' + ltrim(str(@suminsured))
            set @dsum = 'DD' + ltrim(str(@suminsured))
            set @wksum = 'WK' + ltrim(str(@suminsured))

            EXECUTE GetPremium @ansum, @area, @ansumval 
            EXECUTE GetPremium @mnsum, @area, @mnsumval
            EXECUTE GetPremium @ftsum, @area, @ftsumval
            EXECUTE GetPremium @wksum, @area, @dsumval
            EXECUTE GetPremium @dsum, @area, @wksumval

            if @ansumval > 0 UPDATE tmpcore2 SET [Annual premium] = @ansumval
            if @mnsumval > 0 UPDATE tmpcore2 SET [Monthly premium] = @mnsumval
            if @ftsumval > 0 UPDATE tmpcore2 SET [Fortnightly premium] = @ftsumval
            if @dsumval > 0 UPDATE tmpcore2 SET dpremium = @dsumval
            if @wksumval > 0 UPDATE tmpcore2 SET wpremium = @wksumval

-- loop through until all records updated

close cursor1

open cursor2


select * from tmpcore2 order by certno


Fetch Next From Cursor2 into @area, @HA, @cert, @status, @paymeth, @suminsured, @anprem, @monprem, @fortprem, @dprem, @wprem

    While @@Fetch_status = 0
        Begin

            Update core 
                SET [annual premium] = @anprem,
                    [monthly premium] = @monprem,
                    [fortnightly premium] = @fortprem,
                    dpremium = @dprem,
                    wpremium = @wprem
                WHERE certno = @cert

-- loop through until all records updated

        End 

close Cursor2

There is a better way to do it with just one query Isn't this what you're trying to do?

CREATE PROC GetPremium
@premiumColName VARCHAR2(50),
@area VARCHAR2(50)
AS BEGIN
EXEC ('SELECT sc' & @premiumColName & ' FROM re WHERE re.area = ''' & @area & '''')
END    

    UPDATE tblpolicy
    SET scpremium = EXECUTE GetPremium tblpolicy.suminsured, tblpolicy.area

If you can restructure your re table, so instead of

CREATE TABLE re (
    area varchar(10) not null,
    sc12000 int not null,
    sc14000 int not null,
    sc16000 int not null
)

You should change this to be:

CREATE TABLE re (
    area varchar(10) not null,
    suminsured int not null,
    sc int not null
)

Which will then contain three tows (for suminsured = 12000, 14000, 16000)

You would then re-write this as a single query:

UPDATE p
SET
    scpremium = re.sc,
    /* set other columns as required */
FROM
    tblpolicy p
       inner join
    re
        on
             p.area = re.area and
             p.suminsured = re.suminsured
WHERE
    p.[payment method] = 'monthly'

If you're unable to alter the re table for some reason, let me know - we can create a view that looks like the second table I showed, and then use that in your query instead.


I'd still say you're better off building a view that lets you write good, set-based SQL, and pretend that your colleague has a clue about database design. Build a view along the lines of:

CREATE TABLE dbo.re (
    area varchar(10) not null,
    sc12000 int not null,
    sc14000 int not null,
    sc16000 int not null,
    fc12000 int not null,
    fc14000 int not null,
    fc16000 int not null
)
go
insert into dbo.re(area,sc12000,sc14000,sc16000,fc12000,fc14000,fc16000)
select 'abc',1,2,3,4,5,6 union all
select 'def',7,8,9,10,11,12
go
create view re_sane
with schemabinding
as
    select
        area,
        premium_type,
        band,
        CASE premium_type
            when 'fc' then
                CASE band
                    when 12000 then fc12000
                    when 14000 then fc14000
                    when 16000 then fc16000
                end
            when 'sc' then
                CASE band
                    when 12000 then sc12000
                    when 14000 then sc14000
                    when 16000 then sc16000
                end
        end as premium
    from
        dbo.re
            cross join
        (select 'sc' union all select 'fc') as premium_types (premium_type)
            cross join
        (select 12000 union all select 14000 union all select 16000) as bands (band)
go
select * from re_sane

And now you can use the re_sane view for your queries. It's a lot better than using cursors, and having to invoke dynamic SQL, and you should only have to write it once.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM