Primeiras palavras
Você pode ignorar com segurança as seções abaixo (incluindo) JOINs: iniciando se você quiser apenas entender o código. Os antecedentes e os resultados servem apenas como contexto. Consulte o histórico de edições antes de 06/10/2015, se quiser ver como era o código inicialmente.
Objetivo
Por fim, quero calcular coordenadas GPS interpoladas para o transmissor ( X
ou Xmit
) com base nos carimbos de data / hora dos dados de GPS disponíveis na tabela SecondTable
que flanqueiam diretamente a observação na tabela FirstTable
.
Meu objetivo imediato de alcançar o objetivo final é descobrir a melhor forma de aderir FirstTable
a SecondTable
esses pontos de tempo de flanqueamento. Posteriormente, posso usar essas informações para calcular coordenadas intermediárias de GPS, assumindo um ajuste linear ao longo de um sistema de coordenadas equiretangular (palavras sofisticadas para dizer que não me importo que a Terra seja uma esfera nessa escala).
Questões
- Existe uma maneira mais eficiente de gerar os carimbos de hora antes e depois mais próximos?
- Corrigido por mim mesmo apenas pegando o "depois" e depois obtendo o "antes" apenas no que se refere ao "depois".
- Existe uma maneira mais intuitiva que não envolva a
(A<>B OR A=B)
estrutura. - Quaisquer outros pensamentos, truques e conselhos que possa ter.
- Assim, byrdzeye e Phrancis têm sido bastante úteis nesse sentido. Descobri que o conselho de Phrancis foi excelentemente apresentado e prestou ajuda em um estágio crítico, por isso darei a ele a vantagem aqui.
Eu ainda gostaria de receber qualquer ajuda adicional que eu possa receber com relação à pergunta 3. Os marcadores refletem quem eu acredito que me ajudou mais na questão individual.
Definições da tabela
Representação semi-visual
FirstTable
Fields
RecTStamp | DateTime --can contain milliseconds via VBA code (see Ref 1)
ReceivID | LONG
XmitID | TEXT(25)
Keys and Indices
PK_DT | Primary, Unique, No Null, Compound
XmitID | ASC
RecTStamp | ASC
ReceivID | ASC
UK_DRX | Unique, No Null, Compound
RecTStamp | ASC
ReceivID | ASC
XmitID | ASC
SecondTable
Fields
X_ID | LONG AUTONUMBER -- seeded after main table has been created and already sorted on the primary key
XTStamp | DateTime --will not contain partial seconds
Latitude | Double --these are in decimal degrees, not degrees/minutes/seconds
Longitude | Double --this way straight decimal math can be performed
Keys and Indices
PK_D | Primary, Unique, No Null, Simple
XTStamp | ASC
UIDX_ID | Unique, No Null, Simple
X_ID | ASC
Tabela ReceiverDetails
Fields
ReceivID | LONG
Receiver_Location_Description | TEXT -- NULL OK
Beginning | DateTime --no partial seconds
Ending | DateTime --no partial seconds
Lat | DOUBLE
Lon | DOUBLE
Keys and Indicies
PK_RID | Primary, Unique, No Null, Simple
ReceivID | ASC
Tabela ValidXmitters
Field (and primary key)
XmitID | TEXT(25) -- primary, unique, no null, simple
Violino SQL ...
... para que você possa brincar com as definições e o código da tabela Esta pergunta é para o MSAccess, mas como Phrancis apontou, não há estilo de violino SQL para o Access. Portanto, você deve poder ir aqui para ver minhas definições e código de tabela com base na resposta de Phrancis :
http://sqlfiddle.com/#!6/e9942/4 (link externo)
JOINs: começando
Minha atual "coragem interior"
Primeiro, crie uma FirstTable_rekeyed com a ordem das colunas e a chave primária composta, (RecTStamp, ReceivID, XmitID)
todas indexadas / classificadas ASC
. Também criei índices em cada coluna individualmente. Em seguida, preencha assim.
INSERT INTO FirstTable_rekeyed (RecTStamp, ReceivID, XmitID)
SELECT DISTINCT ROW RecTStamp, ReceivID, XmitID
FROM FirstTable
WHERE XmitID IN (SELECT XmitID from ValidXmitters)
ORDER BY RecTStamp, ReceivID, XmitID;
A consulta acima preenche a nova tabela com 153006 registros e retorna em questão de 10 segundos ou mais.
O seguinte é concluído dentro de um ou dois segundos quando todo esse método é agrupado em um "SELECT Count (*) FROM (...)" quando o método da subconsulta TOP 1 é usado
SELECT
ReceiverRecord.RecTStamp,
ReceiverRecord.ReceivID,
ReceiverRecord.XmitID,
(SELECT TOP 1 XmitGPS.X_ID FROM SecondTable as XmitGPS WHERE ReceiverRecord.RecTStamp < XmitGPS.XTStamp ORDER BY XmitGPS.X_ID) AS AfterXmit_ID
FROM FirstTable_rekeyed AS ReceiverRecord
-- INNER JOIN SecondTable AS XmitGPS ON (ReceiverRecord.RecTStamp < XmitGPS.XTStamp)
GROUP BY RecTStamp, ReceivID, XmitID;
-- No separate join needed for the Top 1 method, but it would be required for the other methods.
-- Additionally no restriction of the returned set is needed if I create the _rekeyed table.
-- May not need GROUP BY either. Could try ORDER BY.
-- The three AfterXmit_ID alternatives below take longer than 3 minutes to complete (or do not ever complete).
-- FIRST(XmitGPS.X_ID)
-- MIN(XmitGPS.X_ID)
-- MIN(SWITCH(XmitGPS.XTStamp > ReceiverRecord.RecTStamp, XmitGPS.X_ID, Null))
Consulta JOIN "coragem interior" anterior
Primeiro (fastish ... mas não é bom o suficiente)
SELECT
A.RecTStamp,
A.ReceivID,
A.XmitID,
MAX(IIF(B.XTStamp<= A.RecTStamp,B.XTStamp,Null)) as BeforeXTStamp,
MIN(IIF(B.XTStamp > A.RecTStamp,B.XTStamp,Null)) as AfterXTStamp
FROM FirstTable as A
INNER JOIN SecondTable as B ON
(A.RecTStamp<>B.XTStamp OR A.RecTStamp=B.XTStamp)
GROUP BY A.RecTStamp, A.ReceivID, A.XmitID
-- alternative for BeforeXTStamp MAX(-(B.XTStamp<=A.RecTStamp)*B.XTStamp)
-- alternatives for AfterXTStamp (see "Aside" note below)
-- 1.0/(MAX(1.0/(-(B.XTStamp>A.RecTStamp)*B.XTStamp)))
-- -1.0/(MIN(1.0/((B.XTStamp>A.RecTStamp)*B.XTStamp)))
Segundo (mais lento)
SELECT
A.RecTStamp, AbyB1.XTStamp AS BeforeXTStamp, AbyB2.XTStamp AS AfterXTStamp
FROM (FirstTable AS A INNER JOIN
(select top 1 B1.XTStamp, A1.RecTStamp
from SecondTable as B1, FirstTable as A1
where B1.XTStamp<=A1.RecTStamp
order by B1.XTStamp DESC) AS AbyB1 --MAX (time points before)
ON A.RecTStamp = AbyB1.RecTStamp) INNER JOIN
(select top 1 B2.XTStamp, A2.RecTStamp
from SecondTable as B2, FirstTable as A2
where B2.XTStamp>A2.RecTStamp
order by B2.XTStamp ASC) AS AbyB2 --MIN (time points after)
ON A.RecTStamp = AbyB2.RecTStamp;
fundo
Eu tenho uma tabela de telemetria (com o nome A) de pouco menos de 1 milhão de entradas com uma chave primária composta com base em um DateTime
carimbo, um ID do transmissor e um ID do dispositivo de gravação. Devido a circunstâncias fora do meu controle, minha linguagem SQL é o Jet DB padrão no Microsoft Access (os usuários usarão 2007 e versões posteriores). Apenas cerca de 200.000 dessas entradas são relevantes para a consulta devido ao ID do transmissor.
Há uma segunda tabela de telemetria (alias B) que envolve aproximadamente 50.000 entradas com uma única DateTime
chave primária
Na primeira etapa, concentrei-me em encontrar os carimbos de data e hora mais próximos dos carimbos na primeira tabela da segunda tabela.
Resultados JOIN
Peculiaridades que eu descobri ...
... ao longo do caminho durante a depuração
Parece realmente estranho escrever a JOIN
lógica, FROM FirstTable as A INNER JOIN SecondTable as B ON (A.RecTStamp<>B.XTStamp OR A.RecTStamp=B.XTStamp)
que como @byrdzeye apontou em um comentário (que desapareceu desde então) é uma forma de junção cruzada. Note-se que a substituição LEFT OUTER JOIN
de INNER JOIN
no código acima parece fazer nenhum impacto na quantidade ou identidade das linhas retornadas. Também não consigo deixar de fora a cláusula ON ou dizer ON (1=1)
. O simples uso de uma vírgula para ingressar (em vez de INNER
ou LEFT OUTER
JOIN
) resulta em Count(select * from A) * Count(select * from B)
linhas retornadas nesta consulta, em vez de apenas uma linha por tabela A, conforme JOIN
retornos explícitos (A <> B OR A = B) . Claramente, isso não é adequado. FIRST
parece não estar disponível para uso, devido a um tipo de chave primária composta.
O segundo JOIN
estilo, embora indiscutivelmente mais legível, sofre por ser mais lento. Isso pode ocorrer porque JOIN
são necessários dois s internos adicionais em relação à tabela maior, bem como os dois CROSS JOIN
s encontrados nas duas opções.
Lado: Substituir a IIF
cláusula por MIN
/ MAX
parece retornar o mesmo número de entradas.
MAX(-(B.XTStamp<=A.RecTStamp)*B.XTStamp)
funciona para o MAX
carimbo de data / hora "Antes" ( ), mas não funciona diretamente para o "Depois" ( MIN
) da seguinte maneira:
MIN(-(B.XTStamp>A.RecTStamp)*B.XTStamp)
porque o mínimo é sempre 0 para a FALSE
condição. Esse 0 é menor que qualquer pós-época DOUBLE
(da qual um DateTime
campo é um subconjunto no Access e que esse cálculo transforma o campo em). Os métodos IIF
e MIN
/ MAX
As alternativas propostas para o valor AfterXTStamp funcionam porque a divisão por zero ( FALSE
) gera valores nulos, que as funções agregadas MIN e MAX ignoram.
Próximos passos
Levando isso adiante, desejo encontrar os registros de data e hora na segunda tabela que flanqueiam diretamente os registros de data e hora na primeira tabela e executar uma interpolação linear dos valores dos dados da segunda tabela com base na distância do tempo até esses pontos (por exemplo, se o registro de data e hora de a primeira tabela fica a 25% do caminho entre o "antes" e o "depois", gostaria que 25% do valor calculado viesse dos dados do valor da 2ª tabela associados ao ponto "depois" e 75% do "antes" ) Usando o tipo de junção revisado como parte das entranhas internas, e após as respostas sugeridas abaixo, produzo ...
SELECT
AvgGPS.XmitID,
StrDateIso8601Msec(AvgGPS.RecTStamp) AS RecTStamp_ms,
-- StrDateIso8601MSec is a VBA function returning a TEXT string in yyyy-mm-dd hh:nn:ss.lll format
AvgGPS.ReceivID,
RD.Receiver_Location_Description,
RD.Lat AS Receiver_Lat,
RD.Lon AS Receiver_Lon,
AvgGPS.Before_Lat * (1 - AvgGPS.AfterWeight) + AvgGPS.After_Lat * AvgGPS.AfterWeight AS Xmit_Lat,
AvgGPS.Before_Lon * (1 - AvgGPS.AfterWeight) + AvgGPS.After_Lon * AvgGPS.AfterWeight AS Xmit_Lon,
AvgGPS.RecTStamp AS RecTStamp_basic
FROM ( SELECT
AfterTimestampID.RecTStamp,
AfterTimestampID.XmitID,
AfterTimestampID.ReceivID,
GPSBefore.BeforeXTStamp,
GPSBefore.Latitude AS Before_Lat,
GPSBefore.Longitude AS Before_Lon,
GPSAfter.AfterXTStamp,
GPSAfter.Latitude AS After_Lat,
GPSAfter.Longitude AS After_Lon,
( (AfterTimestampID.RecTStamp - GPSBefore.XTStamp) / (GPSAfter.XTStamp - GPSBefore.XTStamp) ) AS AfterWeight
FROM (
(SELECT
ReceiverRecord.RecTStamp,
ReceiverRecord.ReceivID,
ReceiverRecord.XmitID,
(SELECT TOP 1 XmitGPS.X_ID FROM SecondTable as XmitGPS WHERE ReceiverRecord.RecTStamp < XmitGPS.XTStamp ORDER BY XmitGPS.X_ID) AS AfterXmit_ID
FROM FirstTable AS ReceiverRecord
-- WHERE ReceiverRecord.XmitID IN (select XmitID from ValidXmitters)
GROUP BY RecTStamp, ReceivID, XmitID
) AS AfterTimestampID INNER JOIN SecondTable AS GPSAfter ON AfterTimestampID.AfterXmit_ID = GPSAfter.X_ID
) INNER JOIN SecondTable AS GPSBefore ON AfterTimestampID.AfterXmit_ID = GPSBefore.X_ID + 1
) AS AvgGPS INNER JOIN ReceiverDetails AS RD ON (AvgGPS.ReceivID = RD.ReceivID) AND (AvgGPS.RecTStamp BETWEEN RD.Beginning AND RD.Ending)
ORDER BY AvgGPS.RecTStamp, AvgGPS.ReceivID;
... que retorna 152928 registros, em conformidade (pelo menos aproximadamente) com o número final de registros esperados. O tempo de execução é provavelmente de 5 a 10 minutos no meu i7-4790, 16 GB de RAM, sem SSD, sistema Win 8.1 Pro.
Referência 1: O MS Access pode manipular valores de tempo em milissegundos - realmente e acompanha o arquivo de origem [08080011.txt]