Superfield
Superfield

Reputation: 25

SQL: Taking Column From a Row Picked By Aggregate Function in View

I have three SQL tables: Release (which represents a release of a movie), Media (which represents the individual pieces of recordable media in those releases; i.e. for Blu-ray/DVD combos, there will be two rows in Media, one Blu-ray and one DVD, that point back to the same row in Release) and MediaType (which defines Blu-ray, DVD, VHS, etc.). There's a one-to-many relationship for Release/Media and MediaType/Media, with Media being on the "many" side of both relationships. I have a view for Release, vRelease, which contains aggregate functions, such as a COUNT that shows how many media are associated with that release. This is what I have for this view so far:

SELECT          dbo.Release.ReleaseID
               ,dbo.Release.Name
               ,CASE WHEN Release.Compilation = 0 THEN 'No' WHEN Release.Compilation = 1 THEN 'Yes' END AS Compilation
               ,dbo.Release.Owner
               ,CASE WHEN Release.LentOut = 0 THEN 'No' WHEN Release.LentOut = 1 THEN 'Yes' END AS LentOut
               ,COUNT(dbo.Media.ReleaseID) AS NumberOfMedia
               ,MIN(dbo.Media.MediaID) AS FirstMediaID
               ,MIN(dbo.MediaType.Name) AS FirstMediaType
FROM            dbo.MediaType INNER JOIN
                dbo.Media ON dbo.MediaType.MediaTypeID = dbo.Media.MediaTypeID RIGHT OUTER JOIN
                dbo.Release ON dbo.Media.ReleaseID = dbo.Release.ReleaseID
GROUP BY        dbo.Release.ReleaseID, dbo.Release.Name, dbo.Release.Compilation, dbo.Release.Owner, dbo.Release.LentOut

You'll notice that I've also included two other aggregate columns: FirstMediaID grabs the ID of the media associated with that release that appears first in the Media table (i.e. if a release has two DVDs associated with it, it gets one with the lower ID value). This column on its own isn't useful; what I want to do is then, in turn, get the MediaType that that Media is associated with. In other words, I want a column that shows the MediaType of the first Media that is attached to each Release. The column after that, FirstMediaType, is supposed to do that, but it instead gets the MediaType among all of the Media associated with the Release and picks the one that is alphabetically first - which means that Blu-ray will always be prioritized over DVD (which is fine), but Audio CD will always be prioritized over everything else (which is not fine).

How do I get the FirstMediaType column in this view to get the MediaType of the Media identified in FirstMediaID?

UPDATE: Here are the tables, their columns and some sample rows.

A couple from Release:

+-----------+----------------------------------------+-------+-------------+---------+
| ReleaseID |                  Name                  | Owner | Compilation | LentOut |
+-----------+----------------------------------------+-------+-------------+---------+
|         2 | Alice in Wonderland                    | NULL  |           0 |       0 |
|         6 | 4 Film Favorites - Family Comedies     | NULL  |           1 |       0 |
|         8 | Aladdin                                | NULL  |           0 |       0 |
|       463 | Harry Potter and the Half-Blood Prince | NULL  |           0 |       1 |
|       534 | Spirited Away                          | Ryan  |           0 |       0 |
|       571 | The Original Christmas Classics        | NULL  |           1 |       0 |
+-----------+----------------------------------------+-------+-------------+---------+

Compilation indicates a release that has more than one movie in it.

Corresponding entries in Media:

+---------+-------------+-------------------------------------------------------------------------------------+-----------+
| MediaID | MediaTypeID |                                        Name                                         | ReleaseID |
+---------+-------------+-------------------------------------------------------------------------------------+-----------+
|       2 |           2 | Movie                                                                               |         2 |
|       3 |           1 | Movie                                                                               |         2 |
|      12 |           1 | Space Jam; Looney Tunes: Back in Action                                             |         6 |
|      13 |           1 | Funky Monkey; Osmosis Jones                                                         |         6 |
|      17 |           3 | Movie                                                                               |         8 |
|     620 |           1 | Movie                                                                               |       463 |
|     726 |           1 | Movie                                                                               |       534 |
|     807 |           1 | Rudolph the Red-Nosed Reindeer; Cricket on the Hearth                               |       571 |
|     808 |           1 | Frosty the Snowman; Frosty Returns                                                  |       571 |
|     809 |           1 | Santa Claus is Comin' to Town!; Mr. Magoo's Christmas Carol; The Little Drummer Boy |       571 |
|     810 |           4 | Tracks 1-7                                                                          |       571 |
+---------+-------------+-------------------------------------------------------------------------------------+-----------+

First few in MediaType:

+-------------+--------------+
| MediaTypeID |     Name     |
+-------------+--------------+
|           1 | DVD Disc     |
|           2 | Blu-ray Disc |
|           3 | VHS          |
|           4 | Audio CD     |
+-------------+--------------+

The corresponding entries in vRelease SHOULD be this:

+-----------+----------------------------------------+-------------+-------+---------+---------------+--------------+----------------+
| ReleaseID |                  Name                  | Compilation | Owner | LentOut | NumberOfMedia | FirstMediaID | FirstMediaType |
+-----------+----------------------------------------+-------------+-------+---------+---------------+--------------+----------------+
|         2 | Alice in Wonderland                    | No          | NULL  | No      |             2 |            2 | Blu-ray Disc   |
|         6 | 4 Film Favorites - Family Comedies     | Yes         | NULL  | No      |             2 |           12 | DVD Disc       |
|         8 | Aladdin                                | No          | NULL  | No      |             1 |           17 | VHS            |
|       463 | Harry Potter and the Half-Blood Prince | No          | NULL  | Yes     |             1 |          620 | DVD Disc       |
|       534 | Spirited Away                          | No          | Ryan  | No      |             1 |          726 | DVD Disc       |
|       571 | The Original Christmas Classics        | Yes         | NULL  | No      |             4 |          807 | DVD Disc       |
+-----------+----------------------------------------+-------------+-------+---------+---------------+--------------+----------------+

But it's actually this:

+-----------+----------------------------------------+-------------+-------+---------+---------------+--------------+----------------+
| ReleaseID |                  Name                  | Compilation | Owner | LentOut | NumberOfMedia | FirstMediaID | FirstMediaType |
+-----------+----------------------------------------+-------------+-------+---------+---------------+--------------+----------------+
|         2 | Alice in Wonderland                    | No          | NULL  | No      |             2 |            2 | Blu-ray Disc   |
|         6 | 4 Film Favorites - Family Comedies     | Yes         | NULL  | No      |             2 |           12 | DVD Disc       |
|         8 | Aladdin                                | No          | NULL  | No      |             1 |           17 | VHS            |
|       463 | Harry Potter and the Half-Blood Prince | No          | NULL  | Yes     |             1 |          620 | DVD Disc       |
|       534 | Spirited Away                          | No          | Ryan  | No      |             1 |          726 | DVD Disc       |
|       571 | The Original Christmas Classics        | Yes         | NULL  | No      |             4 |          807 | Audio CD       |
+-----------+----------------------------------------+-------------+-------+---------+---------------+--------------+----------------+

It's that last one that's the problem.

Upvotes: 0

Views: 76

Answers (4)

Superfield
Superfield

Reputation: 25

I ended up finding a simple way to do what I wanted. It isn't as fancy as Used_By_Already's answer (which did end up working, as far as I could tell) and probably breaks a SQL Best Practices rule somewhere, but it's much easier to understand and maintain - at least for my newbie brain.

Since the problem was trying to get the view to use an aggregate column it calculated in a join, I just split the two-step action over two views. vReleasePre has all of the columns I outlined in my original query except for FirstMediaType. vRelease now simply takes all of the columns from vReleasePre and adds FirstMediaType, which takes its value from a join at the end: LEFT OUTER JOIN dbo.vMedia ON dbo.vReleasePre.FirstMediaID = dbo.vMedia.MediaID, where vMedia is a view with all the columns from Media, plus the MediaType column (I already had vMedia lying around).

Since this database is being used in an ASP.NET MVC web application via Entity Framework, and EF has been pretty strange about what it will and won't accept into the data model, I figure that a simple, if roundabout, solution is probably going to be my best option.

vReleasePre:

SELECT    dbo.Release.ReleaseID
         ,dbo.Release.Name
         ,CASE WHEN Release.Compilation = 0 THEN 'No' WHEN Release.Compilation = 1 THEN 'Yes' END AS Compilation
         ,dbo.Release.Owner
         ,CASE WHEN Release.LentOut = 0 THEN 'No' WHEN Release.LentOut = 1 THEN 'Yes' END AS LentOut
         ,COUNT(dbo.Media.ReleaseID) AS NumberOfMedia
         ,MIN(dbo.Media.MediaID) AS FirstMediaID
FROM      dbo.MediaType INNER JOIN
          dbo.Media ON dbo.MediaType.MediaTypeID = dbo.Media.MediaTypeID RIGHT OUTER JOIN
          dbo.Release ON dbo.Media.ReleaseID = dbo.Release.ReleaseID
GROUP BY  dbo.Release.ReleaseID, dbo.Release.Name, dbo.Release.Compilation, dbo.Release.Owner, dbo.Release.LentOut

vRelease:

SELECT   dbo.vReleasePre.ReleaseID
        ,dbo.vReleasePre.Name
        ,dbo.vReleasePre.Compilation
        ,dbo.vReleasePre.Owner
        ,dbo.vReleasePre.LentOut
        ,dbo.vReleasePre.NumberOfMedia
        ,dbo.vMedia.MediaType
FROM     dbo.vReleasePre LEFT OUTER JOIN
         dbo.vMedia ON dbo.vReleasePre.FirstMediaID = dbo.vMedia.MediaID

Upvotes: 1

Paul Maxwell
Paul Maxwell

Reputation: 35603

for the newbie brain, this is the subquery I used

    SELECT
           ROW_NUMBER() OVER(PARTITION BY ReleaseID
                             ORDER BY MediaID) AS rn
         , Media.*
    FROM dbo.Media 

and this is what it does (see the rn column)

| rn | MediaID | MediaTypeID |                                        Name                                         | ReleaseID |
|----|---------|-------------|-------------------------------------------------------------------------------------|-----------|
|  1 |       2 |           2 | Movie                                                                               |         2 |
|  2 |       3 |           1 | Movie                                                                               |         2 |
|  1 |      12 |           1 | Space Jam; Looney Tunes: Back in Action                                             |         6 |
|  2 |      13 |           1 | Funky Monkey; Osmosis Jones                                                         |         6 |
|  1 |      17 |           3 | Movie                                                                               |         8 |
|  1 |     620 |           1 | Movie                                                                               |       463 |
|  1 |     726 |           1 | Movie                                                                               |       534 |
|  1 |     807 |           1 | Rudolph the Red-Nosed Reindeer; Cricket on the Hearth                               |       571 |
|  2 |     808 |           1 | Frosty the Snowman; Frosty Returns                                                  |       571 |
|  3 |     809 |           1 | Santa Claus is Comin' to Town!; Mr. Magoo's Christmas Carol; The Little Drummer Boy |       571 |
|  4 |     810 |           4 | Tracks 1-7                                                                          |       571 |

Now keep only those rows with 1 in the rn column:

| rn | MediaID | MediaTypeID |                         Name                          | ReleaseID |
|----|---------|-------------|-------------------------------------------------------|-----------|
|  1 |       2 |           2 | Movie                                                 |         2 |
|  1 |      12 |           1 | Space Jam; Looney Tunes: Back in Action               |         6 |
|  1 |      17 |           3 | Movie                                                 |         8 |
|  1 |     620 |           1 | Movie                                                 |       463 |
|  1 |     726 |           1 | Movie                                                 |       534 |
|  1 |     807 |           1 | Rudolph the Red-Nosed Reindeer; Cricket on the Hearth |       571 |

Then join just those rows to Releases and MediaType

Bingo

= the wanted result.

Not hard, really not hard. You really will want to learn about those window functions because they can solve heaps of problems.

Upvotes: 0

Paul Maxwell
Paul Maxwell

Reputation: 35603

A very convenient technique that returns whole rows associated with needs such as "First", "Last", "Earliest", "Latest" is to use row_number() over(). Here you want the "first media type", so it is relevant here.

As you will see in the following query joining the [Media] table is replaced with a subquery that includes a row number calculation. Here we partition by ReleaseID and order by MediaID, so, for each ReleaseID the first row will be the one with the lowest MediaID value. Then in the join to this derived table an extra condition is added to only consider rows with a row number of 1.

Proposed Query

SELECT
      r.ReleaseID
    , m.MediaID
    , mt.MediaTypeID
    , mt.name MediaName
    , r.Name
    , CASE
            WHEN r.Compilation = 0 THEN 'No'
            WHEN r.Compilation = 1 THEN 'Yes'
      END                        AS compilation
    , r.Owner
    , CASE
            WHEN r.LentOut = 0 THEN 'No'
            WHEN r.LentOut = 1 THEN 'Yes'
      END                        AS lentout
FROM dbo.Release r 
INNER JOIN (
        SELECT
               Media.*
             , ROW_NUMBER() OVER(PARTITION BY ReleaseID
                                 ORDER BY MediaID) AS rn
        FROM dbo.Media 
        ) m ON  r.ReleaseID = m.ReleaseID and rn = 1
INNER JOIN dbo.MediaType mt ON  m.MediaTypeID = mt.MediaTypeID

Result

| ReleaseID | MediaID | MediaTypeID |  MediaName   |                  Name                  | compilation | Owner  | lentout |
|-----------|---------|-------------|--------------|----------------------------------------|-------------|--------|---------|
|         2 |       2 |           2 | Blu-ray Disc | Alice in Wonderland                    | No          | (null) | No      |
|         6 |      12 |           1 | DVD Disc     | 4 Film Favorites - Family Comedies     | Yes         | (null) | No      |
|         8 |      17 |           3 | VHS          | Aladdin                                | No          | (null) | No      |
|       463 |     620 |           1 | DVD Disc     | Harry Potter and the Half-Blood Prince | No          | (null) | Yes     |
|       534 |     726 |           1 | DVD Disc     | Spirited Away                          | No          | Ryan   | No      |
|       571 |     807 |           1 | DVD Disc     | The Original Christmas Classics        | Yes         | (null) | No      |

Demo available at SQLFiddle

Upvotes: 0

Mathew Paxinos
Mathew Paxinos

Reputation: 964

The easiest way would be to add another join to your MediaType table on FirstMediaId = MediaType.MediaId

;WITH data AS (
    SELECT     dbo.Release.ReleaseID
              ,dbo.Release.Name
              ,CASE WHEN Release.Compilation = 0 THEN 'No' WHEN Release.Compilation = 1 THEN 'Yes' END AS Compilation
              ,dbo.Release.Owner
              ,CASE WHEN Release.LentOut = 0 THEN 'No' WHEN Release.LentOut = 1 THEN 'Yes' END AS LentOut
              ,COUNT(dbo.Media.ReleaseID) AS NumberOfMedia
              ,MIN(dbo.Media.MediaID) AS FirstMediaID
    FROM  dbo.MediaType 
            INNER JOIN dbo.Media 
                ON  dbo.MediaType.MediaTypeID = dbo.Media.MediaTypeID 
            RIGHT OUTER JOIN dbo.Release 
                ON dbo.Media.ReleaseID = dbo.Release.ReleaseID
    GROUP BY dbo.Release.ReleaseID, dbo.Release.Name, dbo.Release.Compilation, dbo.Release.Owner, dbo.Release.LentOut
)
SELECT data.ReleaseId
      ,data.Name
      ,data.Compilation
      ,data.Owner
      ,data.LentOut
      ,data.NumberOfMedia
      ,data.FirstMediaId
      ,MediaType.Name   as FirstMediaName 
FROM data
        LEFT OUTER JOIN dbo.MediaType
            ON  data.FirstMediaId = MediaType.MediaTypeId

Upvotes: 0

Related Questions