There is another way.
If you want any empty strings to always be treated as null/"" instead of "/$c(0), then there is an documented setting (within the scope of namespace), namely:

^%SYS("sql","sys","namespace",<YOUR_NAMESPACE>,"empty string")

Here is a small example:

Class dc.test Extends %Persistent
{

Property As %Integer;

Property str As %String;

/// do ##class(dc.test).Test()
ClassMethod 
Test()
{
 
do ..%KillExtent()
 
 
try{
  
  
do $system.SQL.Purge()
  
set ^%SYS("sql","sys","namespace",$namespace,"empty string")=$c(0)

  ; '' -> $c(0)
  
do ##class(%SQL.Statement).%ExecDirect(,"insert into dc.test(i,str) values(1,'')")
  
  
; null -> ""
  
do ##class(%SQL.Statement).%ExecDirect(,"insert into dc.test(i,str) values(2,null)")
  
  
do $system.SQL.Purge()
  
set ^%SYS("sql","sys","namespace",$namespace,"empty string")=""

  ; '' -> ""
  
do ##class(%SQL.Statement).%ExecDirect(,"insert into dc.test(i,str) values(11,'')")
  
  
; null -> ""
  
do ##class(%SQL.Statement).%ExecDirect(,"insert into dc.test(i,str) values(22,null)")

  zwrite ^dc.testD
  
kill ^%SYS("sql","sys","namespace",$namespace,"empty string")
 
}
 
catch(ex){
  
write ex.DisplayString(),!
 
}
}

}

USER>do ##class(dc.test).Test()
^dc.testD=4
^dc.testD(1)=$lb("",1,$c(0))
^dc.testD(2)=$lb("",2,"")
^dc.testD(3)=$lb("",11,"")
^dc.testD(4)=$lb("",22,"")

Another option just for the fun:

Include %SYS.PTools.Stats

Class dc.test Abstract ]
{

ClassMethod setValue(args...) As %Status
{
  
quit:args<2 $$$ERROR($$$DataMissing)

  $$$convertArrayToList(args,list)
  
quit:$listlength(list)'=args $$$ERROR($$$RequiredArgumentMissing)
  
  
set $list(list,*,*)="",
         
var=##class(%Utility).FormatString(list),
         
$extract(var,1,3)=$name(%sessionData), ##; or $name(%session.Data)
         
@var=args(args)
  
quit $$$OK
}

/// do ##class(dc.test).Test()
ClassMethod 
Test()
{
  
new %sessionData

  try{

    do $system.OBJ.DisplayError(..setValue("key1""val1")),
          
$system.OBJ.DisplayError(..setValue("key1""key2""key3""key4""val2")),
          
$system.OBJ.DisplayError(..setValue("key1""key2", , "key4""val3")),
          
$system.OBJ.DisplayError(..setValue())
    
    
write !
    
zwrite %sessionData
  
}
  
catch(ex){
    
write ex.DisplayString(),!
  
}
}

}

Try adding a new index and don't forget make rebuild index/tunetable/recompile class

Index idx On (prop1, prop3) [ Type = bitmap ];

Here yet need the help of @Kyle.Baxter.

PS: by the way, check

select count(*from mp.test where prop1=2

to insert the correct values in the code

%VID BETWEEN 3000000 AND 30000005

Because of this, is obtained

0 Rows(s) Affected

I don't think I quite understand you.

That is, you need to:

  1. do

    SELECT FROM <table> WHERE <difficult condition> ORDER BY <some field> DESC

  2. then select a certain data page from the resulting result

    select * from (step1where %VID between N1 and N2

If not, provide a ready-made example with data similar to the example by @MARK PONGONIS

 

Source code

USER>##class(mp.test).Fill(5000000)

USER>##class(mp.test).Query()
count=3833346

first 5 [1:5]
ID prop3
3 3
4 3
24 3
30 3
97 3

5 Rows(s) Affected
.000328 secs

last 5 [3833342:3833346]
ID prop3
4999798 1
4999817 1
4999836 1
4999866 1
4999947 1

5 Rows(s) Affected
2.884304 secs

PS: for those who put a minus. May I ask why?

My EAV implementation is the same as your Approach 3, so it will work fine even with fully filled 4.000.000 attributes.
Since the string has a limit of 3,641,144, approaches with serial and %List are dropped.

All other things being equal, everything depends on the specific technical task: speed, support for Objects/SQL, the ability to name each attribute, the number of attributes, and so on.

Here is a ready-made example (works even in last Caché):

Class dc.test Extends %Persistent
{

Property title As %VarString;

Property author As %VarString;

/// do ##class(dc.test).test()
ClassMethod test()
{
  &sql(truncate table dc.test)
  
  &sql(insert into dc.test(title,author)
    select 'For Whom the Bell Tolls','Hemmingway' union
    select 'The Invisible Man','Ellison' union
    select 'Tender is the Night','Fitzgerald')
  
  set provider=##class(%ZEN.Auxiliary.altJSONSQLProvider).%New(),
      provider.sql="select title,author from dc.test",
      provider.arrayName="items",
      provider.maxRows = 0,
      provider.%Format "tw"
  
  do provider.%WriteJSONStreamFromSQL(.stream,,,,,provider)
  
  set json={}.%FromJSON(stream),
      json.results=json.items.%Size()

  write json.%ToJSON()
}

}

Result:

USER>do ##class(dc.test).test()
{"items":[{"title":"For Whom the Bell Tolls","author":"Hemmingway"},{"title":"The Invisible Man","author":"Ellison"},{"title":"Tender is the Night","author":"Fitzgerald"}],"results":3}
 
Added several more types:

Result:

USER>##class(dc.test).test()
"" => undefined
"" => empty
1 => string
1 => integer
1.1 => float
$double(1.1000000000000000888) => double
"1@%Library.ListOfDataTypes" => object
$lb() => list
$zwc(128,4)_$c(1,0,0,0)/*$bit(1)*/ => bitstring
$c(7) => bitstring
"" => array

Extending the reply of @Robert Cemper

  • The following query compiles without errors, even though Studio highlights the error
    &sql(select :fieldname into :fieldvar from :tablename)

    Everything is fine in Caché: the error occurs at the compilation-time.

    In order for an error to occur in IRIS Studio too you need to use the flag /compileembedded=1

  • It is strange that the documentation mentions field name, but there is no mention of table name:
    A host variable cannot be used to pass a field name or keyword into an SQL statement. proof