1

Ok, I have experience in various databases, but not Oracle specifically, so still trying to wrap my head around how it handles things (i.e. the lack of 'Top' drives me mad!) Anyway, here's what we're trying to do...

Note: We're using Oracle 11. I understand a new feature was added to Oracle 12 (i.e. FETCH NEXT n ROWS ONLY) but we can't use that unfortunately.

To start, we have an articles table (technically it's a view, but I'm simplifying it here). Here are the relevant fields...

  • ID (Integer, Primary Key)
  • Title (String)
  • Body (String)
  • IsFavorite ('Y' or 'N')
  • IsFeatured ('Y' or 'N')
  • DatePublished (Date)

Multiple articles can be marked as favorites and/or featured.

What we want to return is a result set that has the following, in order...

  1. The most-recently published article with IsFeatured = 'Y', if any.
  2. The most-recently published article with IsFavorite = 'Y' that's not the row from #1 (this avoids duplicates if the most recent featured is also the most recent favorited and instead selects the next favorited row) if any
  3. The three most-recently published articles that are not #1 or #2, if any

Here's what I've come up with so far (cut/pasted/edited here so there may be some typos), but this just feels so 'clunky' to me... like I'm wasting a lot of unnecessary processing and iterations.

WITH mostRecentlyFeatured as (
    Select * from (
        Select 2 as MAJOR_SORT, A.*
        From Articles A
        where IsFeatured = 'Y'
        order by DatePublished DESC
    )
    where ROWNUM = 1
),
mostRecentlyFavorited as (
    Select * from (
        Select 1 as MAJOR_SORT, A.*
        From Articles A
        minus Select * From mostRecentlyFeatured
        where IsFavorite = 'Y'
        order by DatePublished DESC
    )
    where ROWNUM = 1
),
topThreeOthers as (
    Select * from (
        select 0 as MAJOR_SORT, A.*
        from Articles
        minus
        SELECT * from mostRecentlyFeatured
        minus
        SELECT * from mostRecentlyFavorited
        order by DatePublished desc
    )
    where ROWNUM <= 3
),
finalRows as (
    Select * from mostRecentlyFeatured
    union all
    Select * from mostRecentlyFavorited
    union all
    select * from topThreeOthers
)
Select * from finalRows
Order By MAJOR_SORT    DESC,
         DatePublished DESC;

This isn't the most uncommon of queries so I can't imagine there isn't a better way to do this, but I'm just not yet seeing it. So is there?

Mark A. Donohoe
  • 23,825
  • 17
  • 116
  • 235
  • 1
    Please edit the question and show the table definitions, sample data, and expected output. thanks – OldProgrammer May 01 '20 at 16:09
  • Still missing relevant information. Please show the actual DDL for the table creation, and sample data as a table, and from that table data, expected output. – OldProgrammer May 01 '20 at 18:08
  • There is no actual DDL as I said this is an example from a much larger set. It's more about how you grab the top row based on a condition, then combine it with another top row based on a different condition. In other words, what would be the approach regardless of what those other fields are if you focus on just the fields I've presented. It may be helpful to start by looking at my SQL. – Mark A. Donohoe May 01 '20 at 18:11
  • For instance, is there a way to combine what I'm naming 'allFeatured' with what I'm naming 'mostRecentlyFeatured'. In other languages, that's a simple 'Top' statement on the initial one, but that doesn't seem to be the case here since from what I was told, ROWNUM is based on the source, not the results of the WHERE clause. – Mark A. Donohoe May 01 '20 at 18:18

2 Answers2

2

Here's a possible solution to your query. It uses a set of CTEs to select the first n featured articles, the first m favourite articles (that are not in the first n featured articles) and a total of k articles:

  • the first generates row numbers for featured and favourite articles;
  • the second counts how many of the first n featured articles are also int the first m favourites; and
  • the third generates a row number for articles sorted based on first n featured, first m favourites (unless they were also amongst the first n featured articles, in which case the next favourites are chosen) and date published.

Finally the top k rows are selected from the last CTE.

Here's a query where n = 2, m = 3 and k = 8:

-- first 2 (n) featured, first 3 (m) favourites, then 3 others (8 (k) total)
WITH favfeatrank AS (
  -- rank articles by date and whether they are favourite or featured
  SELECT ID, Title, IsFavorite, IsFeatured, DatePublished,
         ROW_NUMBER() OVER (ORDER BY IsFeatured DESC, DatePublished DESC) feat,
         ROW_NUMBER() OVER (ORDER BY IsFavorite DESC, DatePublished DESC) fav
  FROM Articles
),
numfavfeat AS (
  -- number of favourites that are also in the first n featured
  -- use m in the next line
  SELECT COUNT(CASE WHEN fav <= 3 THEN 1 END) AS featfav
  FROM favfeatrank
  WHERE feat <= 2 -- use n here
), 
articlerank AS (
  -- articles ranked according to featured, favourite and date published
  SELECT ID, Title, IsFavorite, IsFeatured, DatePublished,
         ROW_NUMBER() OVER (ORDER BY CASE WHEN feat <= 2 THEN 0 -- use n here
                                          WHEN fav <= 3 + featfav THEN 1 -- use m here
                                          ELSE 2
                                          END,
                                      DatePublished DESC) AS rn

  FROM favfeatrank
  CROSS JOIN numfavfeat
)
SELECT ID, Title, IsFavorite, IsFeatured, DatePublished
FROM articlerank 
WHERE rn <= 8 -- use k here

Demo on dbfiddle

Nick
  • 118,076
  • 20
  • 42
  • 73
  • @MarkA.Donohoe I've improved this query to be significantly more flexible, allowing for different numbers of featured, favourite and total articles to be returned – Nick May 03 '20 at 00:43
  • Sounds good! Digging through this now. One suggestion though... rather than saying 'CTE', 'CTE2', etc., try and use names that are more descriptive/actually mean something. Yes, there's a little more typing, but it makes code much more readable, and remember, code is for humans, not machines! (Compilers are 'de-humaners' when you think about it! lol) – Mark A. Donohoe May 04 '20 at 15:18
  • Side-note: I've never used ROW_NUMBER() like that before. That's pretty awesome! Makes me think of a lot of cool stuff I can do! – Mark A. Donohoe May 04 '20 at 15:21
  • 1
    @MarkA.Donohoe sorry about the names. Sometimes I struggle to come up with good names so rather than spending time on them I just go `CTE`, `CTE2`, `CTE3` etc. Generally I do try to come back and replace them with something more meaningful. I've updated the answer to do that. – Nick May 04 '20 at 23:31
  • No need to apologize! Just sharing friendly feedback that hopefully helps others. – Mark A. Donohoe May 05 '20 at 00:31
1

the lack of 'Top' drives me mad

Since you are on Oracle 11g, TOP-n is not supported. So ROWNUM is the only way to go. See How ROWNUM works in pagination query.

For example, an OFFSET of 4 and FETCH next 4 using ROWNUM:

SQL> SELECT val
     FROM   (SELECT val, rownum AS rnum
            FROM   (SELECT val
                    FROM   order_test
                    ORDER BY val)
            WHERE rownum <= 8)
     WHERE  rnum >= 5;

       VAL
----------
         3
         3
         4
         4

From Oracle 12c on ward, you could use the Top-n row limiting feature. Since you haven't provided any sample data, here is a simple demo:

SQL> select * from order_test order by val;

       VAL
----------
         1
         1
         2
         2
         3
         3
         4
         4
         5
         5
         6
         6
         7
         7
         8
         8
         9
         9
        10
        10

20 rows selected.

First 4 rows :

SQL> SELECT val
  2  FROM   order_test
  3  ORDER BY VAL
  4  FETCH FIRST 4 ROWS ONLY;

       VAL
----------
         1
         1
         2
         2

Next 4 rows(look at OFFSET) :

SQL> SELECT val
  2  FROM   order_test
  3  ORDER BY VAL
  4  OFFSET 4 ROWS FETCH NEXT 4 ROWS ONLY;

       VAL
----------
         3
         3
         4
         4

Finally, next 4 rows with OFFSET 8 rows :

SQL> SELECT val
  2  FROM   order_test
  3  ORDER BY VAL
  4  OFFSET 8 ROWS FETCH NEXT 4 ROWS ONLY;

       VAL
----------
         5
         5
         6
         6

Using the above approach, you could get the most-recently published article marked with 'IsFeatured' as follows:

SELECT * FROM articles
WHERE isfeatured = true
ORDER BY datepublished DESC
FETCH FIRST ONE ROW ONLY;
Lalit Kumar B
  • 43,345
  • 10
  • 82
  • 112
  • Interesting! Wasn't aware of the FETCH FIRST 'n' ROWS ONLY statement. That's great! What about the other aspect of my query... the 'minus' statement to exclude rows, then using the UNION ALL at the end to join all of them into a single resultset? Is that the most efficient way to do that? – Mark A. Donohoe May 01 '20 at 19:03
  • @MarkA.Donohoe The three result sets you expect could be achieved using `UNION ALL`. Remember, Oracle doesn't support **boolean** data type in SQL, therefore you need to treat it as a string. No need of minus query, use a `WHERE` predicate to make each query return only the filtered output you want. – Lalit Kumar B May 01 '20 at 19:07
  • Darn! Just found out we're using Oracle 11 so that command isn't available. I'll update my question with the information you've shared. Still, for the 'MINUS' thing, how would one say 'Where isFavorite = true AND the row is not in table X?' kinda thing? – Mark A. Donohoe May 01 '20 at 19:12
  • @MarkA.Donohoe Numeric.String is fine but boolean is not supported as data type. Regarding `FETCH FIRST` clause, you could use `ROWNUM`. Remember, ROWNUM is assigned after the predicate is processed. See my updated answer for more details. – Lalit Kumar B May 01 '20 at 19:17
  • Yes, it's assigned ***after the predicate*** but ***before the order by*** clause. Since I am trying to get the top(first) row after the order by, that's why I have them separated out into allFeatured (which is pre-ordered), then mostRecentlyFeatured, which selects from allFeatured where ROWNUM is 1. I then do the same with the favorited, then bring them all together with the UNION ALL. So in other words, it sounds like from what you're saying, what I have *is* already the most efficient way for Oracle 11-based databases, no? – Mark A. Donohoe May 01 '20 at 19:27
  • 1
    That's correct. The only difference in the 11g versus 12c query would be that in 11g you need just one more level deep sub-query to get the sorting done before applying ROWNUM. Remaining logic that you have used so far looks good as per the limited data points you have included in the question. – Lalit Kumar B May 01 '20 at 19:32
  • Thanks, although I do have to say I find it incredibly odd that even by version 11, they don't have the logical concept of TOP for use in a single statement. That didn't come until 12! Just left scratching my head there. But hey... at least what I have isn't flat-out wrong so there's that! Thanks again! – Mark A. Donohoe May 01 '20 at 19:34
  • No problem. 11g support has already ended somewhere in 2015 though, only extended support might be extended till 2020 end. Better to upgrade soon :-) – Lalit Kumar B May 01 '20 at 19:41
  • If you update your answer to say based on the Oracle 11 limitation, what I have is the most effective way (unless you think of another of course!) I'll go ahead and mark yours as the answer. One more question first though... is it better/more efficient to use the separated-out `allFeaturedOrdered` then reference that in `mostRecentlyFeatured` or should I instead just nest the Select statement in a `Select * From (Select [ordered rows])` kinda thing? Is one more efficient, or is that more a readability thing? – Mark A. Donohoe May 01 '20 at 19:44
  • @MarkA.Donohoe Both queries are internally treated same way by the optimizer, also since you are using the CTE which is for sub-query factoring so Oracle only does a table scan once and keeps the sub-query result in cache to re-use. I have already updated the answer and added Oracle 11g limitation and use of ROWNUM with an example. – Lalit Kumar B May 01 '20 at 19:45
  • Thanks again, your comments have been ***really*** helpful in understanding this. Again, if you update your answer to say what I have is the correct way (for 11 and earlier) I'll mark yours as the answer so I can close this out. – Mark A. Donohoe May 01 '20 at 19:47
  • @MarkA.Donohoe Done! – Lalit Kumar B May 01 '20 at 19:51