Postgres使用错误的索引

Ton*_*ony 6 postgresql indexing sql-execution-plan postgresql-performance

我有一个问题:

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);
Run Code Online (Sandbox Code Playgroud)

输出是:

 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
Run Code Online (Sandbox Code Playgroud)

看看这一行:

Index Scan using session_lim_values_idx
Run Code Online (Sandbox Code Playgroud)

如您所见,查询使用三个字段进行扫描:ssid_id,ap_id和associationtime.我有一个索引:

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)}
Run Code Online (Sandbox Code Playgroud)

它被称为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)
Run Code Online (Sandbox Code Playgroud)

session_lim_values_idx:

date    | date | date(associationtime)
Run Code Online (Sandbox Code Playgroud)

你会创建什么指数?

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)
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 8

在谓词中非常常见的值,ssid_id并且ap_id可以使Postgres选择较小的索引session_lim_values_idx(仅1 date列)比看似更好的拟合更便宜,但更大的索引session_date_ssid_apid_idx(4列)并过滤其余的.

在你的情况下,大约4%的行有ssid_id=42 AND ap_id=1731.这通常不应该保证切换到较小的索引.但是其他一些因素正在发挥作用,可能会使规模倾斜,主要是成本设置和统计数据.细节:

该怎么办?

  • 如果您没有按照建议链接上面的答案,请调整您的费用设置.

  • 增加所涉及列的统计目标ssid_id,ap_id并运行ANALYZE:

    这里有一个特殊因素:Postgres 为索引中的表达式收集单独的统计信息.检查:

    SELECT * FROM pg_statistic
    WHERE starelid = 'session_date_ssid_apid_idx'::regclass;
    
    Run Code Online (Sandbox Code Playgroud)

    您将找到表达式的专用行date(associationtime).更多细节:

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

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

此外,通过仅删除/重新创建要测试的索引来测试EXPLAIN ANALYZE查询计划实际上更快.然后你会看到Postgres是否选择了最好的计划.

你有很多索引,我会检查是否所有索引都是实际使用的并且除去其余的索引.索引具有维护成本,如果可能的话,专注于更少的索引通常是有益的(更容易适应缓存并且可以在需要时缓存).权衡成本与收益.

在旁边

我用的是:

SUM(extract(epoch FROM disassociationtime
                     - associationtime)::int) AS sessionduration
Run Code Online (Sandbox Code Playgroud)