如何优化SQLite中的多个嵌套SELECT(用Python)?

5 投票
2 回答
1225 浏览
提问于 2025-04-15 18:54

我正在制作一个CGI脚本,这个脚本会定期查询一个SQLite数据库,并生成一个统计表。下面描述了源数据库的表格和相关的代码片段。虽然功能上都能正常运行,但这个CGI脚本的速度非常慢,因为我有很多嵌套的SELECT COUNT(id)调用。我觉得优化的最好办法就是向Stack Overflow社区求助,因为我在谷歌上找的结果并不理想。

表格:

CREATE TABLE messages (
    id TEXT PRIMARY KEY ON CONFLICT REPLACE,
    date TEXT,
    hour INTEGER,
    sender TEXT,
    size INTEGER,
    origin TEXT,
    destination TEXT,
    relay TEXT,
    day TEXT);

(是的,我知道这个表格没有经过规范化,但它是从邮件日志中提取的……我能把提取和填充搞定就已经很高兴了,更别提规范化了。我觉得表格的结构和我现在的问题关系不大,但我也可能错了。)

示例行:

476793200A7|Jan 29 06:04:47|6|admin@mydomain.com|4656|web02.mydomain.pvt|user@example.com|mail01.mydomain.pvt|Jan 29

还有,构建我表格的Python代码:

#!/usr/bin/python
print 'Content-type: text/html\n\n'

from datetime import date

import re
p = re.compile('(\w+) (\d+)')

d_month = {'Jan':1,'Feb':2,'Mar':3,'Apr':4,'May':5,'Jun':6,'Jul':7,'Aug':8,'Sep':9,'Oct':10,'Nov':11,'Dec':12}
l_wkday = ['Mo','Tu','We','Th','Fr','Sa','Su']

days = []
curs.execute('SELECT DISTINCT(day) FROM messages ORDER BY day')
for day in curs.fetchall():
    m = p.match(day[0]).group(1)
    m = d_month[m]
    d = p.match(day[0]).group(2)
    days.append([day[0],"%s (%s)" % (day[0],l_wkday[date.weekday(date(2010,int(m),int(d)))])])

curs.execute('SELECT DISTINCT(sender) FROM messages')
senders = curs.fetchall()
for sender in senders:
    curs.execute('SELECT COUNT(id) FROM messages WHERE sender=%s',(sender[0]))
    print '  <div id="'+sender[0]+'">'
    print '   <h1>Stats for Sender: '+sender[0]+'</h1>'
    print '   <table><caption>Total messages in database: %d</caption>' % curs.fetchone()[0]
    print '    <tr><td>&nbsp;</td><th colspan=24>Hour of Day</th></tr>'
    print '    <tr><td class="left">Day</td><th>%s</th></tr>' % '</th><th>'.join(map(str,range(24)))
    for day in days:
            print '    <tr><td>%s</td>' % day[1]
            for hour in range(24):
                    sql = 'SELECT COUNT(id) FROM messages WHERE sender="%s" AND day="%s" AND hour="%s"' % (sender[0],day[0],str(hour))
                    curs.execute(sql)
                    d = curs.fetchone()[0]
                    print '    <td>%s</td>' % (d>0 and str(d) or '')
            print '    </tr>'
    print '   </table></div>'

print ' </body>\n</html>\n'

我不确定是否有办法将一些查询合并,或者换个角度来提取数据。我也考虑过建立一个第二个表格,专门存放计数,并在原始表格更新时更新这个表格。今天我盯着这个问题看了太久,所以我决定明天再重新来过,希望能从专家那里获得一些启发;)


编辑:使用下面提供的GROUP BY答案,我成功地在一个查询中从数据库获取了所需的数据。我转用了Perl,因为Python的嵌套字典支持并不适合我需要的方式(以特定的方式构建一组HTML表格)。这是修改后代码的一小段:

my %data;
my $rows = $db->selectall_arrayref("SELECT COUNT(id),sender,day,hour FROM messages GROUP BY sender,day,hour ORDER BY sender,day,hour");
for my $row (@$rows) {
    my ($ct, $se, $dy, $hr) = @$row;
    $data{$se}{$dy}{$hr} = $ct;
}
for my $se (keys %data) {
    print "Sender: $se\n";
    for my $dy (keys %{$data{$se}}) {
    print "Day: ",time2str('%a',str2time("$dy 2010"))," $dy\n";
        for my $hr (keys %{$data{$se}{$dy}}) {
            print "Hour: $hr = ".$data{$se}{$dy}{$hr}."\n";
        }
    }
    print "\n";
}

原本执行大约需要28.024秒,现在只需要0.415秒!

2 个回答

1

首先,创建一个索引:

CREATE INDEX messages_sender_by_day ON messages (sender, day);

(你可能不需要把“小时”也加进去。)

如果这样做没有效果,或者你已经试过了,那么请稍微调整一下你的问题:给我们一些代码来生成测试数据,以及这个表上所有索引的SQL语句。

维护一个计数缓存是比较常见的做法,但我不确定在这里是否需要这样做。

3

首先,你可以使用“分组”这个功能:

select count(*), sender from messages group by sender;

这样你就可以对所有发送者一次性执行一个查询,而不是对每个发送者都执行一个查询。还有一种可能性是:

select count(*), sender, day, hour
    from messages group by sender, day, hour
    order by sender, day, hour;

我没有测试过,但至少现在你知道“分组”这个功能的存在了。这应该能减少查询的次数,我觉得这是提高性能的第一步。

其次,基于搜索的列创建索引,在你的情况下就是发送者、日期和小时。

如果这些还不够,可以使用分析工具找出花费时间最多的地方。你还应该考虑使用fetchmany而不是fetchall,这样可以减少内存的使用。记住,由于sqlite模块是用C语言编写的,所以尽量多用它。

撰写回答