【问题标题】:Postgres uses wrong indexPostgres 使用错误的索引
【发布时间】:2015-01-18 04:29:58
【问题描述】:

我有一个问题:

EXPLAIN ANALYZE
SELECT CAST(DATE(associationtime) AS text) AS date ,
       cast(SUM(extract(epoch
                        FROM disassociationtime) - extract(epoch
                                                           FROM associationtime)) AS bigint) AS sessionduration,
       cast(SUM(tx) AS bigint)AS tx,
       cast(SUM(rx) AS bigint) AS rx,
       cast(SUM(dataRetries) AS bigint) AS DATA,
       cast(SUM(rtsRetries) AS bigint) AS rts,
       count(*)
FROM SESSION
WHERE ssid_id=42
  AND ap_id=1731
  AND DATE(associationtime)>=DATE('Tue Nov 04 00:00:00 MSK 2014')
  AND DATE(associationtime)<=DATE('Thu Nov 20 00:00:00 MSK 2014')
GROUP BY(DATE(associationtime))
ORDER BY DATE(associationtime);

输出是:

 GroupAggregate  (cost=0.44..17710.66 rows=1 width=32) (actual time=4.501..78.880 rows=17 loops=1)
   ->  Index Scan using session_lim_values_idx on session  (cost=0.44..17538.94 rows=6868 width=32) (actual time=0.074..73.266 rows=7869 loops=1)
         Index Cond: ((date(associationtime) >= '2014-11-04'::date) AND (date(associationtime) <= '2014-11-20'::date))
         Filter: ((ssid_id = 42) AND (ap_id = 1731))
         Rows Removed by Filter: 297425
 Total runtime: 78.932 ms

看看这一行:

Index Scan using session_lim_values_idx

如您所见,查询使用三个字段进行扫描:ssid_id、ap_id 和关联时间。我有一个索引:

ssid_pkey                  | btree | {id}
ap_pkey                    | btree | {id}
testingshit_pkey           | btree | {one,two,three}
session_date_ssid_idx      | btree | {ssid_id,date(associationtime),"date_trunc('hour'::text, associationtime)"}
session_pkey               | btree | {associationtime,disassociationtime,sessionduration,clientip,clientmac,devicename,tx,rx,protocol,snr,rssi,dataretries,rtsretries }
session_main_idx           | btree | {ssid_id,ap_id,associationtime,disassociationtime,sessionduration,clientip,clientmac,devicename,tx,rx,protocol,snr,rssi,dataretres,rtsretries}
session_date_idx           | btree | {date(associationtime),"date_trunc('hour'::text, associationtime)"}
session_date_apid_idx      | btree | {ap_id,date(associationtime),"date_trunc('hour'::text, associationtime)"}
session_date_ssid_apid_idx | btree | {ssid_id,ap_id,date(associationtime),"date_trunc('hour'::text, associationtime)"}
ap_apname_idx              | btree | {apname}
users_pkey                 | btree | {username}
user_roles_pkey            | btree | {user_role_id}
session_lim_values_idx     | btree | {date(associationtime)}

它叫做session_date_ssid_apid_idx。但是为什么查询使用错误的索引?

session_date_ssid_apid_idx:

------------+-----------------------------+-------------------------------------------
 ssid_id    | integer                     | ssid_id
 ap_id      | integer                     | ap_id
 date       | date                        | date(associationtime)
 date_trunc | timestamp without time zone | date_trunc('hour'::text, associationtime)

session_lim_values_idx:

date    | date | date(associationtime)

你会创建什么索引?

UPD: \d session

 --------------------+-----------------------------+------------------------------------------------------
 id                 | integer                     | NOT NULL DEFAULT nextval('session_id_seq'::regclass)
 ssid_id            | integer                     | NOT NULL
 ap_id              | integer                     | NOT NULL
 associationtime    | timestamp without time zone | NOT NULL
 disassociationtime | timestamp without time zone | NOT NULL
 sessionduration    | character varying(100)      | NOT NULL
 clientip           | character varying(100)      | NOT NULL
 clientmac          | character varying(100)      | NOT NULL
 devicename         | character varying(100)      | NOT NULL
 tx                 | integer                     | NOT NULL
 rx                 | integer                     | NOT NULL
 protocol           | character varying(100)      | NOT NULL
 snr                | integer                     | NOT NULL
 rssi               | integer                     | NOT NULL
 dataretries        | integer                     | NOT NULL
 rtsretries         | integer                     | NOT NULL
╚эфхъё√:
    "session_pkey" PRIMARY KEY, btree (associationtime, disassociationtime, sessionduration, clientip, clientmac, devicename, tx, rx, protocol, snr, rssi, dataretries, rtsretries)
    "session_date_ap_ssid_idx" btree (ssid_id, ap_id, associationtime)
    "session_date_apid_idx" btree (ap_id, date(associationtime), date_trunc('hour'::text, associationtime))
    "session_date_idx" btree (date(associationtime), date_trunc('hour'::text, associationtime))
    "session_date_ssid_apid_idx" btree (ssid_id, ap_id, associationtime)
    "session_date_ssid_idx" btree (ssid_id, date(associationtime), date_trunc('hour'::text, associationtime))
    "session_lim_values_idx" btree (date(associationtime))
    "session_main_idx" btree (ssid_id, ap_id, associationtime, disassociationtime, sessionduration, clientip, clientmac, devicename, tx, rx, protocol, snr, rssi, dataretries, rtsretries)

【问题讨论】:

  • 这真的是执行计划的完整输出吗?我希望其中至少有另一个步骤来查找其他列。顺便说一句:您可以删除以下索引之一:ssid_pkeyap_pkey 它们是相同的。最好显示 psql 的 \d 命令输出的索引列表,而不是系统目录的(有点混乱)内容(或至少使用视图 pg_indexes
  • 从我目前看到的情况来看,应该使用索引session_date_ssid_apid_idx。您的问题中缺少某些内容,或者您​​的数据库有问题。我会删除该索引(或所有索引),运行VACUUM FULL ANALYZE session,重新创建索引(或所有索引)并重试。或者如果您无法锁定表,请使用pg_repack或者您的大多数列都有ssid_id=42 AND ap_id=1731,因此这些谓词对于索引的选择无关紧要,使用较小的索引并过滤其余的索引会更便宜。
  • @ErwinBrandstetter,看来您对ssid_id=42 AND ap_id=1731 的看法是正确的。如果我将这些值更改为不太受欢迎,则会选择新索引(右索引)。
  • SELECT count(*) AS a, count(ssid_id=42 AND ap_id=1731 OR NULL) AS b FROM session 能得到什么?
  • 对于SELECT count(associationtime BETWEEN '2014-11-04 0:0' AND '2014-11-20 0:0' OR NULL) AS a, count(associationtime BETWEEN '2014-11-04 0:0' AND '2014-11-20 0:0' AND ssid_id=42 AND ap_id=1731 OR NULL) AS b FROM session?

标签: postgresql indexing postgresql-performance sql-execution-plan


【解决方案1】:

ssid_idap_id 的谓词中非常常见的值可以使 Postgres 更便宜地选择较小的索引 session_lim_values_idx(只有 1 个 date 列)而不是看似更合适但更大的索引 @987654329 @(4 列)并过滤其余部分。

在您的情况下,大约 4% 的行具有 ssid_id=42 AND ap_id=1731。这通常不应该保证切换到较小的索引。但还有其他几个可能会影响规模的因素,基本上是成本设置统计数据。详情:

怎么办?

  • 如果您尚未按照linked the answer above 中的建议调整您的费用设置。

  • 增加所涉及列ssid_idap_id 的统计目标并运行ANALYZE

    这里有一个特殊因素:Postgres 收集索引中表达式的单独统计数据。检查:

    SELECT * FROM pg_statistic
    WHERE starelid = 'session_date_ssid_apid_idx'::regclass;
    

    您会找到表达式date(associationtime) 的专用行。更多详情:

  • 通过删除第 4 列 "date_trunc('hour'::text, associationtime),使索引 session_date_ssid_apid_idx 更具吸引力(更小)。查看您后来添加的表定义,您已经这样做了。

  • 我宁愿使用转换的标准语法:cast(associationtime AS date),而不是函数语法date(associationtime)。一点都不重要,我只是知道正常工作的标准方法。您可以在查询中使用简写语法associationtime::date,它与表达式索引兼容,但在索引定义中使用详细形式。

另外,使用EXPLAIN ANALYZE 测试哪个查询计划实际上更快,方法是仅删除/重新创建要测试的索引。然后你会看到 Postgres 是否选择了最好的计划。

你有很多索引,我会检查它们是否都被实际使用并删除其余的。索引有维护成本,如果可能的话,专注于更少的索引通常是有益的(更容易放入缓存中,并且可能在需要时已经缓存)。权衡成本与收益。

一边

我会使用:

SUM(extract(epoch FROM disassociationtime
                     - associationtime)::int) AS sessionduration

【讨论】:

  • 令人沮丧的是,2020 年的数据库选择了完全错误的索引并且计划不周……我们正在重新考虑 MySQL,但需要地理空间支持。我已将随机页面成本设置为 1.2,但它为简单查询选择了错误的索引,具体取决于字段列表中 xyz_id 的长度。较长的值使 postgres 选择不应该的正确索引。
  • 我们必须在系统范围内禁用排序才能选择正确的索引,否则它会选择单列索引并选择对其进行排序以耗费大量时间和成本。无论我们进行多少次全真空分析,甚至设置 default_statistics_target=10000(据说是最准确的设置),它都不起作用。 PG11.
猜你喜欢
  • 2016-09-19
  • 1970-01-01
  • 1970-01-01
  • 2012-06-26
  • 2021-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多