1. Instead of
    Property Product As %String Required ];
    Property Item As %String CalculatedSqlComputeCode = { {*}=$e({Product},1,5)}, SqlComputed ];
    

    it is better to use

    Property Product As %String Required ];
    Property Item As %String(MAXLEN 5) [ RequiredSqlComputeCode = { {*}=$e({Product},1,5)}, SqlComputedSqlComputeOnChange = Product ];
    
  2. Check on the version 2017.2 can not, but checked on 2018.1
    SELECT * FROM Portal.ProductStats ps left JOIN  Portal.ProductCacheUpdates pcu ON (pcu.Item=ps.ItemWHERE ps.Item=?
    • ExtentSize=1 (Portal.ProductStats)
      ExtentSize=1000 (Portal.ProductCacheUpdates)
      
      Relative cost  = 1338
      ◾Read master map Portal.ProductStats.IDKEY, looping on ID.
      
      ◾For each row:
      
       Read index map Portal.ProductCacheUpdates.pcacheUpdsProd, using the given %SQLUPPER(Item), and looping on ID.
       For each row:
       Read master map Portal.ProductCacheUpdates.IDKEY, using the given idkey value.
       Generate a row padded with NULL for table Portal.ProductCacheUpdates if no row qualified.
       Output the row.
    • ExtentSize=1000 (Portal.ProductStats)
      ExtentSize=1 (Portal.ProductCacheUpdates)
      Relative cost  = 1219.2
      ◾Read index map Portal.ProductStats.pcacheUpds, using the given %SQLUPPER(Item), and looping on ID.
      
      ◾For each row:
      
       Read master map Portal.ProductStats.IDKEY, using the given idkey value.
       Read index map Portal.ProductCacheUpdates.pcacheUpdsProd, using the given %SQLUPPER(Item), and looping on ID.
       For each row:
       Read master map Portal.ProductCacheUpdates.IDKEY, using the given idkey value.
       Generate a row padded with NULL for table Portal.ProductCacheUpdates if no row qualified.
       Output the row.

      As you can see in both cases the index pcacheUpdsProd is used.

Have you really set up the tables and cleared the cached queries so that the optimizer can start using the new statistics?

Try to do it manually in the terminal:

blablabla>d $system.SQL.TuneSchema("Portal",1), $SYSTEM.SQL.Purge(), $system.Status.DisplayError($system.OBJ.CompilePackage("Portal","cu-d"))
Class dc.test Extends %RegisteredObject
{

ClassMethod Run(
  a,
  b)
{
  w $$$FormatText("a=%1, b=%2",$g(a,"<null>"),$g(b,"<null>")),!
}

ClassMethod Test()
{
  cName="dc.test",
    mName="Run",

    args=2,
    args(1)="2019-01-01",
    args(2)="1,2,3,4"
    
  d $classmethod(cName,mName,args...)
  
  args
  args=1,
    args(1)=77
  d $classmethod(cName,mName,args...)

  args
  args=2,
    args(2)=33
  d $classmethod(cName,mName,args...)

  args
  args=""
  d $classmethod(cName,mName,args...)
}

}

Result:

USER>##class(dc.test).Test()
a=2019-01-01, b=1,2,3,4
a=77, b=<null>
a=<null>, b=33
a=<null>, b=<null>

I checked for versions 2009.1/2010.1: unfortunately, the $zu(114,0) returns nothing, therefore, remains variant with the command line.

Example for Windows, provided that the system has a single network card:

#include %syConfig
 result
 w $zu(144,1,$$$DEFETHADDR),!,"------",!
 
 d $system.OBJ.DisplayError(##class(%Net.Remote.Utility).RunCommandViaCPIPE("getmac /NH /fo table",,.result))
 w $p(result," ",1)

PS: for other OS command line may be different.

Intel i5-2400

10000 digits ~ 58 sec.

calcPI(n) public { 
  s $lb(len,nines,predigit,r)=$lb(10*n\3,0,0,"")
  
  i=1:1:len a(i)=2
  
  j=1:1:{
    q=0
    i=len:-1:1 x=10*a(i)+(q*i), a(i)=x#(2*i-1), q=x\(2*i-1)
    a(1)=q#10, q=q\10
    q=9 {
      nines=nines+1
    }elseif q=10 {
      r=r_(predigit+1)_$$repeat^%qarfunc(0,nines), predigit=0, nines=0
    }else{
      r=r_predigitpredigit=q
      s:nines r=r_$$repeat^%qarfunc(9,nines), nines=0
    }
  }
  r_predigit
}