2017-02-21 9 views
0

В попытке научить себя программированию я создаю небольшое веб-приложение (Flask, SQLAlchemy, Jijna), чтобы отобразить все книги, которые я когда-либо заказывал от Амазонки.Улучшение скорости запроса во взаимоотношениях «многие ко многим»

В «самых красивых костях», я пытаюсь научиться реплицировать http://pinboard.in -that's my paragon; Macie Cegłowski - это прямая G ... Я понятия не имею, как его сайт работает так быстро: я могу загрузить 160 закладок - все со связанными тегами -in, я не знаю, 500 мс? ... вот почему я знаю, что я делаю что-то ужасно, ужасно неправильно, как обсуждается ниже. (Если бы я мог, я бы просто заплатить ему обучать меня. Lulz.)

В любом случае, я создал многие-ко-многим между моим books класса и моего tag класса, так что пользователь может (1) щелкните по book и просмотрите все его tags, а также (2) щелкните по tag и просмотрите все похожие книги. Вот моя таблица архитектура:

Entity relationship diagram

Вот код для отношений между двумя классами:

assoc = db.Table('assoc', 
    db.Column('book_id', db.Integer, db.ForeignKey('books.book_id')), 
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.tag_id')) 
) 

class Book(db.Model): 
    __tablename__ = 'books' 
    book_id = db.Column(db.Integer, primary_key=True) 
    title = db.Column(db.String(120), unique=True) 
    auth = db.Column(db.String(120), unique=True) 
    comment = db.Column(db.String(120), unique=True) 
    date_read = db.Column(db.DateTime) 
    era = db.Column(db.String(36)) 
    url = db.Column(db.String(120)) 
    notable = db.Column(db.String(1)) 

    tagged = db.relationship('Tag', secondary=assoc, backref=db.backref('thebooks',lazy='dynamic')) 

    def __init__(self, title, auth, comment, date_read, url, notable): 
     self.title = title 
     self.auth = auth 
     self.comment = comment 
     self.date_read = date_read 
     self.era = era 
     self.url = url 
     self.notable = notable 

class Tag(db.Model): 
    __tablename__ = 'tags' 
    tag_id = db.Column(db.Integer, primary_key=True) 
    tag_name = db.Column(db.String(120)) 

проблема

Если я перебирать books таблицы только (~ 400 строк), запрос запускается и отображается в браузере молниеносной скоростью. Нет проблем.

{% for i in book_query %} 
    <li> 
     {{i.notable}}{{i.notable}} 
     <a href="{{i.url}}">{{i.title}}</a>, {{i.auth}} 
     <a href="/era/{{i.era}}">{{i.era}}</a> {{i.date_read}} 
     {% if i.comment %} 
      <p>{{i.comment}}</p> 
     {% else %} 
      <!-- print nothing --> 
     {% endif %} 
    </li> 
{% endfor %} 

Если, однако, я хочу, чтобы показать любые и все теги, связанные с книгой, я изменить код, вкладывая в for loop следующим образом:

{% for i in book_query %} 
    <li> 
     {{i.notable}}{{i.notable}} 
     <a href="{{i.url}}">{{i.title}}</a>, {{i.auth}} 
     <a href="/era/{{i.era}}">{{i.era}}</a> 
     {% for ii in i.tagged %} 
      <a href="/tag/{{ii.tag_name}}">{{ii.tag_name}}</a> 
     {% endfor %} 
     {{i.date_read}} 
     {% if i.comment %} 
      <p>{{i.comment}}</p> 
     {% else %} 
      <!-- print nothing --> 
     {% endif %} 
    </li> 
    {% endfor %} 

Запрос значительно замедляется (занимает около 20 секунд). Я понимаю, что это происходит потому, что для каждой строки в таблице book мой код выполняет итерацию через всю таблицуassoc (т. Е. «Полное сканирование таблицы»).

обсуждение (или «то, что я думаю, что происходит»)

Очевидно, что я полный нуб-I've программировали в течение ~ 3 месяцев. Это мотивирует только заставить все работать, но я понимаю, что у меня большие пробелы в моей базе знаний, которые я пытаюсь заполнить, когда я иду.

Сразу, что летучая мышь, я могу оценить, что это невероятно неэффективно, что с каждой новой книгой, код перебором таблицы всей ассоциации (если это действительно то, что происходит, что я считаю, что это). Я думаю, мне нужно класть (?) Или сортировать (?) Таблицу assoc таким образом, что как только я получаю все теги для book with book_id == 1, я больше никогда не «проверю» строки с book_id == 1 в таблице assoc.

Другими словами, то, что я думаю, что происходит это (в computerspeak):

  • О, он хочет знать, как книга с book_id == 1 в books стол был помечен
  • Хорошо, позвольте мне перейдите на страницу assoc Таблица
  • Строка № 1 ... book_id в assoc Таблица равна 1?
  • Хорошо, это так; то что такое tag_id для строки №1? ... [затем компьютер переходит в tag стол, чтобы получить tag_name, и возвращает его в браузер]
  • Row # 2 ... в assoc таблице book_id равна 1?
  • О, нет, это не ... хорошо, перейдите к строке # 3
  • Хмммм, потому что мой программист глуп и не сделал эту таблицу сортированной или проиндексированной каким-то образом, я собираюсь должно пройти через все assoc стола ищет book_id == 1 когда, возможно, не больше и ...

Тогда, как только мы получаем book_id == 2 в books table компьютере становится действительно безумно:

  • Хорошо, он хочет знать все теги, которые идут с book_id == 2
  • Хорошо, позвольте мне перейти к assoc таблице
  • Row # 1 ... секундочку ... я не проверить это один уже ?? Holy sh # t, я должен сделать это снова и снова ??
  • Dammit ... okay ... Row # 1 ... is book_id == 2? (Я знаю, что это не так! Но я должен проверить в любом случае, потому что мой программист является дум-дум ...)

вопросы

Таким образом, вопрос, могу ли я (1) сортировать (?) или кластер (?) таблицу assoc в некотором роде, которая обеспечивает более «интеллектуальный» обход таблицы assoc, или, как мне показалось, я (2) «научиться писать хорошие SQL-запросы»? (Обратите внимание, я никогда не узнал SQL, так как я все обращения с SQLAlchemy ... проклятые Алхимики ... окутывающих их в магии секрет и этажерки.)

финальные слова

Спасибо за любой вклад. Если у вас есть предложения, которые помогут мне улучшить, как я задаю вопросы о stackoverflow (это мой первый пост!), Пожалуйста, дайте мне знать.

ответ

1

Большая часть ответа находится в вопросе.

В первом примере 1 SQL-запрос выполняется, когда вы перебираете таблицу books. Во втором примере для каждого Book выполняется отдельный запрос assoc. Таким образом, это около 400 SQL-запросов, которые требуют много времени.Вы можете просмотреть их в приложении журнала отладки, если вы установите SQLALCHEMY_ECHO параметр конфигурации:

app.config['SQLALCHEMY_ECHO'] = True 

Или вы можете установить Flask-DebugToolbar и посмотреть на эти вопросы в веб-интерфейсе.

Лучший подход к решению этой проблемы - изучить основы SQL, они понадобятся в любом случае, когда ваши приложения станут больше. Попробуйте написать более оптимизированный запрос в чистом SQL. В вашем случае это может выглядеть следующим образом:

SELECT books.*, tags.tag_name FROM books 
JOIN assoc ON assoc.book_id = books.book_id 
JOIN tags ON assoc.tag_id = tags.tag_id 

Тогда попробуйте переписать его в SQLAlchemy код, а затем группу по книге, прежде чем перейти к HTML визуализатора:

# Single query to get all books and their tags 
query = db.session.query(Book, Tag.tag_name).join('tagged') 
# Dictionary of data to be passed to renderer 
books = {} 
for book, tag_name in query: 
    book_data = books.setdefault(book.book_id, {'book': book, 'tags': []}) 
    book_data['tags'].append(tag_name) 
# Rendering HTML 
return render_template('yourtemplate.html', books=books) 

код шаблона будет выглядеть следующим образом:

{% for book in books %} 
<li> 
    {{ book.book.notable }}{{ book.book.notable }} 
    <a href="{{ book.book.url }}">{{ book.book.title }}</a>, {{ book.book.auth }} 
    <a href="/era/{{ book.book.era }}">{{ book.book.era }}</a> 
    {% for tag in book.tags %} 
    &nbsp;<a href="/tag/{{ tag }}" class="tag-link">{{ tag }}</a>&nbsp; 
    {% endfor %} 
    {{ book.book.date_read }} 
    {% if book.book.comment %} 
     <p>{{ book.book.comment }}</p> 
    {% else %} 
     <!-- print nothing --> 
    {% endif %} 
</li> 
{% endfor %} 

Другой подход

Если база данных PostgreSQL можно написать такой запрос:

SELECT books.title, books.auth (...), array_agg(tags.tag_name) as book_tags FROM books 
JOIN assoc ON assoc.book_id = books.book_id 
JOIN tags ON assoc.tag_id = tags.tag_id 
GROUP BY books.title, books.auth (...) 

В этом случае вы получите книги данных с уже агрегированными тегами как массив. SQLAlchemy позволяет сделать такой запрос:

from sqlalchemy import func 

books = db.session.query(Book, func.array_agg(Tag.tag_name)).\ 
    join('tagged').group_by(Book).all() 
return render_template('yourtemplate.html', books=books) 

И шаблон имеет следующую структуру:

{% for book, tags in books %} 
<li> 
    {{ book.notable }}{{ book.notable }} 
    <a href="{{ book.url }}">{{ book.title }}</a>, {{ book.auth }} 
    <a href="/era/{{ book.era }}">{{ book.era }}</a> 
    {% for tag in tags %} 
    &nbsp;<a href="/tag/{{ tag }}" class="tag-link">{{ tag }}</a>&nbsp; 
    {% endfor %} 
    {{ book.date_read }} 
    {% if book.comment %} 
     <p>{{ book.comment }}</p> 
    {% else %} 
     <!-- print nothing --> 
    {% endif %} 
</li> 
{% endfor %} 
0

Если запрос имеет много книг, забирающие тег для каждой книги, по одному в отдельном SQL-оператор убьет ваше время отклика в сетевом вводе-выводе.

Один из способов оптимизировать это, если вы знаете, что вам всегда понадобятся теги для этого запроса, это означает, что SQLAlchemy запрашивает все зависимые теги в одном запросе через соединение или подзапрос.

я не вижу свой запрос, но я думаю, нагрузка подзапрос будет работать лучше для вашего случая использования:

session.query(Book).options(subqueryload('tagged')).filter(...).all() 
1

В следующей реализации, приспособленного от @ Сергей-Шубина, был работоспособным решением этот вопрос:

классы & таблицы декларация ассоциации

assoc = db.Table('assoc', 
    db.Column('book_id', db.Integer, db.ForeignKey('books.book_id')), 
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.tag_id')) 
    ) 

class Book(db.Model): 
    __tablename__ = 'books' 
    book_id = db.Column(db.Integer, primary_key=True) 
    title = db.Column(db.String(120), unique=True) 
    auth = db.Column(db.String(120), unique=True) 
    comment = db.Column(db.String(120), unique=True) 
    date_read = db.Column(db.DateTime) 
    era = db.Column(db.String(36)) 
    url = db.Column(db.String(120)) 
    notable = db.Column(db.String(1))  

    tagged = db.relationship('Tag', secondary=assoc, backref=db.backref('thebooks',lazy='dynamic')) 

class Tag(db.Model): 
    __tablename__ = 'tags' 
    tag_id = db.Column(db.Integer, primary_key=True) 
    tag_name = db.Column(db.String(120)) 

def construct_dict(query): 
     books_dict = {} 
     for each in query: # query is {<Book object>, <Tag object>} in the style of assoc table - therefore, must make a dictionary bc of the multiple tags per Book object 
      book_data = books_dict.setdefault(each[0].book_id, {'bookkey':each[0], 'tagkey':[]}) # query is a list of like this {index-book_id, {<Book object>}, {<Tag object #1>, <Tag object #2>, ... }} 
      book_data['tagkey'].append(each[1]) 
     return books_dict 

маршрут, SQL-запрос алхимия

@app.route('/query') 
def query(): 
    query = db.session.query(Book, Tag).outerjoin('tagged') # query to get all books and their tags 
    books_dict = construct_dict(query) 

    return render_template("query.html", query=query, books_dict=books_dict) 
Смежные вопросы