--
-- $Id: pg_advisor.sql,v 1.69 2004/03/21 17:38:52 coelho Exp $
--
-- pg_advisor maybe future system schema?
--
-- at the time it is 'xpg_advisor',
-- as the 'pg_' prefix is reserved for system schemas.
--
-- how to localise comments? descriptions?
-- should this use 'information_schema' instead of 'pg_catalog'?
-- more explicit comments about views? with examples?
-- add comments to all columns?
-- give a grade/mark? ;-)
--

DROP SCHEMA xpg_advisor CASCADE;

CREATE SCHEMA xpg_advisor;

COMMENT ON SCHEMA xpg_advisor IS 
'Various advices about database design or performance.
This views check various common \'maybe\' common faults using
available information from the table descriptions. It deals
primarily with constraints (foreign key, primary key, unique)
and attempts to detect inconsistancies.'
;

SET search_path TO xpg_advisor,pg_catalog;

-------------------------------------------------------------------------------
--
-- UTILS
--

CREATE TABLE advice_kind(
  kid INTEGER PRIMARY KEY, 
  description TEXT UNIQUE NOT NULL
);

COPY advice_kind(kid,description) FROM STDIN;
1	misc
2	design
3	performance
\.

COMMENT ON TABLE advice_kind IS
'kind of advice such as \'performance\' or \'design\''
;

CREATE TABLE advice_severity(
  sid INTEGER PRIMARY KEY, 
  description TEXT UNIQUE NOT NULL
);

COPY advice_severity(sid,description) FROM STDIN;
1	info
2	notice
3	warning
4	error
\.

COMMENT ON TABLE advice_severity IS
'severity description of an advice, from \'info\' to \'serious\''
;

CREATE TABLE advice_classification(
  advice OID PRIMARY KEY, -- REFERENCES pg_class(oid), 
  -- the following denormalisation is NECESSARY at the time.
  name TEXT UNIQUE NOT NULL, -- copy of pg_class.relname of above
  kid INTEGER NOT NULL REFERENCES advice_kind,
  sid INTEGER NOT NULL REFERENCES advice_severity,
  title TEXT UNIQUE NOT NULL,
  abstract TEXT UNIQUE NOT NULL,
  description TEXT
);

COMMENT ON TABLE advice_classification IS
'advice classification such as kind, severity...'
;

-- 
-- may be somewhere?
-- 
CREATE OR REPLACE FUNCTION is_system_schema(TEXT) RETURNS BOOLEAN AS '
SELECT $1 LIKE \'pg_%\' OR $1 LIKE \'xpg_%\' OR $1 = \'information_schema\';
' LANGUAGE 'SQL';

COMMENT ON FUNCTION is_system_schema(TEXT) IS '
tells whether a schema name is system or user.
includes extensions as xpg_* or information_schema.
';

-- test:
-- SELECT is_system_schema('pg_catalog');
-- SELECT is_system_schema('public');

--
-- missing array_index function. quick PL/pgSQL fix.
--
CREATE OR REPLACE FUNCTION array_index(anyarray, anyelement) 
RETURNS INTEGER AS '
DECLARE
  tab ALIAS FOR $1;
  item ALIAS FOR $2;
  i INTEGER;
BEGIN
  FOR i IN 1..array_upper(tab, 1) LOOP
    IF tab[i] = item THEN
      RETURN i;
    END IF;
  END LOOP;
  -- item not found in tab
  RETURN NULL;
END;'
LANGUAGE plpgsql;

COMMENT ON FUNCTION array_index(anyarray, anyelement) IS
'missing array_index function... should be already there!'
;

-- test whether two array contents are set-equal.
-- [1,1,2] and [2,1,2] are consided equal.
CREATE OR REPLACE FUNCTION array_ceq(anyarray, anyarray) 
RETURNS BOOLEAN AS '
DECLARE
  tab1 ALIAS FOR $1;
  tab2 ALIAS FOR $2;
  i INTEGER;
BEGIN
  IF array_upper(tab1, 1) != array_upper(tab2, 1) THEN
    RETURN FALSE;
  END IF;
  FOR i IN 1..array_upper(tab1, 1) LOOP
    IF NOT tab1[i] = ANY (tab2) THEN
      RETURN FALSE;
    END IF;
    IF NOT tab2[i] = ANY (tab1) THEN
      RETURN FALSE;
    END IF;
  END LOOP;
  RETURN TRUE;
END;
' LANGUAGE 'plpgsql';

COMMENT ON FUNCTION array_ceq(anyarray, anyarray) IS
'equality of content of two arrays in any order'
;

--
-- how to SELECT COUNT(*) FROM [the view of which I have the oid]?
-- here is the missing function, that may be somewhere I guess?
--
CREATE TABLE counter(call OID);

CREATE OR REPLACE FUNCTION count_tuples(TEXT) 
RETURNS INTEGER AS '
DECLARE
  tablename ALIAS FOR $1;
  res RECORD;
BEGIN
  -- should prevent recursion? how?
  FOR res IN EXECUTE \'SELECT COUNT(*) AS n FROM xpg_advisor.\' || tablename
  LOOP
    RETURN res.n;
  END LOOP;
END;'
LANGUAGE 'plpgsql';

COMMENT ON FUNCTION count_tuples(OID) IS 
'meta function for meta queries...
- count number of rows in table or view, given its oid.
- beware, this function is dangerous (costly, infinite recursion).'
;

-- ??? how to advise postgres optimiser that count_tuples is very expensive?

--
-- add advice classification
--
CREATE OR REPLACE FUNCTION 
  set_advice_info(TEXT, INTEGER, INTEGER, TEXT, TEXT, TEXT)
RETURNS INTEGER AS '
INSERT INTO 
  advice_classification(advice, name, kid, sid, title, abstract, description)
SELECT c.oid, $1, $2, $3, $4, $5, $6
FROM pg_namespace AS n JOIN pg_class AS c ON (c.relnamespace=n.oid)
WHERE n.nspname=current_schema() AND c.relname=$1;
SELECT 1;'
LANGUAGE 'SQL';

COMMENT ON FUNCTION 
set_advice_info(TEXT, INTEGER, INTEGER, TEXT, TEXT, TEXT) IS
'add an advice information to advice_classification table:
- view name of the advice
- kind of advice number (see advice_kind)
- severity of advice number (see advice_severity)
- advice title (a short sentence)
- advice abstract (a paragraph)
- advice description (a longer explanation)'
;

-------------------------------------------------------------------------------
--
-- DESIGN ADVICES views
--
-- the comment should justify really why these advices are given.
-- I'm not even sure they are good advices;-)

--
-- tables without primary keys
--

CREATE VIEW da_tables_without_primary_key AS 
SELECT
  n.nspname AS SchemaName,
  c.relname AS TableName
FROM pg_class AS c 
  JOIN pg_namespace AS n ON (c.relnamespace=n.oid)
WHERE
  -- no comments about system catalogs.
  NOT is_system_schema(n.nspname)
  -- only tables
  AND c.relkind = 'r' 
  -- no primary key
  AND NOT c.relhaspkey
ORDER BY SchemaName ASC, TableName ASC;

COMMENT ON VIEW da_tables_without_primary_key IS 
'advice: all tables without a declared primary key'
;

SELECT set_advice_info('da_tables_without_primary_key', 2, 4,
'no primary key',
'a primary key on every table is better design...
- what is the point of having a table, if you do not have
  and identifiable concept to put inside?
- relational algebra is build upon the set theory. 
  relations are sets. sets only store distinct elements.
- maybe some UNIQUE NOT NULL attribute should be the primary key?',
NULL
);

-- SELECT * FROM da_tables_without_primary_key;

--
-- tables with composite primary keys?
-- or should it be declared only if foreigns??
-- or if partial???
-- or not if parts are foreign keys?
-- or it is ok anyway?
-- 

CREATE VIEW da_tables_with_composite_primary_key AS
SELECT 
  n.nspname AS SchemaName, 
  c.relname AS TableName
FROM pg_class AS c 
  JOIN pg_namespace AS n ON (c.relnamespace=n.oid)
  JOIN pg_constraint AS o ON (o.conrelid=c.oid)
WHERE
  -- no comments about system catalogs.
  NOT is_system_schema(n.nspname)
  -- only tables
  AND c.relkind = 'r' 
  -- with a primary key
  AND c.relhaspkey
  -- the primary key constraint
  AND contype = 'p'
  -- more than 1 element in conkey
  AND array_upper(o.conkey, 1)>1
  -- partial primary key???
  AND relnatts>array_upper(o.conkey, 1)
ORDER BY SchemaName ASC, TableName ASC;

COMMENT ON VIEW da_tables_with_composite_primary_key IS 
'advice: all tables with composite primary keys';

SELECT set_advice_info('da_tables_with_composite_primary_key', 2, 1,
'composite partial primary key',
'- I prefer a simple primary key, without semantics...
  because foreign keys if any will be simpler.',
NULL
);

-- SELECT * FROM da_tables_with_composite_primary_key;

--
-- composite foreign key...
--
CREATE VIEW da_composite_foreign_key AS
SELECT
  n.nspname AS SchemaName,
  c.relname AS TableName,
  o.conname AS ConstraintName
FROM pg_class AS c 
  JOIN pg_namespace AS n ON (c.relnamespace=n.oid)
  JOIN pg_constraint AS o ON (o.conrelid=c.oid)
WHERE
  -- no comments about system catalogs.
  NOT is_system_schema(n.nspname)
  -- only tables
  AND c.relkind = 'r' 
  -- the foreign key constraint
  AND contype = 'f'
  -- more than 1 element in confkey
  AND array_upper(o.confkey, 1)>1
ORDER BY SchemaName ASC, TableName ASC, ConstraintName ASC;

COMMENT ON VIEW da_composite_foreign_key IS 
'advice: composite foreign key'
;

SELECT set_advice_info('da_composite_foreign_key', 2, 2,
'composite foreign key',
'simple foreign keys are better design...
- the foreign key replicates the information that some
  referenced key components belong together.
- on updates of the referenced keys, 
  all corresponding foreign keys must be updated.',
NULL
);

-- SELECT * FROM da_composite_foreign_key;

--
-- I like simple INTEGER PRIMARY KEY better
--
CREATE VIEW da_non_integer_primary_key AS
SELECT -- DISTINCT
  n.nspname AS SchemaName, 
  t.relname AS TableName,
  a.attname AS AttName,
  format_type(a.atttypid, a.atttypmod) AS TypeName
--  c.conname AS ConstraintName
FROM pg_namespace AS n
  JOIN pg_class AS t ON (t.relnamespace=n.oid)
  JOIN pg_constraint AS c ON (c.conrelid=t.oid)
  JOIN pg_attribute AS a ON (a.attnum = ANY (c.conkey) AND a.attrelid=t.oid)
  JOIN pg_type AS typ ON (a.atttypid=typ.oid)
  JOIN pg_namespace AS ntyp ON (typ.typnamespace=ntyp.oid)
WHERE
   -- no comments about system schema
   NOT is_system_schema(n.nspname)
   -- only tables (redundant?)
   AND t.relkind = 'r'
   -- primary key contraint
   AND c.contype = 'p'
   -- the key type is not an integer? or a short type??
   AND ntyp.nspname='pg_catalog' 
   AND NOT typ.typname = ANY (ARRAY['int2', 'int4', 'int8', 'oid'])
ORDER BY SchemaName ASC, TableName ASC;

COMMENT ON VIEW da_non_integer_primary_key IS
'advice: non integer primary key'
;

SELECT set_advice_info('da_non_integer_primary_key', 2, 1,
'primary key is not an integer',
'not really a problem, but:
- I like semantics integer primary keys without application semantics...
- it makes updates easier, as the number will not need to be changed,
  hence matching foreign keys won\'t need to be changed either.',
NULL
);

--
-- unique constraint with nullable attribute?
--

CREATE VIEW da_unique_nullable_attribute AS
SELECT
  nc.nspname AS SchemaName,
  c.relname AS TableName,
  a.attname AS AttName,
  no.nspname AS ConstraintSchemaName,
  o.conname AS ConstraintName
FROM pg_class AS c 
  JOIN pg_namespace AS nc ON (c.relnamespace=nc.oid)
  JOIN pg_constraint AS o ON (o.conrelid=c.oid)
  JOIN pg_namespace AS no ON (o.connamespace=no.oid)
  JOIN pg_attribute AS a ON (a.attrelid=c.oid)
WHERE
  -- no comments about system catalogs.
  NOT is_system_schema(nc.nspname) AND NOT is_system_schema(no.nspname)
  -- only tables (redundant, constraint are necessary on tables?)
  AND c.relkind = 'r'
  -- it is a 'unique' constraint
  AND contype = 'u'
  -- attribute is in the constraint
  AND a.attnum = ANY (o.conkey)
  -- attribute is nullable
  AND NOT a.attnotnull
ORDER BY SchemaName ASC, TableName ASC, AttName ASC,
         ConstraintSchemaName ASC, ConstraintName ASC;

COMMENT ON VIEW da_unique_nullable_attribute IS
'advice: unique constraint with nullable attribute'
;

SELECT set_advice_info('da_unique_nullable_attribute', 2, 2,
'unique constraint with nullable attribute',
'maybe a bad design... either
- the NULL value has a particular semantic for an attribute
- or you forgot a NOT NULL declaration',
NULL
);

-- SELECT * FROM da_unique_nullable_attribute;

CREATE VIEW da_nullable_attribute AS
SELECT 
  n.nspname AS SchemaName,
  c.relname AS TableName,
  a.attname AS AttName
FROM pg_class AS c 
  JOIN pg_namespace AS n ON (c.relnamespace=n.oid)
  JOIN pg_attribute AS a ON (a.attrelid=c.oid)
WHERE
  -- no comments about system catalogs.
  NOT is_system_schema(n.nspname)
  -- only tables (redundant, constraint are necessary on tables?)
  AND c.relkind = 'r'
  -- attribute is nullable
  AND NOT a.attnotnull
ORDER BY SchemaName ASC, TableName ASC, AttName ASC;

COMMENT ON VIEW da_nullable_attribute IS
'advice: nullable attributes'
;

SELECT set_advice_info('da_nullable_attribute', 2, 1,
'nullable attribute',
'- most of the time attributes should be NOT NULL
- but it is perfectly right to have a NULL value of missing fields',
NULL
);

--
-- internal foreign key constraint...
--
CREATE VIEW in_foreign_key_contraint AS
SELECT
  -- constraint
  c.oid AS constraint, 
  -- referencing table
  c.conrelid AS crel, 
  cca.attname AS ccol,
  cca.atttypid AS ctyp,
  cca.atttypmod AS cmod,
  cca.attlen AS clen,
  -- referenced table, foreign part
  c.confrelid AS frel, 
  fka.attname AS fcol, 
  fka.atttypid AS ftyp, 
  fka.atttypmod AS fmod,
  fka.attlen AS flen,
  array_index(c.confkey, fka.attnum) AS component
FROM pg_constraint AS c
     JOIN pg_attribute AS cca ON (c.conrelid=cca.attrelid)
     JOIN pg_attribute AS fka ON (c.confrelid=fka.attrelid)
WHERE
  -- foreign key constraint
  c.contype='f'
  -- column attribute in constraint
  AND cca.attnum = ANY (c.conkey)
  -- foreign key attribute
  AND fka.attnum = ANY (c.confkey)
  -- matching key component
  AND array_index(c.confkey, fka.attnum)=array_index(c.conkey, cca.attnum)
;

-- verbose view
CREATE VIEW in_verbose_foreign_key_contraint AS
SELECT 
  -- ready for pretty printing
  nc.nspname AS SchemaName,
  cc.relname AS TableName,
  fkc.ccol AS AttName,
  format_type(fkc.ctyp, fkc.cmod) AS ColumnType,
  nf.nspname AS FSchemaName,
  cf.relname AS FName,
  fkc.fcol AS FAttName,
  format_type(fkc.ftyp, fkc.fmod) AS ForeignType,
  -- row stuff for selection
  fkc.*
FROM in_foreign_key_contraint AS fkc 
  JOIN pg_class AS cc ON (fkc.crel=cc.oid)
  JOIN pg_namespace AS nc ON (cc.relnamespace=nc.oid)
  JOIN pg_class AS cf ON (fkc.frel=cf.oid)
  JOIN pg_namespace AS nf ON (cf.relnamespace=nf.oid)
WHERE
  -- no comments about system catalogs.
  NOT is_system_schema(nc.nspname) AND NOT is_system_schema(nf.nspname)
  -- only tables (redundant? only tables can have ri contraints?)
  AND cc.relkind='r' AND cf.relkind='r';

--
-- foreign keys which do not exactly match their target key types?
--

CREATE VIEW da_foreign_key_type_dont_match AS
SELECT SchemaName, TableName, AttName, ColumnType,
       FSchemaName, FName, FAttName, ForeignType,
       Component
FROM in_verbose_foreign_key_contraint
WHERE ctyp!=ftyp -- non matching type
ORDER BY SchemaName ASC, TableName ASC, 
         FSchemaName ASC, FName ASC, Component ASC;

COMMENT ON VIEW da_foreign_key_type_dont_match IS
'advice: non matching foreign key types'
;

SELECT set_advice_info('da_foreign_key_type_dont_match', 2, 3,
'non exactly matching foreign key types',
'maybe a bad design...
- do you have any argument why the type shouldn\'t be the same?',
NULL
);

-- SELECT * FROM da_foreign_key_type_dont_match;

--
-- foreign keys which do match their target types but with different sizes?
--
CREATE VIEW da_foreign_key_type_match_but_size AS
SELECT SchemaName, TableName, AttName, ColumnType,
       FSchemaName, FName, FAttName, ForeignType,
       Component
FROM in_verbose_foreign_key_contraint
WHERE ctyp=ftyp -- matching type
  -- but non matching variable length
  AND clen=-1 AND flen=-1 AND cmod!=fmod
ORDER BY SchemaName ASC, TableName ASC,
         FSchemaName ASC, FName ASC, Component ASC;

COMMENT ON VIEW da_foreign_key_type_match_but_size IS
'advice: non matching size in foreign key types'
;

SELECT set_advice_info('da_foreign_key_type_match_but_size', 2, 3,
'non exactly matching foreign key type sizes',
'maybe a bad design... 
- do you have any argument why the type size shouldn\'t be the same?
- as a foreign key must match its referenced key, 
  all matching values will necessarily be the same.
  . if the foreign key is smaller, some referenced key cannot be matched.
  . if the foreign key is larger, the added part cannot be used.',
NULL
);

--
-- tables with large primary keys (such as TEXT, VARCHAR(64)...)?
--   if there are foreign keys that use them...
--
CREATE VIEW da_large_primary_key_with_foreigns AS
SELECT SchemaName, TableName, AttName, ColumnType,
       FSchemaName, FName, FAttName, ForeignType,
       Component
FROM in_verbose_foreign_key_contraint
WHERE ctyp=ftyp -- matching type
  -- variable length
  AND clen=-1 AND flen=-1 
  -- matching (otherwise already in previous table)
  AND cmod=fmod 
  -- but large (16 bytes) or unbounded (e.g. TEXT)
  AND (cmod>16 OR cmod=-1)
ORDER BY SchemaName ASC, TableName ASC,
         FSchemaName ASC, FName ASC, Component ASC;

COMMENT ON VIEW da_large_primary_key_with_foreigns IS
'advice: large primary key with foreign keys'
;

SELECT set_advice_info('da_large_primary_key_with_foreigns', 2, 2,
'large key types for foreign keys',
'maybe a bad design...
- if the type is large, it means that there are some information inside,
  and this information is replicated in foreign keys.
- if the key is meaningful and must be updated, all foreign keys will
  have to be updated as well.
- maybe you should rather have an simple primary key,
  and a UNIQUE NOT NULL field?',
NULL
);

--
-- other design advices?
--

CREATE VIEW da_foreign_key_not_to_primary AS
SELECT
  nf.nspname AS FSchemaName,
  clf.relname AS FTableName,
  np.nspname AS SchemaName,
  clp.relname AS TableName
FROM 
  -- foreign key part
    pg_namespace AS nf
    JOIN pg_class AS clf ON (clf.relnamespace=nf.oid)
    JOIN pg_constraint AS cf ON (cf.conrelid=clf.oid)
  JOIN
  -- primary key part
    pg_namespace AS np
    JOIN pg_class AS clp ON (clp.relnamespace=np.oid)
    JOIN pg_constraint AS cp ON (cp.conrelid=clp.oid)
  ON (cf.confrelid = cp.conrelid)
WHERE
  -- no comments about system tables
  NOT is_system_schema(nf.nspname) 
  AND NOT is_system_schema(np.nspname)
  -- tables (redundant? constraints only on tables?)
  AND clp.relkind='r' AND clf.relkind='r' 
  -- foreign constraint and primary constraint
  AND cf.contype='f' AND cp.contype='p'
  -- that no not match
  AND NOT array_ceq(cp.conkey, cf.confkey)
ORDER BY FSchemaName ASC, FTableName ASC, SchemaName ASC, TableName ASC
;

COMMENT ON VIEW da_foreign_key_not_to_primary IS
'advice: foreign key does not reference primary key'
;

SELECT set_advice_info('da_foreign_key_not_to_primary', 2, 1,
'foreign key not to a primary key',
'not really a problem
- but why not to the primary key of the target?',
NULL
);

-- SELECT * FROM da_foreign_key_not_to_primary;

CREATE VIEW in_tables AS
SELECT DISTINCT n.nspname, t.relname, t.oid
FROM pg_namespace AS n JOIN pg_class AS t ON (t.relnamespace=n.oid)
WHERE NOT is_system_schema(n.nspname) AND t.relkind='r';

CREATE VIEW in_table_src_ri_constraints AS
SELECT * 
FROM in_tables AS t JOIN pg_constraint AS c ON (c.conrelid=t.oid)
WHERE c.contype='f';

CREATE VIEW in_table_dst_ri_constraints AS
SELECT * 
FROM in_tables AS t JOIN pg_constraint AS c ON (c.confrelid=t.oid)
WHERE c.contype='f';

--
-- isolated tables, that have no foreign keys, and that are not referenced...
--
CREATE VIEW da_isolated_table AS
SELECT nspname AS SchemaName, relname AS TableName
FROM in_tables
EXCEPT
SELECT nspname AS SchemaName, relname AS TableName
FROM in_table_src_ri_constraints
EXCEPT
SELECT nspname AS SchemaName, relname AS TableName
FROM in_table_dst_ri_constraints;

COMMENT ON VIEW da_isolated_table IS
'advice: tables which are isolated (not referenced and no reference)'
;

SELECT set_advice_info('da_isolated_table', 2, 3,
'isolated tables, not referenced and no reference',
'maybe missing FOREIGN KEY declarations?
- tables without reference on other tables
- and not referenced by other tables',
NULL);

-------------------------------------------------------------------------------
--
-- PERFORMANCE ADVICES (pa_*)
--
-- no usable primary key index for foreign key referencial integrity checks?
-- no usable index for foreign key on deletes?
-- never used indexes given enough statistics? costly?
--

-------------------------------------------------------------------------------
--
-- count SUMMARY of advices
--
CREATE VIEW summary_of_advices AS
SELECT
  -- optimizer/structural bug if c.relname instead of ac.name
  -- count_tuples get called too often, with infinite recursion in expansion?
  count_tuples(ac.name) AS Nb, -- costly!
  c.relname AS TableName,
  akind.description AS Kind,
  akind.kid AS nkind,
  asev.description AS Severity,
  asev.sid AS nsev,
  ac.title AS Title,
  ac.abstract AS Abstract,
  ac.description AS Description
FROM
  -- system information
  pg_class AS c
  JOIN pg_namespace AS n ON (c.relnamespace=n.oid)
  -- advice information
  JOIN advice_classification AS ac ON (c.oid=ac.advice)
  JOIN advice_kind AS akind ON (ac.kid=akind.kid)
  JOIN advice_severity AS asev ON (ac.sid=asev.sid)
WHERE
  -- only advisor tables!
  n.nspname = 'xpg_advisor'
  -- always true:
  -- AND ac.name=c.relname 
ORDER BY nkind ASC, nsev DESC, Title ASC;

COMMENT ON VIEW summary_of_advices IS 
'count summary of all advices'
;

--
-- the short version...
--

CREATE VIEW short_summary_of_advices AS
SELECT Kind, Severity, Nb, Title
FROM summary_of_advices
WHERE Nb>0;

COMMENT ON VIEW short_summary_of_advices IS
'short summary of currently loaded design advices (da_* views)'
;

COMMENT ON COLUMN short_summary_of_advices.Kind IS
'the kind of advice it is, such as \'performance\'or \'design\''
;

COMMENT ON COLUMN short_summary_of_advices.Severity IS
'the severity of the advice, from \'info\' to \'error\''
;

COMMENT ON COLUMN short_summary_of_advices.Nb IS
'the number of offending objects found.'
;

COMMENT ON COLUMN short_summary_of_advices.Title IS
'short description of the advice.'
;

-- VACUUM FULL ANALYZE;

-- show design advice counts...
\echo '-- summary of advices'
SELECT * FROM short_summary_of_advices;
