Brendan Bannon · Jul 11, 2022 go to post

Craig

If you are defining a Parent-Child Relationship with Default Storage the global structure of the child with change from ^child(id) to ^child(parentid,id) so you will not see any of the old data, no hint will fix that issue, sorry.

If your current child data is valid and you want to keep it I would suggest defining a Foreign Key constraint with Cascade on Delete.  This will keep the global structure the same but let you enforce your relationship.

Brendan Bannon · Dec 15, 2021 go to post

Nice clear description Benjamin.  This should be a big help for query performance.  It is such a common problem in support when we have to help customers deal with outliers.  Now the RTPC should do that work for them!

Brendan Bannon · Nov 28, 2020 go to post

So there is a zip file attached to this article that contains a bunch of examples.  the Class Mapping.ChildPiece should be close to what you need.  the global for that class Parent Child looks like this:

/// ^Par(1)=Parent1\Child11#child12#child13
///      2)=Parent2\Child21#child22
///      3)=Parent3
/// 

and the mapping for the child is:

<SQLMap name="Map1">
<Data name="ChildData">
<Delimiter>"^"</Delimiter>
<Piece>1</Piece>
</Data>
<Global>^Par</Global>
<RowReference>$P($P(^Par({L1}),"\",2),"#",{L2})</RowReference>
<Subscript name="1">
<Expression>{Mapping.ParentPiece.Sub1}</Expression>
</Subscript>
<Subscript name="2">
<AccessType>Piece</AccessType>
<Delimiter>"#"</Delimiter>
<Expression>{PieceCounter}</Expression>
<Invalidcondition name="1">
<Expression>$P(^Par({L1}),"\",2)=""</Expression>
</Invalidcondition>
</Subscript>
<Type>data</Type>
</SQLMap>

If you can get your working let me know and I can make an example for your global.

Brendan

Brendan Bannon · Nov 25, 2020 go to post

Hi Limyandi

I am not a fan of creating List of Objects as it make it hard to query the data.  I would rather expose this data as a child class somehow so SQL can be used to look at the data.  I would look at doing something along the lines of what is in Mapping.ChildPiece.

But you ask for an ugly List of Lists so that is what I did. 

The Serial class is just a normal one:

Class Mapping.Address2 Extends %SerialObject [ ClassType = serial, ProcedureBlock ]
{

Property RoadName As %String(TRUNCATE = 1);

Property SuburbanName As %String(TRUNCATE = 1);

Property StreetNumber As %String(TRUNCATE = 1);

Storage Default
{
<Data name="Address2State">
<Value name="1">
<Value>RoadName</Value>
</Value>
<Value name="2">
<Value>SuburbanName</Value>
</Value>
<Value name="3">
<Value>StreetNumber</Value>
</Value>
</Data>
<State>Address2State</State>
<StreamLocation>^Mapping.Address2S</StreamLocation>
<Type>%Library.CacheSerialState</Type>
}

}

Then for the Person class I needed to write Retrieval code that would get the different address out of the global and put them into the Nested $LIST() that the class is expecting.

Class Mapping.Person2 Extends %Persistent [ StorageStrategy = NewStorage1 ]
{Property Name As %String;

Property Addresses As list Of Mapping.Address2;

Property Sub1 As %Integer;

Index Master On Sub1 [ IdKey ];Storage NewStorage1
{
<ExtentSize>100</ExtentSize>
<SQLMap name="Map1">
<Data name="Addresses">
<RetrievalCode>
      set string=$P(^Person({L1}),"|",2)
      for i=1:1:$L(string,"^") {
           set address=$P(string,"^",i)
           set $LIST({*},i)=$LISTFROMSTRING(address,"~")
      }
 </RetrievalCode>
</Data>
<Data name="Name">
<Delimiter>"|"</Delimiter>
<Piece>1</Piece>
</Data>
<Global>^Person</Global>
<Structure>delimited</Structure>
<Subscript name="1">
<Expression>{sub1}</Expression>
</Subscript>
<Type>data</Type>
</SQLMap>
<StreamLocation>^Mapping.SimpleS</StreamLocation>
<Type>%CacheSQLStorage</Type>
}

}

Brendan Bannon · Oct 15, 2020 go to post

This looks like it is just a bug.  I reproduced this in InterSystems IRIS latest.  Looking at the generated code we are able to resolve the the index global in other places, but not in this one case.

I will report the bug to development to get it fixed.

My question is what do you plan to use this index for?

Brendan Bannon · Jul 20, 2020 go to post

Couple of this this could be.

The storage def is using SQL field names, so {SQLUser.Parent.IdAProp} might fix the error.

Do you have an IDKey index defined in User.Parent on both IdAProp and IdBProp?

Brendan Bannon · Jun 23, 2020 go to post

OK so I would try:  SELECT PROE,ID... and SELECT DESP,ID... to figure out which rows is causing the problem.

I would then look at how that %String is defined and then look at the data returned in the portal or shell to make sure the data matches how the property is defined. 

I have seen problems with some client side tools where they want to treat something we think is a simple VARCHAR as a CLOB.  If your %String has a large MAXLEN size then maybe this is what is going wrong?  To get to the bottom of that we would need to get an CacheODBC.log file ( or IRISODBC.log) and dig through the HEX dump.

Brendan Bannon · Jun 23, 2020 go to post

What are the datatypes for the Properties in the SELECT list?  This error is coming from VB and most likely is caused by the data not matching up with the datatype def. of the Property.  Doing a quick google search for this error the first three hits were related to Streams.

When you run a query in the portal we do not do any type validation so if you have a MAXLEN on a %String set we are not going to check that, we assume it passed on data input.  External tools might be typed and can fall over if a string is longer than we said it was.

Limiting the fields in the SELECT list to figure out what data type is causing the problem is step 1.  Step 2 would be limiting the rows to figure out which one is producing the error and then looking at that data to see why it might be causing the problem.

Brendan Bannon · Jun 16, 2020 go to post

If you load a new version of your classes, or compile your classes on the system that will also purge Cached Queries so you would want to run the method then as well.

Brendan Bannon · Jun 10, 2020 go to post

If you want to be sure your customer will get the same generated code you tested in house you could freeze all the plans after running GenerateEmbedded():  Do $SYSTEM.SQL.FreezePlans(1,1,,.Errors)

To generate the Cached queries for your testing and after installing your product on a customer system you can call:  d $SYSTEM.OBJ.CompilePackage("%sqlcq")

These 3 steps should get you back the behaviors you liked about Embedded SQL.

Brendan Bannon · May 8, 2020 go to post

So the issue with Inheritance and Cache SQL Storage is you can't modify the storage to add new properties in the sub class.  For you that does not matter, you just want to change the global name.  So if there was some way we could write the code so that it would generate a different global name for the different classes you would be all set.  I tried using $parameter($classname,"MyParameter") but could not get it to work.  One issue, I think, is when we are compiling this code we are in a Cached Query class, not the class I want to be referencing.  

The best I could do is hard code it to the current class.  So if you have 2 classes with the info below the only change you need to make is to change the parameter value from A to B.

Class Mapping.SuperClass Extends %Persistent [ Language = objectscript, StorageStrategy = NewStorage1 ]
{Parameter GloParameter = {"A"};Property GlobalName As %String;Property Sub1 As %Integer;Property Data1 As %String;Index Master On (GlobalName, Sub1) [ IdKey ];Storage NewStorage1
{
<SQLMap name="Map1">
<Data name="Data1">
<Delimiter>"^"</Delimiter>
<Piece>1</Piece>
</Data>
<Global>^*</Global>
<Subscript name="1">
<AccessType>Global</AccessType>
<Expression>{GlobalName}</Expression>
<NextCode> i {L1}="" s {L1}=##class(Mapping.SuperClass).#GloParameter QUIT
 s {L1}=""
</NextCode>
</Subscript>
<Subscript name="2">
<AccessType>Sub</AccessType>
<Expression>{Sub1}</Expression>
</Subscript>
<Type>data</Type>
</SQLMap>
<StreamLocation>^Mapping.SuperClassS</StreamLocation>
<Type>%Storage.SQL</Type>
}}

Brendan Bannon · May 7, 2020 go to post

I guess Should have read the last post.

Cache SQL Storage does not work with inheritance.

If you use the example I have and instead of having the first subscript level loop over different values you could set the value there.  You could try using it with a class parameter, I have not tried that but it should work.

Brendan Bannon · May 7, 2020 go to post

Arto

You can have one class look at multiple globals by defining the first subscript level to be of type Global and the global name for the map would be *

I have an example of this in the Mapping zip file that is part of the SQL storage series I did a couple of years back.  Mapping.TwoGlobals.cls

In that class, I have 2 globals:  Member and Provider.

/// 
/// ^Member(1,"08/24/2005 11:39",4)=One Memorial Drive
///                              5)=3rd Floor
///                              7)=Cambridge
///                              8)=MA
///                              9)=02142
///  
/// Array: ^Provider
/// ^Provider(1,"08/24/2005 11:43",10)=
///                                11)=15 Cliff Dr
///                                12)=
///                                13)=
///                                14)=Plainville
///                                15)=MA
///                                16)=02762

Theglobal and first subscript looks like:

<Global>^*</Global>
<Subscript name="1">
<AccessType>Global</AccessType>
<Expression>{GloType}</Expression>
<NextCode> i {L1}="" s {L1}="Member" QUIT
 i {L1}="Member" s {L1}="Provider" QUIT
 s {L1}=""
 </NextCode>
</Subscript>

The class has an extra twist in that the two globals are not exactly the same so the fourth subscript level needs to deal with the data being in different places.

you can get all my mapping examples from here

Brendan Bannon · Mar 4, 2020 go to post

Well, that is strange.   I don't know of any reason ODBC would behave differently for those kits.

Brendan Bannon · Mar 4, 2020 go to post

Not sure if you contacted the WRC or not, but if not here is what I would try.

1)  Can you use a different ODBC tool?  Our fav in support is WinSQL.  If this works then we know we are dealing with an ADO issue, if it fails then most likely we have an ODBC bug.

2)  Turn on the ODBC client logging. If you are using the ODBC Data Source Manager then there is a checkbox ODBC logging.  You will need to stop and start your ADO client to pick up the flag, reproduce the problem and then look for the irisODBC.log file in C:\Users\Public\Logs

Brendan Bannon · Mar 4, 2020 go to post

Ken

We have removed our old FTP server.  The best way to get an ODBC Drive is to contact InterSystems Support:  Phone 1 617 621 0700 or Email Support@InterSystems.com

As for the error you are getting, older versions of Cache did have issues with a long-lasting (days) connection getting into a state where it would no longer work and reconnecting would be needed.

Brendan Bannon · Jan 29, 2020 go to post

OK I needed to do one more click, your Org does not have a support contract so you can't have access to these pages, sorry.

Maybe Learning Services could help you out but I can't grant you access to the kits on the WRC.

Brendan Bannon · Jan 17, 2020 go to post

Neerav

When the optimizer is looking at a query it does not look at the parameter values, in fact, they are removed before the query gets to the optimizer, so it sees:  WHERE Tad1.Id  > ?  Because the value is removed the optimizer can only guess at how restrictive the condition is, and it is an optimist so it assumes it will be restrictive.   That is why the Relative Cost changed so much.  The optimizer thinks this query will return only a small number of rows.

Range conditions that are not restrictive can cause performance problems if there is an index on the property involved.  Since the optimizer thinks the condition is restrictive it might choose to use the index and end up doing a table scan.  If you have a range condition that you know is NOT restrictive you might want to put the %NOINDEX hint in front of the condition so the optimizer knows not to use it.

WHERE %NOINDEX Tad1.Id > ?

Brendan Bannon · Nov 13, 2019 go to post

Hello

I was able to get this to work.  I think the only chagne I made was to add the SQLName to the query

Here is my class with your code

Class DC.ProcClass [ Abstract ]
{Query ClosedFutures(FromDate As %Date, ToDate As %Date) As %Query(ROWSPEC = "FundCode:%String, Asset:%String, TradeDate:%Date,BuySell:%String, Price:%Numeric, Quantity:%Numeric, CumulativePosition:%Numeric, PreviousCumulativePosition:%Numeric, ClosingTrades:%Integer, ClosingTradesInPeriod:%Integer, ClosedTradeValueInPeriod:%Numeric, NetQuantityInPeriod:%Integer, WeightedClosedAverageTradePriceInPeriod:%Numeric, InitialTradeDate:%Date, InitialTrade:%Integer, LastInPeriod:%Integer") [ SqlName = ClosedFuturesProc, SqlProc ]
{
}ClassMethod ClosedFuturesExecute(ByRef qHandle As %Binary, FromDate As %Date, ToDate As %Date) As %Status
{
// Contains complex implementation including setting ^||justClosedTrades
}ClassMethod ClosedFuturesClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ClosedFuturesExecute ]
{
kill ^||justClosedTrades
Quit $$$OK
}ClassMethod ClosedFuturesFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ClosedFuturesExecute ]
{
set nextid=$order(^||justClosedTrades(qHandle),1,rec)
if $length(nextid) set Row=$listbuild($piece(rec,"~",1),$piece(rec,"~",2),$piece(rec,"~",3),$piece(rec,"~",4),$piece(rec,"~",5),$piece(rec,"~",6),$piece(rec,"~",7),$piece(rec,"~",8),$piece(rec,"~",9),$piece(rec,"~",10),$piece(rec,"~",11),$piece(rec,"~",12),$piece(rec,"~",13),$piece(rec,"~",14), $piece(rec,"~",15), $piece(rec,"~",16)),qHandle=nextid }
else set AtEnd=1,Row="" }Quit $$$OK
}}

and here is the view using my schema and SQLName

Class DC.NewClass1 [ ClassType = view, ViewQuery = {
SELECT FROM DC.ClosedFuturesProc(65257,65286) --'2019-09-01','2019-09-30'
} ]
{Parameter READONLY = 1;}

Brendan Bannon · Oct 30, 2019 go to post

Sergey

The biggest difference between the 2 queries is the second one is really a JOIN, not a Left Outer Join.  By adding the second WHERE condition on a property in pcu you are making this a JOIN and allowing the Optimizer to start in either table.  I created the tables on my system and here is what I am seeing

The first query starts in ps using the item index and then joins to the pcu table using a temp file built from the master map (same as the plan you show)

The second query is starting with the pcu table and the doing a join to the ps table using a temp file build from the Item index and the master map.

Why don't we use the Item index in pcu?  I can't tell you exactly why, but I know the 2 biggest factors we are looking at are the size of the maps on disk and the Selectivity of the properties involved.  In both cases, we are going to need to read the master map so the question is this:  Will reading in the Index map reduce the number of blocks we need to read of the master map to make it worth the effort?  Without proper values generated by TuneTable, or supplied by you, the optimizer is left to guess at what these values are.  In the first case, it has decided that reading the index will not reduce the number of block reads of the master map so it is not using it.  This generally happens when we think the Master Map is very small.

To take this any further I would need you to run TuneTable on both of these tables and see if the poor plan is still used.  If it is still used please provide the storage info for both classes so I can investigate this.

Brendan Bannon · Oct 30, 2019 go to post

Hi Kurt

Sorry about that.  I have corrected the issue with downloading studio.

Please let us know if you run into anything else.

Brendan Bannon · Aug 15, 2019 go to post

I agree with Danny.  I would want to see the classes and the Show Plan for the 2 queries.

Did you define any indices in the classes and not populated them?

Brendan Bannon · May 15, 2019 go to post

Ya I am trying to figure that out.  I now have a link that looks like it should work but it does not.

Brendan Bannon · May 15, 2019 go to post

Keith

Here is a simple example I came up with that can show you the 2 different ways to do this.  As the comments in the class state the restriction for Mapping.OneClassParentChild is every parent must have at least 1 child as the IDKey for the rows is the Child row.  If you need to support parents without children this will not work.  I will need to think about how we might be able to do that if that is needed.  I would also push harder for the 2 classes and a view approach if that is needed.

The Main trick here is to make understand that each subscript level is made up of 3 different parts that you can change or reference: 

     1) Data Access Expression and Data Access Variables:  This is the global and subscripts to be used at this level by the Next code, for example, you could have 3 levels that loop over 1 global and then at level 4 switch to a different global and only use 2 of the subscript values....  Data Access Variables are variables you can set at a given level before the Next code is executed.  You can use them at this level or any higher level and you can set them to values from lower levels.

     2) Next Code:  This is the code to get from one value row to the next in a table.  This could be the default $ORDER() code that is generated for a simple Subscript, or it could be override code you provide.

     3) Invalid Conditions:  If you provide Next Code at a given level then you should also provide Invalid Conditions.  This is a test you can use to see if you have a valid row when the subscript value is provided as a constant instead of needing to loop to find the next one.

For your class I reference the Data Access Expression at Subscript Level 3 {3D} in the data mapping.  This would be the global with the subscripts defined from the 2 lower Subscript levels:  ^Mapping({L1},{L2})

OK so that was a lot of details you did not ask for, but just in case someone wanted to understand all the gory details I figured I would write them down.

This xml file has the different classes I created:  Examples

Brendan Bannon · May 15, 2019 go to post

Hi Keith

You can map anything!

the question is do you really want to and is it the best thing to do?

This looks like a classic Parent Child relationship that would be defined by 2 classes.  You could then create a view that would JOIN the 2 tables together and expose that view via ODBC and you would have what you want.

I can create a simple example that will show this data in one class, but I don't think it will add much value.

Brendan Bannon · Apr 23, 2019 go to post

Giray

Your code does not match your class descriptions.  In your code you are using a property, Parent to do the child lookup, but there is no parent property defined in Example.Child.

If you truly want a Many to Many then you can't define a Parent property in the child, unless it is a list, and I am not recommending that!

I am not a fan of Lists of Objects, but this might not be too bad.  I think I would still rather go with an array of objects as that would give you a third table and line up with the classic Many to Many format using 3 tables.  If you want to stay with a List then I would suggest that you pass the List of Child IDs into the QueryChildren method.  You can then use the list of Object IDs to get each child via SQL or Objects.

Brendan Bannon · Feb 13, 2019 go to post

Sorry for not answering this sooner.  In Cache / InterSystems IRIS when you define a Parent Child relationship between 2 classes the Child class has an IDKey made up of 2 values:  a reference back to the Parent and a unique identifier.  Because the IDKey is a compound key you can not define a Bitmap index in the Child Ccass.  That is drawback #1.   Bitmap indices are not the be all / end all, but they can give you GREAT performance so it is nice to keep this option on the table.

The Parent Child relationship Storage is the second drawback, while it is not required, and it might not always be a drawback, the classic storage for a Parent Child relationship is to store all the data in the Parent Global. 

Now to explain this I want to make sure everyone knows what I am talking about when I say Global.  InterSystems stores persistent data in a sparse tree structure that we call Globals.   Subscripts are always stored in ASCII Collated order, so if I did it right the way I show the globals here is the same way they would be stored.  A simple persistent class will store the data in a global like this:  ^Package.ClassD(id), where id in a unique integer.  So 4 rows in this table could look like:

      ^Package.ClassD(1)=$LISTBUILD("data 1")

      ^Package.ClassD(2)=$LISTBUILD("data 2)

      ^Package.ClassD(3)=$LISTBUILD("data 3")

      ^Package.ClassD(4)=$LISTBUILD("data 4")

If we define a Child table in this class then we will store that child data in a lower level subscript

    ^Package.ClassD(1)=$LISTBUILD("data 1")

          ^Package.ClassD(1,"Child",1)=$LISTBUILD("Child 1")

        ^Package.ClassD(1,"Child",2)=$LISTBUILD("Child 2")

      ^Package.ClassD(2)=$LISTBUILD("data 2)

        ^Package.ClassD(12"Child",4)=$LISTBUILD("Child 4")

      ^Package.ClassD(3)=$LISTBUILD("data 3")

      ^Package.ClassD(4)=$LISTBUILD("data 4")

        ^Package.ClassD(4,"Child",3)=$LISTBUILD("Child 3")

Now add a second Parent Child relations where the child class is the parent so we have 3 subscript levels

    ^Package.ClassD(1)=$LISTBUILD("data 1")

         ^Package.ClassD(1,"Child",1)=$LISTBUILD("Child 1")

              ^Package.ClassD(1,"Child",1, "Grand",3)=$LISTBUILD("Grand 3")

        ^Package.ClassD(1,"Child",2)=$LISTBUILD("Child 2")

              ^Package.ClassD(1,"Child",2, "Grand",1)=$LISTBUILD("Grand 1")

              ^Package.ClassD(1,"Child",2, "Grand",2)=$LISTBUILD("Grand 2")

      ^Package.ClassD(2)=$LISTBUILD("data 2)

        ^Package.ClassD(12"Child",4)=$LISTBUILD("Child 4")

      ^Package.ClassD(3)=$LISTBUILD("data 3")

      ^Package.ClassD(4)=$LISTBUILD("data 4")

        ^Package.ClassD(4,"Child",3)=$LISTBUILD("Child 3")

It should be easy to imagine this global becoming very large storing all the different children at all the different levels.  This increases the cost of getting the data for 1 class into memory because we need to load all the above data into memory just to get the 4 rows from the top level parent table.  This increased IO is the second issue with Parent Child Relationships.

When you define a Foreign Key between 2 classes the storage structure is not changed.  Both classes by default will have a positive Integer as the IDKey so they will both support Bitmaps.  They will each have their own D global so you will only need to load the data for the "Child" table when it is needed.  

If you go with the Foreign Key instead of the Parent Child make sure you define the FKey to use Cascade on Delete and you need to define an index in the "Child" class on the reference to the "Parent" class.

OK so that was longer than I thought it would be.  Hope it makes sense.  If you have more questions i will try to answer them faster this time.