PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Obtenir le dernier enfant par parent à partir d'une grande table - la requête est trop lente

Le point principal est très probablement que vous JOIN et GROUP sur tout juste pour obtenir max(created) . Obtenez cette valeur séparément.

Vous avez mentionné tous les index nécessaires ici :sur report_rank.created et sur les clés étrangères. Tu vas bien là. (Si vous êtes intéressé par mieux que "bien", continuez à lire !)

Le LEFT JOIN report_site sera forcé à un simple JOIN par le WHERE clause. J'ai remplacé un simple JOIN . J'ai aussi beaucoup simplifié votre syntaxe.

Mise à jour en juillet 2015 avec des requêtes plus simples et plus rapides et des fonctions plus intelligentes.

Solution pour plusieurs lignes

report_rank.created n'est pas unique et vous voulez tout les dernières lignes.
Utilisation de la fonction window rank() dans une sous-requête.

SELECT r.id, r.keyword_id, r.site_id
     , r.rank, r.url, r.competition
     , r.source, r.country, r.created  -- same as "max"
FROM  (
   SELECT *, rank() OVER (ORDER BY created DESC NULLS LAST) AS rnk
   FROM   report_rank r
   WHERE  EXISTS (
      SELECT *
      FROM   report_site    s
      JOIN   report_profile p ON p.site_id = s.id
      JOIN   crm_client     c ON c.id      = p.client_id
      JOIN   auth_user      u ON u.id      = c.user_id
      WHERE  s.id = r.site_id
      AND    u.is_active
      AND    c.is_deleted = FALSE
      )
   ) sub
WHERE  rnk = 1;

Pourquoi DESC NULLS LAST ?

Solution pour une ligne

Si report_rank.created est unique ou vous êtes satisfait de n'importe quelle ligne avec max(created) :

SELECT id, keyword_id, site_id
     , rank, url, competition
     , source, country, created  -- same as "max"
FROM   report_rank r
WHERE  EXISTS (
    SELECT 1
    FROM   report_site    s
    JOIN   report_profile p ON p.site_id = s.id
    JOIN   crm_client     c ON c.id      = p.client_id
    JOIN   auth_user      u ON u.id      = c.user_id
    WHERE  s.id = r.site_id
    AND    u.is_active
    AND    c.is_deleted = FALSE
   )
-- AND  r.created > f_report_rank_cap()
ORDER  BY r.created DESC NULLS LAST
LIMIT  1;

Devrait être plus rapide, encore. Plus d'options :

Vitesse ultime avec index partiel ajusté dynamiquement

Vous avez peut-être remarqué la partie commentée dans la dernière requête :

AND  r.created > f_report_rank_cap()

Vous avez parlé de 50 millions. lignes, c'est beaucoup. Voici un moyen d'accélérer les choses :

  • Créer un simple IMMUTABLE fonction renvoyant un horodatage qui est garanti plus ancien que les lignes d'intérêt tout en étant aussi jeune que possible.
  • Créer un index partiel sur les lignes plus jeunes uniquement - basé sur cette fonction.
  • Utilisez un WHERE condition dans les requêtes qui correspond à la condition d'index.
  • Créez une autre fonction qui met à jour ces objets à la dernière ligne avec DDL dynamique. (Moins une marge sécurisée au cas où la ou les lignes les plus récentes seraient supprimées/désactivées - si cela peut arriver)
  • Invoquez cette fonction secondaire à des heures creuses avec un minimum d'activité simultanée par cronjob ou à la demande. Aussi souvent que vous le souhaitez, ne peut pas faire de mal, il suffit d'un court verrouillage exclusif sur la table.

Voici une démo de travail complète .
@erikcw, vous devrez activer la partie commentée comme indiqué ci-dessous.

CREATE TABLE report_rank(created timestamp);
INSERT INTO report_rank VALUES ('2011-11-11 11:11'),(now());

-- initial function
CREATE OR REPLACE FUNCTION f_report_rank_cap()
  RETURNS timestamp LANGUAGE sql COST 1 IMMUTABLE AS
$y$SELECT timestamp '-infinity'$y$;  -- or as high as you can safely bet.

-- initial index; 1st run indexes whole tbl if starting with '-infinity'
CREATE INDEX report_rank_recent_idx ON report_rank (created DESC NULLS LAST)
WHERE  created > f_report_rank_cap();

-- function to update function & reindex
CREATE OR REPLACE FUNCTION f_report_rank_set_cap()
  RETURNS void AS
$func$
DECLARE
   _secure_margin CONSTANT interval := interval '1 day';  -- adjust to your case
   _cap timestamp;  -- exclude older rows than this from partial index
BEGIN
   SELECT max(created) - _secure_margin
   FROM   report_rank
   WHERE  created > f_report_rank_cap() + _secure_margin
   /*  not needed for the demo; @erikcw needs to activate this
   AND    EXISTS (
     SELECT *
     FROM   report_site    s
     JOIN   report_profile p ON p.site_id = s.id
     JOIN   crm_client     c ON c.id      = p.client_id
     JOIN   auth_user      u ON u.id      = c.user_id
     WHERE  s.id = r.site_id
     AND    u.is_active
     AND    c.is_deleted = FALSE)
   */
   INTO   _cap;

   IF FOUND THEN
     -- recreate function
     EXECUTE format('
     CREATE OR REPLACE FUNCTION f_report_rank_cap()
       RETURNS timestamp LANGUAGE sql IMMUTABLE AS
     $y$SELECT %L::timestamp$y$', _cap);

     -- reindex
     REINDEX INDEX report_rank_recent_idx;
   END IF;
END
$func$  LANGUAGE plpgsql;

COMMENT ON FUNCTION f_report_rank_set_cap()
IS 'Dynamically recreate function f_report_rank_cap()
    and reindex partial index on report_rank.';

Appel :

SELECT f_report_rank_set_cap();

Voir :

SELECT f_report_rank_cap();

Décommentez la clause AND r.created > f_report_rank_cap() dans la requête ci-dessus et observez la différence. Vérifiez que l'index est utilisé avec EXPLAIN ANALYZE .

Le manuel sur la concurrence et REINDEX :