Para todas as análises de negócios que levam à arquitetura do banco de dados, recomendo escrever regras:
- Uma rota tem 2 ou mais estações
- Uma estação pode ser usada por várias rotas
- As estações em uma rota vêm em uma ordem específica
As 1ª e 2ª regras, como você notou, implicam em um relacionamento de muitos para muitos; portanto, você concluiu com razão a criação de routeStations.
A 3ª regra é interessante. Isso implica que uma coluna extra é necessária para atender ao requisito. Para onde deveria ir? Podemos ver que essa propriedade depende da rota e estação. Portanto, ele deve estar localizado em routeStations.
Eu adicionaria uma coluna à tabela routeStations chamada "stationOrder".
+-------------+---------------+---------------
| routeId(fk) | stationId(fk) | StationOrder |
+-------------+---------------+---------------
| 1 | 1 | 3 |
+-------------+---------------+---------------
| 1 | 3 | 1 |
+-------------+---------------+---------------
| 1 | 4 | 2 |
+-------------+---------------+---------------
| 2 | 1 | 1 |
+-------------+---------------+---------------
| 2 | 4 | 2 |
+-------------+---------------+---------------
Então, a consulta se torna fácil:
select rs.routeID,s.Name
from routeStations rs
join
Stations s
on rs.stationId=s.StationId
where rs.routeId=1
order by rs.StationOrder;
+-------------+---------------+
| routeId(fk) | stationId(fk) |
+-------------+---------------+
| 1 | C |
+-------------+---------------+
| 1 | D |
+-------------+---------------+
| 1 | A |
+-------------+---------------+
Notas:
- Corrigi o StationId no RouteStations no meu exemplo. Você está usando o StationName como o ID.
- Se você não usar um nome de rota, não haverá necessidade de routeId, pois você pode obtê-lo em routeStations
- Mesmo se você vincular à tabela de rotas, o otimizador de banco de dados perceberá que não precisa desse link extra e simplesmente removerá as etapas extras.
Para desenvolver na nota 3, criei o caso de uso:
Este é o Oracle 12c Enterprise.
Observe que, no plano de execução abaixo, as rotas da tabela não são usadas. o CBO (Cost Base Optimizer) sabe que pode obter o routeId diretamente da chave primária do routeStations (etapa 5, VERIFICAÇÃO DE INTERVALO DO ÍNDICE em ROUTESTATIONS_PK, Informações do Predicado 5 - acesso ("RS". "ROUTEID" = 1))
--Table ROUTES
create sequence routeId_Seq start with 1 increment by 1 maxvalue 9999999999999 cache 1000;
CREATE TABLE routes
(
routeId INTEGER NOT NULL
);
ALTER TABLE routes ADD (
CONSTRAINT routes_PK
PRIMARY KEY
(routeId)
ENABLE VALIDATE);
insert into routes values (routeId_Seq.nextval);
insert into routes values (routeId_Seq.nextval);
commit;
--TABLE STATIONS
create sequence stationId_seq start with 1 increment by 1 maxvalue 9999999999999 cache 1000;
create table stations(
stationID INTEGER NOT NULL,
name varchar(50) NOT NULL
);
ALTER TABLE stations ADD (
CONSTRAINT stations_PK
PRIMARY KEY
(stationId)
ENABLE VALIDATE);
insert into stations values (stationId_seq.nextval,'A');
insert into stations values (stationId_seq.nextval,'B');
insert into stations values (stationId_seq.nextval,'C');
insert into stations values (stationId_seq.nextval,'D');
commit;
--
--Table ROUTESTATIONS
CREATE TABLE routeStations
(
routeId INTEGER NOT NULL,
stationId INTEGER NOT NULL,
stationOrder INTEGER NOT NULL
);
ALTER TABLE routeStations ADD (
CONSTRAINT routeStations_PK
PRIMARY KEY
(routeId, stationId)
ENABLE VALIDATE);
ALTER TABLE routeStations ADD (
FOREIGN KEY (routeId)
REFERENCES ROUTES (ROUTEID)
ENABLE VALIDATE,
FOREIGN KEY (stationId)
REFERENCES STATIONS (stationId)
ENABLE VALIDATE);
insert into routeStations values (1,1,3);
insert into routeStations values (1,3,1);
insert into routeStations values (1,4,2);
insert into routeStations values (2,1,1);
insert into routeStations values (2,4,2);
commit;
explain plan for select rs.routeID,s.Name
from ndefontenay.routeStations rs
join
ndefontenay.routes r
on r.routeId=rs.routeId
join ndefontenay.stations s
on rs.stationId=s.stationId
where rs.routeId=1
order by rs.StationOrder;
set linesize 1000
set pages 500
select * from table (dbms_xplan.display);
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 2617709240
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 79 | 1 (100)| 00:00:01 |
| 1 | SORT ORDER BY | | 1 | 79 | 1 (100)| 00:00:01 |
| 2 | NESTED LOOPS | | | | | |
| 3 | NESTED LOOPS | | 1 | 79 | 0 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID| ROUTESTATIONS | 1 | 39 | 0 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | ROUTESTATIONS_PK | 1 | | 0 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | STATIONS_PK | 1 | | 0 (0)| 00:00:01 |
| 7 | TABLE ACCESS BY INDEX ROWID | STATIONS | 1 | 40 | 0 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("RS"."ROUTEID"=1)
6 - access("RS"."STATIONID"="S"."STATIONID")
Agora a parte divertida, vamos adicionar um nome de coluna à tabela de rotas. Agora há uma coluna que realmente precisamos em "rotas". O CBO usa o índice para encontrar o ID da linha da rota 1, acessa a tabela (acesso à tabela pelo índice rowid) e pega a coluna "routes.name".
ALTER TABLE ROUTES
ADD (name VARCHAR2(50));
update routes set name='Old Town' where routeId=1;
update routes set name='North County' where routeId=2;
commit;
explain plan for select r.name as routeName,s.Name as stationName
from routeStations rs
join
routes r
on r.routeId=rs.routeId
join stations s
on rs.stationId=s.stationId
where rs.routeId=1
order by rs.StationOrder;
set linesize 500
set pages 500
select * from table (dbms_xplan.display);
PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------
Plan hash value: 3368128430
----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 119 | 1 (100)| 00:00:01 |
| 1 | SORT ORDER BY | | 1 | 119 | 1 (100)| 00:00:01 |
| 2 | NESTED LOOPS | | | | | |
| 3 | NESTED LOOPS | | 1 | 119 | 0 (0)| 00:00:01 |
| 4 | NESTED LOOPS | | 1 | 79 | 0 (0)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| ROUTES | 1 | 40 | 0 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | ROUTES_PK | 1 | | 0 (0)| 00:00:01 |
| 7 | TABLE ACCESS BY INDEX ROWID| ROUTESTATIONS | 1 | 39 | 0 (0)| 00:00:01 |
|* 8 | INDEX RANGE SCAN | ROUTESTATIONS_PK | 1 | | 0 (0)| 00:00:01 |
|* 9 | INDEX UNIQUE SCAN | STATIONS_PK | 1 | | 0 (0)| 00:00:01 |
| 10 | TABLE ACCESS BY INDEX ROWID | STATIONS | 1 | 40 | 0 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
6 - access("R"."ROUTEID"=1)
8 - access("RS"."ROUTEID"=1)
9 - access("RS"."STATIONID"="S"."STATIONID")