10
votes

Comment déplacer le tri au niveau de la base de données

J'ai une application Rails, qui utilise PostgreSQL pour une base de données, qui trie différents types d'utilisateurs par emplacement, puis par des points de réputation qu'ils reçoivent pour diverses activités sur le site. Ceci est un exemple de requête xxx pré>

la requête appelle les avocats de périmètre_by_province sur le modèle User.rb: p> xxx pré>

et ensuite sur le Utilisateur.rb modèle, il calcule les points de réputation qu'ils ont. P> xxx pré>

On m'a dit que si le site atteint un certain nombre d'utilisateurs, il deviendra incroyablement lent, car Il trie dans Ruby plutôt qu'au niveau de la base de données. Je sais que le commentaire fait référence à la méthode totale_votes, mais je ne sais pas si les avocats_by_province se produisent au niveau de la base de données ou à Ruby, il utilise des aides à Rails pour interroger la DB. On dirait comme un mélange de moi, mais je ne suis pas sûr de l'effet de cela sur l'efficacité. P>

Pouvez-vous me montrer comment écrire cela afin que la requête se produise au niveau de la DB et donc de manière plus efficace qui ne brisera pas mon site? p>

mise à jour Voici les trois schémas pour les modèles dans la méthode totale_votes. P>

PG::Error: ERROR: column reference "user_id" is ambiguous LINE 1: ..."user_id" = "users"."id" left outer join ( SELECT user_id as... ^ : SELECT users.*, sum(total_votes.value) as total_votes FROM "users" INNER JOIN "contacts" ON "contacts"."user_id" = "users"."id" left outer join ( SELECT user_id as tv_user_id, value FROM "answer_votes" INNER JOIN "answers" ON "answers"."id" = "answer_votes"."answer_id" UNION ALL SELECT user_id as tv_user_id, value FROM "contributions" UNION ALL SELECT user_id as tv_user_id, value FROM "best_answers" INNER JOIN "answers" ON "answers"."id" = "best_answers"."answer_id") as total_votes on users.id = total_votes.tv_user_id WHERE "contacts"."province_id" = 6 AND "users"."lawyer" = 't' ORDER BY total_votes desc


13 commentaires

En fait, votre étendue : avocats_by_province est traduit en SQL avant d'être exécuté, jusqu'à présent, ça va. La méthode totale_votes ... Eh bien, je pourrais ne pas avoir la meilleure réponse ici, ni la bonne mais je pense que le moyen le plus simple que vous puissiez obtenir le tri et tout est en écrivant pure sql aciverecord :: base.connection.execute ("SQL. ici ") .


@SPARDA OK, merci ou est-ce correct d'ajouter le code total_votes dans les avocats_by_province Scope? De toute façon, pourriez-vous aider avec le SQL et où mettais-je ce code ActiveRecord? à l'intérieur d'une méthode?


Eh bien, cela dépend de si vous appelez souvent : avocats_by_province sans tri ou non. Vous faites alors la portée est justifiée et je suppose que ce n'est pas un problème de faire le trutant de tri dans une autre méthode. Si les deux sont toujours appelés ensemble, les mélangez ensemble peut être une bonne idée. Maintenant, pour le SQL, j'écrirais une méthode de classe. Je vais essayer d'écrire une réponse.


@Sparda je les appellerai ensemble la plupart du temps.


Ok donc je suggérerais de convertir les 3 requêtes de votre total_votes en SQL.


@sparda Je ferai probablement les deux, comme l'application n'est pas encore terminée. Si vous avez des conseils sur la façon d'écrire la méthode / SQL pour le total_votes, je vous serais vraiment reconnaissant si vous pouviez partager. Merci


En fait, ce n'est pas quelque chose que je peux sortir avec tel quel, j'aurais besoin d'un environnement configuré comme le vôtre à tester afin que je ne puisse pas vraiment fournir une réponse de travail sur la façon d'écrire le SQL.


Veuillez poster le SQL et les schémas réels. On dirait que vous pouviez maintenir un champ de karma pour chaque avocat, ce qui donnerait le plan le plus rapide avec un indice approprié.


@Denis I a affiché le schéma des trois modèles de la méthode totale_votes, ainsi que le schéma du modèle de contacts mentionné dans la périmètre de lawyers_by_province du modèle User.RB.


Envisagez d'ajouter un champ de karma à votre table de contacts qui est maintenue à l'aide de déclencheurs sur INSERT, Mettre à jour et supprimez vos trois tables de réponse. Cela donnera directement à la commande par Karma, avec un index sur (emplacement_id, karma desc) .


@Denis Ajouter le champ Karma est assez facile. Je suppose par emplacement_id que vous vouliez dire province_id?


Ouais ... puis, précalculer le karma (idéalement en utilisant des déclencheurs, mais les rails fonctionne aussi si vous êtes plus à l'aise avec cela), et vous n'avez pas besoin de vous soucier des requêtes compliquées.


@Denis, alors quand vous dites d'ajouter un champ de karma aux contacts «maintenus», voulez-vous dire que des points à tout moment sont attribués à un utilisateur que je devrais ensuite mettre à jour le champ Contacts.Karma?


5 Réponses :


1
votes

Prenez l'union de votre total de vos questions de vote, faites-en une sous-requête, joignez-vous à vos utilisateurs. Cela vous donne également l'attribut totaux_votes.

def self.total_vote_sql
    "(select user_id, sum(value) as total_votes from ( " +
    [
     AnswerVote.joins(:answer).select("answers.user_id, value"),
     Contribution.select("user_id, value"),
     BestAnswer.joins(:answer).select("answers.user_id, value")
    ].map(&:to_sql) * " UNION ALL " + 
    ") as total_votes group by user_id) as tv "
end

User.select("users.*, tv.total_votes").
joins("left outer join #{User.total_vote_sql} on users.id = tv.user_id").
order("total_votes desc").lawyers_by_province(province)


14 commentaires

L'opérateur du syndicat rendra les choses effrayées ralentissement à long terme ... surtout si vous vous joignez à l'ensemble résultant.


J'aurais dû spécifier un syndicat pour éliminer le tri et l'élimination inutiles des doublons.


Merci beaucoup pour cela. Je vais essayer et revenir à vous une fois que je m'assurais que cela fonctionne avec Postgres.


Je reçois un message d'erreur de votre code à long terme pour la section Commentaires. Je mettrai à jour le OP si vous pouvez aider.


@ShawnBalestracci merci, mais cela ne résout pas le problème. Je posterai une erreur en op mais je pense que c'est fondamentalement la même chose.


@Brainlikikeadullpcencil oh, réponses et bonsanswers ont les deux user_id, j'ai raté cela auparavant, désolé. Édité à nouveau.


Merci mais une autre erreur. Je suis tellement nouveau dans lesquels je ne sais pas vraiment comment résoudre les complexes comme ceci est (par rapport à ce que je suis habitué à) pg :: Erreur: erreur: colonne "users.id" doit apparaître dans le groupe par clause ou être utilisé dans une ligne de fonction globale 1:


Édité pour ajouter le groupe par ce postgreal nécessite mais MySQL ne le fait pas.


Sainte Moly, merci pour votre patience! Ne fonctionne toujours pas. C'est le problème. Lorsque j'utilise votre code exactement tel quel, il est indiqué variable local indéfini ou méthode total_vote_sql '`C'est pourquoi je l'ai ensuite changé en user.total_vote_sql mais quand je le fais, je "MM toujours Obtenir ce pg :: Erreur: Erreur: colonne" users.id "doit apparaître dans la clause du groupe par la clause ou être utilisée dans une ligne de fonction globale 1:


Savez-vous pourquoi je pourrais toujours obtenir cette erreur de groupe, même si vous avez ajouté le groupe à la fin?


Vous devrez peut-être répertorier tous les champs d'utilisateurs de ce groupe, on dirait que vous ne pouvez pas regrouper par * à Postgres. L'autre option serait d'ajouter une autre couche qui fait les utilisateurs de groupe_by.Id et de somme et joindises à la requête extérieure. Il faudrait un peu de violation de moi pour proposer la syntaxe pour cette deuxième option, je ne pourrai pas en rendre compte pendant quelques jours.


@Brainlikikeadullpencil J'ai ajouté une sous-requête extérieure à la jointure pour le groupe par requis pour la somme.


Merci pour tous vos efforts, cependant, je ne peux toujours pas le faire fonctionner: pg :: Erreur: Erreur: colonne TV.Total_votes n'existe pas


@Brainlikikeadullpencil, je l'ai accidentellement changé à partir de TOTAL_VOTES à TOTAL_VALUE. J'ai corrigé ça.



8
votes

CAVEATAIER: Je suis assez nouveau dans les rails, mais c'est ma technique pour rester sain d'esprit tout en ayant besoin d'aller directement à la base de données pour des raisons de performance, que je dois faire tout le temps car vous ne pouvez avoir que deux de la Suivant

  1. Traitement des données en vrac
  2. une technique pure-rails
  3. bonnes performances

    Quoi qu'il en soit, une fois que vous devez entrer dans ces méthodologies hybrides, qui sont parti-rubis part-sql, je me sens comme si vous allez aussi bien aller tout le porc et optez pour une solution pure SQL.

    1. Il est plus facile de déboguer car vous isolez plus efficacement les deux couches de code.
    2. Il est plus facile d'optimiser le SQL parce que vous avez une meilleure chance d'obtenir une personne SQL dédiée à la regarder pour vous si ce n'est pas votre point fort.

      Je pense que le SQL que vous recherchez ici est le long des lignes de: xxx

      Ceci devrait vous donner la meilleure performance possible - Les sous-sollicielles de la sélection ne sont pas idéales mais la technique applique un filtrage sur lequel user_id vous vérifiez le score pour.

      Intégration dans les rails: Si vous définissez SQL_String comme code SQL: < Pré> xxx

      ... puis vous obtenez un tableau de hachage que vous travaillez avec comme ceci: xxx


9 commentaires

Je pense que votre analyse est sur place et est la meilleure mise. Allez directement à la DB.


Juste pour vous assurer que nous sommes sur la même page, j'ai enveloppé l'instruction SQL dans des citations doubles (car true était en guillemets simples). Cependant, il lance une erreur pg :: Erreur: erreur: erreur de syntaxe à ou proche "". Ligne 9: Contacts sur (contact.user_id = user.id) ^: Je ne suis pas sûr de ce que le problème est depuis contact.rb a une colonne User_id: Entier Colonne


Aurait dû être "contacts.user_id = users.id"


thx, a fait ce changement, maintenant il dit pg :: Erreur: Erreur: entrée de la clause manquante pour la table "Réponse_votes" Line 5: J'ai examiné cette erreur ( Stackoverflow.com / Questions / 9643859 / ... ) Mais je ne peux pas comprendre où la clause de la clause devrait aller (et me demander pourquoi l'erreur existe de toute façon, puisqu'il y a déjà une clause)


Désolé, bâclé de ma part. Corrigé je pense


ok, merci, je l'ai eu pour travailler, cependant, je me rendais compte que c'est probablement important pour vous de savoir comment les données seront présentées ... J'utilise cela pour trier toute une base de données (en supposant que les utilisateurs s'inscrivent) de potentiellement des milliers d'utilisateurs, puis pour les énumérer avec pagination. Par conséquent, faire de l'utilisateur.find sur chacun d'eux après que je trie semble être la mauvaise approche, et j'ai besoin de l'instance pour chaque utilisateur. Je voudrais trier et obtenir l'instance en même temps si possible.


En fait, je peux simplement obtenir les autres informations dont j'ai besoin (nom, email) sur la même requête SQL, sans faire de l'utilisateur ... Merci, j'ai beaucoup appris. Aller essayer votre autre réponse et comparer des vitesses.


Ma pensée était que vous recherchez uniquement les résultats Top-N, mais si vous voulez vraiment des milliers, je vais certainement aller pour l'autre solution.


En fait, vous remarquerez que votre SQL ne reproduit pas le SQL de TOTAL_VOTES IN OP (c'est-à-dire un total différent). Dans l'OP, vous remarquerez qu'Ipéblier (comme Bestanswer le fait également) rejoint la réponse, puis récupère des enregistrements dans lesquels la réponse user.id = auto.Id = self.id répond.joints (: réponse) .Où (Réponses: {user_id: Self.Id}). Somme («valeur») L'ID utilisateur sur répondvevote ne sera pas identique à celui de l'utilisateur sur la réponse (identique à Bestannswer). Probablement une mauvaise façon d'écrire le code (comme cela nécessite la jointure de réponse) mais je me demande si vous indiquiez comment vous ajouteriez cela à votre instruction SQL (si vous avez le temps).



1
votes

Une approche différente qui pourrait bien fonctionner, c'est de maintenir le montant total au niveau de l'utilisateur avec des rappels sur les trois modèles de notation: - réponse_value, best_answer_value et contribution_value (non nullables et valeurs de zéro)

Bien que Il s'agit d'un problème de verrouillage potentiel sur les enregistrements d'utilisateurs individuels Le processus de vote est susceptible d'être suffisamment rapide de ne pas être perceptible.

en maintenant des colonnes séparées pour les trois scores et créant un expression basée sur (et éventuellement partiel ) Index Vous obtiendriez des requêtes très hautes performances pour top-n: xxx


4 commentaires

Juste pour m'assurer que je comprends, suggérez-vous que quelque temps, par exemple, une valeur est entrée dans best_answer_value pour effectuer ensuite un rappel avant_save ou après_save sur le modèle utilisateur et ajouter le score là-bas? Donc, j'aurai trois autres colonnes sur le modèle utilisateur, à savoir best_answer_value, réponse_value et contribution_value?


Oui, c'est le plan. Vous répandez essentiellement la douleur du calcul des totaux sur un grand nombre de petites transactions, ce qui évite la réapprovisionnement des valeurs que vous avez agrégées auparavant.


OK, merci, pourriez-vous me montrer comment vous modifieriez la méthode totale_votes dans le modèle User.rb (voir la op pour la version actuelle) si j'ai créé ces trois colonnes sur user.rb? Apprécier ton aide


Étant donné que les valeurs Réponsekarma, la contributionkarma et Bestanswer sont stockées comme des attributs de la table, il devient simplement la somme des trois valeurs, supprimez donc les trois lignes qui les dérivent d'autres modèles



2
votes

Voulez-vous vraiment calculer de manière dynamique la réputation d'un utilisateur à chaque fois? La bonne façon est de pré-calculer la réputation d'un utilisateur. Dans les rails, vous feriez cela comme suit:

# app/models/reputation_change_observer.rb
class ReputationChangeObserver < ActiveRecord::Observer
  observe :answer, :contribution # observe things linked to a users reputation

  def after_update(record)
    record.user.update_reputation
  end
end

# app/models/user.rb
class User
  # Add a column called "reputation"

  def update_reputation
    answerkarma = AnswerVote.joins(:answer).where(answers: {user_id: self.id}).sum('value') 
    contributionkarma = Contribution.where(user_id: self.id).sum('value')
    bestanswer = BestAnswer.joins(:answer).where(answers: {user_id: self.id}).sum('value') 
    total_votes = contributionkarma + bestanswer

    # Save the updated reputation in the "reputation" field
    self.update_attribute :reputation, total_votes
  end
end


2 commentaires

Merci pour cela. Je vais jouer avec elle et vous revenir à vous.


Très bonne réponse! :) Bien que le modèle d'observateur soit enlevé dans des rails 4. :( Stackoverflow.com/Questtions/15165260/... Utilisation également Memcached to cache Le (s) tableau pourrait également aider, comme cela ressemble à des statistiques" en temps réel ".



0
votes

Pour le tri et le filtrage Vous pouvez utiliser gem 'wice_grid' c'est très facile à utiliser et à mettre en œuvre ... Grid Wice .


0 commentaires