美文网首页
PostgreSQL查询SQL语义分析(1)—解析查询对象add

PostgreSQL查询SQL语义分析(1)—解析查询对象add

作者: hemny | 来源:发表于2018-08-13 17:10 被阅读52次

    本文主要介绍PG在执行查询时,对SQL的语义分析重写过程中的查询对象解析过程,处理的函数为addRangeTableEntry,分析查询对象信息。

    一、源码解读

    本函数是解析查询(包含增删改查操作)执行过程中涉及的查询对象(表、视图、子查询等)的信息。

    每次调用只解析一个对象。

    /* src/backend/parser/parse_relation.c */
    
    /*
     * Add an entry for a relation to the pstate's range table (p_rtable).
     *
     * Note: formerly this checked for refname conflicts, but that's wrong.
     * Caller is responsible for checking for conflicts in the appropriate scope.
     */
    RangeTblEntry *
    addRangeTableEntry(ParseState *pstate,  // 查询信息
                       RangeVar *relation,  // 本次分析的关系对象
                       Alias *alias,  // 别名
                       bool inh,  // 是否是被继承的表
                       bool inFromCl)  // 是否是from 语句中的对象
    {
        RangeTblEntry *rte = makeNode(RangeTblEntry); // 创建一个表实例
        char       *refname = alias ? alias->aliasname : relation->relname;
        LOCKMODE    lockmode;
        Relation    rel; // 关系缓存实体
    
        Assert(pstate != NULL);
    
        rte->rtekind = RTE_RELATION;
        rte->alias = alias;
    
        /*
         * Get the rel's OID.  This access also ensures that we have an up-to-date
         * relcache entry for the rel.  Since this is typically the first access
         * to a rel in a statement, be careful to get the right access level
         * depending on whether we're doing SELECT FOR UPDATE/SHARE.
         */
        lockmode = isLockedRefname(pstate, refname) ? RowShareLock : AccessShareLock; // 根据查询分析,判断锁表类型
        rel = parserOpenTable(pstate, relation, lockmode); // 从缓存中获取关系缓存实体
        rte->relid = RelationGetRelid(rel);   // 获取关系缓存实体ID
        rte->relkind = rel->rd_rel->relkind; // 关系实体类型
    
        /*
         * Build the list of effective column names using user-supplied aliases
         * and/or actual column names.
         */
        rte->eref = makeAlias(refname, NIL);
        buildRelationAliases(rel->rd_att, alias, rte->eref); // 构建别名信息
    
        /*
         * Drop the rel refcount, but keep the access lock till end of transaction
         * so that the table can't be deleted or have its schema modified
         * underneath us.
         */
        heap_close(rel, NoLock);
    
        /*
         * Set flags and access permissions.
         *
         * The initial default on access checks is always check-for-READ-access,
         * which is the right thing for all except target tables.
         */
        rte->lateral = false;
        rte->inh = inh;
        rte->inFromCl = inFromCl;
    
        rte->requiredPerms = ACL_SELECT;
        rte->checkAsUser = InvalidOid;  /* not set-uid by default, either */
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
    
        /*
         * Add completed RTE to pstate's range table list, but not to join list
         * nor namespace --- caller must do that if appropriate.
         */
        pstate->p_rtable = lappend(pstate->p_rtable, rte);  // 将表实例rte附加到pstate->p_rtable列表中
    
        return rte;
    }
    

    二、数据结构信息

    1. ParseState -SQL语义分析过程信息对象
    /*
     * State information used during parse analysis
     *
     * parentParseState: NULL in a top-level ParseState.  When parsing a subquery,
     * links to current parse state of outer query.
     *
     * p_sourcetext: source string that generated the raw parsetree being
     * analyzed, or NULL if not available.  (The string is used only to
     * generate cursor positions in error messages: we need it to convert
     * byte-wise locations in parse structures to character-wise cursor
     * positions.)
     *
     * p_rtable: list of RTEs that will become the rangetable of the query.
     * Note that neither relname nor refname of these entries are necessarily
     * unique; searching the rtable by name is a bad idea.
     *
     * p_joinexprs: list of JoinExpr nodes associated with p_rtable entries.
     * This is one-for-one with p_rtable, but contains NULLs for non-join
     * RTEs, and may be shorter than p_rtable if the last RTE(s) aren't joins.
     *
     * p_joinlist: list of join items (RangeTblRef and JoinExpr nodes) that
     * will become the fromlist of the query's top-level FromExpr node.
     *
     * p_namespace: list of ParseNamespaceItems that represents the current
     * namespace for table and column lookup.  (The RTEs listed here may be just
     * a subset of the whole rtable.  See ParseNamespaceItem comments below.)
     *
     * p_lateral_active: true if we are currently parsing a LATERAL subexpression
     * of this parse level.  This makes p_lateral_only namespace items visible,
     * whereas they are not visible when p_lateral_active is FALSE.
     *
     * p_ctenamespace: list of CommonTableExprs (WITH items) that are visible
     * at the moment.  This is entirely different from p_namespace because a CTE
     * is not an RTE, rather "visibility" means you could make an RTE from it.
     *
     * p_future_ctes: list of CommonTableExprs (WITH items) that are not yet
     * visible due to scope rules.  This is used to help improve error messages.
     *
     * p_parent_cte: CommonTableExpr that immediately contains the current query,
     * if any.
     *
     * p_target_relation: target relation, if query is INSERT, UPDATE, or DELETE.
     *
     * p_target_rangetblentry: target relation's entry in the rtable list.
     *
     * p_is_insert: true to process assignment expressions like INSERT, false
     * to process them like UPDATE.  (Note this can change intra-statement, for
     * cases like INSERT ON CONFLICT UPDATE.)
     *
     * p_windowdefs: list of WindowDefs representing WINDOW and OVER clauses.
     * We collect these while transforming expressions and then transform them
     * afterwards (so that any resjunk tlist items needed for the sort/group
     * clauses end up at the end of the query tlist).  A WindowDef's location in
     * this list, counting from 1, is the winref number to use to reference it.
     *
     * p_expr_kind: kind of expression we're currently parsing, as per enum above;
     * EXPR_KIND_NONE when not in an expression.
     *
     * p_next_resno: next TargetEntry.resno to assign, starting from 1.
     *
     * p_multiassign_exprs: partially-processed MultiAssignRef source expressions.
     *
     * p_locking_clause: query's FOR UPDATE/FOR SHARE clause, if any.
     *
     * p_locked_from_parent: true if parent query level applies FOR UPDATE/SHARE
     * to this subquery as a whole.
     *
     * p_resolve_unknowns: resolve unknown-type SELECT output columns as type TEXT
     * (this is true by default).
     *
     * p_hasAggs, p_hasWindowFuncs, etc: true if we've found any of the indicated
     * constructs in the query.
     *
     * p_last_srf: the set-returning FuncExpr or OpExpr most recently found in
     * the query, or NULL if none.
     *
     * p_pre_columnref_hook, etc: optional parser hook functions for modifying the
     * interpretation of ColumnRefs and ParamRefs.
     *
     * p_ref_hook_state: passthrough state for the parser hook functions.
     */
    struct ParseState
    {
        struct ParseState *parentParseState;    /* stack link */
        const char *p_sourcetext;   /* 查询的脚本源码 source text, or NULL if not available */
        List       *p_rtable;       /* 查询设计的对象 range table so far */
        List       *p_joinexprs;    /*对象关联条件 JoinExprs for RTE_JOIN p_rtable entries */
        List       *p_joinlist;     /*对象关联列表 join items so far (will become FromExpr
                                     * node's fromlist) */
        List       *p_namespace;    /* currently-referenceable RTEs (List of
                                     * ParseNamespaceItem) */
        bool        p_lateral_active;   /* p_lateral_only items visible? */
        List       *p_ctenamespace; /* current namespace for common table exprs */
        List       *p_future_ctes;  /* common table exprs not yet in namespace */
        CommonTableExpr *p_parent_cte;  /* this query's containing CTE */
        Relation    p_target_relation;  /* INSERT/UPDATE/DELETE target rel */
        RangeTblEntry *p_target_rangetblentry;  /* target rel's RTE */
        bool        p_is_insert;    /* process assignment like INSERT not UPDATE */
        List       *p_windowdefs;   /* raw representations of window clauses */
        ParseExprKind p_expr_kind;  /* what kind of expression we're parsing */
        int         p_next_resno;   /* next targetlist resno to assign */
        List       *p_multiassign_exprs;    /* junk tlist entries for multiassign */
        List       *p_locking_clause;   /* 锁 raw FOR UPDATE/FOR SHARE info */
        bool        p_locked_from_parent;   /* 锁 parent has marked this subquery
                                             * with FOR UPDATE/FOR SHARE */
        bool        p_resolve_unknowns; /* resolve unknown-type SELECT outputs as
                                         * type text */
    
        QueryEnvironment *p_queryEnv;   /* curr env, incl refs to enclosing env */
    
        /* Flags telling about things found in the query: */
        bool        p_hasAggs;
        bool        p_hasWindowFuncs;
        bool        p_hasTargetSRFs;
        bool        p_hasSubLinks;
        bool        p_hasModifyingCTE;
    
        Node       *p_last_srf;     /* most recent set-returning func/op found */
    
        /*
         * Optional hook functions for parser callbacks.  These are null unless
         * set up by the caller of make_parsestate.
         */
        PreParseColumnRefHook p_pre_columnref_hook;
        PostParseColumnRefHook p_post_columnref_hook;
        ParseParamRefHook p_paramref_hook;
        CoerceParamHook p_coerce_param_hook;
        void       *p_ref_hook_state;   /* common passthrough link for above */
    };
    
    1. RangeVar -查询的对象
    /*
     * RangeVar - range variable, used in FROM clauses 查询的对象
     *
     * Also used to represent table names in utility statements; there, the alias
     * field is not used, and inh tells whether to apply the operation
     * recursively to child tables.  In some contexts it is also useful to carry
     * a TEMP table indication here.
     */
    typedef struct RangeVar
    {
        NodeTag     type;
        char       *catalogname;    /* 编目/数据库名 the catalog (database) name, or NULL */
        char       *schemaname;     /* 模式表 the schema name, or NULL */
        char       *relname;        /* 对象名称 the relation/sequence name */
        bool        inh;            /* expand rel by inheritance? recursively act
                                     * on children? */
        char        relpersistence; /* see RELPERSISTENCE_* in pg_class.h */
        Alias      *alias;          /* 查询中的别名 table alias & optional column aliases */
        int         location;       /* token location, or -1 if unknown */
    } RangeVar;
    
    1. Alias-查询对象化名
    /*
     * Alias - 化名
     *    specifies an alias for a range variable; the alias might also
     *    specify renaming of columns within the table.
     *
     * Note: colnames is a list of Value nodes (always strings).  In Alias structs
     * associated with RTEs, there may be entries corresponding to dropped
     * columns; these are normally empty strings ("").  See parsenodes.h for info.
     */
    typedef struct Alias
    {
        NodeTag     type;
        char       *aliasname;      /* 化名 aliased rel name (never qualified) */
        List       *colnames;       /* 字段名列表 optional list of column aliases */
    } Alias;
    
    1. RangeTblEntry - 查询树节点中的表实体对象
    /* src/include/nodes/parsenodes.h */
    
    /*--------------------
     * RangeTblEntry -
     *    A range table is a List of RangeTblEntry nodes.
     *
     *    A range table entry may represent a plain relation, a sub-select in
     *    FROM, or the result of a JOIN clause.  (Only explicit JOIN syntax
     *    produces an RTE, not the implicit join resulting from multiple FROM
     *    items.  This is because we only need the RTE to deal with SQL features
     *    like outer joins and join-output-column aliasing.)  Other special
     *    RTE types also exist, as indicated by RTEKind.
     *
     *    Note that we consider RTE_RELATION to cover anything that has a pg_class
     *    entry.  relkind distinguishes the sub-cases.
     *
     *    alias is an Alias node representing the AS alias-clause attached to the
     *    FROM expression, or NULL if no clause.
     *
     *    eref is the table reference name and column reference names (either
     *    real or aliases).  Note that system columns (OID etc) are not included
     *    in the column list.
     *    eref->aliasname is required to be present, and should generally be used
     *    to identify the RTE for error messages etc.
     *
     *    In RELATION RTEs, the colnames in both alias and eref are indexed by
     *    physical attribute number; this means there must be colname entries for
     *    dropped columns.  When building an RTE we insert empty strings ("") for
     *    dropped columns.  Note however that a stored rule may have nonempty
     *    colnames for columns dropped since the rule was created (and for that
     *    matter the colnames might be out of date due to column renamings).
     *    The same comments apply to FUNCTION RTEs when a function's return type
     *    is a named composite type.
     *
     *    In JOIN RTEs, the colnames in both alias and eref are one-to-one with
     *    joinaliasvars entries.  A JOIN RTE will omit columns of its inputs when
     *    those columns are known to be dropped at parse time.  Again, however,
     *    a stored rule might contain entries for columns dropped since the rule
     *    was created.  (This is only possible for columns not actually referenced
     *    in the rule.)  When loading a stored rule, we replace the joinaliasvars
     *    items for any such columns with null pointers.  (We can't simply delete
     *    them from the joinaliasvars list, because that would affect the attnums
     *    of Vars referencing the rest of the list.)
     *
     *    inh is true for relation references that should be expanded to include
     *    inheritance children, if the rel has any.  This *must* be false for
     *    RTEs other than RTE_RELATION entries.
     *
     *    inFromCl marks those range variables that are listed in the FROM clause.
     *    It's false for RTEs that are added to a query behind the scenes, such
     *    as the NEW and OLD variables for a rule, or the subqueries of a UNION.
     *    This flag is not used anymore during parsing, since the parser now uses
     *    a separate "namespace" data structure to control visibility, but it is
     *    needed by ruleutils.c to determine whether RTEs should be shown in
     *    decompiled queries.
     *
     *    requiredPerms and checkAsUser specify run-time access permissions
     *    checks to be performed at query startup.  The user must have *all*
     *    of the permissions that are OR'd together in requiredPerms (zero
     *    indicates no permissions checking).  If checkAsUser is not zero,
     *    then do the permissions checks using the access rights of that user,
     *    not the current effective user ID.  (This allows rules to act as
     *    setuid gateways.)  Permissions checks only apply to RELATION RTEs.
     *
     *    For SELECT/INSERT/UPDATE permissions, if the user doesn't have
     *    table-wide permissions then it is sufficient to have the permissions
     *    on all columns identified in selectedCols (for SELECT) and/or
     *    insertedCols and/or updatedCols (INSERT with ON CONFLICT DO UPDATE may
     *    have all 3).  selectedCols, insertedCols and updatedCols are bitmapsets,
     *    which cannot have negative integer members, so we subtract
     *    FirstLowInvalidHeapAttributeNumber from column numbers before storing
     *    them in these fields.  A whole-row Var reference is represented by
     *    setting the bit for InvalidAttrNumber.
     *
     *    securityQuals is a list of security barrier quals (boolean expressions),
     *    to be tested in the listed order before returning a row from the
     *    relation.  It is always NIL in parser output.  Entries are added by the
     *    rewriter to implement security-barrier views and/or row-level security.
     *    Note that the planner turns each boolean expression into an implicitly
     *    AND'ed sublist, as is its usual habit with qualification expressions.
     *--------------------
     */
    typedef enum RTEKind   // 实体类型
    {
        RTE_RELATION,               /* 引用 ordinary relation reference */
        RTE_SUBQUERY,               /* 子查询 subquery in FROM */
        RTE_JOIN,                   /* 链接 join */
        RTE_FUNCTION,               /* 函数 function in FROM */
        RTE_TABLEFUNC,              /* 表函数 TableFunc(.., column list) */
        RTE_VALUES,                 /* 值 VALUES (<exprlist>), (<exprlist>), ... */
        RTE_CTE,                    /* 公共表表达式 common table expr (WITH list element) */
        RTE_NAMEDTUPLESTORE         /* tuple存储  tuplestore, e.g. for AFTER triggers */
    } RTEKind;
    
    typedef struct RangeTblEntry
    {
        NodeTag     type;
    
        RTEKind     rtekind;        /* see above */
    
        /*
         * XXX the fields applicable to only some rte kinds should be merged into
         * a union.  I didn't do this yet because the diffs would impact a lot of
         * code that is being actively worked on.  FIXME someday.
         */
    
        /*
         * Fields valid for a plain relation RTE (else zero):
         *
         * As a special case, RTE_NAMEDTUPLESTORE can also set relid to indicate
         * that the tuple format of the tuplestore is the same as the referenced
         * relation.  This allows plans referencing AFTER trigger transition
         * tables to be invalidated if the underlying table is altered.
         */
        Oid         relid;          /* OID of the relation */
        char        relkind;        /* relation kind (see pg_class.relkind) */
        struct TableSampleClause *tablesample;  /* sampling info, or NULL */
    
        /*
         * Fields valid for a subquery RTE (else NULL):
         */
        Query      *subquery;       /* 子查询 the sub-query */
        bool        security_barrier;   /* is from security_barrier view? */
    
        /*
         * Fields valid for a join RTE (else NULL/zero):
         *
         * joinaliasvars is a list of (usually) Vars corresponding to the columns
         * of the join result.  An alias Var referencing column K of the join
         * result can be replaced by the K'th element of joinaliasvars --- but to
         * simplify the task of reverse-listing aliases correctly, we do not do
         * that until planning time.  In detail: an element of joinaliasvars can
         * be a Var of one of the join's input relations, or such a Var with an
         * implicit coercion to the join's output column type, or a COALESCE
         * expression containing the two input column Vars (possibly coerced).
         * Within a Query loaded from a stored rule, it is also possible for
         * joinaliasvars items to be null pointers, which are placeholders for
         * (necessarily unreferenced) columns dropped since the rule was made.
         * Also, once planning begins, joinaliasvars items can be almost anything,
         * as a result of subquery-flattening substitutions.
         */
        JoinType    jointype;       /* type of join */
        List       *joinaliasvars;  /* list of alias-var expansions */
    
        /*
         * Fields valid for a function RTE (else NIL/zero):
         *
         * When funcordinality is true, the eref->colnames list includes an alias
         * for the ordinality column.  The ordinality column is otherwise
         * implicit, and must be accounted for "by hand" in places such as
         * expandRTE().
         */
        List       *functions;      /* 表函数列表 list of RangeTblFunction nodes */
        bool        funcordinality; /* is this called WITH ORDINALITY? */
    
        /*
         * Fields valid for a TableFunc RTE (else NULL):
         */
        TableFunc  *tablefunc;
    
        /*
         * Fields valid for a values RTE (else NIL):
         */
        List       *values_lists;   /* list of expression lists */
    
        /*
         * Fields valid for a CTE RTE (else NULL/zero):
         */
        char       *ctename;        /* name of the WITH list item */
        Index       ctelevelsup;    /* number of query levels up */
        bool        self_reference; /* is this a recursive self-reference? */
    
        /*
         * Fields valid for table functions, values, CTE and ENR RTEs (else NIL):
         *
         * We need these for CTE RTEs so that the types of self-referential
         * columns are well-defined.  For VALUES RTEs, storing these explicitly
         * saves having to re-determine the info by scanning the values_lists. For
         * ENRs, we store the types explicitly here (we could get the information
         * from the catalogs if 'relid' was supplied, but we'd still need these
         * for TupleDesc-based ENRs, so we might as well always store the type
         * info here).
         *
         * For ENRs only, we have to consider the possibility of dropped columns.
         * A dropped column is included in these lists, but it will have zeroes in
         * all three lists (as well as an empty-string entry in eref).  Testing
         * for zero coltype is the standard way to detect a dropped column.
         */
        List       *coltypes;       /* OID list of column type OIDs */
        List       *coltypmods;     /* integer list of column typmods */
        List       *colcollations;  /* OID list of column collation OIDs */
    
        /*
         * Fields valid for ENR RTEs (else NULL/zero):
         */
        char       *enrname;        /* name of ephemeral named relation */
        double      enrtuples;      /* estimated or actual from caller */
    
        /*
         * Fields valid in all RTEs:
         */
        Alias      *alias;          /* 别名 user-written alias clause, if any */
        Alias      *eref;           /* expanded reference names */
        bool        lateral;        /* subquery, function, or values is LATERAL? */
        bool        inh;            /* inheritance requested? */
        bool        inFromCl;       /* present in FROM clause? */
        AclMode     requiredPerms;  /* bitmask of required access permissions */
        Oid         checkAsUser;    /* 对象ID if valid, check access as this role */
        Bitmapset  *selectedCols;   /* columns needing SELECT permission */
        Bitmapset  *insertedCols;   /* columns needing INSERT permission */
        Bitmapset  *updatedCols;    /* columns needing UPDATE permission */
        List       *securityQuals;  /* security barrier quals to apply, if any */
    } RangeTblEntry;
    
    1. Relation - 关系缓存实体 的内容
      里面主要缓存了各种描述、主键、外键、触发器、索引、约束、对象锁...等对象的所有信息
    /* src/include/utils/rel.h */
    
    /*
     * Here are the contents of a relation cache entry.
     */
    
    typedef struct RelationData
    {
        RelFileNode rd_node;        /* relation physical identifier */
        /* use "struct" here to avoid needing to include smgr.h: */
        struct SMgrRelationData *rd_smgr;   /* cached file handle, or NULL */
        int         rd_refcnt;      /* reference count */
        BackendId   rd_backend;     /* owning backend id, if temporary relation */
        bool        rd_islocaltemp; /* rel is a temp rel of this session */
        bool        rd_isnailed;    /* rel is nailed in cache */
        bool        rd_isvalid;     /* relcache entry is valid */
        char        rd_indexvalid;  /* state of rd_indexlist: 0 = not valid, 1 =
                                     * valid, 2 = temporarily forced */
        bool        rd_statvalid;   /* is rd_statlist valid? */
    
        /*
         * rd_createSubid is the ID of the highest subtransaction the rel has
         * survived into; or zero if the rel was not created in the current top
         * transaction.  This can be now be relied on, whereas previously it could
         * be "forgotten" in earlier releases. Likewise, rd_newRelfilenodeSubid is
         * the ID of the highest subtransaction the relfilenode change has
         * survived into, or zero if not changed in the current transaction (or we
         * have forgotten changing it). rd_newRelfilenodeSubid can be forgotten
         * when a relation has multiple new relfilenodes within a single
         * transaction, with one of them occurring in a subsequently aborted
         * subtransaction, e.g. BEGIN; TRUNCATE t; SAVEPOINT save; TRUNCATE t;
         * ROLLBACK TO save; -- rd_newRelfilenode is now forgotten
         */
        SubTransactionId rd_createSubid;    /* rel was created in current xact */
        SubTransactionId rd_newRelfilenodeSubid;    /* new relfilenode assigned in
                                                     * current xact */
    
        Form_pg_class rd_rel;       /* RELATION tuple */
        TupleDesc   rd_att;         /* tuple descriptor */
        Oid         rd_id;          /* relation's object id */
        LockInfoData rd_lockInfo;   /* lock mgr's info for locking relation */
        RuleLock   *rd_rules;       /* rewrite rules */
        MemoryContext rd_rulescxt;  /* private memory cxt for rd_rules, if any */
        TriggerDesc *trigdesc;      /* Trigger info, or NULL if rel has none */
        /* use "struct" here to avoid needing to include rowsecurity.h: */
        struct RowSecurityDesc *rd_rsdesc;  /* row security policies, or NULL */
    
        /* data managed by RelationGetFKeyList: */
        List       *rd_fkeylist;    /* list of ForeignKeyCacheInfo (see below) */
        bool        rd_fkeyvalid;   /* true if list has been computed */
    
        MemoryContext rd_partkeycxt;    /* private memory cxt for the below */
        struct PartitionKeyData *rd_partkey;    /* partition key, or NULL */
        MemoryContext rd_pdcxt;     /* private context for partdesc */
        struct PartitionDescData *rd_partdesc;  /* partitions, or NULL */
        List       *rd_partcheck;   /* partition CHECK quals */
    
        /* data managed by RelationGetIndexList: */
        List       *rd_indexlist;   /* list of OIDs of indexes on relation */
        Oid         rd_oidindex;    /* OID of unique index on OID, if any */
        Oid         rd_pkindex;     /* OID of primary key, if any */
        Oid         rd_replidindex; /* OID of replica identity index, if any */
    
        /* data managed by RelationGetStatExtList: */
        List       *rd_statlist;    /* list of OIDs of extended stats */
    
        /* data managed by RelationGetIndexAttrBitmap: */
        Bitmapset  *rd_indexattr;   /* columns used in non-projection indexes */
        Bitmapset  *rd_projindexattr;   /* columns used in projection indexes */
        Bitmapset  *rd_keyattr;     /* cols that can be ref'd by foreign keys */
        Bitmapset  *rd_pkattr;      /* cols included in primary key */
        Bitmapset  *rd_idattr;      /* included in replica identity index */
        Bitmapset  *rd_projidx;     /* Oids of projection indexes */
    
        PublicationActions *rd_pubactions;  /* publication actions */
    
        /*
         * rd_options is set whenever rd_rel is loaded into the relcache entry.
         * Note that you can NOT look into rd_rel for this data.  NULL means "use
         * defaults".
         */
        bytea      *rd_options;     /* parsed pg_class.reloptions */
    
        /* These are non-NULL only for an index relation: */
        Form_pg_index rd_index;     /* pg_index tuple describing this index */
        /* use "struct" here to avoid needing to include htup.h: */
        struct HeapTupleData *rd_indextuple;    /* all of pg_index tuple */
    
        /*
         * index access support info (used only for an index relation)
         *
         * Note: only default support procs for each opclass are cached, namely
         * those with lefttype and righttype equal to the opclass's opcintype. The
         * arrays are indexed by support function number, which is a sufficient
         * identifier given that restriction.
         *
         * Note: rd_amcache is available for index AMs to cache private data about
         * an index.  This must be just a cache since it may get reset at any time
         * (in particular, it will get reset by a relcache inval message for the
         * index).  If used, it must point to a single memory chunk palloc'd in
         * rd_indexcxt.  A relcache reset will include freeing that chunk and
         * setting rd_amcache = NULL.
         */
        Oid         rd_amhandler;   /* OID of index AM's handler function */
        MemoryContext rd_indexcxt;  /* private memory cxt for this stuff */
        /* use "struct" here to avoid needing to include amapi.h: */
        struct IndexAmRoutine *rd_amroutine;    /* index AM's API struct */
        Oid        *rd_opfamily;    /* OIDs of op families for each index col */
        Oid        *rd_opcintype;   /* OIDs of opclass declared input data types */
        RegProcedure *rd_support;   /* OIDs of support procedures */
        FmgrInfo   *rd_supportinfo; /* lookup info for support procedures */
        int16      *rd_indoption;   /* per-column AM-specific flags */
        List       *rd_indexprs;    /* index expression trees, if any */
        List       *rd_indpred;     /* index predicate tree, if any */
        Oid        *rd_exclops;     /* OIDs of exclusion operators, if any */
        Oid        *rd_exclprocs;   /* OIDs of exclusion ops' procs, if any */
        uint16     *rd_exclstrats;  /* exclusion ops' strategy numbers, if any */
        void       *rd_amcache;     /* available for use by index AM */
        Oid        *rd_indcollation;    /* OIDs of index collations */
    
        /*
         * foreign-table support
         *
         * rd_fdwroutine must point to a single memory chunk palloc'd in
         * CacheMemoryContext.  It will be freed and reset to NULL on a relcache
         * reset.
         */
    
        /* use "struct" here to avoid needing to include fdwapi.h: */
        struct FdwRoutine *rd_fdwroutine;   /* cached function pointers, or NULL */
    
        /*
         * Hack for CLUSTER, rewriting ALTER TABLE, etc: when writing a new
         * version of a table, we need to make any toast pointers inserted into it
         * have the existing toast table's OID, not the OID of the transient toast
         * table.  If rd_toastoid isn't InvalidOid, it is the OID to place in
         * toast pointers inserted into this rel.  (Note it's set on the new
         * version of the main heap, not the toast table itself.)  This also
         * causes toast_save_datum() to try to preserve toast value OIDs.
         */
        Oid         rd_toastoid;    /* Real TOAST table's OID, or InvalidOid */
    
        /* use "struct" here to avoid needing to include pgstat.h: */
        struct PgStat_TableStatus *pgstat_info; /* statistics collection area */
    } RelationData;
    
    typedef struct RelationData *Relation;
    
    /* ----------------
     *      RelationPtr is used in the executor to support index scans
     *      where we have to keep track of several index relations in an
     *      array.  -cim 9/10/89
     * ----------------
     */
    typedef Relation *RelationPtr;
    
    

    三、函数调用信息

    1. 执行的SQL
    postgres=# select s.*
    from t_student s
    where s.sdept='IS'
    order by s.sno desc
    limit 2;
    
    1. 函数调用栈信息
    Breakpoint 3, addRangeTableEntry (pstate=0x1f98638, relation=0x1f97e48,
        alias=0x1f97ea0, inh=true, inFromCl=true) at parse_relation.c:1200
    1200            RangeTblEntry *rte = makeNode(RangeTblEntry);
    (gdb) bt
    /*  函数调用栈信息 */
    #0  addRangeTableEntry (pstate=0x1f98638, relation=0x1f97e48, alias=0x1f97ea0,
        inh=true, inFromCl=true) at parse_relation.c:1200
    #1  0x00000000005ec7ed in transformTableEntry (pstate=0x1f98638, r=0x1f97e48)
        at parse_clause.c:435
    #2  0x00000000005ee210 in transformFromClauseItem (pstate=0x1f98638,
        n=0x1f97e48, top_rte=0x7ffc63ea0a28, top_rti=0x7ffc63ea0a24,
        namespace=0x7ffc63ea0a18) at parse_clause.c:1121
    #3  0x00000000005ec0d3 in transformFromClause (pstate=0x1f98638,
        frmList=0x1f97f00) at parse_clause.c:139
    #4  0x00000000005b54a3 in transformSelectStmt (pstate=0x1f98638,
        stmt=0x1f981c0) at analyze.c:1212
    #5  0x00000000005b3a43 in transformStmt (pstate=0x1f98638, parseTree=0x1f981c0)
        at analyze.c:301
    #6  0x00000000005b3919 in transformOptionalSelectInto (pstate=0x1f98638,
        parseTree=0x1f981c0) at analyze.c:246
    #7  0x00000000005b37d2 in transformTopLevelStmt (pstate=0x1f98638,
        parseTree=0x1f985a0) at analyze.c:196
    #8  0x00000000005b361b in parse_analyze (parseTree=0x1f985a0,
        sourceText=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;", paramTypes=0x0, numParams=0, queryEnv=0x0)
        at analyze.c:116
    #9  0x00000000008c31dd in pg_analyze_and_rewrite (parsetree=0x1f985a0,
        query_string=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\nor---Type <return> to continue, or q <return> to quit---
    der by s.sno desc \nlimit 2;", paramTypes=0x0, numParams=0, queryEnv=0x0)
        at postgres.c:666
    #10 0x00000000008c3847 in exec_simple_query (
        query_string=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;") at postgres.c:1047
    #11 0x00000000008c7d23 in PostgresMain (argc=1, argv=0x1fc31a0,
        dbname=0x1fc3000 "postgres", username=0x1fc2fe0 "appusr")
        at postgres.c:4153
    #12 0x000000000082405c in BackendRun (port=0x1fb8fb0) at postmaster.c:4361
    #13 0x00000000008237c0 in BackendStartup (port=0x1fb8fb0) at postmaster.c:4033
    #14 0x000000000081fb58 in ServerLoop () at postmaster.c:1706
    #15 0x000000000081f3f0 in PostmasterMain (argc=3, argv=0x1f91ce0)
        at postmaster.c:1379
    #16 0x00000000007469d4 in main (argc=3, argv=0x1f91ce0) at main.c:228
    /*  函数调用参数 */
    (gdb) p *pstate
    $122 = {parentParseState = 0x0,
      p_sourcetext = 0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;", p_rtable = 0x0, p_joinexprs = 0x0,
      p_joinlist = 0x0, p_namespace = 0x0, p_lateral_active = false,
      p_ctenamespace = 0x0, p_future_ctes = 0x0, p_parent_cte = 0x0,
      p_target_relation = 0x0, p_target_rangetblentry = 0x0, p_is_insert = false,
      p_windowdefs = 0x0, p_expr_kind = EXPR_KIND_NONE, p_next_resno = 1,
      p_multiassign_exprs = 0x0, p_locking_clause = 0x0,
      p_locked_from_parent = false, p_resolve_unknowns = true, p_queryEnv = 0x0,
      p_hasAggs = false, p_hasWindowFuncs = false, p_hasTargetSRFs = false,
      p_hasSubLinks = false, p_hasModifyingCTE = false, p_last_srf = 0x0,
      p_pre_columnref_hook = 0x0, p_post_columnref_hook = 0x0,
      p_paramref_hook = 0x0, p_coerce_param_hook = 0x0, p_ref_hook_state = 0x0}
    (gdb) p *relation
    $123 = {type = T_RangeVar, catalogname = 0x0, schemaname = 0x0,
      relname = 0x1f97e00 "t_student", inh = true, relpersistence = 112 'p',
      alias = 0x1f97ea0, location = 16}
    (gdb) p *relation->alias
    $124 = {type = T_Alias, aliasname = 0x1f97e28 "s", colnames = 0x0}
    (gdb) p *alias
    $125 = {type = T_Alias, aliasname = 0x1f97e28 "s", colnames = 0x0}
    /*  函数运行过程 */
    (gdb) n
    1201            char       *refname = alias ? alias->aliasname : relation->relname;
    (gdb) p *rte
    $126 = {type = T_RangeTblEntry, rtekind = RTE_RELATION, relid = 0,
      relkind = 0 '\000', tablesample = 0x0, subquery = 0x0,
      security_barrier = false, jointype = JOIN_INNER, joinaliasvars = 0x0,
      functions = 0x0, funcordinality = false, tablefunc = 0x0,
      values_lists = 0x0, ctename = 0x0, ctelevelsup = 0, self_reference = false,
      coltypes = 0x0, coltypmods = 0x0, colcollations = 0x0, enrname = 0x0,
      enrtuples = 0, alias = 0x0, eref = 0x0, lateral = false, inh = false,
      inFromCl = false, requiredPerms = 0, checkAsUser = 0, selectedCols = 0x0,
      insertedCols = 0x0, updatedCols = 0x0, securityQuals = 0x0}
    (gdb) n
    1205            Assert(pstate != NULL);
    (gdb) p *refname
    $127 = 115 's'
    (gdb) p refname
    $128 = 0x1f97e28 "s"
    (gdb) n
    1207            rte->rtekind = RTE_RELATION;
    (gdb) n
    1208            rte->alias = alias;
    (gdb) n
    1216            lockmode = isLockedRefname(pstate, refname) ? RowShareLock : AccessShareLock;// 根据查询分析,判断锁表类型
    (gdb) n
    1217            rel = parserOpenTable(pstate, relation, lockmode); // 从缓存中获取关系缓存实体
    (gdb) p lockmode
    $129 = 1
    (gdb) n
    1218            rte->relid = RelationGetRelid(rel); // 获取关系缓存实体ID
    (gdb) p *rel
    $130 = {rd_node = {spcNode = 1663, dbNode = 13878, relNode = 16408},
      rd_smgr = 0x2021f40, rd_refcnt = 1, rd_backend = -1, rd_islocaltemp = false,
      rd_isnailed = false, rd_isvalid = true, rd_indexvalid = 1 '\001',
      rd_statvalid = true, rd_createSubid = 0, rd_newRelfilenodeSubid = 0,
      rd_rel = 0x7fcfc6d309a0, rd_att = 0x7fcfc6d30ab8, rd_id = 16408,
      rd_lockInfo = {lockRelId = {relId = 16408, dbId = 13878}}, rd_rules = 0x0,
      rd_rulescxt = 0x0, trigdesc = 0x0, rd_rsdesc = 0x0, rd_fkeylist = 0x0,
      rd_fkeyvalid = false, rd_partkeycxt = 0x0, rd_partkey = 0x0, rd_pdcxt = 0x0,
      rd_partdesc = 0x0, rd_partcheck = 0x0, rd_indexlist = 0x7fcfc6c72668,
      rd_oidindex = 0, rd_pkindex = 16411, rd_replidindex = 16411,
      rd_statlist = 0x0, rd_indexattr = 0x0, rd_projindexattr = 0x0,
      rd_keyattr = 0x0, rd_pkattr = 0x0, rd_idattr = 0x0, rd_projidx = 0x0,
      rd_pubactions = 0x0, rd_options = 0x0, rd_index = 0x0, rd_indextuple = 0x0,
      rd_amhandler = 0, rd_indexcxt = 0x0, rd_amroutine = 0x0, rd_opfamily = 0x0,
      rd_opcintype = 0x0, rd_support = 0x0, rd_supportinfo = 0x0,
      rd_indoption = 0x0, rd_indexprs = 0x0, rd_indpred = 0x0, rd_exclops = 0x0,
      rd_exclprocs = 0x0, rd_exclstrats = 0x0, rd_amcache = 0x0,
      rd_indcollation = 0x0, rd_fdwroutine = 0x0, rd_toastoid = 0,
      pgstat_info = 0x2015c60}
    (gdb) n
    1219            rte->relkind = rel->rd_rel->relkind; // 关系实体类型
    (gdb) p rte->relid
    $131 = 16408
    (gdb) n
    1225            rte->eref = makeAlias(refname, NIL);
    (gdb) p rte->relkind
    $132 = 114 'r'
    (gdb) n
    1226            buildRelationAliases(rel->rd_att, alias, rte->eref); // 构建别名信息
    (gdb)
    1233            heap_close(rel, NoLock);
    (gdb)
    1241            rte->lateral = false;
    (gdb)
    1242            rte->inh = inh;
    (gdb)
    1243            rte->inFromCl = inFromCl;
    (gdb)
    1245            rte->requiredPerms = ACL_SELECT;
    (gdb)
    1246            rte->checkAsUser = InvalidOid;  /* not set-uid by default, either */
    (gdb)
    1247            rte->selectedCols = NULL;
    (gdb)
    1248            rte->insertedCols = NULL;
    (gdb)
    1249            rte->updatedCols = NULL;
    (gdb)
    1255            pstate->p_rtable = lappend(pstate->p_rtable, rte); // 将表实例rte附加到pstate->p_rtable列表中
    (gdb)
    1257            return rte;
    (gdb)
    1258    }
    // 返回RangeTblEntry 对象指针
    (gdb) p *rte
    $133 = {type = T_RangeTblEntry, rtekind = RTE_RELATION, relid = 16408,
      relkind = 114 'r', tablesample = 0x0, subquery = 0x0,
      security_barrier = false, jointype = JOIN_INNER, joinaliasvars = 0x0,
      functions = 0x0, funcordinality = false, tablefunc = 0x0,
      values_lists = 0x0, ctename = 0x0, ctelevelsup = 0, self_reference = false,
      coltypes = 0x0, coltypmods = 0x0, colcollations = 0x0, enrname = 0x0,
      enrtuples = 0, alias = 0x1f97ea0, eref = 0x1f98980, lateral = false,
      inh = true, inFromCl = true, requiredPerms = 2, checkAsUser = 0,
      selectedCols = 0x0, insertedCols = 0x0, updatedCols = 0x0,
      securityQuals = 0x0}
    (gdb) p *rte->alias
    $134 = {type = T_Alias, aliasname = 0x1f97e28 "s", colnames = 0x0}
    (gdb) p *rte->eref
    $135 = {type = T_Alias, aliasname = 0x1f989b8 "s", colnames = 0x1f98a48}
    (gdb) p *rte->eref->colnames
    $136 = {type = T_List, length = 5, head = 0x1f98a20, tail = 0x1f98c18}
    (gdb) p *((Node*)rte->eref->colnames->head->data->ptr_value)
    $137 = {type = T_String}
    (gdb) p *((Value*)rte->eref->colnames->head->data->ptr_value)
    $138 = {type = T_String, val = {ival = 33130968, str = 0x1f989d8 "sno"}}
    (gdb) p *((Value*)rte->eref->colnames->head->next->data->ptr_value)
    $139 = {type = T_String, val = {ival = 33131136, str = 0x1f98a80 "sname"}}
    (gdb) p *((Value*)rte->eref->colnames->head->next->next->data->ptr_value)
    $140 = {type = T_String, val = {ival = 33131248, str = 0x1f98af0 "ssex"}}
    (gdb) p *((Value*)rte->eref->colnames->head->next->next->next->data->ptr_value)
    $141 = {type = T_String, val = {ival = 33131360, str = 0x1f98b60 "sage"}}
    (gdb) p *((Value*)rte->eref->colnames->head->next->next->next->next->data->ptr_value)
    $142 = {type = T_String, val = {ival = 33131472, str = 0x1f98bd0 "sdept"}}
    (gdb) n
    // 回到上一级函数transformTableEntry 
    transformTableEntry (pstate=0x1f98638, r=0x1f97e48) at parse_clause.c:437
    437             return rte;
    (gdb) n
    438     }
    (gdb) n
    // 回到上一级函数 transformFromClauseItem
    transformFromClauseItem (pstate=0x1f98638, n=0x1f97e48,
        top_rte=0x7ffc63ea0a28, top_rti=0x7ffc63ea0a24, namespace=0x7ffc63ea0a18)
        at parse_clause.c:1124
    1124                    rtindex = list_length(pstate->p_rtable);
    (gdb) bt
    #0  transformFromClauseItem (pstate=0x1f98638, n=0x1f97e48,
        top_rte=0x7ffc63ea0a28, top_rti=0x7ffc63ea0a24, namespace=0x7ffc63ea0a18)
        at parse_clause.c:1124
    #1  0x00000000005ec0d3 in transformFromClause (pstate=0x1f98638,
        frmList=0x1f97f00) at parse_clause.c:139
    #2  0x00000000005b54a3 in transformSelectStmt (pstate=0x1f98638,
        stmt=0x1f981c0) at analyze.c:1212
    #3  0x00000000005b3a43 in transformStmt (pstate=0x1f98638, parseTree=0x1f981c0)
        at analyze.c:301
    #4  0x00000000005b3919 in transformOptionalSelectInto (pstate=0x1f98638,
        parseTree=0x1f981c0) at analyze.c:246
    #5  0x00000000005b37d2 in transformTopLevelStmt (pstate=0x1f98638,
        parseTree=0x1f985a0) at analyze.c:196
    #6  0x00000000005b361b in parse_analyze (parseTree=0x1f985a0,
        sourceText=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;", paramTypes=0x0, numParams=0, queryEnv=0x0)
        at analyze.c:116
    #7  0x00000000008c31dd in pg_analyze_and_rewrite (parsetree=0x1f985a0,
        query_string=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;", paramTypes=0x0, numParams=0, queryEnv=0x0)
        at postgres.c:666
    #8  0x00000000008c3847 in exec_simple_query (
        query_string=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\nor---Type <return> to continue, or q <return> to quit---
    der by s.sno desc \nlimit 2;") at postgres.c:1047
    #9  0x00000000008c7d23 in PostgresMain (argc=1, argv=0x1fc31a0,
        dbname=0x1fc3000 "postgres", username=0x1fc2fe0 "appusr")
        at postgres.c:4153
    #10 0x000000000082405c in BackendRun (port=0x1fb8fb0) at postmaster.c:4361
    #11 0x00000000008237c0 in BackendStartup (port=0x1fb8fb0) at postmaster.c:4033
    #12 0x000000000081fb58 in ServerLoop () at postmaster.c:1706
    #13 0x000000000081f3f0 in PostmasterMain (argc=3, argv=0x1f91ce0)
        at postmaster.c:1379
    #14 0x00000000007469d4 in main (argc=3, argv=0x1f91ce0) at main.c:228
    (gdb) n
    1125                    Assert(rte == rt_fetch(rtindex, pstate->p_rtable));
    (gdb)
    1126                    *top_rte = rte;
    (gdb)
    1127                    *top_rti = rtindex;
    (gdb)
    1128                    *namespace = list_make1(makeDefaultNSItem(rte));
    (gdb)
    1129                    rtr = makeNode(RangeTblRef);
    (gdb) p *namespace
    $143 = (List *) 0x1f98cf0
    (gdb) p **namespace
    $144 = {type = T_List, length = 1, head = 0x1f98cc8, tail = 0x1f98cc8}
    (gdb) p *(*namespace)->head->data->ptr_value
    Attempt to dereference a generic pointer.
    (gdb) p *((Node*)(*namespace)->head->data->ptr_value)
    $145 = {type = 33130600}
    (gdb) n
    1130                    rtr->rtindex = rtindex;
    (gdb) n
    1131                    return (Node *) rtr;
    (gdb) p *rtr
    $146 = {type = T_RangeTblRef, rtindex = 1}
    (gdb) finish // 回到上一级函数 transformFromClause 
    Run till exit from #0  transformFromClauseItem (pstate=0x1f98638, n=0x1f97e48,
        top_rte=0x7ffc63ea0a28, top_rti=0x7ffc63ea0a24, namespace=0x7ffc63ea0a18)
        at parse_clause.c:1131
    0x00000000005ec0d3 in transformFromClause (pstate=0x1f98638, frmList=0x1f97f00)
        at parse_clause.c:139
    139                     n = transformFromClauseItem(pstate, n,
    Value returned is $147 = (Node *) 0x1f98d28
    (gdb) bt
    #0  0x00000000005ec0d3 in transformFromClause (pstate=0x1f98638,
        frmList=0x1f97f00) at parse_clause.c:139
    #1  0x00000000005b54a3 in transformSelectStmt (pstate=0x1f98638,
        stmt=0x1f981c0) at analyze.c:1212
    #2  0x00000000005b3a43 in transformStmt (pstate=0x1f98638, parseTree=0x1f981c0)
        at analyze.c:301
    #3  0x00000000005b3919 in transformOptionalSelectInto (pstate=0x1f98638,
        parseTree=0x1f981c0) at analyze.c:246
    #4  0x00000000005b37d2 in transformTopLevelStmt (pstate=0x1f98638,
        parseTree=0x1f985a0) at analyze.c:196
    #5  0x00000000005b361b in parse_analyze (parseTree=0x1f985a0,
        sourceText=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;", paramTypes=0x0, numParams=0, queryEnv=0x0)
        at analyze.c:116
    #6  0x00000000008c31dd in pg_analyze_and_rewrite (parsetree=0x1f985a0,
        query_string=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;", paramTypes=0x0, numParams=0, queryEnv=0x0)
        at postgres.c:666
    #7  0x00000000008c3847 in exec_simple_query (
        query_string=0x1f971c8 "select s.*\nfrom t_student s\nwhere s.sdept='IS'\norder by s.sno desc \nlimit 2;") at postgres.c:1047
    #8  0x00000000008c7d23 in PostgresMain (argc=1, argv=0x1fc31a0,
        dbname=0x1fc3000 "postgres", username=0x1fc2fe0 "appusr")
    ---Type <return> to continue, or q <return> to quit---
        at postgres.c:4153
    #9  0x000000000082405c in BackendRun (port=0x1fb8fb0) at postmaster.c:4361
    #10 0x00000000008237c0 in BackendStartup (port=0x1fb8fb0) at postmaster.c:4033
    #11 0x000000000081fb58 in ServerLoop () at postmaster.c:1706
    #12 0x000000000081f3f0 in PostmasterMain (argc=3, argv=0x1f91ce0)
        at postmaster.c:1379
    #13 0x00000000007469d4 in main (argc=3, argv=0x1f91ce0) at main.c:228
    (gdb) n
    144                     checkNameSpaceConflicts(pstate, pstate->p_namespace, namespace);
    (gdb) n
    147                     setNamespaceLateralState(namespace, true, true);
    (gdb) n
    149                     pstate->p_joinlist = lappend(pstate->p_joinlist, n);
    (gdb) n
    150                     pstate->p_namespace = list_concat(pstate->p_namespace, namespace);
    (gdb) n
    132             foreach(fl, frmList)
    (gdb) n
    159             setNamespaceLateralState(pstate->p_namespace, false, true);
    (gdb) n
    160     }
    (gdb) 
    
    

    四、总结

    1、通过观察addRangeTableEntry的执行过程,了解SQL语义解析transformFromClause的处理过程。
    2、表结构信是从缓存中结构读取,然后获取自己需要的信息。
    3、语义分析后转换为relid(关联对象id),提升查询执行的处理效率。

    相关文章

      网友评论

          本文标题:PostgreSQL查询SQL语义分析(1)—解析查询对象add

          本文链接:https://www.haomeiwen.com/subject/gmxcbftx.html