Sql problem - Interbase / delphi

Hi

I'm trying to create an interbase stored procedure to achieve an objective but I can't even run my query as a basic sql statement so I am wondering if someone can help me please with the basic syntax.


The situation

Clients are associated with a number of jobs
Jobs are associated with a number of transactions
At inception of each job a figure for estimated total sales value is entered in the jobs table.
As time passes sales transactions are entered in the transactions table..

What I want to do is to create a single query to summarise results by client

This involves summing estimated sales from jobs for any given client and comparing with actual sales computed by summing the transactions across the jobs for the same client..

I feel sure that this must be possible but I cannot see how to do it...

Help PLEASE !!!!

Regards John






jbehAsked:
Who is Participating?
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

RBertoraCommented:
Sounds like a piece of cake,
give me the table definitions and I'll give you a sql query.

Rob ;-)
0
jbehAuthor Commented:
OK

I'll get back to you with them in a little while
0
RBertoraCommented:
I'll only get a chance to look at this tommorrow morning anyway, but do post as I know of several other experts that would be eager to give it a bash anyway.. If no one does then tommorrow I'll do it.

Rob ;-)
0
Cloud Class® Course: Microsoft Exchange Server

The MCTS: Microsoft Exchange Server 2010 certification validates your skills in supporting the maintenance and administration of the Exchange servers in an enterprise environment. Learn everything you need to know with this course.

jbehAuthor Commented:
Ok - Here are extracts from the ISQL extract screens

I've left the domain definitions in in a shortened form in case they help



/* Domain definitions */
CREATE DOMAIN IDNUMSMALL AS SMALLINT NOT NULL;
CREATE DOMAIN MONEY AS NUMERIC(15, 2);
CREATE DOMAIN IDNUM AS INTEGER NOT NULL;
CREATE DOMAIN IDNUMSMALL AS SMALLINT NOT NULL;


/* Table: CLIENTS, Owner: SYSDBA */
CREATE TABLE CLIENTS (CLIENTID IDNUMSMALL,
        CLIENTNAME VARCHAR(35),
        CLIENTTYPEID IDNUMSMALL NOT NULL,
        KEYCLIENT VARCHAR(3) default "No"
 NOT NULL,
        KEYEXEC SMALLINT,
PRIMARY KEY (CLIENTID));



/* Table: JOBS, Owner: SYSDBA */
CREATE TABLE JOBS (JOBID IDNUM,
        EXECID IDNUMSMALL,
        EXEC2ID IDNUMSMALL,
        COMMENCED DATE default "31-Dec-1989"
 NOT NULL,
        FWTYPEID IDNUMSMALL,
        JOBTYPEID IDNUMSMALL,
        CLIENTID IDNUMSMALL,
        JOBDESC VARCHAR(30),
        ESTSALES MONEY  default 0
 NOT NULL,
        ESTDPEXPS MONEY  default 0
 NOT NULL,
        ESTEXECEXPS MONEY  default 0
 NOT NULL,
        ESTFWEXPS MONEY  default 0
 NOT NULL,
        ESTOTHEREXPS MONEY  default 0
 NOT NULL,
        ESTINCOME COMPUTED BY (Estsales -(estdpexps + EstExecExps + EstFWExps + EstOtherExps) ),
        ESTTIME MONEY  default 0
 NOT NULL,
        PITCH VARCHAR(3),
        PITCHNUM VARCHAR(3),
        COMMISSIONED VARCHAR(3),
        COMMDATE DATE,
        LOSTREASON VARCHAR(100),
        LIKELIHOOD SMALLINT,
        YRMNTHNUM INTEGER,
        TEMP INTEGER,
PRIMARY KEY (JOBID));



/* Table: TRANS, Owner: SYSDBA */
CREATE TABLE TRANS (TRANSID IDNUM,
        JOBID IDNUM,
        TIMESHEETS MONEY,
        DPEXPS MONEY,
        EXECEXPS MONEY,
        FIELDEXPS MONEY,
        OTHEREXPS MONEY,
        SALES MONEY,
        TRANDATE DATE,
        DESCRIPTION VARCHAR(35),
        EXECID IDNUMSMALL,
        HOURS MONEY,
        YRMNTHNUM INTEGER,
PRIMARY KEY (TRANSID));




Sorry - Doesn't format very well on this narrow comment window


Regards


John

0
karooCommented:
Rob, sorry to jump in like this, i do not know if this is the simplest solution for John's problem, any comments?

John,
I created a db with your script and entered some data, tested it with the following procedure and it works, at first the problem seemed very simple but as it turns out the problem for me was getting the correct Estimated sales from JOBS.


CREATE PROCEDURE GET_SUM
  RETURNS (
  CLIENTID SMALLINT,
  ESTSALES DOUBLE PRECISION,
  SALES DOUBLE PRECISION
) AS    
BEGIN
  FOR SELECT C.CLIENTID, SUM(J.ESTSALES)
  FROM (CLIENTS C JOIN JOBS J ON C.CLIENTID = J.CLIENTID)
  GROUP BY C.CLIENTID
  INTO :clientid, :estsales
  DO BEGIN
    FOR SELECT SUM(T.SALES)
    FROM ((CLIENTS C JOIN JOBS J ON C.CLIENTID = J.CLIENTID)
           JOIN TRANS T ON J.JOBID = T.JOBID)
    WHERE C.CLIENTID = :clientid
    GROUP BY C.CLIENTID
    INTO :sales
    DO SUSPEND;
  END
END

best of luck
Ben:)
0
jbehAuthor Commented:
Ben - Hi

Yes - I was having problems with the estimated sales bit too!!!!

I've tried your suggestion and have 2 comments.

1) I need to parametise the whole thing - I only need the result on a client by client basis and that's clearly a lot less work for the engine than doing the whole lot but that's relatively minor.  Much more important is ---

2) I couldn't get it to work - It completely crashed my system and the only way out was a full reboot...

I've repeated this 3 or 4 times - and the ISQL window just freezes up every time..








0
jbehAuthor Commented:
OK - I have now got

"Execute procedure Get_Sum" to work unfortunately it only returns a single line of data

"Select * from Get_Sum" crashes every time

So something is not quite right I fear!!

0
RBertoraCommented:
As per you original question, and from the data structures you posted.

Here are two queries, you asked only for the second.

Rob ;-)

// this shows summing per job estimated Vs Sales
Select
  C.clientid,C.clientname,J.Jobid,J.estincome, Sales = sum(T.sales)
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname,J.jobId,J.EstIncome


// this shows summing per client estimated Vs Sales
Select
  C.clientid,C.clientname, estincome = sum(J.estincome), Sales = sum(T.sales)
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

Karoo: no problemo Howzit dude?
0
jbehAuthor Commented:
Hi Karoo

No problemo is a teensy weensy exaggeration I am sorry to say..

The Sql works absolutely fine and produces a stunning answer..

The only problem is that the answer is wrong!!

I AM NOT CERTAIN BUT the Estimated values appear to be incorporated into the sum of the estimated sales for every iteration round the transaction file .

So a client with 1 job with 10 transactions (of which only 1 is a sale) gives a correct Total Actual Sale
but gives estimated sales wrong by a factor of 10 !!!!!




0
RBertoraCommented:
Yes, I didn't notice that.
here is the correction:


Select
  C.clientid,C.clientname, MyEstincome = avg(J.estincome)*(Select count(*) from Jobs  JJ where C.ClientID = JJ.ClientID), Sales = sum(T.sales)
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname


Rob;-)
0
karooCommented:
Hi Rob glad it's not a problem.
btw: exelent response on the Q!:)

jbeh,
I reversed engineered your sql into Data architect, something must have been messed up in the process, sorry to have caused a crash.
I checked Rob's query and get the same result that i did with my SP, weird...
anyway best of luck

Ben:)
0
jbehAuthor Commented:
Sorry to take so long getting back to you

I have tried and tried with your latest answer

I have slightly reworded your statement to read

Select
C.clientid,C.clientname,
avg(J.estsales)*(Select count(*) from Jobs JJ where C.ClientID = JJ.ClientID) TotEstsales,
sum(T.sales) TotActsales  
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

This is solely to allow interbase to run it at all.

Result
Interbase crashes immediately
Reboot required  - No explanation - Just SPLAT!!!!
 
I have fiddled around and tried

Select
C.clientid,C.clientname,
avg((J.estsales)*(Select count(*) from Jobs JJ where C.ClientID = JJ.ClientID)) TotEstsales,
sum(T.sales) TotActsales  

from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

No Crash - But Invalid Aggregate reference SQL error code 104



I have also tried umpteen other permutations but all give errors of one sort or another - some are gentle and give error messages - Most cause fatal crashes


regards John

0
jbehAuthor Commented:
I think this is more difficult than I had first imagined so I thought it only fair to up the points somewhat
0
RBertoraCommented:
Hmm even the reworded statement works perfectly on SQL Server 6.5.

Rob ;-)
0
RBertoraCommented:
This also works:

Select
C.clientid,C.clientname,
  sum(distinct J.estsales) TotEstsales,
  sum(T.sales) TotActsales  
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

Rob ;-)
0
RBertoraCommented:
Sorry doesn't work! unless all your costs are distinct.. what was I thinking!
Rob ;-)
0
karooCommented:
John,

very strange, like i said tested my SP and Rob's query(which is the better option), both works.

try the creating the fillowing SP it limits the records retrieved, wan't to see what happens:

CREATE PROCEDURE GET_SUM (
  ID SMALLINT
) RETURNS (
  CLIENTID SMALLINT,
  CLIENTNAME VARCHAR(35),
  ESTSALES DOUBLE PRECISION,
  SALES DOUBLE PRECISION
) AS      
BEGIN
  For Select
    C.clientid, C.clientname,
    avg(J.estsales)*(Select count(*) from Jobs JJ where C.ClientID = JJ.ClientID),
    sum(T.sales)
  from
    clients C, Jobs J, Trans T
  where
    C.clientID = :ID and
    C.clientID = J.clientID and
    J.JobID = T.JobID
  group by
    C.clientid, C.clientname
  into
    :clientid, :clientname, :estsales, :sales
  do suspend;
END
0
jbehAuthor Commented:
B I N G O   !!!!!!


That looks like it might truly be the answer!!

Let me check it out a little but then I'll get back to you


John
0
RBertoraCommented:
Ok how about this one:

Select
   C.clientid,C.clientname,
   SumSales = (Select sum(estsales) from Jobs JJ where C.ClientID = JJ.ClientID),
   sum(T.sales) TotActsales  
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

Rob ;-)
0
jbehAuthor Commented:
Ok

Short of testing to destruction that looks like the answer I have been seeking - If you'd propose that as an answer - I'll do the needful

Thank you VERY much !!!!!
0
RBertoraCommented:
Which one the stored proc or the sql query? Karoos effort or RBertora effort?

Rob ;-)
0
jbehAuthor Commented:
Very good question - I am now slightly confused myself -.

What I was delighted by was the following

From: RBertora Date: Tuesday, November 23 1999 - 01:59PM GMT  

It worked - gave sensible answers and I could use it as a base for further modification

Now I am not so sure - as Rob himself points out there is a potential flaw with this in the use of DISTINCT so I am now back to where I was before and trying to pick up the rest of this thread.

OK I'll print it all out and mull it over on the train going home I've got a leetle lost and get back to you..

The true answer is that ultimatly I am going to need an SP so that for preference however working SQL will deal with the bit that I'm struggling with..

It is unfortunate that as far as i can tell the Experts Exchange clock is at least an hour out of kilter
0
jbehAuthor Commented:
OK - I've had a go at the revised SP

Sorry about the length of this message


I've tried 3 different variants

The first works as a stored procedure but doesn't give the answer

The error for the second is
"Statement failed, SQLCODE = -104
Dynamic SQL Error
-SQL error code = -104
-Invalid aggregate reference"

The error for the third is
"Statement failed, SQLCODE = -901
connection lost to database"

The third also gives an "illegal operation error" and requires a reboot to clean things up..



SET TERM ## ;

CREATE PROCEDURE GETSUM01
( ID SMALLINT )
RETURNS
( CLIENTID SMALLINT,
  CLIENTNAME VARCHAR(35),
  SALES DOUBLE PRECISION )
AS      
BEGIN
For Select
C.clientid, C.clientname,
sum(T.sales)  sales

from
    clients C, Jobs J, Trans T
  where
    C.clientID = :ID and
    C.clientID = J.clientID and
    J.JobID = T.JobID
  group by
    C.clientid, C.clientname
  into
    :clientid, :clientname, :sales
  do
suspend;
end ##

set term ; ##


SET TERM ## ;


CREATE PROCEDURE GETSUM02
( ID SMALLINT )
RETURNS
( CLIENTID SMALLINT,
  CLIENTNAME VARCHAR(35),
  ESTSALES DOUBLE PRECISION,  
  SALES DOUBLE PRECISION )
AS      
BEGIN
For Select
  C.clientid, C.clientname,
  avg ((J.estsales)*(Select count(*) from Jobs JJ where C.ClientID = JJ.ClientID))  estsales,
  sum(T.sales)  sales
from
    clients C, Jobs J, Trans T
  where
    C.clientID = :ID and
    C.clientID = J.clientID and
    J.JobID = T.JobID
  group by
    C.clientid, C.clientname
  into
    :clientid, :clientname, :estsales, :sales
  do
suspend;
end ##

Statement failed, SQLCODE = -104
Dynamic SQL Error
-SQL error code = -104
-Invalid aggregate reference


set term ; ##

SET TERM ## ;


CREATE PROCEDURE GETSUM03
( ID SMALLINT )
RETURNS
( CLIENTID SMALLINT,
  CLIENTNAME VARCHAR(35),
  ESTSALES DOUBLE PRECISION,  
  SALES DOUBLE PRECISION )
AS      
BEGIN
For Select
  C.clientid, C.clientname,
  avg (J.estsales)*(Select count(*) from Jobs JJ where C.ClientID = JJ.ClientID)  estsales,
  sum(T.sales)  sales
from
    clients C, Jobs J, Trans T
  where
    C.clientID = :ID and
    C.clientID = J.clientID and
    J.JobID = T.JobID
  group by
    C.clientid, C.clientname
  into
    :clientid, :clientname, :estsales, :sales
  do
suspend;
end ##

Statement failed, SQLCODE = -901
connection lost to database


set term ; ##



0
karooCommented:
jbeh,

results:
1) yep
2) same, SQL incorrect => avg ((
3) good from SQL tool, checking Delphi

4)
changed:
>> avg (J.estsales)*(Select count(*) from Jobs JJ where C.ClientID = JJ.ClientID) estsales
<<
to:
(Select sum(estsales) from Jobs JJ where C.ClientID = JJ.ClientID) estsales

be back in a NY minute:) Ben
0
karooCommented:
John,

hmmm the SQL is working on my side...  
i can run the sp and queries from delphi, WISQL etc
couple of q's
1) Interbase version
2) where does the crash happen, in delphi or WISQL

this bugs me
<<
connection lost to database
>>
0
jbehAuthor Commented:
Hi Karoo

1) Interbase version
4.2

2) where does the crash happen, in delphi or WISQL
WISQL  

this bugs me
Me tooooooo!

I've been searching the interbase news groups as well as here and it seems that Interbase 4.2 is distinctly "iffy".

Apparently I am going to need to do some brute force hack to persuade interbase to play..

something like SET TERM ## ;

   create procedure client_sales
      returns (client_id integer, estsales numeric (15,2),
               actualsales numeric (15,2))
   as
   declare variable x numeric (15,2);
   declare variable y numeric (15,2);
   declare variable job_id integer;

   
   begin
       for select c.client_id
          from clients c
          into :client_id
       do begin
          estsales = 0;
          actualsales = 0;
          for select j.estsales, j.jobid
             from jobs j
             where j.clientid = :client_id
             into :x, :job_id
          do begin
             select sum (t.sales)
                from trans t
                where t.jobid = :job_id
                into :y        
             estsales = estsales + x;
             actualsales = actualsales + y;
          end
       suspend;
    end
   end
SET TERM  ; ##

although I can't get this to work either yet.....

GOD THIS IS FRUSTRATING !!!!!!!!!!!!!!!
0
jbehAuthor Commented:
I now have a "working" version as follows

Unfortunately it gives the wrong answers

In particular it gives null values for actualsales under some circumstances..

I think in situations in which there are jobs in existence but for which there are not as yet transactions


Still progress is progress  


/***************************************************************
*FIRST COMPILED VERSION - WRONG ANSWER BUT COMPILED NEVERTHELESS

SET TERM ## ;
create procedure client_sales
      returns
      (clientid integer, estsales numeric (15,2),actualsales numeric (15,2))
   as
      declare variable x numeric (15,2);
      declare variable y numeric (15,2);
      declare variable jobid integer;
      begin
            for select c.clientid from clients c
            into :clientid
            do
            begin
                  estsales = 0;
                  actualsales = 0;
                  for select j.estsales, j.jobid from jobs j
                  where j.clientid = :clientid
                  into :x, :jobid
                  do
                  begin
                        select sum (t.sales) from trans t
                        where t.jobid = :jobid
                        into :y ;
                        estsales = estsales + x ;
                        actualsales = actualsales + y;  
                  end
                  suspend;
            end
      end ##
SET TERM  ;##


0
RBertoraCommented:
jbeh:

This RBertora not Karoo :-)


Just wondering if you tried my last comment, as you said nothing about it:

Select
   C.clientid,C.clientname,
   SumSales = (Select sum(estsales) from Jobs JJ where C.ClientID = JJ.ClientID),
   sum(T.sales) TotActsales    
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

Rob;-)

P.S.
If that doesn't work, its all yours Karoo. :-)
0
karooCommented:
jbeh,

hmmm: IB 4.2, yep that is where i remember connection lost to db from, been a while:)

try the following query:
SELECT c.clientid, j.estsales, t.sales
  FROM clients c, jobs j, trans t
  WHERE j.clientid = c.clientid
    AND t.jobid = j.jobid
  ORDER BY c.clientid, j.clientid, t.jobid

if this query works then i have a possible solution for you otherwise well have to do nested selects, as in the example you provided, which i tried and got the same results as you did.

Also try the query from Rob's previous comment.

we will get this to work, Ben:)
0
jbehAuthor Commented:
Hi Karoo

I have tried Rob's suggestion with mixed results.

A direct cut and paste fails but I think that is partly a result of how the columns are defined.

I have tried this in a number of forms and in particular

Select
   C.clientid,C.clientname,
   (Select sum(estsales) from Jobs JJ where C.ClientID = JJ.ClientID)  estsales ,
   sum(T.sales) TotActsales    
from
  clients C, Jobs J, Trans T
where
  C.clientId = J.clientID and
  J.JobID = T.JobID
group by
  C.clientid,C.clientname

But quite honestly I haven't got any variants that I can think of to work...  The worst give "Illegal operation"  the best fail to run..

I am increasingly convinced that there is something very wrong with IB 4.2





I have tried your suggestion..

It runs and at a brief glance it returns the whole of the transactions file..

To give you an idea of sizes

Clients - 650
Jobs -  2,160
Transactions say 33,034

Your query worked in the sense that it returned a result set of some 30,000 lines see next



Records affected: 33034
Current memory = 2469888
Delta memory = 3072
Max memory = 2473984
Elapsed time= 187.62 sec
Buffers = 10000
Reads = 6
Writes 0
Fetches = 84156




we will get this to work, Ben:)

Your confidence is inspiring !!!!!!!!!!!!!!!!
0
karooCommented:
I know my query returns the whole result set, that is exactly what i want:) because there seem to be a problem any time we used sum or avg in the sql statement.

now we've got something to work with, i'm half way done with the SP will do it lunch time and get back to you soon after.

regards:)
0
RBertoraCommented:
good luck Ben :-)
0
karooCommented:
John,

lets try this one:

CREATE PROCEDURE CLIENT_SALES RETURNS (
  CLIENTID INTEGER,
  JOBID INTEGER,
  ESTSALES NUMERIC(15, 2),
  SALES NUMERIC(15, 2)
) AS
declare variable x numeric (15,2);
declare variable y numeric (15,2);
declare variable prevclientid integer;
declare variable prevjobid integer;
declare variable dummyclientid integer;
declare variable dummyjobid integer;
declare variable first smallint;
begin
  prevclientid = -1;
  prevjobid = -1;
  estsales = 0;
  sales = 0;
  first = -1;

  FOR SELECT c.clientid, j.jobid, j.estsales, t.sales
  FROM clients c, jobs j, trans t
  WHERE j.clientid = c.clientid
    AND t.jobid = j.jobid
  ORDER BY c.clientid, j.clientid, t.jobid
  INTO :clientid, :jobid, :x, :y
  DO
  begin
    if (prevclientid != :clientid) then begin
      if (first != -1) then begin
        dummyclientid = clientid;
        dummyjobid = jobid;
        clientid = prevclientid;
        jobid = prevjobid;
        SUSPEND;
        clientid = dummyclientid;
        jobid = dummyjobid;
        estsales = 0;
        sales = 0;
      end
      else begin
        first = 0;
      end
    end

    sales = sales + y;
    if (prevjobid != :jobid) then begin
        estsales = estsales + x;
    end

    prevclientid = :clientid;
    prevjobid = :jobid;
  end
  SUSPEND;
end

btw: ever thought about upgrading to IB 5.6:)

anyway, if this works on your side good, otherwise we have to do a couple of other checks i.e. NULL values or possible checksum errors / transactions in limbo etc. on your DB.
0
karooCommented:
oh, forgot to say: ignore the value returned by jobid, it was needed in the for select loop that is why it is included.

it will return the last jobid for a specific client, hmmm can optimize this a bit.

later Ben:)
0
jbehAuthor Commented:
Hi Ben

Got your note - Will play in the next hour and get back to you..

John



0
jbehAuthor Commented:
OK

I've played - It works !!!!!!!!!!!!!!!!!!!!!!!!!!!!

There are no obvious errors

Actualsales gives realistic results (with no null values)  EstSales also gives realistic results  and the very brief check I have done shows no obvious errors....

I haven't understood what you've done - I just copied the sql straight in and ran it but I'll have a real go at understanding later.


>>> btw: ever thought about upgrading to IB 5.6:)

Yes and after this little problem it has moved rapidly up from moderate priority to high priority  !!!!

I assume IB 5.6 wouldn't have given this grief

Regards John



0
karooCommented:
hey John,

glad it worked:), the data returned will be ok - i did some limited testing on my side. Do some spot checks on clients and include the first and last client in those checks.

and yes with IB5.6 all the queries Rob an I would have worked - they are actually better solutions for your problem.

you can optimize the SP by removing any reference to dummyjobid.

best of luck
Ben:)
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
jbehAuthor Commented:
Ben / Rob

I'm grateful to you both for all your efforts..

The major conclusion is that I need a better database

Regards

John
0
RBertoraCommented:
Well done Ben!
jbeh good luck with your project
and hope you upgrade soon
Rob ;-)
0
karooCommented:
:)
thanks Rob

John, i concur with Rob, upgrade IB
best of luck
Ben:)
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
Delphi

From novice to tech pro — start learning today.