Access (Jet) SQL: tampons DateTime dans TableB encadrant chaque tampon DateTime dans TableA

21

Premiers mots

Vous pouvez ignorer en toute sécurité les sections ci-dessous (et y compris) JOINs: Démarrage si vous voulez juste prendre une fissure du code. Le contexte et les résultats servent simplement de contexte. Veuillez consulter l'historique des modifications avant le 06/10/2015 si vous souhaitez voir à quoi ressemblait le code au départ.


Objectif

En fin de compte, je veux calculer les coordonnées GPS interpolées pour l'émetteur ( Xou Xmit) sur la base des tampons DateTime des données GPS disponibles dans le tableau SecondTablequi encadrent directement l'observation dans le tableau FirstTable.

Mon objectif immédiat d'atteindre l'objectif ultime est de savoir comment mieux rejoindre FirstTablepour SecondTableobtenir ces points de temps flanquant. Plus tard, je peux utiliser ces informations, je peux calculer des coordonnées GPS intermédiaires en supposant un ajustement linéaire le long d'un système de coordonnées équirectangulaires (des mots fantaisistes pour dire que je me fiche que la Terre soit une sphère à cette échelle).


Des questions

  1. Existe-t-il un moyen plus efficace de générer les horodatages avant et après les plus proches?
    • Fixé par moi-même en saisissant simplement «l'après», puis en obtenant «l'avant» uniquement en ce qui concerne «l'après».
  2. Existe-t-il une manière plus intuitive qui n'implique pas la (A<>B OR A=B)structure.
    • Byrdzeye a fourni les alternatives de base, mais mon expérience "dans le monde réel" ne correspondait pas aux quatre stratégies de jointure qui effectuaient la même chose. Mais tout le mérite lui revient d'avoir abordé les styles de jointure alternatifs.
  3. Toutes autres pensées, astuces et conseils que vous pourriez avoir.
    • Ainsi, byrdzeye et Phrancis ont été très utiles à cet égard. J'ai trouvé que les conseils de Phrancis étaient très bien présentés et ont fourni de l'aide à un stade critique, alors je vais lui donner l'avantage ici.

J'apprécierais toujours toute aide supplémentaire que je pourrais recevoir en ce qui concerne la question 3. Les puces reflètent qui, selon moi, m'a le plus aidé sur la question individuelle.


Définitions de table

Représentation semi-visuelle

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

Tableau 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

Table ValidXmitters

Field (and primary key)
  XmitID    | TEXT(25) -- primary, unique, no null, simple

Violon SQL ...

... afin que vous puissiez jouer avec les définitions et le code de la table Cette question s'adresse à MSAccess, mais comme Phrancis l'a souligné, il n'y a pas de style SQL pour Access. Donc, vous devriez pouvoir aller ici pour voir mes définitions de table et mon code basé sur la réponse de Phrancis :
http://sqlfiddle.com/#!6/e9942/4 (lien externe)


JOINs: Démarrage

Ma stratégie "JOIN"

Créez d'abord un FirstTable_rekeyed avec l'ordre des colonnes et la clé primaire composée (RecTStamp, ReceivID, XmitID)tous indexés / triés ASC. J'ai également créé des index sur chaque colonne individuellement. Remplissez-le ainsi.

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;

La requête ci-dessus remplit la nouvelle table avec 153006 enregistrements et retourne en l'espace de 10 secondes environ.

Ce qui suit se termine en une seconde ou deux lorsque toute cette méthode est enveloppée dans un "SELECT Count (*) FROM (...)" lorsque la méthode de sous-requête TOP 1 est utilisée

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))

Requête JOIN "entrailles" précédente

D'abord (fastish ... mais pas assez bon)

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)))

Deuxième (plus lent)

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; 

Contexte

J'ai une table de télémétrie (aliasée A) d'un peu moins de 1 million d'entrées avec une clé primaire composée basée sur un DateTimetampon, un ID d'émetteur et un ID de périphérique d'enregistrement. En raison de circonstances indépendantes de ma volonté, mon langage SQL est le Jet DB standard dans Microsoft Access (les utilisateurs utiliseront les versions 2007 et ultérieures). Seulement environ 200 000 de ces entrées sont pertinentes pour la requête en raison de l'ID de l'émetteur.

Il existe une deuxième table de télémétrie (alias B) qui implique environ 50 000 entrées avec une seule DateTimeclé primaire

Pour la première étape, je me suis concentré sur la recherche des horodatages les plus proches des tampons dans le premier tableau à partir du second tableau.


Résultats JOIN

Des bizarreries que j'ai découvertes ...

... en cours de route lors du débogage

Cela semble vraiment étrange d'écrire la JOINlogique FROM FirstTable as A INNER JOIN SecondTable as B ON (A.RecTStamp<>B.XTStamp OR A.RecTStamp=B.XTStamp)qui, comme @byrdzeye l'a souligné dans un commentaire (qui a depuis disparu) est une forme de jointure croisée. Notez que le remplacement LEFT OUTER JOINde INNER JOINdans le code ci-dessus semble n'avoir aucun impact sur la quantité ou l'identité des lignes retournées. Je n'arrive pas non plus à laisser de côté la clause ON ou à dire ON (1=1). Le simple fait d'utiliser une virgule pour joindre (plutôt que INNERou LEFT OUTER JOIN) entraîne des Count(select * from A) * Count(select * from B)lignes renvoyées dans cette requête, plutôt qu'une seule ligne par table A, comme le JOINrenvoie explicitement (A <> B OU A = B) . Ce n'est clairement pas approprié. FIRSTne semble pas être disponible à utiliser étant donné un type de clé primaire composé.

Le deuxième JOINstyle, bien que sans doute plus lisible, souffre d'être plus lent. Cela peut être dû au fait que deux internes supplémentaires JOINsont nécessaires par rapport à la plus grande table ainsi que les deux CROSS JOINdisponibles dans les deux options.

En plus: le remplacement de la IIFclause par MIN/ MAXsemble renvoyer le même nombre d'entrées.
MAX(-(B.XTStamp<=A.RecTStamp)*B.XTStamp)
fonctionne pour l' MAXhorodatage "Before" ( ), mais ne fonctionne pas directement pour "After" ( MIN) comme suit:
MIN(-(B.XTStamp>A.RecTStamp)*B.XTStamp)
car le minimum est toujours 0 pour la FALSEcondition. Ce 0 est inférieur à n'importe quelle post-époque DOUBLE(dans laquelle un DateTimechamp est un sous-ensemble d'Access et que ce calcul transforme le champ). Les méthodes IIFet MIN/ MAXLes alternatives proposées pour la valeur AfterXTStamp fonctionnent car la division par zéro ( FALSE) génère des valeurs nulles, que les fonctions d'agrégation MIN et MAX ignorent.

Prochaines étapes

Pour aller plus loin, je souhaite trouver les horodatages dans le deuxième tableau qui flanquent directement les horodatages dans le premier tableau et effectuer une interpolation linéaire des valeurs de données du deuxième tableau en fonction de la distance temporelle à ces points (c'est-à-dire si l'horodatage de la première table est à 25% du chemin entre "avant" et "après", je voudrais que 25% de la valeur calculée provienne des données de la 2ème valeur de table associées au point "après" et 75% de la "avant" ). En utilisant le type de jointure révisé dans le cadre des entrailles intérieures, et après les réponses suggérées ci-dessous, je produis ...

    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;

... qui renvoie 152928 enregistrements, conformes (au moins approximativement) au nombre final d'enregistrements attendus. Le temps d'exécution est probablement de 5 à 10 minutes sur mon i7-4790, 16 Go de RAM, pas de SSD, système Win 8.1 Pro.


Référence 1: MS Access peut gérer des valeurs de temps en millisecondes - Vraiment et le fichier source qui l'accompagne [08080011.txt]

mpag
la source

Réponses:

10

Je dois d'abord vous féliciter pour votre courage à faire quelque chose comme ça avec une base de données Access, qui d'après mon expérience est très difficile de faire quoi que ce soit de type SQL. Quoi qu'il en soit, passons à l'examen.


Première adhésion

Vos IIFsélections de champs peuvent bénéficier de l'utilisation d'une instruction Switch à la place. Il semble parfois être le cas, en particulier avec les choses SQL, qu'un SWITCH(plus communément appelé CASESQL typique) est assez rapide lorsqu'il ne fait que des comparaisons simples dans le corps d'un SELECT. La syntaxe dans votre cas serait presque identique, bien qu'un commutateur puisse être étendu pour couvrir une grande partie des comparaisons dans un champ. Quelque chose à considérer.

  SWITCH (
    expr1, val1,
    expr2, val2,
    val3        -- default value or "else"
  )

Un commutateur peut également aider à la lisibilité, dans les plus grandes déclarations. Dans le contexte:

  MAX(SWITCH(B.XTStamp <= A.RecTStamp,B.XTStamp,Null)) as BeforeXTStamp,
  --alternatively MAX(-(B.XTStamp<=A.RecTStamp)*B.XTStamp) as BeforeXTStamp,
  MIN(SWITCH(B.XTStamp>A.RecTStamp,B.XTStamp,Null)) as AfterXTStamp

Quant à la jointure elle-même, je pense qu'elle (A.RecTStamp<>B.XTStamp OR A.RecTStamp=B.XTStamp)est à peu près aussi bonne que vous allez l'obtenir, compte tenu de ce que vous essayez de faire. Ce n'est pas si rapide, mais je ne m'attendrais pas à ce que ce soit non plus.


Deuxième jointure

Vous avez dit que c'était plus lent. Il est également moins lisible du point de vue du code. Étant donné des ensembles de résultats tout aussi satisfaisants entre 1 et 2, je dirais opter pour 1. Au moins, c'est évident ce que vous essayez de faire de cette façon. Les sous-requêtes ne sont souvent pas très rapides (bien que souvent inévitables), en particulier dans ce cas, vous ajoutez une jointure supplémentaire dans chacune, ce qui doit certainement compliquer le plan d'exécution.

Une remarque, j'ai vu que vous utilisiez l'ancienne syntaxe de jointure ANSI-89. Il vaut mieux éviter cela, les performances seront identiques ou meilleures avec la syntaxe de jointure plus moderne, et elles sont moins ambiguës ou plus faciles à lire, plus difficiles à faire des erreurs.

FROM (FirstTable AS A INNER JOIN 
  (select top 1 B1.XTStamp, A1.RecTStamp 
   from SecondTable as B1
   inner join FirstTable as A1
     on B1.XTStamp <= A1.RecTStamp
   order by B1.XTStamp DESC) AS AbyB1 --MAX (time points before)

Nommer les choses

Je pense que la façon dont vos choses sont nommées est au mieux inutile et au pire cryptique. A, B, A1, B1etc. en tant qu'alias de table, je pense que cela pourrait être mieux. De plus, je pense que les noms de champs ne sont pas très bons, mais je me rends compte que vous n'avez peut-être pas le contrôle sur cela. Je vais juste citer rapidement Le Code sans code sur le thème des noms, et en rester là ...

"Invective!" Répondit la prêtresse. "Verbe tes substantifs!"


Requête "Prochaines étapes"

Je ne pouvais pas vraiment comprendre comment il était écrit, je devais le prendre dans un éditeur de texte et faire quelques changements de style pour le rendre plus lisible. Je sais que l'éditeur SQL d'Access est plus que maladroit, donc j'écris généralement mes requêtes dans un bon éditeur comme Notepad ++ ou Sublime Text. Certains des changements stylistiques que j'ai appliqués pour le rendre plus lisible:

  • 4 espaces en retrait au lieu de 2 espaces
  • Espaces autour des opérateurs mathématiques et de comparaison
  • Placement plus naturel des accolades et indentation (je suis allé avec des accolades de style Java, mais pourrait également être de style C, à votre préférence)

Il s'avère donc que c'est une requête très compliquée. Pour donner un sens à cela, je dois partir de la requête la plus profonde, votre IDensemble de données, que je comprends est le même que votre première jointure. Il renvoie les ID et les horodatages des appareils où les horodatages avant / après sont les plus proches, dans le sous-ensemble des appareils qui vous intéressent. Donc, au lieu de IDpourquoi ne pas l'appeler ClosestTimestampID.

Votre Detjointure n'est utilisée qu'une seule fois:

entrez la description de l'image ici

Le reste du temps, il rejoint uniquement les valeurs dont vous disposez déjà ClosestTimestampID. Donc, à la place, nous devrions être en mesure de le faire:

    ) AS ClosestTimestampID
    INNER JOIN SecondTable AS TL1 
        ON ClosestTimestampID.BeforeXTStamp = TL1.XTStamp) 
    INNER JOIN SecondTable AS TL2 
        ON ClosestTimestampID.AfterXTStamp = TL2.XTStamp
    WHERE ClosestTimestampID.XmitID IN (<limited subset S>)

Peut-être pas un énorme gain de performances, mais tout ce que nous pouvons faire pour aider le pauvre optimiseur Jet DB vous aidera!


Je ne peux pas secouer le sentiment que les calculs / algorithmes pour BeforeWeightet AfterWeightque vous utilisez pour interpoler pourraient être mieux faits, mais malheureusement je ne suis pas très bon avec ceux-ci.

Une suggestion pour éviter de planter (même si ce n'est pas l'idéal en fonction de votre application) serait de décomposer vos sous-requêtes imbriquées en leurs propres tables et de les mettre à jour si nécessaire. Je ne sais pas combien de fois vous avez besoin que vos données sources soient actualisées, mais si ce n'est pas trop souvent, vous pourriez penser à écrire du code VBA pour planifier une mise à jour des tables et des tables dérivées, et laisser simplement votre requête la plus à l'extérieur pour tirer à partir de ces tables au lieu de la source d'origine. Juste une pensée, comme je l'ai dit pas idéale mais vu l'outil vous n'aurez peut-être pas le choix.


Tout ensemble:

SELECT
    InGPS.XmitID,
    StrDateIso8601Msec(InGPS.RecTStamp) AS RecTStamp_ms,
       -- StrDateIso8601MSec is a VBA function returning a TEXT string in yyyy-mm-dd hh:nn:ss.lll format
    InGPS.ReceivID,
    RD.Receiver_Location_Description,
    RD.Lat AS Receiver_Lat,
    RD.Lon AS Receiver_Lon,
    InGPS.Before_Lat * InGPS.BeforeWeight + InGPS.After_Lat * InGPS.AfterWeight AS Xmit_Lat,
    InGPS.Before_Lon * InGPS.BeforeWeight + InGPS.After_Lon * InGPS.AfterWeight AS Xmit_Lon,
    InGPS.RecTStamp AS RecTStamp_basic
FROM (
    SELECT 
        ClosestTimestampID.RecTStamp,
        ClosestTimestampID.XmitID,
        ClosestTimestampID.ReceivID,
        ClosestTimestampID.BeforeXTStamp, 
        TL1.Latitude AS Before_Lat, 
        TL1.Longitude AS Before_Lon,
        (1 - ((ClosestTimestampID.RecTStamp - ClosestTimestampID.BeforeXTStamp) 
            / (ClosestTimestampID.AfterXTStamp - ClosestTimestampID.BeforeXTStamp))) AS BeforeWeight,
        ClosestTimestampID.AfterXTStamp, 
        TL2.Latitude AS After_Lat, 
        TL2.Longitude AS After_Lon,
        (     (ClosestTimestampID.RecTStamp - ClosestTimestampID.BeforeXTStamp) 
            / (ClosestTimestampID.AfterXTStamp - ClosestTimestampID.BeforeXTStamp)) AS AfterWeight
        FROM (((
            SELECT 
                A.RecTStamp, 
                A.ReceivID, 
                A.XmitID,
                MAX(SWITCH(B.XTStamp <= A.RecTStamp, B.XTStamp, Null)) AS BeforeXTStamp,
                MIN(SWITCH(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)
            WHERE A.XmitID IN (<limited subset S>)
            GROUP BY A.RecTStamp, ReceivID, XmitID
        ) AS ClosestTimestampID
        INNER JOIN FirstTable AS Det 
            ON (Det.XmitID = ClosestTimestampID.XmitID) 
            AND (Det.ReceivID = ClosestTimestampID.ReceivID) 
            AND (Det.RecTStamp = ClosestTimestampID.RecTStamp)) 
        INNER JOIN SecondTable AS TL1 
            ON ClosestTimestampID.BeforeXTStamp = TL1.XTStamp) 
        INNER JOIN SecondTable AS TL2 
            ON ClosestTimestampID.AfterXTStamp = TL2.XTStamp
        WHERE Det.XmitID IN (<limited subset S>)
    ) AS InGPS
INNER JOIN ReceiverDetails AS RD 
    ON (InGPS.ReceivID = RD.ReceivID) 
    AND (InGPS.RecTStamp BETWEEN <valid parameters from another table>)
ORDER BY StrDateIso8601Msec(InGPS.RecTStamp), InGPS.ReceivID;
Phrancis
la source
5
  • Ajout d'attributs et de conditions de filtre supplémentaires.
  • Toute forme de jointure croisée est éliminée à l'aide de requêtes imbriquées min et max. Il s'agit du gain de performances le plus important.
  • Les valeurs de flanc min et max renvoyées par la requête interne la plus imbriquée sont des valeurs de clé primaire (analyses) qui sont utilisées pour récupérer des attributs de flanc supplémentaires (lat et lon) à l'aide d'une recherche de calculs finaux (l'accès a un équivalent applicable).
  • Les attributs des tables primaires sont récupérés et filtrés dans la requête la plus interne et devraient améliorer les performances.
  • Il n'est pas nécessaire de formater (StrDateIso8601Msec) la valeur de temps pour le tri. L'utilisation de la valeur datetime de la table est équivalente.

Plans d'exécution de SQL Server (car Access ne peut pas afficher cela)
Sans la commande finale en raison de son coût:
analyse d'index en cluster [ReceiverDetails]. [PK_ReceiverDetails] Coût 16% d'
index cluster Recherche [FirstTable]. [PK_FirstTable] Coût 19%
Index clusterisé Rechercher [SecondTable]. [PK_SecondTable] Coût 16%
Index Clustered Rechercher [SecondTable]. [PK_SecondTable] Coût 16%
Clustered Index Rechercher [SecondTable]. [PK_SecondTable] [TL2] Coût 16%
Clustered Index Rechercher [SecondTable]. [PK_SecondTable] [TL1] Coût 16%

Avec la commande finale par:
Tri Coût 36%
Analyse d'index clusterisé [ReceiverDetails]. [PK_ReceiverDetails] Coût 10%
Index indexé clusterisé [FirstTable]. [PK_FirstTable] Coût 12%
Clustered Index Seek [SecondTable]. [PK_SecondTable] Coût 10%
Clustered Index Seek [SecondTable]. [PK_SecondTable] Coût 10%
Clustered Index Seek [SecondTable]. [PK_SecondTable] [TL2] Coût 10%
Clustered Index Seek [SecondTable]. [ PK_SecondTable] [TL1]

Code de coût de 10% :

select
     ClosestTimestampID.XmitID
    --,StrDateIso8601Msec(InGPS.RecTStamp) AS RecTStamp_ms
    ,ClosestTimestampID.ReceivID
    ,ClosestTimestampID.Receiver_Location_Description
    ,ClosestTimestampID.Lat
    ,ClosestTimestampID.Lon
,[TL1].[Latitude] * (1 - ((ClosestTimestampID.RecTStamp - ClosestTimestampID.BeforeXTStamp) / (ClosestTimestampID.AfterXTStamp - ClosestTimestampID.BeforeXTStamp))) + [TL2].[Latitude] * ((ClosestTimestampID.RecTStamp - ClosestTimestampID.BeforeXTStamp) / (ClosestTimestampID.AfterXTStamp - ClosestTimestampID.BeforeXTStamp)) AS Xmit_Lat
,[TL1].[Longitude] * (1 - ((ClosestTimestampID.RecTStamp - ClosestTimestampID.BeforeXTStamp) / (ClosestTimestampID.AfterXTStamp - ClosestTimestampID.BeforeXTStamp))) + [TL2].[Longitude] * ((ClosestTimestampID.RecTStamp - ClosestTimestampID.BeforeXTStamp) / (ClosestTimestampID.AfterXTStamp - ClosestTimestampID.BeforeXTStamp)) AS Xmit_Lon
    ,ClosestTimestampID.RecTStamp as RecTStamp_basic
from (
        (
            (
                select
                     FirstTable.RecTStamp
                    ,FirstTable.ReceivID
                    ,FirstTable.XmitID
                    ,ReceiverDetails.Receiver_Location_Description
                    ,ReceiverDetails.Lat
                    ,ReceiverDetails.Lon
                    ,(
                        select max(XTStamp) as val
                        from SecondTable
                        where XTStamp <= FirstTable.RecTStamp
                     ) as BeforeXTStamp
                    ,(
                        select min(XTStamp) as val
                        from SecondTable
                        where XTStamp > FirstTable.RecTStamp
                     ) as AfterXTStamp
                from FirstTable
                inner join ReceiverDetails
                on ReceiverDetails.ReceivID = FirstTable.ReceivID
                where FirstTable.RecTStamp between #1/1/1990# and #1/1/2020#
                and FirstTable.XmitID in (100,110)
            ) as ClosestTimestampID
            inner join SecondTable as TL1
            on ClosestTimestampID.BeforeXTStamp = TL1.XTStamp
        )
        inner join SecondTable as TL2
        on ClosestTimestampID.AfterXTStamp = TL2.XTStamp
    )
order by ClosestTimestampID.RecTStamp, ClosestTimestampID.ReceivID;

Test de performance de ma requête par rapport à la requête contenant la jointure croisée.

FirstTable a été chargé avec 13 enregistrements et SecondTable avec 1 000 000.
Les plans d'exécution de ma requête n'ont pas beaucoup changé par rapport à ce qui a été publié.
Plans d'exécution pour la jointure croisée:
Nested Loops Cost 81% using INNER JOIN SecondTable AS B ON (A.RecTStamp <> B.XTStamp OR A.RecTStamp = B.XTStamp
Nested Loops tombe à 75% si vous utilisez CROSS JOIN SecondTable AS B' or ',SecondTable AS B
Stream Aggregate 8%
Index Scan [SecondTable] [UK_ID] [B] 6%
Table Spool 5%
Plusieurs autres Clustered Index Seek et Index Seeks (similaire à ma requête telle que publiée) avec un coût de 0%.

Le temps d'exécution est .007 et 8-9 secondes pour ma requête et le CROSS JOIN.
Comparaison des coûts 0% et 100%.

J'ai chargé FirstTable avec 50 000 enregistrements et un seul enregistrement dans ReceiverDetails pour une condition de jointure et j'ai exécuté ma requête.
50 013 sont revenus entre 0,9 et 1,0 seconde.

J'ai exécuté une deuxième requête avec la jointure croisée et l'ai autorisée à s'exécuter pendant environ 20 minutes avant de la tuer.
Si la requête de jointure croisée est filtrée pour renvoyer uniquement le 13 d'origine, le temps d'exécution est à nouveau de 8 à 9 secondes.
Le placement de la condition du filtre était à l'intérieur le plus sélectionné, à l'extérieur le plus sélectionné et les deux. Aucune différence.

Il y a une différence entre ces deux conditions de jointure en faveur du CROSS JOIN, le premier utilise un prédicat, le CROSS JOIN ne fait pas:
INNER JOIN SecondTable AS B ON (A.RecTStamp <> B.XTStamp OR A.RecTStamp = B.XTStamp) CROSS JOIN SecondTable AS B

byrdzeye
la source
L'exécution de la partie ClosestTimestampID sur mon système renvoie instantanément 152928 enregistrements lorsqu'elle est encapsulée dans un nombre (*). Mon MSAccess s'est verrouillé lors du retour des enregistrements réels à ce stade - peut-être que les tables temporaires de l'autre méthode occupaient toutes sortes de mémoire. Je pense que la dernière requête que je produis à partir de votre méthodologie sera très similaire à celle que j'utilise actuellement. Ce qui, je suppose, est une bonne chose :)
mpag
1
Dans votre commentaire d'origine, vous avez indiqué que vous aviez récupéré immédiatement certains enregistrements. Ceci est important en ce qui concerne le fonctionnement de l'accès, l'élaboration d'une stratégie d'accès et la définition des attentes en termes de temps d'exécution. C'est ce qu'on appelle une exécution différée. (Il s'est écrasé lorsque vous avez atteint le dernier enregistrement.) Quel est le nombre d'enregistrements de retour de limite supérieure prévu dans la requête finale?
byrdzeye
Je crois 152928
mpag
Quelle est la nature des valeurs DateTime dans les deux tables lorsque de nouveaux enregistrements sont ajoutés. Sont-ils des horodatages actuels ou des valeurs récentes ou complètement aléatoires?
byrdzeye
le premier tableau a des tampons DateTime qui sont 2013 ou plus récents. Le deuxième tableau a des tampons DateTime qui se trouvent dans quelques mois à la mi-2015. Si de nouvelles valeurs sont ajoutées, elles seront probablement (mais pas garanties) après l'ensemble existant. De nouvelles valeurs peuvent être ajoutées à l'une ou l'autre table.
mpag
2

En ajoutant une deuxième réponse, pas meilleure que la première mais sans changer aucune des exigences présentées, il existe plusieurs façons de battre Access dans la soumission et d'apparaître accrocheur. «Matérialiser» les complications un peu à la fois en utilisant des «déclencheurs». Les tables d'accès n'ont pas de déclencheurs, donc interceptez et injectez les processus crud.

--*** Create a table for flank values.
    create table Flank (
         RecTStamp      datetime not null
        ,BeforeXTStamp  datetime null
        ,AfterXTStamp   datetime null
        ,constraint PK_Flank primary key clustered ( RecTStamp asc )
        )

--*** Create a FlankUpdateLoop sub. (create what is missing)
    -- loop until rowcount < 5000 or rowcount = 0
    -- a 5K limit appears to be manageable for Access, especially for the initial population.
    insert into Flank (
         RecTStamp
        ,BeforeXTStamp
        ,AfterXTStamp
        )
    select top 5000 FirstTable.RecTStamp
        ,(
            select max(XTStamp) as val
            from SecondTable
            where XTStamp <= FirstTable.RecTStamp
            ) as BeforeXTStamp
        ,(
            select min(XTStamp) as val
            from SecondTable
            where XTStamp > FirstTable.RecTStamp
            ) as AfterXTStamp
    from FirstTable
    left join Flank
        on FirstTable.RecTStamp = Flank.RecTStamp
    where Flank.RecTStamp is null;

--*** For FirstTable Adds, Changes or Deletes:
    delete from Flank where Flank.RecTStamp = CRUD_RecTStamp
    execute FlankUpdateLoop --See above. This will handle Adds, Changes or Deletes.

--*** For SecondTable Adds, Changes or Deletes:
    --delete from Flank where the old value is immediately before and after the new flank value.
    --They may or may not get be assigned a new value. Let FlankUpdate figure it out.

    --execute deletes for both beforextstamp and afterxtstamp
    --then update flank

    delete *
    from flank
    where beforextstamp between (
                    select min(beforextstamp)
                    from flank
                    where beforextstamp >= '3/16/2009 10:00:46 AM'
                    ) and (
                    select max(beforextstamp)
                    from flank
                    where beforextstamp <= '3/16/2009 10:00:46 AM'
                    );

    delete *
    from flank
    where afterxtstamp between (
                    select min(afterxtstamp)
                    from flank
                    where afterxtstamp >= '3/16/2009 10:00:46 AM'
                    ) and (
                    select max(afterxtstamp)
                    from flank
                    where afterxtstamp <= '3/16/2009 10:00:46 AM'
                    );

    execute FlankUpdateLoop

--*** Final Report Query***--
    --Should execute without issues including 'deferred execution' problem.
    --Add filters as needed.
    select FirstTable.XmitID
        ,FirstTable.ReceivID
        ,ReceiverDetails.Lat
        ,ReceiverDetails.Lon
        ,BeforeTable.Latitude * (1 - ((FirstTable.RecTStamp - BeforeXTStamp) / (AfterXTStamp - BeforeXTStamp))) + AfterTable.Latitude * ((FirstTable.RecTStamp - BeforeXTStamp) / (AfterXTStamp - BeforeXTStamp)) as Xmit_Lat
        ,BeforeTable.Longitude * (1 - ((FirstTable.RecTStamp - BeforeXTStamp) / (AfterXTStamp - BeforeXTStamp))) + AfterTable.Longitude * ((FirstTable.RecTStamp - BeforeXTStamp) / (AfterXTStamp - BeforeXTStamp)) as Xmit_Lon
        ,FirstTable.RecTStamp as RecTStamp_basic
    from (((
        FirstTable
    inner join Flank on FirstTable.RecTStamp = Flank.RecTStamp)
    inner join SecondTable as BeforeTable on Flank.BeforeXTStamp = BeforeTable.XTStamp)
    inner join SecondTable as AfterTable on Flank.AfterXTStamp = AfterTable.XTStamp)
    inner join ReceiverDetails on FirstTable.ReceivID = ReceiverDetails.ReceivID
    order by FirstTable.RecTStamp;
byrdzeye
la source