MySQL group by with ordering/priority of another c

2020-07-14 05:12发布

问题:

I've already looked at these two questions:

  • Grouping by Column with Dependence on another Column
  • MySQL GROUP BY with preference

However both of them use an aggregate function MAX in order to obtain the highest or filled in value, which doesn't work for my case.

For the purposes of this question, I've simplified my situation. Here is my current data:

I'd like to obtain the operator name for each route, but with respect to direction of travel (i.e. ordering or "preferring" values). This is my pseudo-code:

if(`direction` = 'west' AND `operatorName` != '') then select `operatorName`
else if(`direction` = 'north' AND `operatorName` != '') then select `operatorName`
else if(`direction` = 'south' AND `operatorName` != '') then select `operatorName`
else if(`direction` = 'east' AND `operatorName` != '') then select `operatorName`

My current SQL query is:

SELECT route, operatorName
FROM test
GROUP BY route

This gives me the grouping, but wrong operator for my purposes:

route | operatorName
--------------------
  95  | James
  96  | Mark
  97  | Justin

I have tried applying a ORDER BY clause but GROUP BY takes precedence. What my desired result is:

route | operatorName
--------------------
  95  | Richard
  96  | Andrew
  97  | Justin

I cannot do MAX() here as "north" comes before "south" in alphabetical order. How do I explicitly state my preference/ordering before the GROUP BY clause is applied?

Also keep in mind that empty strings are not preferred.

Please note that this is a simplified example. The actual query selects a lot more fields and joins with three other tables, but there are no aggregate functions in the query.

回答1:

You can use that MAX example, you just needed to "fake it". See here: http://sqlfiddle.com/#!2/58688/5

SELECT *
FROM test
JOIN (SELECT 'west' AS direction, 4 AS weight
      UNION
      SELECT 'north',3
      UNION
      SELECT 'south',2
      UNION
      SELECT 'east',1) AS priority
  ON priority.direction = test.direction
JOIN (
      SELECT route, MAX(weight) AS weight
      FROM test
      JOIN (SELECT 'west' AS direction, 4 AS weight
            UNION
            SELECT 'north',3
            UNION
            SELECT 'south',2
            UNION
            SELECT 'east',1) AS priority
        ON priority.direction = test.direction
      GROUP BY route
) AS t1
  ON t1.route = test.route
    AND t1.weight = priority.weight


回答2:

I came up with this solution, however, it is ugly. Anyway, you may try it:

CREATE TABLE test (
  route INT,
  direction VARCHAR(20),
  operatorName VARCHAR(20)
);

INSERT INTO test VALUES(95, 'east', 'James');
INSERT INTO test VALUES(95, 'west', 'Richard');
INSERT INTO test VALUES(95, 'north', 'Dave');
INSERT INTO test VALUES(95, 'south', 'Devon');
INSERT INTO test VALUES(96, 'east', 'Mark');
INSERT INTO test VALUES(96, 'west', 'Andrew');
INSERT INTO test VALUES(96, 'south', 'Alex');
INSERT INTO test VALUES(96, 'north', 'Ryan');
INSERT INTO test VALUES(97, 'north', 'Justin');
INSERT INTO test VALUES(97, 'south', 'Tyler');

SELECT
    route,
    (SELECT operatorName
       FROM test
     WHERE route = t2.route
       AND direction =
              CASE
                WHEN direction_priority = 1 THEN 'west'
                WHEN direction_priority = 2 THEN 'north'
                WHEN direction_priority = 3 THEN 'south'
                WHEN direction_priority = 4 THEN 'east'
              END) AS operator_name
  FROM (
    SELECT
        route,
        MIN(direction_priority) AS direction_priority
      FROM (
        SELECT
            route,
            operatorName,
            CASE
              WHEN direction = 'west' THEN 1
              WHEN direction = 'north' THEN 2
              WHEN direction = 'south' THEN 3
              WHEN direction = 'east' THEN 4
            END AS direction_priority
          FROM test
      ) t
    GROUP BY route
  ) t2
;

Firstly, we select all records with direction changed to a number so it is in required order. Then, we GROUP by each route and get the minimal direction. What is left remains in the outermost query - select the operator name based on the lowest found direction.

Output:

ROUTE  OPERATOR_NAME
95  Richard
96  Andrew
97  Justin

Please, next time attach sample data not as a picture, but either as a plain text, or as inserts (best at SQLFiddle).

Check this solution at SQLFiddle



回答3:

you can enumerate the directions using a case construct to make them sortable in your order. then rank the directions partitioned by route and then only select the first candidate.

set @c = 1;
set @r = '';

select route
     , direction
     , operatorName
  from (
         select route
              , direction
              , operatorName
              , @c := if (@r = route, @c + 1, 1) as cand
           from (
                  select route
                       , case when direction = 'west'
                              then 1
                              when direction = 'north'
                              then 2
                              when direction = 'south'
                              then 3
                              when direction = 'east'
                              then 4
                              else 5
                         end as enum_direction
                       , direction
                       , operatorName
                )
          order by
                route
              , enum_direction
       )


回答4:

select *
from routes r1
where exists (
    select 1
    from routes r2
    where r1.route_id = r2.route_id
    group by r2.route_id
    having min(case r1.direction
                 when 'west' then 1
                 when 'north' then 2
                 when 'south' then 3
                 when 'east' then 4 end) = min(case r2.direction
                                                  when 'west' then 1
                                                  when 'north' then 2
                                                  when 'south' then 3
                                                  when 'east' then 4 end)
)

demo