views:

3354

answers:

8

Usually pagination queries look like this. Is there a better way instead of making two almost equal methods, one of which executing "select *..." and the other one "count *..."?

public List<Cat> findCats(String name, int offset, int limit) {

    Query q = session.createQuery("from Cat where name=:name");

    q.setString("name", name);

    if (offset > 0) {
     q.setFirstResult(offset);
    }
    if (limit > 0) {
     q.setMaxResults(limit);
    }

    return q.list();

}

public Long countCats(String name) {
    Query q = session.createQuery("select count(*) from Cat where name=:name");
    q.setString("name", name);
    return (Long) q.uniqueResult();
}
+2  A: 

If you don't need to display the total number of pages then I'm not sure you need the count query. Lots of sites including google don't show the total on the paged results. Instead they just say "next>".

Kyle Dyer
Agreed! I solved this problem the same way that Kyle is suggesting you do. If your manager gives you a problem about it, point out the performance hit and then use real-world examples like Google to back up your claim.
bpapa
Google returns an estimate of the number of results and it won't show you anything past the first thousand, that's why what google as done is acceptable - it's only an approximation.In other cases, having links to a number of pages (including the last) is often a quite an important feature.
Andrew Ingram
@Andrew, But how to approximate the number of results without issuing a 'select count(*)'
Reddy
A: 
anjanb
That wasn't what he was asking. Take a look at his code posted, you'll see he uses both those methods. He's asking whether or not there is a way to avoid using "count" as well as doing the select when doing pagination.
MetroidFan2002
+2  A: 

There is a way

mysql> SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name
    -> WHERE id > 100 LIMIT 10;
mysql> SELECT FOUND_ROWS();

The second SELECT returns a number indicating how many rows the first SELECT would have returned had it been written without the LIMIT clause.

Reference: FOUND_ROWS()

michal kralik
Would this only be applicable in MySQL?
MetroidFan2002
Not sure about different dbs, but I'd say no
michal kralik
Pretty sure this is an extension to SQL-92 and not cross platform. I've been wrong before, and it was 6 years ago when I first came across this issue, but I remember that I had to avoid this approach for compatibility reasons.
Josh
Only downside to this is that it usually has a greater burden than just doing two queries, one with COUNT(*). The reason is that it must access all rows, and COUNT(*) can often be satisfied just be accessing index entries.
thomasrutter
Another issue with this example code specifically, is that FOUND_ROWS() would return the number of rows with id > 100. I'm not sure that this is what was intended.
thomasrutter
+1  A: 

I know this problem and have faced it before. For starters, the double query mechanism where it does the same SELECT conditions is indeed not optimal. But, it works, and before you go off and do some giant change, just realize it might not be worth it.

But, anyways:

1) If you are dealing with small data on the client side, use a result set implementation that lets you set the cursor to the end of the set, get its row offset, then reset the cursor to before first.

2) Redesign the query so that you get COUNT(*) as an extra column in the normal rows. Yes, it contains the same value for every row, but it only involves 1 extra column that is an integer. This is improper SQL to represent an aggregated value with non aggregated values, but it may work.

3) Redesign the query to use an estimated limit, similar to what was being mentioned. Use rows per page and some upper limit. E.g. just say something like "Showing 1 to 10 of 500 or more". When they browse to "Showing 25o to 260 of X", its a later query so you can just update the X estimate by making the upper bound relative to page * rows/page.

Josh
+5  A: 

Baron Schwartz at MySQLPerformanceBlog.com authored a post about this. I wish there was a magic bullet for this problem, but there isn't. Summary of the options he presented:

  1. On the first query, fetch and cache all the results.
  2. Don't show all results.
  3. Don't show the total count or the intermediate links to other pages. Show only the "next" link.
  4. Estimate how many results there are.
Eric Rath
+2  A: 

You can use MultiQuery to execute both queries in a single database call, which is much more efficient. You can also generate the count query, so you don't have to write it each time. Here's the general idea ...

var hql = "from Item where i.Age > :age"
var countHql = "select count(*) " + hql;

IMultiQuery multiQuery = _session.CreateMultiQuery()
    .Add(s.CreateQuery(hql)
            .SetInt32("age", 50).SetFirstResult(10))
    .Add(s.CreateQuery(countHql)
            .SetInt32("age", 50));

var results = multiQuery.List();
var items = (IList<Item>) results[0];
var count = (long)((IList<Item>) results[1])[0];

I imagine it would be easy enough to wrap this up into some easy-to-use method so you can have paginateable, countable queries in a single line of code.

As an alternative, if you're willing to test the work-in-progress Linq for NHibernate in nhcontrib, you might find you can do something like this:

var itemSpec = (from i in Item where i.Age > age);
var count = itemSpec.Count();
var list = itemSpec.Skip(10).Take(10).AsList();

Obviously there's no batching going on, so that's not as efficient, but it may still suite your needs?

Hope this helps!

Tobin Harris
i couldnt find the equivalent for Hibernate for Java (which is what the op needs). Did you know if there is an java version?
Leonel Martins
A: 

I think the solution depends on database you are using. For example, we are using MS SQL and using next query

select 
  COUNT(Table.Column) OVER() as TotalRowsCount,
  Table.Column,
  Table.Column2
from Table ...

That part of query can be changed with database specified SQL.

Also we set the query max result we are expecting to see, e.g.

query.setMaxResults(pageNumber * itemsPerPage)

And gets the ScrollableResults instance as result of query execution:

ScrollableResults result = null;
try {
    result = query.scroll();
    int totalRowsNumber = result.getInteger(0);
    int from = // calculate the index of row to get for the expected page if any

    /*
     * Reading data form page and using Transformers.ALIAS_TO_ENTITY_MAP
     * to make life easier.
     */ 
}
finally {
    if (result != null) 
        result.close()
}
ruslan
A: 

At this Hibernate wiki page:

https://www.hibernate.org/314.html

I present a complete pagination solution; in particular, the total number of elements is computed by scrolling to the end of the resultset, which is supported by now by several JDBC drivers. This avoids the second "count" query.

Pietro Polsinelli