提高多对多关系中的查询速度

时间:2017-02-21 00:11:06

标签: sql database indexing sqlalchemy clustered-index

为了自学如何编程,我正在制作一个小型网络应用程序(Flask,SQLAlchemy,Jijna)来显示我从亚马逊订购过的所有书籍。

在“barest bones”可能的方式中,我正在努力学习如何复制http://pinboard.in - 那是我的典范; MaciejCegł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表(即“全表扫描”)。

讨论(或“我认为正在发生的事情”)

显然,我是一个完整的菜鸟 - 我已经编程了大约3个月。它只是为了让事情变得有效,但我意识到我的知识库中存在很大的差距,我正在努力填补。

关于那个蝙蝠,我可以理解这是非常低效的,每本新书,代码都在遍历整个关联表(如果这确实发生了什么,我相信它是)。我想我需要对assoc表进行集群(?)或排序(?),这样一旦我检索book with book_id == 1的所有标记,我再也不会用{{1}“检查”这些行在book_id == 1表中。

换句话说,我认为正在发生的事情是这种情况(在计算机语言中):

  • 哦,他想知道assoc表中book_id == 1的书是如何标记的
  • 好的,让我转到books
  • 第1行...... assoc表中的book_id是否等于assoc
  • 好的,是的;第1行的1是什么? ... [然后计算机转到tag_id表获取tag,并将其返回给浏览器]
  • 第{2行} tag_name表中book_id等于assoc
  • 哦,不,不是......好吧,去第3行
  • 嗯,因为我的程序员很愚蠢并且没有以某种方式对这个表进行排序或编制索引,所以我可能必须通过整个1表来查找assoc没有了......

然后,一旦我们到达book_id == 1 book_id == 2,计算机真的很生气:

  • 好的,他想知道books table
  • 附带的所有标签
  • 好的,让我转到book_id == 2
  • 排#1 ......等一下......我不是已经检查了这个?天哪,我必须重新做这件事吗?
  • 该死...好吧......排#1 ......是assoc? (我知道事实并非如此!但我还是要检查,因为我的程序员是个笨蛋...)

的问题

所以问题是,我可以(1)以某种方式对book_id == 2表进行排序(?)或集群(?),以确保通过assoc表进行更“智能”的遍历,或者,作为我的朋友建议,我(2)“学会编写好的SQL查询”吗? (注意,我从来没有学过SQL,因为我一直用SQLAlchemy处理所有事情......该死的炼金术士......秘密地把他们的魔法包裹起来等等。)

最后的话

感谢您的任何意见。如果您有任何建议可以帮助我改进我在stackoverflow上提问的方式(这是我的第一篇文章!),请告诉我。

3 个答案:

答案 0 :(得分:1)

大部分答案都在问题中。

在第一个示例中,当您遍历books表时,将执行SQL查询。在第二个示例中,为每个assoc执行单独的Book查询。因此,大约400个SQL查询非常耗时。如果设置SQLALCHEMY_ECHO config参数:

,则可以在应用调试日志中查看它们
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 %}

答案 1 :(得分:1)

以下实施改编自@ Sergey-Shubin,是这个问题的可行解决方案:

课程&amp;表关联声明

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

route,sql-alchemy query

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

答案 2 :(得分:0)

如果您的查询包含大量书籍,则在单独的SQL语句中逐个获取每本书的标记将会消耗您在网络I / O中的响应时间。

一种优化方法,如果你知道你总是需要这个查询的标签,那就是提示SQLAlchemy通过join或子​​查询来获取一个查询中的所有依赖标签。

我没有看到您的查询,但我的猜测是子查询加载最适合您的用例:

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