$LISTFIND certainly does its job well, but there is a better solution.

As the data grows, the search speed will drop, since this solution does not use indexes in any way.
Therefore, the best solution is to use the predicate FOR SOME %ELEMENT.

For more details with examples, see one of my articles: SQL Performance Resources (item k. Indexing of non-atomic attributes)

Only customers with the corresponding paid valid service have access to the download from WRC (see in the price list the section Services Fees: Software Updates, Technical Assistance).
Those who use the Community Edition or who have expired, can not download anything from there.

Therefore, in this case, open resources, such as Github or FTP, are more preferable.

The interactive tutorials with exercises built into the documentation helped me a lot in my time:

Also included was a database "SAMPLES" (code and data) with examples for every taste. I often used it for performance testing.

Note: I give a link to version 2016.2, because since version 2017.1, for some reason, the developers have changed the display styles in the online documentation and removed some information. Local documentation does not have these disadvantages.

In addition, in the <cachesys>\dev\ folder there were numerous demos with source code for various programming languages and connection technologies (С++, C#, Java, PHP, Python, ODBC, JDBC, ADO.NET, ActiveX, XEP, etc.):

 
Incomplete content

Examining an XML Subtree

  try{

    $$$ThrowOnError(##class(%XML.XPATH.Document).CreateFromString("<a><b><c>some content</c></b></a>", .doc))
    
$$$ThrowOnError(doc.EvaluateExpression("/a""b", .field))
    
#dim obj As %XML.XPATH.DOMResult field.GetAt(1)
    
    
while obj.Read() {
      
if obj.HasValue {
        
write obj.Path,": ",obj.Value,!
      
}
    }
    
  }
catch(ex){
    
write "Error "ex.DisplayString(),!
  
}

Result:
b\c: some content

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

 

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?