sp_pkeys and Primary Key Metadata in SQL Server
Which columns make up a table's primary key, and in what order are they declared? For a single table, the system stored procedure sp_pkeys answers in one line; for a database-wide inventory, the catalog views behind it give you full control. This post covers both, from the quick lookup to the complete metadata query.
Purpose and Overview
sp_pkeys returns primary key information for one table in the current database. It is an ODBC-catalog procedure — the T-SQL equivalent of the SQLPrimaryKeys ODBC function — and its result set is deliberately compact: TABLE_QUALIFIER, TABLE_OWNER, TABLE_NAME, COLUMN_NAME, KEY_SEQ, and PK_NAME. The KEY_SEQ column is the one that matters most: it gives the sequence number of each column within a multicolumn primary key, so a composite key returns one row per column in declared order.
The procedure is ideal for a fast, scriptable check of a known table, and because its shape is standardized across drivers, tools and ORMs rely on it heavily. What it cannot do is report across an entire database, return unique constraints, or expose the underlying index — for those you go to the catalog views it sits on top of.
Those views are sys.key_constraints (one row per primary key or unique constraint), sys.index_columns (the columns of the enforcing unique index, in key order), and sys.columns. They are part of the same proprietary sys catalog layer used throughout SQL Server metadata work. If you need a portable, ANSI-standard alternative, INFORMATION_SCHEMA.KEY_COLUMN_USAGE provides one, as discussed in the data-dictionary queries post. For related tasks, see primary key discovery and finding tables without primary keys.
Code Breakdown
The quick lookup for a single table:
1EXEC sp_pkeys
2 @table_name = N'SalesOrderHeader',
3 @table_owner = N'Sales';
The full database-wide inventory, returning every primary key column in declared order with its data type:
1SELECT
2 s.name AS schema_name,
3 t.name AS table_name,
4 kc.name AS pk_name,
5 ic.key_ordinal,
6 c.name AS column_name,
7 ty.name AS data_type,
8 ic.is_descending_key
9FROM sys.key_constraints AS kc
10INNER JOIN sys.tables AS t
11 ON kc.parent_object_id = t.object_id
12INNER JOIN sys.schemas AS s
13 ON t.schema_id = s.schema_id
14INNER JOIN sys.index_columns AS ic
15 ON kc.parent_object_id = ic.object_id
16 AND kc.unique_index_id = ic.index_id
17INNER JOIN sys.columns AS c
18 ON ic.object_id = c.object_id
19 AND ic.column_id = c.column_id
20INNER JOIN sys.types AS ty
21 ON c.user_type_id = ty.user_type_id
22WHERE kc.type = 'PK'
23ORDER BY s.name, t.name, ic.key_ordinal;
Anchoring on sys.key_constraints
sys.key_constraints holds a row for every primary key and unique constraint, distinguished by its type column (PK versus UQ). Filtering WHERE kc.type = 'PK' isolates primary keys. Because this view inherits from sys.objects, it also carries the constraint name and parent_object_id, which links the constraint back to its table.
Following unique_index_id to the columns
A primary key is enforced by a unique index, and sys.key_constraints.unique_index_id is the ID of that index. Joining it to sys.index_columns on both object_id and index_id returns the columns of the enforcing index. The key_ordinal column then orders them exactly as the key was declared — the catalog-view equivalent of sp_pkeys's KEY_SEQ.
Resolving names and data types
sys.index_columns stores only numeric column_id values, so the query joins to sys.columns to get column names and to sys.types to get the data-type name. Including the data type makes the output useful for documentation and for spotting wide or inappropriate key columns, such as a uniqueidentifier or a long varchar carrying the clustered key.
The ANSI alternative
INFORMATION_SCHEMA.KEY_COLUMN_USAGE returns one row per key column with an ORDINAL_POSITION that mirrors key_ordinal. Joined to INFORMATION_SCHEMA.TABLE_CONSTRAINTS filtered on CONSTRAINT_TYPE = 'PRIMARY KEY', it produces a portable version of the same report at the cost of the type and index detail the sys views expose:
1SELECT
2 kcu.TABLE_SCHEMA,
3 kcu.TABLE_NAME,
4 tc.CONSTRAINT_NAME AS pk_name,
5 kcu.ORDINAL_POSITION,
6 kcu.COLUMN_NAME
7FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
8INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
9 ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
10 AND tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
11WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
12ORDER BY kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.ORDINAL_POSITION;
The join is on constraint name and schema, because constraint names are unique only within a schema. This form runs unchanged on any ISO-compliant engine, which makes it the right choice for tooling that must target more than SQL Server. The trade-off is depth: it cannot tell you the enforcing index, whether a key column is descending, or the underlying data type, all of which the catalog-view query above returns. Choose the sys views when you need that detail and the INFORMATION_SCHEMA form when portability wins.
Why KEY_SEQ and key_ordinal matter
Both sp_pkeys's KEY_SEQ and sys.index_columns's key_ordinal report the position of a column within the key, and that order is not cosmetic. A composite primary key is physically an index on those columns in that sequence, so the leading column drives seekability: a query that filters on the first key column can seek efficiently, while one that filters only on a later column cannot. Reading the declared order tells you which access patterns the key supports — a routine check when diagnosing why a query on a multi-column key is scanning rather than seeking.
Key Benefits and Use Cases
- One-line single-table lookup —
sp_pkeysis the fastest way to confirm a table's key and column order interactively. - Database-wide inventory — the catalog-view query lists every primary key in the database, with column order and data type.
- Composite-key clarity —
key_ordinal/KEY_SEQreveal the exact declared order, which drives index seek efficiency. - Migration and ORM checks — verify that every table has a key before enabling features (replication, change tracking) that require one.
- Portable option — the
INFORMATION_SCHEMAform runs unchanged across SQL platforms that implement the ISO views.
Performance Considerations
sp_pkeysis single-table only: it takes a table name and cannot scan a whole database. For inventory work, use the catalog-view query instead of loopingsp_pkeysover every table.- Permission scoping applies: both
sp_pkeysand the catalog views respect metadata visibility, so a low-privilege user may see fewer keys than exist. @table_ownermatters: without it,sp_pkeysfalls back to default visibility rules and may resolve to a different schema's table of the same name — always pass the owner in multi-schema databases.- Catalog reads are cheap: these are metadata-only queries, but on huge schemas filter by
OBJECT_ID('schema.table')to avoid returning every key at once.
Practical Tips
- To find tables that lack a primary key entirely, query
sys.tablesand useOBJECTPROPERTY(object_id, 'TableHasPrimaryKey') = 0— the inverse of this report. - Swap
WHERE kc.type = 'PK'to'UQ'to inventory unique constraints with the identical join. - Add
ic.is_descending_key(already in the query) to detect descending key columns, which affect index ordering and range-scan direction. - Capture the inventory to a table during a release and diff it afterward to catch unintended key changes in deployment scripts.
- When documenting for developers, concatenate the ordered column names with
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal)to show each key as a single readable column list.
Conclusion
sp_pkeys is the right tool when you know the table and want its key fast; the sys.key_constraints → sys.index_columns → sys.columns join is the right tool when you need every primary key in the database with column order and types. Both surface the same underlying metadata, and INFORMATION_SCHEMA.KEY_COLUMN_USAGE offers a portable third option. Together they cover every primary-key question a DBA routinely has to answer.
References
- sp_pkeys (Transact-SQL) — Microsoft Learn — Parameters and the
KEY_SEQresult-set semantics for the single-table lookup. - sys.key_constraints (Transact-SQL) — Microsoft Learn — One row per primary key or unique constraint, with
unique_index_id. - sys.index_columns (Transact-SQL) — Microsoft Learn — The
key_ordinalcolumn that orders key columns as declared. - INFORMATION_SCHEMA.KEY_COLUMN_USAGE (Transact-SQL) — Microsoft Learn — Portable, ISO-standard view of key columns and
ORDINAL_POSITION.
Posts in this series
- sp_pkeys: SQL Server Primary Key Discovery Script
- SQL Server: Find Tables with Clustered Indexes for Rebuild
- SQL Server: Find Tables Without Primary Keys
- SQL Server Foreign Keys Without Indexes Script
- SQL Server List All Foreign Keys with Referenced Tables
- SQL Server Find Columns by Data Type Across the Database
- SQL Server Catalog Views: sys.tables, sys.indexes, sys.objects
- sysindexes vs sys.indexes: Legacy and Modern Catalog Views
- sp_pkeys and Primary Key Metadata in SQL Server
- SQL Server Database Schema and Data Dictionary Queries
- System-Versioned (Temporal) Tables in SQL Server